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种形式
object.instanceMethod
:对象调用实例方法Class.staticMethod
:类调用静态方法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//...
当使用类调用实例方法时,第一个参数会成为方法的目标,第二个参数作为方法的参数需求证。
方法引用种可以使用this
和super
关键字,分别表示调用当前类和超类的方法。
变量作用域 #
在使用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表达式的“体”和“嵌套块”具有相同的作用域。