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

时间: 2024-10-16 01:31:00

Java 终于有 Lambda 表达式啦~Java 8 语言变化——Lambda 表达式和接口类更改【转载】的相关文章

C#6.0语言规范(七) 表达式

表达式是运算符和操作数的序列.本章定义了操作数和运算符的语法,求值顺序以及表达式的含义. 表达式分类 表达式分类为以下之一: 一个值.每个值都有一个关联的类型. 一个变量.每个变量都有一个关联的类型,即声明的变量类型. 命名空间.具有此分类的表达式只能显示为member_access(成员访问)的左侧.在任何其他上下文中,分类为命名空间的表达式会导致编译时错误. 一种.具有此分类的表达式只能显示为member_access(成员访问)的左侧,或者作为运算as符(作为运算符),is运算符(运算符)

好炫的Lambda表达式,Java党用起来!(最简易Lambda教程)

刷微博时偶然看到Lambda 的 HelloWorld 教程,觉得很酷炫!分享一下! 效果示例 总体看起来效果就是代码简洁,如下 //之前的写法 btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { System.out.println("hello lambda"); ... } }); //Lambda表达式写法 btn.setsetOnClickLis

Lambda 表达式 in java 8

Lambda 表达式 in Java 8 Lambda表达式是java 8 新增的特性 Lambda表达式主要作用:支持将代码块作为方法参数,允许使用更简洁的代码创建函数式接口的实例,是匿名内部类的一种简化,可以部分取代匿名内部类的作用. 函数式接口:只有一个抽象方法的接口. Lambda表达式又称为匿名函数(anonymous function),表示一类无需定义标识符(函数名)的函数或者子程序,可以看做是一种语法糖. Lambda 表达式语法格式 Lambda表达式在java中由三部分组成:

利用 Lambda 表达式实现 Java 中的惰性求值

Java 中惰性求值的潜能,完全被忽视了(在语言层面上,它仅被用来实现 短路求值 ).更先进的语言,如 Scala,区分了传值调用与传名调用,或者引入了 lazy 这样的关键字. 尽管 Java 8 通过延迟队列的实现(java.util.stream.Stream)在惰性求值的方面有些改进,但是我们会先跳过 Stream,而把重点放在如何使用 lambda 表达式实现一个轻量级的惰性求值. 基于 lambda 的惰性求值 Scala 当我们想对 Scala 中的方法参数进行惰性求值时,我们用"

深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)

最近看了一下java 8的一些新特性,其中最重要的莫过于Lambda表达式了,通过一阵子的学习,原本准备自己写一篇博客的,后来阅读了一位学长翻译过来的博客(原文是Brain Goetz的State of Lambda,下面会给出原文链接),觉得写的十分完美,把我想要写的和我没想到的都罗列了出来,就把这个分享给大家了. 注:原译文见  http://lucida.me/blog/java-8-lambdas-insideout-language-features/ 英语原版见:http://cr.

[转]深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)

以下内容转自: 作者:Lucida 微博:@peng_gong 豆瓣:@figure9 原文链接:http://zh.lucida.me/blog/java-8-lambdas-insideout-language-features 本文谢绝转载,如需转载需征得作者本人同意,谢谢. -------------------------------------内容分割线--------------------------------------------------------- 关于 本文是深入

java语言运算符和表达式

java中的运算符: 算术运算符:+ ,—, *, /(除求整),%(取模求余),++(自增),——(自减)  负值运算符:=,+=,-=,/=,%= 关系运算符:>,<,>=,<=,==,!=        关系运算符作用是比较两边的操作数,结果总是boolean型的 逻辑运算符:!,&,|,^,&&,|| 位运算符::&,|,^,~,>>,<<,>>> 字符串连接运算符:+   除了可用于字符串相连接,也

JSP:使用EL表达式调用java函数

JSP:使用EL表达式调用java函数 使用一个例子 编写一个使用JSP表达式语言的JSP程序,输入一个数字,提交后输出该数字的九九乘法表. java代码实现打印9*9乘法表 Solution.java package method; public class Solution { public static String Mul99(int n){ String result=""; for(int i=1;i<=9;i++){ int temp=i*n; result=res

转 : 终于搞清楚了为什么Java桌面程序总是感觉慢的原因

转自: http://my.oschina.net/u/2306127/blog/370495 终于搞清楚了为什么Java桌面程序总是感觉慢的原因!    按照道理服务器和浏览器都在用脚本和虚拟机,那些不慢,而Java的桌面程序总是感觉响应迟钝呢?主要有两个原因,一是服务器和浏览器都是异步的,提交到渲染出来这中间有足够的时间去处理,而且网络IO要远远慢于本地CPU调用:二是浏览器的界面其实不是JavaScript渲染(很多人可能都会以为HTML是JS绘制的),而是浏览器在操作系统层原生支持的,甚