介绍

Lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。在其他语言中,可以直接处理代码块,但是Java设计者因为不想随意添加特性让Java变得无法管理所以一直拒绝添加这个特性。知道现在Java SE8,设计者们便引入了Lambda表达式来支持函数式编程处理代码块了。

语法

先举一个栗子,如果我们想传入代码来检查一个字符串是否比另一个字符串短时,我们可以这样做:

(String first, String second) -> first.length() - second.length()

这就是一个lambda表达式。lambd表达式就是一个代码块,以及必须传入代码的变量规范。它的形式是:参数,箭头(->)以及一个表达式。如果代码要完成的计算无法放在一个表达式中,就可以像写方法一样,把这些代码放在{}并包含显式的return语句。

  1. //例如上面的栗子,短返回-1,长返回1,否则返回0
  2. (String first, String second) -> {
  3. if(first.length() < second.length()) return -1;
  4. else if(first.length() > second.length()) return 1;
  5. else return 0;
  6. }
  7. //如果lambda表达式没有参数,仍然要写空括号
  8. () -> {return "hello";}
  9. //如果可以根据上下文推导出参数的类型,也可以忽略其类型
  10. Comparator<String> comparator = (first,second) -> first.length() - second.length();
  11. //如果只有一个参数,而且参数类型可以推导出来,甚至可以省略小括号
  12. ActionListener listener = event -> System.out.println("hello"); //代替 (event) 或 (ActionEvent event)

需要知道是,我们无需指定lambda表达式的返回类型。lambda表达式的返回类型总是会由上下文推导得出。

注意:如果一个lambd表达式只在某些条件下返回值,某些不返回值,这时不合法的。例如,(int x) - > {if(x>=0)return 1;} 就不合法

函数式接口

介绍

在Java中已经有很多封装代码块的接口,如ActionListener或Comparator。lambda表达式与这些接口是兼容的。

对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口(functional interface)

规范

  • FunctionalInterface注解标注一个函数式接口,不能标注类,方法,枚举,属性这些。
  • 如果接口被标注了@FunctionalInterface,这个类就必须符合函数式接口的规范
  • 即使一个接口没有标注@FunctionalInterface,如果这个接口满足函数式接口规则,依旧被当作函数式接口

重写Object中的方法,不会计入接口方法中,除了final不能重写的,Object中所能重写的方法,写到接口中,不会影响函数式接口的特性 Java8 允许接口中含有非抽象方法,这种在接口中使用default修饰的非抽象方法称为默认方法,默认方法也不会影响函数式接口的特性。我们依然可以认为这是一个函数式接口。

  1. public interface Test {
  2. void get();
  3. }

使用

下面看一个栗子,在Arrays.sort方法里,第二个参数需要一个Comparator实例,Comparator就是只有一个方法的接口,所以可以提供一个lambda表达式:

  1. Arrays.sort(words, ((first, second) -> first.length() - second.length()))

在底层,Arrays.sort方法会接受实现了Comparator的某个类的对象。在这个对象上调用compare方法会执行这个lambda表达式的体。因此最好把lambda表达式看作是一个函数,而不是一个对象,另外要接受lambda表达式可以传递到函数式接口。

实际上,在Java中,对lambda表达式所能做的也只是能转换为函数式接口

方法引用

介绍

方法引用(method reference)是用来直接访问类或者实例的已经存在的方法或者构造方法。方法引用提供了一种引用而不执行方法的方式,它需要由兼容的函数式接口构成的目标类型上下文。计算时,方法引用会创建函数式接口的一个实例。
当Lambda表达式中只是执行一个方法调用时,不用Lambda表达式,直接通过方法引用的形式可读性更高一些。方法引用是一种更简洁易懂的Lambda表达式。

表达式 System.out::println 是一个方法引用,它等价于 x -> System.out.println(x)

作用

方法引用的唯一用途是支持Lambda的简写。
方法引用提高了代码的可读性,也使逻辑更加清晰。(优点)

使用

用::操作符分隔方法名与对象或类名。主要有三种情况:

  • object::instanceMethod
  • Class::staticMethod
  • Class::instanceMethod

在前两种情况,方法引用等价于提供方法参数的lambda表达式。对于第三种情况,第一个参数会成为方法的目标。例如,String::compareToIgnoreCase等同于(x,y) -> x.compareToIgnoreCase(y)。

构造器引用

构造器引用与方法引用很类似,只不过方法名为new。例如,Person::new是Person构造器的一个引用。如果有多个构造器,则会取决于上下文来决定使用哪一个构造器。

  1. ArrayList<String> names = ...;
  2. Stream<Person> stream = names.stream().map(Person::new);
  3. List<Person> people = stream.collect(Collectors.toList());

另外还有个数组引用,可以用数组类型建立构造器引用。例如,int[]::new是一个构造器引用,它有一个参数:即数组的长度。这等价于lambda表达式x -> new int[x];
Java有一个限制,无法构造泛型类型T的数组,表达式new T[n]会产生错误,因为这会改为new Object[n]。

假设我们需要一个Person对象数组。Stream接口有一个toArray方法可以返回Object数组:

  1. Object[] people = stream.toArray();

不过用户希望得到的是Person引用数组,流库利用构造器引用解决了这个问题。

  1. Person[] people = stream.toArray(Person[]::new);

变量作用域

首先看一段代码

  1. //lambda表达式里引用了外围的变量text,看似没什么问题
  2. public static void repeat(String text, int delay){
  3. ActionListener listener = event -> {
  4. System.out.println(text);
  5. };
  6. new Timer(delay, listener).start();
  7. }
  8. //再看看这段代码
  9. public static void count(int start, int delay){
  10. //同样地使用外围变量和lambda表达式,现在问题来了
  11. ActionListener listener = event -> {
  12. //报错!不可以改变外围变量的值
  13. start--;
  14. System.out.println(start);
  15. };
  16. new Timer(delay, listener).start();
  17. }

lambda表达式可以捕获外围作用域中变量的值。在Java中,要确保所捕获的值是明确定义的,这里有一个重要的限制。在lambda表达式中,只能引用值不会改变的变量。如果不做这个限定,在lambda表达式中改变变量,并发执行多个动作时就会不安全。
另外如果在lambda表达式中引用变量,这个变量可能在外部改变,这也是不合法的。

因此,总结:lambda表达式中捕获的变量必须实际上时最终变量(effectively final),实际上的最终变量是指,这个变量初始化后就不会再为它赋新值。
**
在一个lambda表达式里使用this关键字时,是指创建这个lambda表达式的方法的this参数。例如:

  1. public class Application{
  2. public void init(){
  3. ActionListener listener = event -> {
  4. System.out.println(this.toString());
  5. }
  6. }
  7. }

这里表达式this.toString()会调用Application对象的toString()方法,而不是ActionListener实例的方法。

常用函数式接口

函数式接口 参数类型 返回类型 抽象方法名 描述 其他方法
Runnable void run 作为无参数或返回值的动作运行
Supplier T get 提供一个T类型的值
Consumer T void accept 处理一个T类型的值 andThen
BiConsumer T,U void accept 处理T和U类型的值 andThen
Function T R apply 有一个T类型参数的函数 compose,andThen,identity
BiFunction T,U R apply 有T和U类型参数的函数 andThen
UnayOperator T T apply 类型T上的一元操作符 compose,andThen,identity
BinaryOperator T,T T apply 类型T上的二元操作符 andThen,maxBy,minBy
Predicate T boolean test 布尔值函数 and,or,negate,isEqual
BiPredicate T,U boolean test 有两个参数的布尔值函数 and,or,negate

基本类型的函数式接口

函数式接口 参数类型 返回类型 抽象方法名
BooleanSupplier none boolean getAsBoolean
_P_Supplier none p getAsP
_P_Consumer p void accept
Obj_P_Consumer T,p void accept
_P_Function p T apply
_P_To_Q_Function p q applyAsQ
To_P_Function T p applyAsP
To_P_BiFunction T,U p applyAsP
_P_UnaryOperator p p applyAsP
_P_BinaryOperator p,p p applyAsP
_P_Predicate p boolean test

注:p,q为int,long,double; P,Q为Int,Long,Double