Lambda管中窥豹

  • 可以把Lambda表达式理解为简洁的表示为可传递的匿名函数的一种方式:其没有名称,但是有参数列表、函数主体、返回类型,可能还有一个可抛出的异常列表。
  • 匿名:匿名指的是,他不像普通方法一样有一个具体的名称
  • 函数:我们说它是函数,是因为Lambda函数不向方法那样属于某个特定的类,但是其和方法一样,Lambda但是有参数列表、函数主体、返回类型,可能还有一个可抛出的异常列表。
  • 传递:Lambda表达式可以作为参数传递给方法或者存储在变量中。
  • 简洁:无需像匿名类那样写很多模板代码。
  • 为什么需要关心Lambda表达式?在上一篇中,我们看到在Java中传递代码十分繁琐和冗长,我们也尝试使用Lambda表达式来表示同样的含义。
  • Lambda表达式:它可以让你十分简明地传递代码。
  • 比如上篇的苹果排序的例子 ```java public class Test8 { public static void main(String[] args) {
    1. AppleClient.getApples().sort(new Comparator<Apple>() {
    2. @Override
    3. public int compare(Apple o1, Apple o2) {
    4. return o1.getWeight() - o2.getWeight();
    5. }
    6. });
    } }
  1. - 使用Lambda之后
  2. ```java
  3. public class Test8 {
  4. public static void main(String[] args) {
  5. AppleClient.getApples().sort((Apple o1, Apple o2) -> o1.getWeight() - o2.getWeight());
  6. }
  7. }
  • 代码变得更清晰了,但是可能觉得Lambda怎么变成这样的不好理解。下面我们会描述。
  • 但是值得注意的是,现在上面的代码,只是传递了比较两个苹果重量所真正需要的代码,看起来就像是只传递了compare方法的主体。
  • Lambda表达式的组成部分

image.png

  • 参数列表:这里它采用了Comparator中compare方法的参数,两个Apple。
  • 箭头:箭头->把参数列表与Lambda主体分隔开。
  • Lambda主体:比较两个Apple的重量。表达式就是Lambda的返回值了。
  • Lambda的语法很简单。以下提供一些使用的案例 | 使用案例 | Lambda示例 | | —- | —- | | 布尔表达式 | (List list) -> list.isEmpty() | | 创建对象 | () -> new Apple(10) | | 消费一个对象 | (Apple a) -> {System.out.println(a);} | | 从一个对象中选择/抽取 | (String s) -> s.length() | | 组合两个值 | (int a, int b) -> a * b | | 比较两个对象 | (Apple o1, Apple o2) -> o1.getWeight() - o2.getWeight() |

在哪里以及如何使用Lambda

  • 在之前的例子中,使用Lambda表达式赋给了一个Comparator类型的变量,当然,也可以在上一篇的filter方法中使用Lambda表达式。
  • 所以到底在哪里可以使用呢?可以在函数式接口上使用Lambda表达式,但是什么是函数式接口呢?
  • 函数式接口:只定义一个抽象方法的接口
  • 例如如下的函数式接口

    1. @FunctionalInterface
    2. public interface Comparator<T> {
    3. int compare(T o1, T o2);
    4. }
    1. @FunctionalInterface
    2. public interface Runnable {
    3. public abstract void run();
    4. }
    1. @FunctionalInterface
    2. public interface Callable<V> {
    3. V call() throws Exception;
    4. }
  • 前面几篇谈到了接口还可以有默认方法(即在类没有对方法进行实现时,其主体为方法提供默认实现的方法)。哪怕有很多默认方法,只要定义了一个抽象方法,它就是函数式接口。

  • 函数式接口可以干什么呢?
  • Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法实现,具体的说就是,是函数式接口一个具体实现的实例。使用匿名内部类也可以完成这种操作,但是比较笨拙:需要提供一个实现,然后在直接内联将其实例化。内联可以理解为匿名。 ```java public class Test9 { public static void main(String[] args) {

    1. Runnable r1 = () -> System.out.println("Test9.main");
    2. Runnable r2 = new Runnable() {
    3. @Override
    4. public void run() {
    5. System.out.println("Test9.run");
    6. }
    7. };
    8. process(r1);
    9. process(r2);
    10. process(() -> System.out.println("Test9.main"));

    }

    private static void process(Runnable r) {

    1. r.run();

    } }

  1. - **函数描述符**
  2. - 函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫作函数描述符。
  3. - 比如Runnable接口可以看作为一个什么也不接收也不返回的函数的前面,其只有一个run抽象方法,这个方法什么也不接受,什么也不返回。
  4. - 在上面的例子中,使用了 `()-> System.out.println("Test9.run")` 来表示参数列表为空,且返回`void`因为只是打印一句话,没有返回值。
  5. - 在举个例子,`(Apple,Apple) -> int`表示接受2Apple参数,并且返回一个`int`类型参数。
  6. - 一些常见的函数式标识符和函数式接口可参见下表
  7. | **函数式接口** | **函数描述符** | **原始类型特化** |
  8. | --- | --- | --- |
  9. | Predicate<T> | T->Boolean | IntPredicate<br />LongPredicate<br />DoublePredicate |
  10. | Consumer<T> | T->void | IntConsumer<br />LongConsumer<br />DoubleConsumer |
  11. | Function<T,R> | T->R | IntFunction<R><br />IntToDoubleFunction<br />IntToLongFunction<br />LongFunction<R><br />LongToDoubleFunction<br />LongToIntFunction<br />DoubleFunction<R><br />DoubleToIntFunction<br />DoubleToLongFunction<br />ToIntFunction<br />ToDoubleFunction<br />ToLongFunction |
  12. | Supplier<T> | ()->T | BooleanSupplier<br />IntSupplier<br />LongSupplier<br />DoubleSupplier |
  13. | UnaryOperator<T> | T->T | IntUnaryOperator<br />LongUnaryOperator<br />DoubleUnaryOperator |
  14. | BinaryOperator<T> | (T,T)->T | IntBinaryOperator<br />LongBinaryOperator<br />DoubleBinaryOperator |
  15. | BiPredicate<L,R> | (L,R)->Boolean | / |
  16. | BiConsumer<T,U> | (T,U)->void | ObjIntConsumer<T><br />ObjLongConsumer<T><br />ObjDoubleConsumer<T> |
  17. | BiFunction<T,U,R> | (T,U)->R | ToIntBiFunction<T,U><br />ToLongBiFunction<T,U><br />ToDoubleBiFunction<T,U> |
  18. - `@FunctionalInterface`是什么意思?其是一个注解,标记当前接口是函数式接口,并且在强大的编译器中会对方法进行检查。如果你用@FunctionalInterface定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。
  19. <a name="vgScJ"></a>
  20. # 环绕执行模式
  21. - 资源处理(例如处理文件或者是数据库)时一个常见的模式就是打开一个资源,做一些处理,然后关闭资源。这个设置和清理阶段总是相似,并且会环绕这执行处理的那些重要代码:环绕指的是开闭资源操作把执行业务代码做夹心。
  22. - 如下所示
  23. ```java
  24. public class Test10 {
  25. public static String processFile() throws IOException {
  26. try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
  27. return br.readLine();
  28. }
  29. }
  30. }
  • 第1步:行为参数化
  • 现在这段代码是有局限的,只能读取文件的第一行。如果需要返回前几行,怎么处理呢?
  • 在理想的情况下,需要重新执行和清理代码。并告诉processFile执行不同的操作,是不是行为。不同操作指的就是不同的行为,所以此处需要将行为参数化。
  • 而传递行为,则是Lambda擅长的事情,读2行代码如下,我们希望这样做

    1. public class Test10 {
    2. public static void main(String[] args) {
    3. String s = processFile((BufferedReader br) -> br.readLine() + br.readLine());
    4. }
    5. }
  • 第2步:使用函数式接口来传递行为

  • 定义函数式接口如下 ```java @FunctionalInterface public interface BufferReaderProcessor { String process(BufferedReader b) throws IOException; }
  1. - 修改之前的代码
  2. ```java
  3. public static String processFile(BufferReaderProcessor p) throws IOException {
  4. }
  • 第3步:执行一个行为

    1. public static String processFile(BufferReaderProcessor p) throws IOException {
    2. try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
    3. return p.process(br);
    4. }
    5. }
  • 第4步:传递Lambda

    1. public class Test10 {
    2. public static void main(String[] args) throws IOException {
    3. String s = processFile((BufferedReader br) -> br.readLine() + br.readLine());
    4. }
    5. public static String processFile(BufferReaderProcessor p) throws IOException {
    6. try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
    7. return p.process(br);
    8. }
    9. }
    10. }
  • 上述就完成了,是不是比较happy

    使用函数式接口

  • Predicate:java.util.function.Predicate 定义了一个test抽象方法,接受泛型T,返回Boolean,当你需要判断某个参数的时候,就可以使用它。

  • Consumer:java.util.function.Consumer 定义了一个accept抽象方法,接受泛型T,没有返回值,当你需要对某个对象做什么事情的时候,就可使用它。
  • Function:java.util.function.Consumer 定义了一个apply抽象方法,接受泛型T,返回R,当你需要对某个对象T做什么事情并且返回R,就可使用它。
  • 原始类型特化
  • Java类型要么是引用类型(比如Byte、Integer、Object、List),要么是原始类型(比如int、double、byte、char)。但是泛型(比如Consumer中的T)只能绑定到引用类型。这是由泛型内部的实现方式造成的。
  • [插图]因此,在Java里有一个将原始类型转换为对应的引用类型的机制。这个机制叫作装箱(boxing)。相反的操作,也就是将引用类型转换为对应的原始类型,叫作拆箱(unboxing)。Java还有一个自动装箱机制来帮助程序员执行这一任务:装箱和拆箱操作是自动完成的。
  • 但是装箱和拆箱是要耗费性能的。装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。
  • Java 8为我们前面所说的函数式接口带来了一个专门的版本,以便在输入和输出都是原始类型时避免自动装箱的操作。比如,在下面的代码中,使用IntPredicate就避免了对值1000进行装箱操作,但要是用Predicate就会把参数1000装箱到一个Integer对象中

    1. public class PredicateTest {
    2. public static void main(String[] args) {
    3. IntPredicate predicate = (int i) -> i % 2 == 0;
    4. Predicate<Integer> predicate2 = (Integer i) -> i % 2 == 0;
    5. }
    6. }
  • 一般来说,针对专门的输入参数类型的函数式接口的名称都要加上对应的原始类型前缀,比如DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction等。Function接口还有针对输出参数类型的变种:ToIntFunction、IntToDoubleFunction等。

  • 案例 | 使用案例 | Lambda的例子 | 对应的函数式接口 | | —- | —- | —- | | 布尔表达式 | (List list) -> list.isEmpty() | Predicate> | | 创建对象 | () -> new Apple(10) | Supplier | | 消费一个对象 | (Apple a) -> System.out.println(a) | Consumer | | 从一个对象中选择/提取 | (String s) -> s.length() | Function
    ToIntFunction | | 合并两个值 | (int a, int b) -> a * b | IntBinaryOperator | | 比较两个对象 | Apple o1, Apple o2) -> o1.getWeight() - o2.getWeight() | Consumer
    BiFunction
    ToIntBiFunction |

  • 异常、Lambda:请注意,任何函数式接口都不允许抛出受检异常(checked exception)。如果你需要Lambda表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda包在一个try/catch块中。

  • 但是尽量不要有异常,因为会破坏优雅Lambda的编写。

    类型检查、类型推断和限制

  • Lambda的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型。

  • 举例如下

image.png

  • 类型检查过程如下
    • 首先,找到filter方法的声明
    • 其二,要求它是Predicate (目标类型)对象的第二个正式参数
    • 其三,Predicate是一个函数式接口,定义了一个叫作test的抽象方法
    • 第四,test方法描述了一个函数描述符,它可以接受一个Apple,并返回一个boolean
    • 最后,filter的任何实际参数都必须匹配这个要求
    • 注意:如果Lambda表达式抛出一个异常,那么抽象方法所声明的throws语句也必须与之匹配
  • 同样的Lambda,不同的函数式接口:有了目标类型的概念,同一个Lambda表达式就可以与不同的函数式接口联系起来,只要它们的抽象方法签名能够兼容。 ```java public class Test1 { public static void main(String[] args) {
    1. Callable<Integer> c = () -> 42;
    2. PrivilegedAction<Integer> p = () -> 42;
    } }
  1. - 第一个赋值的目标类型是Callable<Integer>,第二个赋值的目标类型是PrivilegedAction<Integer>
  2. - 特殊的void兼容规则,如果一个Lambda的主体是一个语句表达式,它就和一个返回void的函数描述符兼容(当然需要参数列表也兼容)。例如,以下两行都是合法的,尽管Listadd方法返回了一个boolean,而不是Consumer上下文(T-> void)所要求的void
  3. ```java
  4. public class Test2 {
  5. public static void main(String[] args) {
  6. List<String> list = new ArrayList<>();
  7. Predicate<String> p = s -> list.add(s);
  8. Consumer<String> c = s -> list.add(s);
  9. }
  10. }
  • 类型推断:我们可以进一步简化代码,Java编译器会从上下文(目标类型)推断出用什么函数式接口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的签名,因为函数描述符可以通过目标类型来得到。这样做的好处在于,编译器可以了解Lambda表达式的参数类型,这样就可以在Lambda语法中省去标注参数类型。 ```java public class Test8 { public static void main(String[] args) {
    1. AppleClient.getApples().sort((Apple o1, Apple o2) -> o1.getWeight() - o2.getWeight());
    2. AppleClient.getApples().sort((o1, o2) -> o1.getWeight() - o2.getWeight());
    } }
  1. - 有的时候显式写出类型更容易阅读,有时候去掉更好。
  2. - **使用局部变量**
  3. - 我们迄今为止所介绍的所有Lambda表达式都只用到了其主体里面的参数。但Lambda表达式也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。它们被称作捕获Lambda。例如,下面的Lambda捕获了portNumber变量
  4. ```java
  5. public class Test3 {
  6. public static void main(String[] args) {
  7. int portNumber = 1337;
  8. Runnable runnable = () -> System.out.println(portNumber);
  9. }
  10. }
  • 但是会有局限性:Lambda可以没有限制地捕获(也就是在其主体中引用)实例变量和静态变量。但局部变量必须显式声明为final,或事实上是final。
  • 对局部变量的限制:你可能会问自己,为什么局部变量有这些限制。第一,实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此,Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了这个限制。
  • 闭包:用科学的说法来说,闭包就是一个函数的实例,且它可以无限制地访问那个函数的非本地变量。例如,闭包可以作为参数传递给另一个函数。它也可以访问和修改其作用域之外的变量。现在,Java 8的Lambda和匿名类可以做类似于闭包的事情:它们可以作为参数传递给方法,并且可以访问其作用域之外的变量。但有一个限制:它们不能修改定义Lambda的方法的局部变量的内容。这些变量必须是隐式最终的。可以认为Lambda是对值封闭,而不是对变量封闭。

    方法引用

  • 有关方法引用,在之前已经谈及,这里将其CV一下

    使用场景

  • 我们用Lambda表达式来实现匿名方法,但是有些情况下,我们使用Lambda表达式仅仅是调用一些已经存在的方法,除了调用动作之外,没有其他任何多余的动作,在这种情况下,我们更倾向于通过方法名来调用它,而Lambda表达式可以帮助我们实现这一要求,它使得Lambda在调用那些已经拥有方法名的代码更加简洁、更容易理解。方法引用可以理解为Lambda表达式的另外一种表现形式。

    方法引用的分类

    | 类型 | 语法 | 对应的Lambda表达式 | | —- | —- | —- | | 静态方法引用 | 类名::staticMethod | (args) -> 类名.staticMethod(args) | | 实例方法引用 | inst::instMethod | (args) -> inst.instMethod(args) | | 对象方法引用 | 类名::instMethod | (inst,args) -> 类名.instMethod(args) | | 构建方法引用 | 类名::new | (args) -> new 类名(args) |

方法引用使用举例

静态方法引用

  • 类名::静态方法
  • Person类 ```java @Data public class Person {

    private String name;

    private Integer age;

    public static int compareByAge(Person a, Person b) {

    1. return a.age.compareTo(b.age);

    } }

class PersonAgeComparator implements Comparator { public int compare(Person a, Person b) { return a.getAge().compareTo(b.getAge()); } }

  1. - 现假设,一个部门有30人,把他们放在一个数组中,并按照年龄排序,可以自顶一个比较器,如上
  2. ```java
  3. public class Client {
  4. public static void main(String[] args) {
  5. Random random = new Random();
  6. Person[] personList = new Person[30];
  7. for (int i = 0; i < 30; i++) {
  8. Person person = new Person();
  9. person.setName(String.valueOf(i));
  10. person.setAge(random.nextInt(40));
  11. personList[i] = person;
  12. }
  13. Arrays.sort(personList, new PersonAgeComparator());
  14. for (Person person : personList) {
  15. System.out.println(person);
  16. }
  17. }
  18. }
  • Arrays.sort的声明为:public static void sort(T[] a, Comparator<? super T> c),比较器参数Comparator为一个函数式接口,利用上一节Lambda表达式所学知识,可以改写为以下代码 ```java public class Client { public static void main(String[] args) {
    1. Random random = new Random();
    2. Person[] personList = new Person[30];
    3. for (int i = 0; i < 30; i++) {
    4. Person person = new Person();
    5. person.setName(String.valueOf(i));
    6. person.setAge(random.nextInt(40));
    7. personList[i] = person;
    8. }
    9. Arrays.sort(personList, (a, b) -> {
    10. return a.getAge().compareTo(b.getAge());
    11. });
    12. for (Person person : personList) {
    13. System.out.println(person);
    14. }
    } }
  1. - 然而,你会发现,Perdon类中已经有了一个静态方法的比较器:compareByAge,因此,我们改用Person类已经提供的比较器
  2. ```java
  3. public class Client {
  4. public static void main(String[] args) {
  5. Random random = new Random();
  6. Person[] personList = new Person[30];
  7. for (int i = 0; i < 30; i++) {
  8. Person person = new Person();
  9. person.setName(String.valueOf(i));
  10. person.setAge(random.nextInt(40));
  11. personList[i] = person;
  12. }
  13. Arrays.sort(personList, (a,b) -> Person.compareByAge(a,b));
  14. for (Person person : personList) {
  15. System.out.println(person);
  16. }
  17. }
  18. }
  • 以上代码,因为Lambda表达式调用了一个已经存在的静态方法,根据我们第2节表格中的语法,上面的代码可以最终改写成静态方法引用 ```java public class Client { public static void main(String[] args) {
    1. Random random = new Random();
    2. Person[] personList = new Person[30];
    3. for (int i = 0; i < 30; i++) {
    4. Person person = new Person();
    5. person.setName(String.valueOf(i));
    6. person.setAge(random.nextInt(40));
    7. personList[i] = person;
    8. }
    9. Arrays.sort(personList, Person::compareByAge);
    10. for (Person person : personList) {
    11. System.out.println(person);
    12. }
    } }
  1. <a name="PbNoa"></a>
  2. ### 实例方法引用
  3. - **实例::非静态方法**
  4. - 实例方法引用,顾名思义就是调用已经存在的实例的方法,与静态方法引用不同的是类要先实例化,静态方法引用类无需实例化,直接用类名去调用。
  5. ```java
  6. public class TestInstanceReference {
  7. public static void main(String[] args) {
  8. Person person = new Person();
  9. person.setName("icanci");
  10. person.setAge(18);
  11. Supplier<String> supplier = () -> person.getName();
  12. System.out.println(supplier.get());
  13. Supplier<String> supplier2 = person::getName;
  14. System.out.println(supplier2.get());
  15. }
  16. }

对象方法引用

  • 实例::静态方法
  • 若Lambda参数列表中的第一个参数是实例方法的参数调用者,而第二个参数是实例方法的参数时,可以使用对象方法引用。String的equals()方法
    1. public boolean equals(Object anObject) {
    2. if (this == anObject) {
    3. return true;
    4. }
    5. if (anObject instanceof String) {
    6. String anotherString = (String)anObject;
    7. int n = value.length;
    8. if (n == anotherString.value.length) {
    9. char v1[] = value;
    10. char v2[] = anotherString.value;
    11. int i = 0;
    12. while (n-- != 0) {
    13. if (v1[i] != v2[i])
    14. return false;
    15. i++;
    16. }
    17. return true;
    18. }
    19. }
    20. return false;
    21. }
    ```java public class StringTest { public static void main(String[] args) {
    1. BiPredicate<String, String> bp = (x, y) -> x.equals(y);
    2. BiPredicate<String, String> bp1 = String::equals;
    3. boolean test = bp1.test("xy", "xx");
    4. System.out.println(test);
    } }
  1. - BiPredicatetest()方法接受两个参数,xy,具体实现为x.equals(y),满足Lambda参数列表中的第一个参数是实例方法的参数调用者,而第二个参数是实例方法的参数,因此可以使用对象方法引用。
  2. <a name="MG5qL"></a>
  3. ### 构造方法的引用
  4. - **类名::new**
  5. - 注意:需要调用的构造器的参数列表要与函数式接口中抽象方法的参数列表保持一致
  6. - 如:要获取一个空的Person列表
  7. ```java
  8. public class Client2 {
  9. public static void main(String[] args) {
  10. Supplier<List<Person>> supplier = () -> new ArrayList<Person>();
  11. List<Person> personList = supplier.get();
  12. Supplier<List<Person>> supplier2 = ArrayList<Person>::new;
  13. List<Person> personList1 = supplier2.get();
  14. }
  15. }

转换案例

Lambda 等效的方法引用
(Apple a) -> a.getWeight() Apple::getWeight
(str, i) -> str.subString(i) String::subString
(String s) -> System.out.println(s) System.out::println

方法引用实战

  • 还是以苹果为例,最终的需要的结果如下 apples.sort(comparing(Apple::getWeight));
  • 第1步:传递代码
  • Java 8的API已经为你提供了一个List可用的sort方法,你不用自己去实现它。那么最困难的部分已经搞定了!但是,如何把排序策略传递给sort方法呢?你看,sort方法的签名是这样的:default void sort(Comparator<? super E> c)
  • 你的第一个解决方案看上去是这样的 ```java public class AppleComparator implements Comparator { @Override public int compare(Apple o1, Apple o2) {
    1. return o1.getWeight() - o2.getWeight();
    } }
  1. ```java
  2. public class Test4 {
  3. public static void main(String[] args) {
  4. List<Apple> apples = AppleClient.getApples();
  5. apples.sort(new AppleComparator());
  6. }
  7. }
  • 第2步:使用匿名类 ```java public class Test4 { public static void main(String[] args) {
    1. List<Apple> apples = AppleClient.getApples();
    2. apples.sort(new Comparator<Apple>() {
    3. @Override
    4. public int compare(Apple o1, Apple o2) {
    5. return o1.getWeight() - o2.getWeight();
    6. }
    7. });
    } }
  1. - **第3步:使用Lambda表达式**
  2. ```java
  3. public class Test4 {
  4. public static void main(String[] args) {
  5. List<Apple> apples = AppleClient.getApples();
  6. apples.sort((o1, o2) -> o1.getWeight() - o2.getWeight());
  7. }
  8. }
  • 第4步:还能再简单一点吗?

    1. public class Test4 {
    2. public static void main(String[] args) {
    3. List<Apple> apples = AppleClient.getApples();
    4. Comparator<Apple> comparing = Comparator.comparing((Apple a) -> a.getWeight());
    5. apples.sort(comparing);
    6. }
    7. }
  • 还能再简单一点吗?

    1. public class Test4 {
    2. public static void main(String[] args) {
    3. List<Apple> apples = AppleClient.getApples();
    4. apples.sort(Comparator.comparing((Apple a) -> a.getWeight()));
    5. }
    6. }
  • 第5步:使用方法引用

    1. public class Test4 {
    2. public static void main(String[] args) {
    3. List<Apple> apples = AppleClient.getApples();
    4. apples.sort(Comparator.comparing(Apple::getWeight));
    5. }
    6. }

    Lambda复合

  • Java 8的好几个函数式接口都有为方便而设计的方法。具体而言,许多函数式接口,比如用于传递Lambda表达式的Comparator、Function和Predicate都提供了允许你进行复合的方法。这是什么意思呢?在实践中,这意味着你可以把多个简单的Lambda复合成复杂的表达式。比如,你可以让两个谓词之间做一个or操作,组合成一个更大的谓词。

  • 比较器复合

    1. public class Test4 {
    2. public static void main(String[] args) {
    3. List<Apple> apples = AppleClient.getApples();
    4. // 逆序
    5. apples.sort(Comparator.comparing(Apple::getWeight).reversed());
    6. }
    7. }
  • 比较器链:先根据weight倒序排,然后weight一样的使用color排序 ```java public class Test4 { public static void main(String[] args) {

    1. List<Apple> apples = AppleClient.getApples();
    2. apples.sort(Comparator.comparing(Apple::getWeight).reversed().thenComparing(Apple::getColor));

    } }

  1. - **谓词复合**
  2. - **使用negate构造Predicate的非**
  3. ```java
  4. public class Test5 {
  5. public static void main(String[] args) {
  6. Predicate<Apple> applePredicate = (a) -> a.getWeight() > 120;
  7. applePredicate.negate();
  8. }
  9. }
  • 你可能想要把两个Lambda用and方法组合起来,比如一个苹果既是红色又比较重 ```java public class Test6 { public static void main(String[] args) {
    1. Predicate<Apple> applePredicate = (a) -> a.getWeight() > 120;
    2. Predicate<Apple> red = applePredicate.and(o -> o.getColor().equals("red"));
    } }
  1. - 你可以进一步组合谓词,表达要么是重的红苹果,要么是绿苹果
  2. ```java
  3. public class Test7 {
  4. public static void main(String[] args) {
  5. Predicate<Apple> red = o -> "red".equals(o.getColor());
  6. Predicate<Apple> res = red.and(o -> o.getWeight() > 150).or(o -> "green".equals(o.getColor()));
  7. }
  8. }
  • 请注意,and和or方法是按照在表达式链中的位置,从左向右确定优先级的。因此,a.or(b).and(c)可以看作(a || b) && c
  • 函数复合
  • 可以把Function接口所代表的Lambda表达式复合起来。Function接口为此配了andThen和compose两个默认方法,它们都会返回Function的一个实例
  • andThen方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。比如,假设有一个函数f给数字加1 (x-> x+1),另一个函数g给数字乘2,你可以将它们组合成一个函数h,先给数字加1,再给结果乘2
  • 下面案例类似:g(f(x)) ```java public class Test8 { public static void main(String[] args) {
    1. Function<Integer, Integer> f = x -> x + 1;
    2. Function<Integer, Integer> g = x -> x * 1;
    3. Function<Integer, Integer> h = f.andThen(g);
    4. Integer apply = h.apply(1); // 4
    } }
  1. - 你也可以类似地使用compose方法,先把给定的函数用作compose的参数里面给的那个函数,然后再把函数本身用于结果。比如在上一个例子里用compose的话,它将意味着f(g(x)),而andThen则意味着g(f(x))
  2. ```java
  3. public class Test8 {
  4. public static void main(String[] args) {
  5. Function<Integer, Integer> f = x -> x + 1;
  6. Function<Integer, Integer> g = x -> x * 1;
  7. Function<Integer, Integer> h = f.compose(g);
  8. Integer apply = h.apply(1); // 3
  9. }
  10. }
  • 总结如下

image.png

小结

  • Lambda表达式可以理解为一种匿名函数:它没有名称,但有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常的列表
  • Lambda表达式让你可以简洁地传递代码
  • 只有在接受函数式接口的地方才可以使用Lambda表达式
  • Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例
  • Java 8自带一些常用的函数式接口,放在java.util.function包里,包括Predicate、Function、Supplier、Consumer和BinaryOperator
  • 为了避免装箱操作,对Predicate和Function等通用函数式接口的原始类型特化:IntPredicate、IntToLongFunction等
  • 环绕执行模式(即在方法所必需的代码中间,你需要执行点儿什么操作,比如资源分配和清理)可以配合Lambda提高灵活性和可重用性
  • Lambda表达式所需要代表的类型称为目标类型
  • 方法引用让你重复使用现有的方法实现并直接传递它们
  • Comparator、Predicate和Function等函数式接口都有几个可以用来结合Lambda表达式的默认方法

    参考文章

  • 《Java 8 in Action》

  • 《Java8函数式编程》