你的位置:首页 > Java教程

[Java教程]Java 终于有 Lambda 表达式啦~Java 8 语言变化Lambda 表达式和接口类更改【转载】


原文地址 en cn

下载 Demo

Java™ 8 包含一些重要的新的语言功能,为您提供了构建程序的更简单方式。Lambda 表达式 为内联代码块定义一种新语法,其灵活性与匿名内部类一样,但样板文件要少得多。接口更改使得接口可以添加到现有接口中,同时又不会破坏与现有代码的兼容性。本文将了解这些更改是如何协同工作的。

Java 8 的最大变化在于添加了对 lambda 表达式 的支持。Lambda 表达式是可按引用传递的代码块。类似于一些其他编程语言中的闭包:它们是实现某项功能的代码,可接受一个或多个输入参数,而且可返回一个结果值。闭包是在一个上下文中定义的,可访问(对于 lambda 表达式而言是只读访问)来自上下文的值。

如果您不熟悉闭包,不用害怕。Java 8 lambda 表达式其实是匿名内部类的一种特殊化,而几乎所有 Java 开发人员都熟悉匿名内部类。匿名内部类提供了一个接口的内联实现,或者一个基类的子类,我们一般只会在代码中的一个地方使用它。Lambda 表达式的使用方式一样,但它有一个简写的语法,使得它们比标准内部类定义更简洁。

在本文中,您将了解如何在各种情形下使用 lambda 表达式,还将了解 Java 语言 interface 定义的相关扩展。

Java 终于有 Lambda 表达式了~

.NET 关于 Lambda 表达式以及函数委托等的实现,从 2008 年开始,经历了一个技术的演化过程,参看“没有 Lambda 演算何来匿名函数——匿名函数(匿名方法和Lambda)、委托、LINQ”和“.NET C# 声明、实例化和使用委托以及委托在 C# 中的发展”~

了解 lambdas


Lambda 表达式始终是 Java 8 称为函数式接口 的一个对象的实现:定义单一抽象方法的 interface 类。对单一抽象方法的限制很重要,因为 lambda 表达式语法不使用方法名。相反,该表达式使用了 duck typing(匹配参数和返回类型,就像在许多动态语言中所做的那样)来确保所提供的 lambda 与预期的接口方法相兼容。

duck typing,鸭子类型,James Whitcomb Riley 提出的一个著名论断,“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”我们并不关心对象是什么类型,到底是不是鸭子,只关心行为。

在清单 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 表达式立即赢得了 Java 8 程序员的器重。(在本例中,内部类和 lambda 都使用在 Name 类中实现的一个方法来执行比较工作。如果 compareTo() 方法代码内联在 lambda 中,那么表达式就不怎么简洁了。)

标准函数式接口


新的 java.util.function 包定义旨在使用 lambdas 的广泛函数式接口。这些接口分为几大类:

  • Function:接受一个参数,基于参数值返回结果
  • Predicate:接受一个参数,基于参数值返回一个布尔值
  • BiFunction:接受两个参数,基于参数值返回结果
  • Supplier:不接受参数,返回一个结果
  • Consumer:接受一个参数,无结果 (void)

这些类别中大部分都包含几个用于处理基本原始参数或返回类型的变量。许多接口定义可用于组合实例的方法,如清单 2 中所示。

清单 2. 组合谓词

// 使用谓词组合删除匹配的名称
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(与 Java 中的逻辑 || 运算符一样),则返回 trueList.removeIf() 方法应用这个组合谓词从列表中删除匹配的名称。

Java 8 定义了 java.util.function 接口的许多有用的组合,但组合不一致。谓词变量(DoublePredicateIntPredicateLongPredicatePredicate<T>)都定义了相同的组合和修改方法:and()negate()or()。但 Function<T> 的原始变量不定义任何组合或修改方法。如果您有使用函数式编程语言的经验,那么您可能会发现这些差异和遗漏很古怪。

更改 interfaces


interface 类的结构(比如 清单 1 中使用的 Comparator)在 Java 8 中有了变化,部分原因是为了让 lambda 表达式更可用。Java 8 之前的接口只能定义常量和稍后必须实现的抽象方法。Java 8 增加了在接口中同时定义 staticdefault 方法的能力。一个接口中的静态方法实际上与一个抽象类中的静态方法相同。默认方法更像是旧式的接口方法,但有一个附带的实现,只有在重写方法时才会使用该实现。

默认方法的一个重要特性是,可以将它们添加到一个现有的 interface 中,同时不会破坏与使用该接口的其他代码的兼容性(除非您的现有代码正好出于另一个目的使用相同的方法名)。这是一个强大的特性,Java 8 设计人员使用它来改进对许多预置 Java 库的 lambda 表达式的支持。清单 3 显示一个示例,采用第三种方式对添加到 清单 1 代码中的名称进行排序。

清单 3. 串连 key-extractor Comparator

// 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 创建一个 Comparator(从技术上来讲,key-extraction lambda 是 java.util.function.Function<T,R> 接口的一个实例,其中生成的 Comparator 的类型在分配时与 T 兼容,而且所提取的键类型 R 实现了 Comparable 接口。)另外还展示如何使用新的 Comparator.thenComparing() 默认方法组合 Comparator,在 清单 3 中,该方法返回了一个新 comparator,它按照姓氏对第一个数组进行排序,按照名字对第二个数组进行排序。

您可能认为您可以将 comparator 构造函数内联为:

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 参数的类型添加到 lambda 表达式:(Name name1) -> name1.lastName。有了这一协助,编译器就可以了解其余要做的工作是什么。第二种形式将传递给 comparing() 方法的函数式接口(在本例中由 lambda 实现)的类型 TR 告诉编译器。

轻松构造和串连 comparator 的能力是 Java 8 的一个有用功能,但其代价是增加了复杂性。Java 7 Comparator 接口定义了两个方法(compare() 和保证要为每个对象定义的无处不在的 equals())。Java 8 版本定义了 18 个方法(原始的 2 个方法,加上 9 个新静态方法和 7 个新的默认方法)。您会发现,为使用 lambdas 而产生的这一大规模接口膨胀模式会在 Java 标准库的相当一部分中重复出现。

使用 lambdas 这样的现有方法


如果有一个现有的方法已经满足了您的需要,那么您可以使用方法引用 来直接传递该方法。清单 4 展示了该方法。

清单 4. 使用 lambdas 这样的现有方法

...
// 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 ClassName::methodName 方法引用语法,像使用 lambda 表达式一样使用任意方法。这与定义调用该方法的 lambda 具有完全相同的效果。您可以对静态方法、lambda 的特定对象或输入类型的实例方法(如 清单 4 所示,其中 getFirstName()getLastName() 方法是所比较的 Name 的实例方法)以及构造函数使用方法引用。

方法引用不仅使用方便,比起使用 lambda 表达式它们可能更有效,而且对于编译器(这就是为什么在清单 4 最后一部分对比 lambdas 出现问题而使用方法引用工作正常的原因)也提供了更好的类型信息。如果您在使用一个已经存在的方法引用和使用一个 lambda 之间做出选择,您应该总是更倾向于使用方法引用。

捕获的和非捕获的 lambdas


本文中您看到的 lambda 示例都是非捕获的,也就是说,它们是简单的表达式,仅使用作为接口方法参数的等效值传递进来的值。Java 8 中捕获的 lambdas 使用了所包含的上下文中的值。捕获的 lambdas 类似于其他一些 JVM 语言(包括 Scala)中使用的闭包,但不同之处在于,在 Java 8 中,所包含的上下文中的任何值必须是 effectively final。即该值必须是真正的 final(因为引用自匿名内部类的值必须在早期 Java 版本中)或者 在上下文中从未被修改过。这一标准同时适用于 lambda 表达式和匿名内部类使用的值。

您可以使用一些解决方法来应对 effectively final 限制。例如,如果要在一个 lambda 表达式中仅使用某些变量的当前值,那么您可以添加一个新方法,接受这些值作为参数,并为 lambda 表达式(以适当接口引用的形式)返回捕获的值。如果想要一个 lambda 表达式来修改封闭的上下文中的值,那么可以将该值包装到一个可变容器中。

与捕获的 lambdas 相比,非捕获的 lambdas 可以得到更高效的处理,因为编译器可以将它们生成为包含类中的静态方法,而且运行时可以直接内联调用。捕获的 lambdas 可能效率稍差一点,但在相同的上下文中它的性能应至少与匿名内部类一样。

Lambdas 幕后揭秘


Lambda 表达式看起来非常像匿名内部类,但实现方式不同。Java 内部类是庞大的构造函数;一直到字节码级别,每个内部类都有一个独立的类文件。很多数据是重复的(主要采用常量池项的形式),类加载增加了相当大的运行时开销,这一切都只是为了支持少量增加的代码。

Java 8 没有为 lambdas 使用独立的类文件,而是依赖 Java 7 中添加的 invokedynamic 字节码指令。invokedynamic 以一个 bootstrap 方法为目标,该方法在首次被调用时创建 lambda 表达式实现。随后,返回的实现被直接调用。这样就避免了独立类文件的空间开销以及加载类的大量运行时开销。lambda 函数究竟是如何 实现的就交由 bootstrap 来决定。Java 8 当前生成的 bootstrap 代码在运行时为 lambda 构建了一个新类,但未来的实现可自由使用不同的方法。

Java 8 结合了一些优化措施,使得通过 invokedynamic 进行的 lambdas 实现在实践中行之有效。其他大部分 JVM 语言,包括 Scala (2.10.x),为闭包使用编译器生成的内部类。这些语言的未来版本可能转向 invokedynamic 方法,以便利用 Java 8(和更高版本)的优化。

Lambda 的限制


正如我在文章开头所提到的,lambda 表达式几乎是一些特殊函数接口的实现。您只可以通过 lambdas 作为接口引用和其他接口的实现,您只可使用一个 lambda 作为将要创建的具体接口。清单 5 通过一对相同(除了名称)的函数式接口展示了这一限制。Java 8 编译器接受 String::length 方法作为两个接口的 lambda 实现。但在将 lambda 定义为第一个接口的实例之后,就不能将其用作第二个接口的实例。

清单 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, but ugly (wraps in a new lambda)
  b = (x) -> a.valueA(x);
  System.out.println(b.valueB("abc"));
}


Scala 等函数式编程语言使用函数类型(而不是接口)来定义变量。在这种语言中使用高阶函数 是很常见的事情:高阶函数是将函数作为参数传递或将函数作为值返回的函数。其编程风格要比 lambdas 灵活得多,包括能够将函数作为构建块来组建其他函数。由于 Java 8 没有定义函数类型,所以您不能以这种方式创建 lambdas。您可以创建接口(如 清单 3 所示),但编写的代码仅用于处理所涉及到的特定接口。仅仅在新的 java.util.function 程序包中,就专门创建了 43 个用于 lambdas 的接口。将这些接口添加到上百个现有接口中,您可以看到构建接口的方式总是受到极大的限制。

在进行使用接口(而不是添加函数类型到 Java)的选择时,一定要深思熟虑。这样做会排除对 Java 库进行重大变动的需要,同时支持对现有的库使用 lambda 表达式。这样做的弊端在于,它将 Java 8 限定为所谓的 “接口编程” 或类似函数式的编程,而非真正的函数式编程。但随着 JVM 上开始支持其他多种语言,包括函数式语言,这一限制就没那么严重了。

结束语


Lambdas 是 Java 语言的一个重大扩展,而且随着所有 Java 人员将其应用程序迁移到 Java 8,Lambda 表达式很快将成为他们不可缺少的一个工具。在与 Java 8 streams 结合使用时,Lambdas 特别有用。参阅 “JVM 并发性:Java 8 并发性基础”,了解 lambdas 与 Java 8 streams 如何共同简化并发编程和提高应用程序性能。

参考资料


  • Lambda 表达式:Java 教程中的这一主题解释了在各种上下文中使用 lambda 表达式的细节。
  • Lambda: A Peek Under the Hood:查看由 Java 语言架构师和 IBM developerWorks 作家 Brian Goetz 制作的这一 JavaOne 2013 演示文稿,了解 Java 8 lambda 表达式的设计和实现背后的逻辑。
  • 使用 Lambda 表达式在 Java 中编程:在由 Venkat Subramaniam 于 JavaOne 2013 上提供的这一实时编码演示中查看 lambda 使用示例。

下载 Demo