创建并使用 Lambda 表达式

什么是 lambda 表达式?

  • 传统
List<Car> compactCars = findCars(cars,
     new Searchable() {
        public boolean test(Car car) {
           return car.getType().equals(
                     CarTypes.COMPACT);
        }
});
  • Java8引入了一种符号,可以:
List compactCars = findCars(cars,
     (Car c) ->
        c.getType().equals(CarTypes.COMPACT)
);

image.png
使用 lambdas,允许你使用一种更简单的方式来编程,这种方式叫做函数式编程,这是一种不同于面向对象程序设计的范式

Lambda 表达式的组成

(Object arg1, int arg2) -> arg1.equals(arg2);

参数列表

  • 一个 lambda 表达式可以有零个,一个或多个参数:
() -> System.out.println("Hi");
(String s) -> System.out.println(s);
(String s1, String s2) -> System.out.println(s1 + s2);
  • 参数的类型可以显式声明,也可以从上下文中推断出来
(s) -> System.out.println(s);
  • 如果只有一个参数,类型就会被推断出来,并且不强制使用括号
s -> System.out.println(s);
// This doesn't compile
String s = ""; s -> System.out.println(s);

箭头->

用于分离参数和主体代码块

代码块

  • Lambda 表达式的主体可以包含一个或多个语句
  • 如果正文有一个语句,则不需要大括号,并返回表达式的值(如果有的话)
() -> 4; (int a) -> a*6;
  • 如果主体包含多个语句,则需要使用大括号,如果表达式返回一个值,则必须返回一个 return 语句
() -> {
     System.out.println("Hi");
     return 4;
}
(int a) -> {
     System.out.println(a);
     return a*6;
}
  • 如果 lambda 表达式没有返回结果,返回语句是可选的,和void方法一样,使用return表示退出
() -> System.out.println("Hi");
() -> {
     System.out.println("Hi");
     return;
}

函数式接口和Lambda表达式关系

  • 函数接口的抽象方法的签名提供了 lambda 表达式的签名(这个签名称为函数描述符)。要使用 lambda 表达式,首先需要一个函数接口
interface Searchable {
     boolean test(Car car);
}
  • 可以创建一个 lambda 表达式,以 Car 对象作为参数并返回一个布尔值
Searchable s = (Car c) -> c.getCostUSD() > 20000;

编译器推断 lambda 表达式可以被分配给 Searchable 接口,仅仅通过它的签名。lambda 表达式并不包含它们正在实现的函数接口的信息。表达式的类型是从使用 lambda 的上下文中推导出来的。当 lambda 表达式被分配给 Searchable 接口时,lambda 必须获取其抽象方法的签名。

  • 使用 lambda 作为方法的参数,编译器将使用方法的定义来推断表达式的类型
class Test {
     public void find() {
         find(c -> c.getCostUSD() > 20000);
     }
     private void find(Searchable s) {
         // Here goes the implementation
     }
}
  • 如果相同的 lambda 表达式具有兼容的抽象方法签名,那么它们可以与不同的函数接口关联。例如:
interface Searchable {
     boolean test(Car car);
}
interface Saleable {
     boolean approve(Car car);
}
//...
Searchable s1 = c -> c.getCostUSD() > 20000;
Saleable s2 = c -> c.getCostUSD() > 20000;
  • Lambda 表达式是一个函数。这和我们知道的方法有点不同,因为它实际上是和一个数学函数相关的。
  • 一个数学函数接受一些输入并产生一些输出,只要我们用相同的参数调用它,它总是返回相同的结果。
  • 可以将 lambda 表达式看作匿名方法(或函数) ,因为它们没有名称,就像匿名类一样

Lambda 表达式与匿名类

表达式是匿名类的一个替代选择,但它们是不一样的

相似

  • 局部变量(方法中定义的变量或参数)只有在声明为最终变量或实际为最终变量时才能使用
  • 可以访问封闭类的实例或静态变量
  • 它们不能抛出比函数接口方法的 throws 子句中指定的更多检查过的异常。只有相同的类型或子类型

区别

  • 对于匿名类,this关键字解析为匿名类本身。对于lambda表达式,它解析为写入lambda的封闭类。
  • 不能从lambda表达式中访问函数接口的默认方法。匿名类可以。
  • 由于不需要加载另一个类,lambda表达式更有效。
  • 匿名类被编译成内部类。但是lambda表达式被转换成其封闭类的私有静态(在某些情况下)方法,并使用invokedynamic指令(在java7中添加)对它们进行动态绑定。

为什么局部变量必须为final

为什么局部变量必须是最终的,而实例或者静态变量在匿名类或者 lambda 表达式中使用时不是这样,这是因为这些变量在 Java 中的实现方式。
实例变量存储在堆(计算机内存中的任何地方都可以访问的区域)上,而局部变量存储在堆栈(计算机内存中存储每个函数创建的临时变量的特殊区域)上。
堆上的变量由多个线程共享,但堆栈上的变量隐式地局限于它们所在的线程。
因此,当您创建匿名内部类或 lambda 表达式的实例时,局部变量的值将被复制。换句话说,您不是使用变量,而是使用复制的值。
首先,这可以防止线程相关的问题。其次,由于您使用的是一个副本,如果变量可以被方法的其余部分修改,那么您将使用一个过时的变量。
final定义的(实际上是最后一个变量)消除了这些可能性。由于该值不能更改,不必担心这些更改是可见的,或者会导致线程问题。

总结

  • Lambda表达式有三个部分:参数列表、箭头和正文:(Object o)->System.out.println(o)
  • 您可以将lambda表达式视为匿名方法(或函数),因为它们没有名称。
  • lambda表达式可以有零(用空括号表示)、一个或多个参数。
  • 参数的类型可以显式声明,也可以从上下文推断。
  • 如果只有一个参数,则类型是推断的,并且不必使用括号。
  • 如果lambda表达式使用与封闭上下文的变量名相同的参数名,则会生成编译错误。
  • 如果主体有一条语句,则不需要大括号,并返回表达式的值(如果有)。
  • 如果主体有多条语句,则需要大括号,如果表达式返回值,则必须返回一条return语句。
  • 如果lambda表达式不返回结果,则返回语句是可选的。
  • 函数接口的抽象方法的签名提供lambda表达式的签名(该签名称为函数描述符)。
  • 要使用lambda表达式,首先需要一个函数接口。但是,lambda表达式不包含有关实现哪个函数接口的信息。
  • 表达式的类型是从使用lambda的上下文中推导出来的。这种类型称为目标类型。
  • 可以推断lambda表达式的目标类型的上下文包括赋值、方法或构造函数参数以及强制转换表达式。
  • 与匿名类一样,lambda表达式可以访问实例和静态变量,但只能访问final或有效的final局部变量。
  • 不能抛出未在函数接口方法的throws子句中定义的异常。
  • lambda表达式,它解析为写入lambda的封闭类。
  • 不能从lambda表达式中访问函数接口的默认方法。

这个家伙很懒,啥也没有留下😋