Lambda
表达式是一个可传递的代码块,可以在以后执行一次或多次。在 Java 中传递一个代码段并不容易,不能直接传递代码段。Java 是一种面向对象语言,所以必须构造一个对象,这个对象的类需要有一个方法能包含所需的代码。
在其他语言中,可以直接处理代码块。Java 设计者很长时间以来一直拒绝增加这个特性。毕竟,Java 的强大之处就在于其简单性和一致性。如果只要一个特性能够让代码稍简洁一些,就把这个特性增加到语言中,这个语言很快就会变得一团糟,无法管理。不过,在另外那些语言中,并不只是创建线程或注册按钮点击事件处理器更容易;它们的大部分 API 都更简单、更一致而且更强大。在 Java 中,也可以编写类似的 API 利用类对象实现特定的功能,不过这种 API 使用可能很不方便。
就现在来说,问题已经不是是否增强 Java 来支持函数式编程,而是要如何做到这一点。设计者们做了多年的尝试,终于找到一种适合 Java 的设计——Lambda
表达式。
基本形式:参数,箭头(->),表达式。
// 基本形式
(String first, String second) -> first.length() - second.length()
// 多表达式语句块
(String first, String second) ->
{
if (first.length() < second.length())
return -1;
else if (first.length() > second.length())
return 1;
else
return 0;
}
// 没有参数
() -> {
for (int i = 100; i >= 0; i--)
System.out.println(i);
}
简单来说,lambda
表达式使我们更方便地传递了一段语句块,实现的功能类似于其他语言的“函数”,可选择是否传入参数,不需要定义返回值。
虽然 lambda
表达式提供了种种便利,但仍不提倡使用 lambda
表达式实现过于复杂的功能,因为这是“方法”需要完成的事情。而且 lambda
表达式能够简化代码,关注于实现核心逻辑,若功能过于复杂,则违背了 lambda
表达式的设计初衷了。
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个 lambda
表达式。这种接口称为函数式接口 (functional interface)。
为了展示如何转换为函数式接口,下面考虑 Arrays.sort
方法。它的第二个参数需要一个 Comparator
实例,Comparator
就是只有一个方法的接口,所以可以提供一个 lambda
表达式:
Arrays.sort (words, (first, second) -> first.length() - second.length()) ;
在底层,Arrays.sort
方法会接收实现了 Comparator
的某个类的对象。在这个对象上调用 compare
方法会执行这个 lambda
表达式的体。这些对象和类的管理完全取决于具体实现,与使用传统的内联类相比,这样可能要高效得多。
ArrayList
类有一个 removelf
方法,它的参数就是一个 Predicate
。这个接口专门用来传递 lambda
表达式。例如,下面的语句将从一个数组列表删除所有 null
值:list.removelf(e -> e == null);
有时,可能已经有现成的方法可以完成你想要传递到其他代码的某个动作。例如,假设你希望只要出现一个定时器事件就打印这个事件对象。
// lambda 表达式
Timer t = new Timer(1000, event -> System.out.println(event));
// 方法引用
Timer t = new Timer(1000, System.out::println);
表达式 System.out::println
是一个方法引用 (method reference),它等价于 lambda
表达式 x -> System.out.println(x)
。
使用 ::
操作符可以分隔方法名与对象或类名,主要有以下三种情况:
object::instanceMethod
Class::staticMethod
Class::instanceMethod
在前 2 种情况中,方法引用等价于提供方法参数的 lambda
表达式。前面已经提到,System.out::println
等价于 x -> System.out.println(x)
。 类似地,Math::pow
等价于(x, y) -> Math.pow(x, y)
。
对于第 3 种情况,第 1 个参数会成为方法的目标。例如,String::compareToIgnoreCase
等同于 (x, y) -> x.compareToIgnoreCase(y)
。
可以在方法引用中使用 this
参数,例如:this::equals
等同于 x -> this.equals(x)
。使用 super
也是合法的,例如:super::instanceMethod
。
构造器引用与方法引用很类似,只不过方法名为 new
。例如,Person::new
是 Person
构造器的一个引用。
可以用数组类型建立构造器引用。例如,int[]::new
是一个构造器引用,它有一个参数:即数组的长度。这等价于 lambda
表达式 x -> new int[x]
。
Java 有一个限制,无法构造泛型类型 T 的数组。数组构造器引用对于克服这个限制很有用。表达式 new T[n]
会产生错误,因为这会改为 new Object[n]
。
假设我们需要一个 Person 对象数组。Stream
接口有一个 toArray
方法可以返回 Object
数组:
Object[] people = stream.toArray();
不过,这并不让人满意。用户希望得到一个 Person
引用数组,而不是 Object
引用数组。流库利用构造器引用解决了这个问题。可以把 Person[]::new
传入 toArray
方法:
Person口 people = stream.toArray(Person[]::new);
toArray
方法调用这个构造器来得到一个正确类型的数组。然后填充这个数组并返回。
lambda
表达式也可以访问表达式以外的变量:
public static void repeatMessage(String text, int delay)
{
ActionListener listener = event ->
{
System.out.println(text);
Toolkit.getDefaultToolkit().beep():
};
new Timer(delay, listener).start();
}
repeatMessage("Hello", 1000);
在上例中,text
不是 lambda
表达式的参数,而是静态方法 repeatMessage
的参数。而当 lambda
表达式开始回调时,静态方法可能已经结束,此时 text
变量已经不存在,从而产生错误。
可以看到,lambda
表达式可以捕获外围作用域中变量的值。 在 Java 中,要确保所捕获的值是明确定义的,这里有一个重要的限制。在 lambda
表达式中,只能引用值不会改变的变量。
之所以有这个限制的原因在于如果在 lambda
表达式中改变变量,并发执行多个动作时就会不安全。另外如果在 lambda
表达式中引用变量,而这个变量可能在外部改变,这也是不合法的。
这里有一条规则:lambda 表达式中捕获的变量必须实际上是最终变量 (effectively final) 实际上的最终变量是指,这个变量初始化之后就不会再为它赋新值。
public class ApplicationO
{
public void init()
{
ActionListener listener * event ->
{
System.out.print n(this.toString());
...
}
...
}
}
在一个 lambda
表达式中使用 this
关键字时,是指创建这个 lambda
表达式的方法的 this
参数。表达式 this.toString()
会调用 Application
对象的 toString
方法,而不是 ActionListener
实例的方法。
使用 lambda
表达式的重点是延迟执行 (deferred execution) 毕竟,如果想要立即执行代码,完全可以直接执行,而无需把它包装在一个 lambda
表达式中。之所以希望以后再执行代码,有很多原因:
public interface IntConsumer
{
void accept(int value);
}
public static void repeat(int n, IntConsumer action)
{
for (int i = 0; i < n; i++) action.accept(i);
}
repeat(10, i-> System.out.println("Countdown: " + (9 - i));
上述程序利用一个函数式接口,实现了告诉我们每次调用 lambda
表达式在第几次迭代中。接下来分析上述代码:
repeat
定义了一个函数式接口,调用 repeat
后,传入lambda
表达式。repeat
内部,用 for
循环调用 action.accept(i)
。action
对应的是一个 lambda
表达式,同时也是一个函数式接口,只有一个参数 i
。action.accept(i)
,将参数 i
传入 lambda
表达式,执行 System.out.println("Countdown: " + (9 - i)
。for
循环后,repeat
方法结束,程序结束。参考资料: