一个实例 使用lambda表达式可以创建简洁的匿名方法。不过,有时候lambda表达式只是简单的调用了已有的方法。此时,使用方法引用无疑是一个更简洁易读的方案。 再来看看之前使用过的Person类: 假设所有的Person对象都保存在一个数组中,然后想按年龄对数组成员进行排序,可以使用如下的代码进行实现: 这里调用的sort方法的声明是这样子的: 注意这里的Comparator是一个函数式接口,因此无需再定义一个PersonAgeComparator类并创建一个实例,直接使用lambda表达式实现即可: 不过Person类中已经有了一个compareByAge方法,因此可以对上面的表达式作进一步的简化: 因为这个lambda表达式只是调用了一个已有的方法,因此可以使用方法引用替换lambda表达式: 这里的方法引用Person::compareByAge和lambda表达式(a, b) -> Person.compareByAge(a, b)在语义上是一样的。它们都有如下的特性: 参数列表copy自Comparator<Person>.compare方法,即(Person, Person); 调用了Person.compareByAge方法。 在这个例子以及下面的示例中可以看到,方法引用使用的场合大致是和lambda表达式重合的。 方法引用的类型 方法引用有四种类型: 类型 示例 静态方法引用 ContainingClass::staticMethodName 实例方法引用 containingObject::instanceMethodName 一个类任意对象的实例方法引用 ContainingType::methodName 构造方法引用 ClassName::new 静态方法引用 前面示例中的Person::compareByAge就是一个静态方法引用。 实例方法引用 下面的代码演示了实例方法引用: 方法引用myComparisonProvider::compareByName中调用的方法compareByName是对象myComparisonProvider的一部分。JRE会推断出方法的参数类型。在这个例子里就是(Person, Person)。 一个类的任意对象的方法引用 如下的代码演示了一个类的任意对象的实例方法引用: 与方法引用String::compareToIgnoreCase对等的lambda表达式需要有(String a, String b)这样的参数列表。这里的a和b只是随意起的名字,只是为了描述参数。方法引用会触发这样的方法:a.compareToIgnoreCase(b)。 构造方法引用 可以使用new关键字像创建静态方法引用一样创建构造方法引用。下面的代码将一个集合中的元素拷贝到了另一个集合: 函数式接口Supplier有一个方法get。这个方法没有任何参数,只是返回一个对象: 可以使用一个lambda表达式调用transferElements : 也可以使用一个构造方法引用来替换lambda表达式: java编译器可以推断出你想创建一个HashSet集合,其中包含的元素类型是Person。当然也可以显式声明: 就这样。 参考文档 https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html #################
[阅读更多...]-
lambda表达式3 – 方法引用
-
Lambda表达式2 – 语法说明
语法说明 一个lambda表达式由如下几个部分组成: 1. 在圆括号中以逗号分隔的形参列表。在CheckPerson.test方法中包含一个参数p,代表了一个Person类的实例。注意:lambda表达式中的参数的类型是可以省略的;此外,如果只有一个参数的话连括号也是可以省略的。比如上一节曾提到的代码: 2. 箭头符号:->。用来分隔参数和函数体。 3. 函数体。由一个表达式或代码块组成。在上一节的例子中使用了这样的表达式: 如果使用的是表达式,java运行时会计算并返回表达式的值。另外,还可以选择在代码块中使用return语句: 不过return语句并不是表达式。在lambda表达式中需要将语句用花括号括起来,然而却没有必要在只是调用一个返回值为空的方法时也用花括号括起来,所以如下的写法也是正确的: lambda表达式和方法的声明看起来有很多类似的地方。所以也可以把lambda表达式视为匿名方法,也就是没有定义名字的方法。 以上提到的lambda表达式都是只使用了一个参数作为形参的表达式。下面的实例类,Caulator,演示了如何使用多个参数作为形参: 代码中operateBinary方法使用了两个整型参数执行算数操作。这里的算数操作本身就是IntegerMath接口的一个实例。在上面的程序中使用lambda表达式定义了两个算数操作:addition和subtraction。执行程序会打印如下内容: 访问外部类的局部变量 类似于局部类或匿名类,lambda表达式也可以访问外部类的局部变量。不同的是,使用lambda表达式时无需考虑覆盖之类的问题。lambda表达式只是一个词法上的概念,这意味着它不需要从超类中继承任何名称,也不会引入新的作用域。也就是说,在lambda表达式中的声明和在它的外部环境中的声明意义是一样的。在下面的例子中对此作了演示: 这段代码会输出如下内容: 如果使用示例中lambda表达式myConsumer中的参数y替换为x,编译器就会报错: 编译器报错信息是:“variable x is already defined in method methodInFirstLevel(int)”,就是说在方法methodInFirstLevel中已经定义了变量x。报错是因为lambda表达式不会引入新的作用域。也因此呢,可以在lambda表达式中直接访问外部类的域字段、方法以及形参。在这个例子中,lambda表达式myConsumer直接访问了方法methodInFirstLevel的形参x。而访问外部类的成员时也是直接使用this关键字。在这个例子中this.x指的就是FirstLevel.x。 然而,和局部类或匿名类一样,lambda表达式也只能访问局部变量或外部被声明为final(或等同于final)的成员。比如,我们将示例代码methodInFirstLevel方法中“x=99”前面的注释去掉: 因为在这段语句中修改了参数x的值,使得methodInFirstLevel的参数x不可以再被视为final式的。因此java编译器就会在lambda表达式访问局部变量x的地方报出类似“local variables referenced from a lambda expression must be final or effectively final”这样的错误。 目标类型 该如何判断lambda表达式的类型呢。再来看一下筛选适龄服兵役人员的代码: 这段代码在两处用到过: public static void printPersons(List<Person> roster, CheckPerson tester) —— 方案三 public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester) —— 方案六 在调用printPersons方法时,这个方法期望一个CheckPerson 类型的参数,此时上面的那个表达式就是一个CheckPerson 类型的表达式。在调用printPersonsWithPredicate方法时,期望一个Predicate<Person>类型的参数,此时同样的表达式就是Predicate<Person>类型的。像这样子的,由方法期望的类型来决定的类型就叫做目标类型(其实我觉得scala中的类型推断用在这里更合适)。java编译器就是通过目标类型的上下文语境或者发现lambda表达式时的位置来判断lambda表达式的类型的。这也就意味着只能在Java编译器可以推断出类型的位置使用Lambda表达式: 变量声明; 赋值; 返回语句; 数组初始化; 方法或者构造器参数; lambda表达式方法体; 条件表达式(?:); 抛出异常时。 目标类型和方法参数 对于方法参数,Java编译器还需要依赖两个语言特性来决定目标类型:重载解析和类型参数推断。 看一下下面的这两个函数式接口( java.lang.Runnable and java.util.concurrent.Callable<V>): Runnable.run()方法没有返回值,而Callable.call()方法有。 假设我们像下面这样重载了invoke方法: 那么在下面的语句中将会调用哪个方法呢: 调用的是invoke(Callable<T>),因为这个方法有返回值,而invoke(Runnable<T>)没有返回值。在这种情况下lambda表达式(() -> “done”)的类型是Callable<T>。 序列化 如果一个lambda表达式的目标类型还有它调用的参数的类型都是可序列化的,那么lambda表达式也是可序列化的。然而就像内部类一样,强烈不建议对lambda表达式进行序列化。 #########
[阅读更多...] -
Lambda表达式1 – 一个简单用例
我对java中lambda表达式的看法是相当纠结的: 一个我这么想:lambda表达式降低了java程序的阅读体验。java程序一直不以表现力出众,正相反使Java流行的一个因素正是它的安全和保守——即使是初学者只要注意些也能写出健壮且容易维护的代码来。lambda表达式对开发人员的要求相对来说高了一层,因此也增加了一些维护难度。 另一个我这么想:作为一个码代码的,有必要学习并接受语言的新特性。如果只是因为它的阅读体验差就放弃它在表现力方面的长处,那么即使是三目表达式也有人觉得理解起来困难呢。语言也是在发展的,跟不上的就自愿被丢下好了。 我不愿意被丢下。不过非让我做出选择的话,我的决定还是比较保守的:没必要一定在java语言中使用lambda——它让目前Java圈子中的很多人不习惯,会造成人力成本的上升。如果非常喜欢的话,可以考虑使用scala。 不管怎样,我还是开始试着掌握Lambda了,毕竟工作中维护的部分代码使用了Lambda(相信我,我会逐步把它去掉的)。学习的教程是在Oracla – Java官网的相关教程。 —————————— 假设目前正在创建一个社交网络应用。其中的一个特性是管理员可以对符合指定条件的会员执行某些操作,如发送消息。下面的表格详细描述了这个用例: Field 描述 名称 要执行的操作 主要参与者 管理员 前提条件 管理员登录系统 后置条件 只对符合指定标准的会员执行操作 主成功场景 1. 管理员对要执行操作的目标会员设置过滤标准;2. 管理员选择要执行的操作;3. 管理员点击提交按钮;4. 系统找到符合指定标准的会员;5. 系统对符合指定标准的会员执行预先选择的操作。 扩展 在选择执行操作前或者点击提交按钮前,管理员可以选择是否预览符合过滤标准的会员信息。 发生频率 一天中会发生许多次。 使用下面的Person类来表示社交网络中的会员信息: 假设所有的会员都保存在一个List<Person>实例中。 这一节我们从一个非常简单的方法开始,然后尝试使用局部类和匿名类进行实现,到最后会逐步深入体验Lambda表达式的强大和高效。可以在这里找到完整的代码。 方案一:一个个地创建查找符合指定标准的会员的方法 这是实现前面提到的案例最简单粗糙的方案了:就是创建几个方法、每个方法校验一项标准(比如年龄或是性别)。下面的一段代码校验了年龄大于一个指定值的情况: 这是一种很脆弱的方案,极有可能因为一点更新就导致应用无法运行。假如我们为Person类添加了新的成员变量或者更改了标准中衡量年龄的算法,就需要重写大量的代码来适应这种变化。再者,这里的限制也太过僵化了,比方说我们要打印年龄小于某个指定值的成员又该怎么做呢?再添加一个新方法printPersonsYoungerThan?这显然是一个笨方法。 方案二:创建更通用的方法 下面的方法较之printPersonsOlderThan适应性更好一些;这个方法打印了在指定年龄段内的会员信息: 此时又有新的想法了:如果我们要打印指定性别的会员信息,或者同时符合指定性别又在指定年龄段内的会员信息该怎么办呢?如果我们调整了Person类,添加了诸如友好度和地理位置这样的属性又该怎么办呢。尽管这样写方法要比printPersonsYoungerThan通用性更强一些,但是如果为每一种可能的查询都写一个方法也会导致代码的脆弱。倒不如把标准检查这一块代码给独立到一个新的类中。 方案三:在一个局部类中实现标准检查 下面的方法打印了符合检索标准的会员信息: 在程序里使用了一个CheckPerso对象tester对List参数roster中的每个实例进行校验。如果tester.test()返回true,就会执行printPerson()方法。为了设置检索标准,需要实现CheckPerson接口。 下面的这个类实现了CheckPerson并提供了test方法的具体实现。这个类中的test方法过滤了满足在美国服兵役条件的会员信息:即性别为男、且年龄在18~25岁之间。 要使用这个类,需要创建一个实例并触发printPersons方法: 现在的代码看起来不那么脆弱了——我们不需要因为Person类结构的变化而重写代码。不过这里仍有额外的代码:一个新定义的接口、为应用中每个搜索标准定义了一个内部类。 因为CheckPersonEligibleForSelectiveService实现了一个接口,所以可以使用匿名类,而不需要再为每种标准分别定义一个内部类。 方案四:使用匿名类实现标准检查 下面调用的printPersons方法中的一个参数是匿名类。这个匿名类的作用和方案三中的CheckPersonEligibleForSelectiveService类一样:都是过滤性别为男且年龄在18和25岁之间的会员。 这个方案减少了编码量,因为不再需要为每个要执行的检索方案创建新类。不过这样做仍让人有些不舒服:尽管CheckPerson接口只有一个方法,实现的匿名类仍是有些冗长笨重。此时可以使用Lambda表达式替换匿名类,下面会说下如何使用Lambda表达式替换匿名类。 方案五:使用Lambda表达式实现标准检查 CheckPerson接口是一个函数式接口。所谓的函数式接口就是指任何只包含一个抽象方法的接口。(一个函数式接口中也可以有多个default方法或静态方法)。既然函数式接口中只有一个抽象方法,那么在实现这个函数式接口的方法的时候可以省略掉方法的方法名。为了实现这个想法,可以使用Lambda表达式替换匿名类表达式。在下面重写的printPersons方法中,相关的代码做了高亮处理: 在这里还可以使用一个标准的函数接口来替换CheckPerson接口,从而进一步简化代码。 方案六:在Lambda表达式中使用标准函数式接口 再来看一下CheckPerson接口: 这是一个非常简单的接口。因为只有一个抽象方法,所以它也是一个函数式接口。这个抽象方法只接受一个参数并返回一个boolean值。这个抽象接口太过简单了,以至于我们会考虑是否有必要在应用中定义一个这样的接口。此时可以考虑使用JDK定义的标准函数式接口,可以在java.util.function包下找到这些接口。 在这个例子中,我们就可以使用Predicate<T>接口来替换CheckPerson。在这个接口中有一个boolean test(T t)方法: Predicate<T>接口是一个泛型接口。泛型类(或者是泛型接口)使用一对尖括号(<>)指定了一个或多个类型参数。在这个接口中只有一个类型参数。在使用具体类声明或实例化一个泛型类时,就会获得一个参数化类。比如说参数化类Predicate<Person>就是这样的: 在这个参数化类中有一个与CheckPerson.boolean test(Person p)方法的参数和返回值都一致的方法。因此就可以同如下方法所演示的一样使用Predicate<T>接口来替换CheckPerson接口: 然后使用下面的代码就可以像方案三中一样筛选适龄服兵役的会员了: 有没有注意到,这里使用Predicate<Person>作为参数类型时并没有显式指定参数类型。这里并不是唯一适用lambda表达式的地方,下面的方案会介绍更多lambda表达式的用法。 方案七:在整个应用中使用lambda表达式 再来看一下printPersonsWithPredicate 方法,考虑是否可以在这里使用lambda表达式: 在这个方法中使用Predicate实例tester检查了roster中的每个Person实例。如果Person实例符合tester中定义的检查标准,将会触发Person实例的printPerson方法。 除了触发printPerson方法,满足tester标准的Person实例还可以执行其他的方法。可以考虑使用lambda表达式指定要执行的方法(私以为这个特性很好,解决了java中方法不能作为对象传递的问题)。现在需要一个类似printPerson方法的lambda表达式——一个只需要一个参数且返回为void的lambda表达式。记住一点:要使用lambda表达式,需要先实现一个函数式接口。在这个例子中需要一个函数式接口,其中只包含一个抽象方法,这个抽象方法有个类型为Person的参数,且返回为void。可以看一下JDK提供的标准函数式接口Consumer<T>,它有一个抽象方法void accept(T t)正好满足这个要求。在下面的代码中使用一个Consumer<T>的实例调用accept方法替换了p.printPerson(): 对应这里,可以使用如下代码筛选适龄服兵役的会员: 如果我们想做的事情不只是打印会员信息,而是更多的事情,比如验证会员身份、获取会员联系方式等等。此时,我们需要一个有返回值方法的函数式接口。JDK的标准函数式接口Function<T,R>就有一个这样的方法R apply(T t)。下面的方法从参数mapper中获取数据,并在这些数据上执行参数block指定的行为: 下面的代码获取了roster中适龄服兵役的所有会员的邮箱信息并打印出来: 方案八:多使用泛型 再来回顾一下processPersonsWithFunction方法。下面是这个方法的泛型版本,新方法在参数类型上要求更为宽容: 要打印适龄服兵役的会员信息可以像下面这样调用processElements方法: 在方法的调用过程中执行了如下行为: 从一个集合中获取对象信息,在这个例子里是从集合实例roster中获取Person对象信息。 过滤能够匹配Predicate实例tester的对象。在这个例子里,Predicate对象是一个lambda表达式,它指定了过滤适龄服兵役的条件。 将过滤后的对象交给一个Function对象mapper处理,mapper会为这个对象匹配一个值。在这个例子中Function对象mapper是一个lambda表达式,它返回了每个会员的邮箱地址。 由Consumer对象block为mapper匹配的值指定一个行为。在这个例子里,Consumer对象是一个lambda表达式,它的作用是打印一个字符串,也就是Function实例mapper返回的会员邮件地址。 方案九:使用将lambda表达式作为参数的聚集操作 下面的代码中使用了聚集操作来打印roster集合中适龄服兵役会员的邮件地址: 分析下如上代码的执行过程,整理如下表: 行为 聚集操作 获取对象 Stream<E> stream() 过滤匹配Predicate实例指定标准的对象 Stream<T> filter(Predicate<? super T> predicate) 通过一个Function实例获取对象匹配的值 <R> Stream<R> map(Function<? super T,? extends R> mapper) 执行Consumer实例指定的行为 void forEach(Consumer<? super T> action) 表中的filter、map和forEach操作都是聚集操作。聚集操作处理的元素来自Stream,而非是直接从集合中获取(就是因为这示例程序中调用的第一个方法是stream())。Stream是一个数据序列。和集合不同,Stream并没有用特定的结构存储数据。相反的,Stream从一个特定的源获取数据,比如从集合获取数据,通过一个pipeline。pipeline是一个Stream操作序列,在这个例子中就是filter-map-forEach。此外,聚集操作通常采用lambda表达式作为参数,这也给了我们许多自定义的空间。 ########
[阅读更多...]