java8新特性
了解 Java 8 中的 lambda 表达式
一、背景:函数式对象
java一直需要函数式对象,在早起,我们需要代码块响应用户事件,例如窗口打开和关闭,按钮按下和滚动条移动。在 Java 1.0 中,Abstract Window Toolkit (AWT) 应用程序需要像它们的 C++ 前辈一样扩展窗口类并覆盖选择的事件方法;这被认为是笨拙和行不通的。因此,在 Java 1.1 中,Sun 为我们提供了一组“侦听器”接口,每个接口都有一个或多个与 GUI 中的事件相对应的方法。但是为了更容易编写必须实现这些接口及其相应方法的类,Sun 给了我们内部类,包括在现有类的主体中编写这样一个类的能力,而无需指定名称——无处不在的匿名内部类。内部类在语法和语义方面都有些陌生。例如,内部类要么是静态内部类要么是实例内部类,这不取决于任何特定的关键字(尽管静态内部类可以使用static关键字显式声明为这样),而是取决于创建实例的词法上下文. 实际上,这意味着 Java 开发人员在编程面试中经常会遇到诸如 代码 1 之类的问题。
class InstanceOuter {
public InstanceOuter(int xx) { x = xx; }
private int x;
class InstanceInner {
public void printSomething() {
System.out.println("The value of x in my outer is " + x);
}
}
}
class StaticOuter {
private static int x = 24;
static class StaticInner {
public void printSomething() {
System.out.println("The value of x in my outer is " + x);
}
}
}
public class InnerClassExamples {
public static void main(String... args) {
InstanceOuter io = new InstanceOuter(12);
// 这是编译错误吗?
InstanceOuter.InstanceInner ii = io.new InstanceInner();
// 这打印的是什么?
ii.printSomething(); // 打印 12
// 这打印的是什么?
StaticOuter.StaticInner si = new StaticOuter.StaticInner();
si.printSomething(); // 打印 24
}
}
诸如内部类之类的“特性”常常让 Java 开发人员相信,此类功能最好归于语言的极端情况,适合编程面试而不是其他太多——除非他们需要它们。即便如此,在大多数情况下,它们的使用纯粹是出于事件处理的原因。
二、Lambda、目标类型和词法范围
1.Lambda 表达式
Java 8 引入了一些新的语言特性,旨在使编写此类代码块变得更加容易——关键特性是lambda 表达式,也通俗地称为闭包(原因我们将在后面讨论)或匿名方法
代码 2 和 代码 3
public class Lambdas {
public static void main(String... args) {
Runnable r = new Runnable() {
public void run() {
System.out.println("Howdy, world!");
}
};
r.run();
}
}
public static void main(String... args) {
Runnable r2 = () -> System.out.println("Howdy, world!");
r2.run();
}
这两种方法具有相同的效果:一个Runnable-implementing 对象,其run()方法被调用以将某些内容打印到控制台。然而,在底层,Java 8 版本所做的不仅仅是生成一个实现Runnable接口的匿名类——其中一些与invoke dynamic Java 7 中引入的字节码有关
2.功能接口
上述 Runnable接口类的Callable接口,该Comparator接口,以及中已定义的其他接口一大堆Java的是什么样的Java 8调用functional interface:它是只需要一个方法,以满足该接口的要求,要实现一个接口. 这就是语法实现其简洁性的方式,因为对于 lambda 试图定义的接口方法没有歧义。
Java 8 的设计者选择给我们一个注解,@FunctionalInterface作为文档提示,接口被设计为与 lambdas 一起使用,但编译器不需要这样做——它从结构中确定“功能接口”接口,而不是来自注释。
代码 4
interface Something {
public String doit(Integer i);
}
3.句法
lambda 基本上由三部分组成:带括号的参数集、箭头和主体,主体可以是单个表达式或 Java 代码块代码 5
public static void main(String... args) {
Comparator<String> c =
(String lhs, String rhs) -> lhs.compareTo(rhs);
int result = c.compare("Hello", "World");
}
Comparator接受两个字符串并返回一个整数,其值为负(对于“小于”)和正(对于“大于”) , 和零(表示“相等”)。
如果 lambda 的主体需要多个表达式,则表达式返回的值可以通过return关键字返回,就像任何 Java 代码块一样 如下代码 6。
public static void main(String... args) {
Comparator<String> c =
(String lhs, String rhs) ->
{
System.out.println("I am comparing" +
lhs + " to " + rhs);
return lhs.compareTo(rhs);
};
int result = c.compare("Hello", "World");
}
对于可以在 lambda 的主体中执行的操作有一些限制,其中大部分都非常直观——一个 lambda 体不能“中断”或“继续”出 lambda,如果 lambda 返回一个值,每个代码路径都必须返回一个值或抛出异常,依此类推。这些规则与标准 Java 方法的规则大致相同。
4.类型推断
如果目标类型是 a Comparator,则传递给 lambda 的对象必须是字符串(或某个子类型);否则,代码首先不会编译。在这种情况下,和参数String前面的声明完全是多余的,而且由于 Java 8 增强的类型推断功能,它们完全是可选的 如下 代码7
public static void main(String... args) {
Comparator<String> c =
(lhs, rhs) ->
{
System.out.println("I am comparing" +
lhs + " to " + rhs);
return lhs.compareTo(rhs);
};
int result = c.compare("Hello", "World");
}
Java 的 lambda 语法的一个有趣的副作用是,在 Java 的历史上,我们第一次发现一些不能分配给类型引用的东西Object(参见代码 8)——至少在没有帮助的情况下是这样。
public static void main4(String... args) {
Object o = () -> System.out.println("Howdy, world!");
// 不会编译
}
编译器会抱怨这Object不是一个函数式接口,尽管真正的问题是编译器不能完全弄清楚这个 lambda 应该实现哪个函数式接口:Runnable还是别的什么?我们可以一如既往地帮助编译器进行强制转换。如代码9
public static void main4(String... args) {
Object o = (Runnable) () -> System.out.println("Howdy, world!");
}
lambda 语法适用于任何接口,因此推断为自定义接口的 lambda 也将同样容易推断。原始类型与其在 lambda 类型签名中的包装类型一样可行,Java 8 只是将 Java 长期存在的原则、模式和语法应用于新功能。代码10
Something s = (Integer i) -> { return i.toString(); };
System.out.println(s.doit(4));
5 语法范围
然而,编译器在lambda函数体中对待名称(标识符)的方式与在内部类中对待名称的方式是如何的呢?
代码 11
class Hello {
public Runnable r = new Runnable() {
public void run() {
System.out.println(this);
System.out.println(toString());
}
};
public String toString() {
return "Hello's custom toString()";
}
}
public class InnerClassExamples {
public static void main(String... args) {
Hello h = new Hello();
h.r.run();
}
}
class Hello {
public Runnable r = new Runnable() {
public void run() {
System.out.println(Hello.this);
System.out.println(Hello.this.toString());
}
};
public String toString() {
return "Hello's custom toString()";
}
}
class Hello {
public Runnable r = () -> {
System.out.println(this);
System.out.println(toString());
};
public String toString() {
return "Hello's custom toString()";
}
}
代码11中的第一段 匿名实现中的关键字this和调用都绑定到匿名内部类实现上,因为那是满足表达式的最内层作用域。如果我们想要打印出Hello的版本toString,我们必须使用thisJava 规范内部类部分中的“外部”语法明确限定它,就如第二段,坦率地说,这是一个内部类只是造成的混乱比它们解决的更多的领域。诚然,只要解释了this关键字出现在这种相当不直观的语法中的原因,它就有点道理了,但它的道理与政治家的特权也有道理一样。然而,Lambdas是词法作用域,这意味着 lambda 将其定义周围的直接环境识别为下一个最外层作用域。因此第三段比第二段语法更直观。
6 变量捕获
将 lambda 称为闭包的部分原因是函数字面量(例如我们一直在编写的内容)可以“关闭”封闭范围内函数字面量主体之外的变量引用(在这种情况下Java 的,通常是定义 lambda 的方法)。内部类也可以做到这一点,但在所有让 Java 开发人员感到沮丧的关于内部类的主题中,内部类只能引用封闭范围中的“最终”变量这一事实接近顶部。
Lambda 放宽了这一限制,但只是放宽了一点:只要变量引用是“有效最终的”,这意味着它除了名称之外的所有内容都是最终的,lambda 可以引用它(参见代码12)。因为message在main包含被定义的 lambda的方法的范围内永远不会被修改,它实际上是最终的,因此有资格从Runnable存储在 r 中的lambda 中引用。
public static void main(String... args) {
String message = "Howdy, world!";
Runnable r = () -> System.out.println(message);`在这里插入代码片`
r.run();
}
虽然从表面上看这可能听起来没什么,但请记住,lambda 语义规则不会改变整个 Java 的性质——在 lambda 定义很久之后,引用另一侧的对象仍然可以访问和修改,如代码13所示。
public static void main(String... args) {
StringBuilder message = new StringBuilder();
Runnable r = () -> System.out.println(message);
message.append("Howdy, ");
message.append("world!");
r.run();
}
熟悉旧内部类的语法和语义的精明开发人员会记得,对于在内部类中引用的声明为“final”的引用也是如此——final修饰符仅应用于引用,而不应用于引用另一侧的对象参考资料。在 Java 社区眼中,这究竟是 bug 还是特性还有待观察,但事实就是如此,开发人员最好了解 lambda 变量捕获的工作原理,以免出现意外的 bug。(事实上,这种行为并不新鲜——它只是通过更少的击键次数和编译器的更多支持来重铸 Java 的现有功能。)
7 方法参考
到目前为止,我们检查过的所有 lambda 表达式都是匿名字面量——本质上是在使用时定义 lambda 表达式。这对于一次性行为非常有用,但是当在多个地方需要或想要这种行为时,它并没有多大帮助。例如,考虑以下Person类。(暂时忽略缺乏适当的封装。)
当 Person放入 SortedSet或需要按某种形式的列表排序时,我们希望有不同的机制来对Person实例进行排序——例如,有时按名字,有时按姓氏。这是Comparator为了允许我们通过传入Comparator实例来定义强加的排序。
Lambdas 当然可以更轻松地编写排序代码,如代码13 所示。但是,按Person名字排序实例可能需要在代码库中多次完成,并且多次编写这种算法显然违反了不要重复自己 (DRY) 原则。
class Person { //新建一个类
public String firstName;
public String lastName;
public int age;
});
public static void main(String... args) {
Person[] people = new Person[] {
new Person("Ted", "Neward", 41),
new Person("Charlotte", "Neward", 41),
new Person("Michael", "Neward", 19),
new Person("Matthew", "Neward", 13)
};
// Sort by first name
Arrays.sort(people,
(lhs, rhs) -> lhs.firstName.compareTo(rhs.firstName));
for (Person p : people)
System.out.println(p);
}
Comparator肯定可以被捕获为Person自身的成员,如代码 14 所示。Comparator然后可以像引用任何其他静态字段一样被引用。而且,说实话,函数式编程狂热者会更喜欢这种风格,因为它允许以各种方式组合功能。
class Person {
public String firstName;
public String lastName;
public int age;
public final static Comparator<Person> compareFirstName =
(lhs, rhs) -> lhs.firstName.compareTo(rhs.firstName);
public final static Comparator<Person> compareLastName =
(lhs, rhs) -> lhs.lastName.compareTo(rhs.lastName);
public Person(String f, String l, int a) {
firstName = f; lastName = l; age = a;
}
public String toString() {
return "[Person: firstName:" + firstName + " " +
"lastName:" + lastName + " " +
"age:" + age + "]";
}
}
public static void main(String... args) {
Person[] people = . . .;
// Sort by first name
Arrays.sort(people, Person.compareFirstName);
for (Person p : people)
System.out.println(p);
}
但是对于传统的 Java 开发人员来说,这感觉很奇怪,而不是简单地创建一个符合 的签名的方法Comparator然后直接使用它——这正是方法引用所允许的(代码15)。注意双冒号的方法命名风格,它告诉编译器应该在此处使用compareFirstNames定义于的方法Person,而不是方法文字。
class Person {
public String firstName;
public String lastName;
public int age;
public static int compareFirstNames(Person lhs, Person rhs) {
return lhs.firstName.compareTo(rhs.firstName);
}
// ...
}
public static void main(String... args) {
Person[] people = . . .;
// Sort by first name
Arrays.sort(people, Person::compareFirstNames);
for (Person p : people)
System.out.println(p);
}
对于那些好奇的人来说,另一种方法是使用该compareFirstNamest方法创建一个Comparator实例 比如 :
Comparator cf = Person::compareFirstNames;
而且,为了更简洁,我们可以通过使用一些新的库功能来完全避免一些语法开销,编写以下代码,它使用了一个高阶函数(意思是,粗略地说,一个函数传递函数)基本上避免了所有先前的代码,而支持一行就地使用:
Arrays.sort(people, comparing(Person::getFirstName));
这在一定程度上就是为什么 lambda 以及它们附带的函数式编程技术如此强大的原因。
8 虚拟扩展方法
经常提到的关于接口的缺点之一是它们没有默认实现,即使该实现非常明显。例如,考虑一个虚构的Relational接口,它定义了一系列方法来模拟关系方法(大于、小于、大于或等于等)。一旦定义了这些方法中的任何一个,就很容易看出如何根据第一个方法定义其他方法。事实上,如果提前知道方法的定义,所有这些都可以根据 aComparable的compare方法定义compare。但是接口不能有默认行为,抽象类仍然是一个类,占据任何潜在子类的一个实现继承槽。
然而,在 Java 8 中,随着这些函数文字变得越来越普遍,能够指定默认行为而不失去接口的“接口性”变得更加重要。因此,Java 8 现在引入了虚拟扩展方法(在之前的草案中曾经被称为防御者方法),基本上允许接口指定方法的默认行为,如果派生实现中没有提供。
考虑一下Iterator界面。目前,它具有三个方法(hasNext、next和remove),并且每个方法都必须定义。但是“跳过”迭代流中的下一个对象的能力可能会有所帮助。并且因为Iterator的实现很容易根据其他三个来定义,所以我们可以提供它,如代码16 所示。
interface Iterator<T> {
boolean hasNext();
T next();
void remove();
void skip(int i) default {
for (; i > 0 && hasNext(); i--) next();
}
}
顾名思义,虚拟扩展方法提供了一种强大的机制来扩展现有接口,而不会将扩展降级为某种二等状态。使用这种机制,Oracle 可以为现有库提供额外的、强大的行为,而无需开发人员跟踪不同种类的类。SkippingIterator对于那些支持它的集合,开发人员现在不需要降级到任何类。事实上,任何地方的代码都不必更改,所有的Iterators,无论何时编写,都会自动具有这种跳过行为。
Collection类中发生的绝大多数更改都是通过虚拟扩展方法实现的。
总结
Lambda 将为 Java 带来很多变化,无论是在 Java 代码的编写方式还是它的设计方式方面。其中一些受函数式编程语言启发的变化将改变 Java 程序员对编写代码的看法——这既是机遇也是麻烦。
我们将在本系列的下一篇文章中更多地讨论这些变化对 Java 库的影响,我们将花一点时间讨论这些新的 API、接口和类如何开启一些新的设计由于内部类语法的笨拙,以前不实用的方法。
Java 8 将是一个非常有趣的版本。系好安全带,这将是一次火箭飞船之旅。