Lambda与函数式接口

Lambda与函数式接口

Java 函数式接口和Lambda表达式是 Java 8 中引入的一个重要概念,它允许你将行为作为参数传递给方法,从而实现更简洁、更灵活的代码。

Lambda表达式 #

Lambda表达式是一个可传递的代码块,可以在以后执行一次或多次

从一个比较器说起:

 1public class Intro {
 2    public static void main(String[] args) {
 3        String[] s = new String[]{"baidu","alibaba","baida","kingdee"};
 4      	// String类实现了Comparable接口,可以直接使用sort方法实现字典序排序
 5      	// 为什么是字典序?因为String类的实现逻辑是字典序
 6        Arrays.sort(s);
 7        System.out.println(Arrays.toString(s));
 8
 9        Arrays.sort(s, new StringLengthComparator());
10      	//等效使用Lambda表达式实现
11      	//Arrays.sort(s, (o1, o2) -> o1.length() - o2.length());
12        System.out.println(Arrays.toString(s));
13    }
14}
15// 比较器实现——先按字符串长度排序
16class StringLengthComparator implements Comparator<String>{
17    @Override
18    public int compare(String o1, String o2) {
19        return o1.length() - o2.length();
20    }
21}
22/* output
23[alibaba, baida, baidu, kingdee]
24[baida, baidu, alibaba, kingdee]
25*///:~

上例中,compare方法不是立即调用,在数组完成排序之前,sort方法会一直调用compare方法,只要元素的排列顺序不正确就会重新排列元素。

1API:	 public static <T> void sort(T[] a, Comparator<? super T> c)

sort方法需要一个比较器作为参数,接口Comparator只有一个抽象方法compare,要实现排序,实现compare方法即可,这正是StringLengthComparator类所做的事情。

由于StringLengthComparator类只有一个方法,这相当于将一段代码块(函数)传递给sort。实际上这就是Java处理函数式编程的方式:Java是面向对象语言,因此必须构造一个对象,这个对象有一个方法包含所需的逻辑代码

此例中,如果使用Lambda表达式Arrays.sort(s, (o1, o2) -> o1.length() - o2.length());,那么Arrays.sort会接收实现了Comparator<String>的某个类的对象,并在这个对象上调用compare方法去执行Lambda表达式的“体”,这些对象和类的管理完全取决于具体实现,与传统的内联类相比,更加高效。

Lambda表达式可以将代码块传递给某个对象。形如:

1(String o1, String o2) -> o1.length() - o2.length()

就是一个Lambda表达式,由参数箭头(->)以及表达式3部分组成。为什么和代码示例中有细微差别?这里声明了String参数类型。

如果可以推导出Lambda表达式的参数类型(多数情况如此),那么可以省略类型声明:

1(o1, o2) -> o1.length() - o2.length()

即便Lambda表达式没有参数,也必须保留参数括号,以下Lambda展示了表达式有多句的情况——使用{},看起来就像一个Java方法:

1() -> {for (int i = 0, i<10, i++) System.out.println(i);}

如果Lambda表达式只有一个参数,并且这个参数类型可以推导得出,甚至连()都可以省略:

1new ArrayList().removeIf(e -> e == null)

无需指定Lambda表达式的返回类型。Lambda表达式的返回类型总是可以根据上下文推导得出

函数式接口 #

对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个Lambda表达式,这种接口叫函数式接口

java.util.Comparator接口就是一个函数式接口,它只有一个抽象方法:

1int compare(T o1, T o2);

其他方法均被声明为默认方法

java.util.function包中定义了很多通用的函数式接口,上文中的Predicate便是。ArrayList中的forEach方法参数就是此包中的另一个函数式接口Consumer:

1public void forEach(Consumer<? super E> action)

可以用此接口快速遍历集合元素

list.forEach(e -> System.out.println(e))

list.forEach(System.out::println)方法引用

Java API使用@FunctionalInterface注解来标注函数式接口。

类似地,org.springframework.jdbc.core.RowMapper也被声明为一个函数式接口,它只有一个方法mapRow,用来处理SQL语句的回调:

1T mapRow(ResultSet rs,int rowNum) throws SQLException

方法引用 #

如果有现成的方法完成想要传递到其他代码的操作,例如你只想通过forEach打印集合中的元素,可以使用

1list.forEach(e -> System.out.println(e))

就像之前提到的那样,但是,也可以直接把println方法传递给forEach,就像这样:

1list.forEach(System.out::println)

这就是方法引用,它和上面的Lambda表达式是等价的。

如果Lambda表达式的“体”直接调用了某个方法,而没有其他多余代码,那么这个Lambda表达式可以等价转换为方法引用

还是参考比较器的例子:

 1public class Intro {
 2    public static void main(String[] args) {
 3        String[] s = new String[]{"baidu", "alibaba", "baida", "kingdee"};
 4		// lmabda statement original
 5		/*Arrays.sort(s, (o1,o2) -> {
 6          if (o1.length() != o2.length()) return o1.length() - o2.length();
 7        	return o1.compareTo(o2);
 8        })*/
 9
10      	// Lambda expression with method reference
11        Arrays.sort(s, (o1,o2) -> localCompare(o1, o2) );
12
13      	// method reference
14      	Arrays.sort(s, Intro::localCompare)
15        System.out.println(Arrays.toString(s));
16
17    private static int localCompare(String o1, String o2) {
18        if (o1.length() != o2.length()) return o1.length() - o2.length();
19        return o1.compareTo(o2);
20    }
21}

原始的Lambda表达式有2行代码(2个逻辑),可以将其重构为一个方法,并在Lambda表达式中引用该方法,这样做之后,原Lambda表达式的“体”就变成了一个简单的方法调用,那么它便可以等价为方法引用:

1Arrays.sort(s, Intro::localCompare)

方法引用根据调用者和方法类型区分,有3种形式

  1. object.instanceMethod:对象调用实例方法

  2. Class.staticMethod:类调用静态方法

  3. Class.instanceMethod:类调用实例方法

前2者较容易理解,第3种情况需要特殊说明,参考如下示例:

 1//...
 2Arrays.sort(s, new Comparator<String>() {
 3            @Override
 4            public int compare(String s3, String str) {
 5                return s3.compareToIgnoreCase(str);
 6            }
 7        });
 8Arrays.sort(s, (s3, str) -> s3.compareToIgnoreCase(str));
 9Arrays.sort(s, String::compareToIgnoreCase);
10//...

当使用类调用实例方法时,第一个参数会成为方法的目标,第二个参数作为方法的参数需求证

方法引用种可以使用thissuper关键字,分别表示调用当前类和超类的方法。

变量作用域 #

在使用Spring JDBC操作数据库时,需要用到RowMapper的回调来处理返回数据,前文已提及,RowMapper是一个函数式接口,可以等价为Lambda表达式:

1public List<Spitter> findAll() {
2        return jdbcOperations.query(
3            SPITTER_SELECT,
4            (rs, rowNum) -> this.mapResult(rs, rowNum));
5    }
6// skip mapResult...

可以看到,Lambda表达式中使用了this关键字,指定的是创建这个Lambda表达式的方法this,通俗地讲,就是调用传入Lambda参数方法的实例,此处的this可以省略。

之前的所述的Lambda表达式都没有涉及一个概念:自由变量,这是除了表达式和参数之外,Lambda的另一个组成部分,指的是不是参数且不在表达式中定义的变量

1static void repeatMessage(String msg, int delay) {
2        ActionListener listener = e -> {
3            System.out.println(msg);
4            Toolkit.getDefaultToolkit().beep();
5        };
6        new Timer(delay, listener).start();
7    }

当调用repeatMessage("Hello World", 1000);时,控制台每隔1s输出Hello World

上例的Lambda中,msg就是一个自由变量,它来自于repeatMessage方法的参数变量。在运行过程中,Lambda表达式可能会延迟执行或者执行很多次,这时候主线程可能已经结束,repeatMessage方法的参数变量也可能已经销毁了,这个变量是如何保存的呢?

实际上,Lambda表达式在运行时“捕获”了自由变量的值,可以把Lambda表达式理解为一个含有方法的实例对象,自由变量的的值便复制到了这个实例对象中

当在Lambda表达式中使用自由变量时,有几个约束

1) 不能在Lambda表达式中改变自由变量的值

1 static void countDown(int start, int delay){
2   ActionListener listener = evevt -> {
3     // start--; // ERROR! can't mutate captured variable
4     System.out.println(start);
5   };
6   new Timer(delay, listener).start();
7 }

这是出于线程安全的考虑。

2) 不能引用在外部改变了值的自由变量

1 static void repeat(String text, int count){
2   for (int i = 1, i<= count, i++){
3       ActionListener listener = evevt -> {
4       	// System.out.println(i + "text"); // ERROR! can't refer to changing i
5     	};
6     new Timer(1000, listener).start();
7   }
8 }

3) 注意变量的命名

1int first = 1;
2Compartor<String> comp = (first, second) -> first.length() - senond.length();
3// ERROR variable first already exists

Lambda表达式的“体”和“嵌套块”具有相同的作用域。