Java 8的语言变化--理解Lambda表达式和变化的接口类是如何使Java 8成为新的语言
本文是IBM developerWorks中的一篇介绍Java 8关键新特性的文章,它主要关注Lambda表达式和改进的接口。(2014.04.19最后更新) Java 8包含了一组重要的新的语言特性,使你能够更方便地构造程序。Lambda表达为内联的代码块定义了一种新的语法,给予你与匿名内部类相同的灵活性,但又没有那么多模板代码。接口的改变使得能够为已有接口加入新的特性,而不必打破现有代码的兼容性。了解这些语言变化是怎样一起工作的,请阅读本系列另一篇文章"Java 8并发基础",可以看到如何在Java 8流中使用Lambda。 Java 8的最大改变就是增加了对Lambda表达式的支持。Lambda表达式一种通过引用进行传递的代码块。它类似于某些其它语言的闭包:代码实现了一个功能,可以传入一个或多个参数,还可以返回一个结果值。闭包被定义在一个上下文中,它可以访问(在Lambda中是只读访问)上下文中的值。 如果你不熟悉闭包,也不必担心。Java 8的Lambda表达式是几乎每个Java开发者都熟悉的匿名内部类的一个高效版规范。如果你只想在一个位置实现一个接口,或是创建一个基类的子类时,匿名内部类为此提供了一种内联实现。Lambda表达式也用于相同的方式,但是它使用一种缩略的语法,使得这些实现比一个标准的内部类定义更为简洁。 在本文中,你将看到如何在不同的场景下使用Lambda表达式,并且你会学到与Java接口定义相关的扩展。在本文章的姊妹篇JVM并发系列的"Java 8并发基础"一文中,可以看到更多使用Lambda表达式的例子,包括在Java 8流特性中的应用。进入Lambda Lambda表达式就是Java 8所称的函数接口的实现:一个接口只定义一个抽象方法。只定义一个抽象方法的限制是非常重要的,因为Lambda表达式的语法并不会使用方法名。相反,该表达式会使用动态类型识别(匹配参数和返回类型,很多动态语言都这么做)去保证提供的Lambda能够与期望的接口方法兼容。 在清单1所示的简单例子中,一个Lambda表达式被用来对Name实例进行排序。main()方法中的第一个代码块使用一个匿名内部类去实现Comparator<Name>接口,第二个语句块则使用Lambda表达式。清单1. 比较Lambda表达式与匿名内部类public class Name {
public final String firstName;
public final String lastName;
public Name(String first, String last) {
firstName = first;
lastName = last;
}
// only needed for chained comparator
public String getFirstName() {
return firstName;
}
// only needed for chained comparator
public String getLastName() {
return lastName;
}
// only needed for direct comparator (not for chained comparator)
public int compareTo(Name other) {
int diff = lastName.compareTo(other.lastName);
if (diff == 0) {
diff = firstName.compareTo(other.firstName);
}
return diff;
}
}
public class NameSort {
private static final Name[] NAMES = new Name[] {
new Name("Sally", "Smith"),
};
private static void printNames(String caption, Name[] names) {
}
public static void main(String[] args) {
// sort array using anonymous inner class
Name[] copy = Arrays.copyOf(NAMES, NAMES.length);
Arrays.sort(copy, new Comparator<Name>() {
@Override
public int compare(Name a, Name b) {
return a.compareTo(b);
}
});
printNames("Names sorted with anonymous inner class:", copy);
// sort array using lambda expression
copy = Arrays.copyOf(NAMES, NAMES.length);
Arrays.sort(copy, (a, b) -> a.compareTo(b));
printNames("Names sorted with lambda expression:", copy);
}
} 在清单1中,Lambda被用于取代匿名内部类。这种匿名内部类在应用中非常普遍,所以Lambda表达式很快就赢得了Java8程序员们的青睐。(在本例中,同时使用匿名内部类和Lambda表达式去实现Name类中的一个方法,以方便对这两种方法进行比较。如果在Lambda中对compareTo()方法进行内联的话,该表达式将会更加简洁。)标准的函数式接口 为了应用Lambda,新的包java.util.function中定义了广泛的函数式接口。它们被归结为如下几个类别: 函数:使用一个参数,基于参数的值返回结果。 谓语:使用一个参数,基于参数的值返回布尔结果。 双函数:使用两个参数,基于参数的值返回结果。 供应器:不使用任何参数,但会返回结果。 消费者:使用一个参数,但不返回任何结果。多数类别都包含多个不同的变体,以便能够作用于基本数据类型的参数和返回值。许多接口所定义的方法都可被用于组合对象,如清单2所示:清单2. 组合谓语// use predicate composition to remove matching names
List<Name> list = new ArrayList<>();
for (Name name : NAMES) {
list.add(name);
}
Predicate<Name> pred1 = name -> "Sally".equals(name.firstName);
Predicate<Name> pred2 = name -> "Queue".equals(name.lastName);
list.removeIf(pred1.or(pred2));
printNames("Names filtered by predicate:", list.toArray(new Name[list.size()]));
清单2定义了一对Predicate<Name>变量,一个用于匹配名为Sally的名字,另一个用于匹配姓为Queue的名字。调用方法pred1.or(pred2)会构造一个组合谓语,该谓语先后使用了两个谓语,当它们中的任何一个返回true时,这个组合谓语就将返回true(这就相当于早期Java中的逻辑操作符||)。List.removeIf()方法就应用这个组合谓语去删除列表中的匹配名字。 Java 8定义了许多有用的java.util.function包中接口的组合接口,但这种组合并不都是一样的。所有的谓语的变体(DoublePredicate,IntPredicate,LongPredicate和Predicate<T>)都定义了相同的组合与修改方法:and(),negate()和or()。但是Function<T>的基本数据类型变体就没有定义任何组合与修改方法。如果你拥有使用函数式编程语言的经验,那么你可能就发会发现这些不同之处和奇怪的忽略。改变接口 在Java 8中,接口(如清单1的Comparator)的结构已发生了改变,部分原因是为了让Lambda更好用。Java 8之前的接口只能定义常量,以及必须被实现的抽象方法。而Java 8中的接口则能够定义静态与默认方法。接口中的静态方法与抽象类中的静态方法是完全一样的。默认方法则更像旧式的接口方法,但提供了该方法的一个实现。该方法实现可用于该接口的实现类,除非它被实现类覆盖掉了。 默认方法的一个重要特性就是它可以被加入到已有接口中,但又不会破坏已使用了这些接口的代码的兼容性(除非已有代码恰巧使用了相同名字的方法,并且其目的与默认方法不同)。这是一个非常强大的功能,Java 8的设计者们利用这一特性为许多已有Java类库加入了对Lambda表达式的支持。清单3就展示了这样的一个例子,它是清单1中对名字进行排序的第三种实现方式。清单3. 键-提取比较器链// sort array using key-extractor lambdas
copy = Arrays.copyOf(NAMES, NAMES.length);
Comparator<Name> comp = Comparator.comparing(name -> name.lastName);
comp = comp.thenComparing(name -> name.firstName);
Arrays.sort(copy, comp);
printNames("Names sorted with key extractor comparator:", copy);
清单3首先展示了如何使用新的Comparator.comparing()静态方法去创建一个基于键-提取(Key-Extraction) Lambda的比较器(从技术上看,键-提取Lambda就是java.util.function.Function<T,R>接口的一个实例,它返回的比较器的类型适用于类型T,而提取的键的类型R则要实现Comparable接口)。它还展示了如何使用新的Comparator.thenComparing()默认方法去组合使用比较器,清单3就返回了一个新的比较器,它会先按姓排序,再按名排序。 你也许期望能够对比较器进行内联,如:Comparator<Name> comp = Comparator.comparing(name -> name.lastName)
.thenComparing(name -> name.firstName);
但不幸地是,Java 8的类型推导不允许这么做。为从静态方法中得到期望类型的结果,你需要为编译器提供更多的信息,可以使用如下任何一种形式:Comparator<Name> com1 = Comparator.comparing((Name name1) -> name1.lastName)
.thenComparing(name2 -> name2.firstName);
Comparator<Name> com2 = Comparator.<Name,String>comparing(name1 -> name1.lastName)
.thenComparing(name2 -> name2.firstName);
第一种方式在Lambda表达式中加入参数的类型:(Name name1) -> name1.lastName。有了这个辅助信息,编译才能知道下面它该做些什么。第二种方式是告诉编译器要传递给Function接口(在此处,该接口通过Lambda表达式实现)中comparing()方法的泛型变量T和R的类型。 能够方便地构建比较器以及比较器链是Java 8中很有用的特性,但它的代价是增加了复杂度。Java 7的Comparator接口定义了两个方法(compare()方法,以及遍布于每个对象中的equals()方法)。而在Java 8中,该接口则定义了18个方法(除了原有的2个方法,还新加入了9个静态方法和7个默认方法)。你将发现,为了能够使用Lambda而造成的这种接口膨胀会重现于相当一部分Java标准类库中。像Lambda那样使用已有方法 如果一个存在的方法已经实现了你的需求,你可以直接使用一个方法引用对它进行传递。清单4展示了这种方法。清单4. 对已有方法使用Lambda
// sort array using existing methods as lambdas
copy = Arrays.copyOf(NAMES, NAMES.length);
comp = Comparator.comparing(Name::getLastName).thenComparing(Name::getFirstName);
Arrays.sort(copy, comp);
printNames("Names sorted with existing methods as lambdas:", copy); 清单4做着与清单3相同的事情,但它使用了已有方法。使用Java 8的形为"类名:方法名"的方法引用语法,你可以使用任意方法,就像Lambda表达式那样。其效果就与你定义一个Lambda表达式去调用该方法一样。对类的静态方法,特定对象或Lambda输入类型的实例方法(如在清单4中,getFirstName()和getLastName()方法就是Name类的实例方法),以及类构造器,都可以使用方法引用。 方法引用不仅方便,因为它们比使用Lambda表达式可能更高效,而且为编译器提供了更好的类型信息(这也就是为什么在上一节的Lambda中使用.thenComparing()构造Comparator会出现问题,而在清单4却能正常工作)。如果既可以使用对已有方法的方法引用,也可以使用Lambda表达式,请使用前者。捕获与非捕获Lambda 你在本文中已见过的Lambda表达式都是非捕获的,意即,它们都是把传入的值当作接口方法参数使用的简单Lambda表达式。Java 8的捕获Lambda表达式则是使用外围环境中的值。捕获Lambda类似于某些JVM语言(如Scala)使用的闭包,但Java 8的实现与之有所不同,因为来自在外围环境中的值必须声明为final。也就是说,这些值要么确实为final(就如同以前的Java版本中由匿名内部类所引用的值),要么在外围环境中不会被修改。这一规范适用于Lambda表达式和匿名内部类。有一些方法可以绕过对值的final限制。例如,在Lambda中仅使用特定变量的当前值,你可以添加一个新的方法,把这些值作为方法参数,再将捕获的值(以恰当的接口引用这种形式)返回给Lambda。如果期望一个Lambda去修改外围环境中的值,那么可以用一个可修改的持有器类(Holder)对这些值进行包装。 相比于捕获Lambda,可以更高效地处理非捕获Lambda,那是因为编译能够把它生成为类中的静态方法,而运行时环境可以直接内联的调用这些方法。捕获Lambda也许低效一些,但在相同上下文环境中它至少可以表现的和匿名内部类一样好。幕后的Lambda Lambda表达式看起来像匿名内部类,但它们的实现方法不同。Java的内部类有很多构造器;每个内部类都会有一个字节码级别的独立类文件。这就会产生大量的重复代码(大部分是在常量池实体中),类加载时会造成大量的运行时开销,哪怕只有少量的代码也会有如此后果。 Java 8没有为Lambda生成独立的类文件,而是使用了在Java 7中引入的invokedynamic字节码指令。invokedynamic作用于一个启动方法,当该方法第一次被调用时它会转而去创建Lambda表达式的实现。然后,该实现会被返回并被直接调用。这样就避免了独立类文件带来的空间开销,以及加载类的大量运行时开销。确切地说,Lambda功能的实现被丢给了启动程序。目前Java 8生成的启动程序会在运行时为Lambda创建一个新类,但在将来会使用不同的方法去实现。 Java 8使用的优化使得通过invokedynamic指令实现的Lambda在实际中运行正常。多数其它的JVM语言,包括Scala (2.10.x),都会为闭包使用编译器生成的内部类。在将来,这些语言可能会转而使用invokedynamic指令,以便利用到Java 8(及其后继版本)的优化。Lambda的局限 如在本文开始时我所提到的,Lambda表达式总是某些特殊函数式接口的实现。你可以仅把Lambda当作接口引用去传递,而对于其它的接口实现,你也可以只是把Lambda当作这些特定接口去使用。清单5展示了这种局限性,在该示例使用了一对相同的(名称除外)函数式接口。Java 8编译接受String::lenght来作为这两个接口的Lambda实现。但是,在一个Lambd表达式被定义为第一个接口的实例之后,它不能够用于第二个接口的实例。清单5. Lambda的局限private interface A {
public int valueA(String s);
}
private interface B {
public int valueB(String s);
}
public static void main(String[] args) {
A a = String::length;
B b = String::length;
// compiler error!
// b = a;
// ClassCastException at runtime!
// b = (B)a;
// works, using a method reference
b = a::valueA;
System.out.println(b.valueB("abc"));
}
任何对Java接口概念有所了解的人都不会对清单5中的程序感到惊讶,因为那就是Java接口一直所做的事情(除了最后一点,那是Java 8新引入的方法引用)。但是使用其它函数式编程语言,例如Scala,的开发者们则会认为接口的这种限制是不自然的。 函数式编程语言是用函数类型,而不是接口,去定义变量。在这些编程语言中会很普遍的使用高级函数:把函数作为参数传递给其它的函数,或者把函数当作值去返回。其结果就是你会得到比Lambda更为灵活的编程风格,这包括使用函数去组合其它函数以构建语句块的能力。因为Java 8没有定义函数类型,你不能使用这种方法去组合Lambda表达式。你可以组合接口(如清单3所示),但只能是与Java 8中已写好的那些接口相关的特定接口。仅在新的java.util.function包内,就特殊设定了43个接口去使用Lambda。把它们加入到数以百计的已有接口中,你将看到这种方法在组合接口时总是会有严重的限制。 使用接口而不是在向Java中引入函数类型是一个精妙的选择。这样就在防止对Java类库进行重大改动的同时也能够对已有类库使用Lambda表达式。它的坏作用就是对Java 8造成了极大的限制,它只能称为"接口编程"或是类函数式编程,而不是真正的函数式编程。但依靠JVM上其它语言,也包括函数式语言,的优点,这些限制并不可怕。结论 Lambda是Java语言的最主要扩展,伴着它们的兄弟新特性--方法引用,随着程序被移植到Java 8,Lambda将很快成为所有Java开发者不可或缺的工具。当与Java 8流结合起来时,Lambda就特别有用。查看文章"JVM并发: Java 8并发基础",可以了解到将Lambda和流结合起来使用是如何简化并发编程以及提高程序效率的。