原文链接 作者:Ted Neward 译者:赵峰
了解Java8 中的lambda表达式
对开发人员来说没有什么比自己选择的语言或平台发布新版本更令人激动了。Java开发者也不例外。实际上,我们更期待新版本的发布,有一部分原因是因为在不久前我们还在考虑Java的前途,因为Java的创造者——Sun在衰落。一次与死亡的擦肩而过会使人更加珍惜生命。但在这种情况下,我们的热情来源不像以前发布版本时那样,这次是来源于事实。Java 8最终会获得一些我们期待了几十年的“现代”语言特性。
当然,Java 8主要的改变集中在lambdas(或者叫闭包),这也是这两篇文章主要讨论的内容。但是一个语言特性,就其本身而言它的出现除非其背后有一定的支持,如果它不实用或有趣。Java 7的几个特点符合这种描述:例如,增强数值文本不能让大多数人注意。
然而,这次不仅仅是作为Java 8函数式语言改变的一个核心部分,而且它们的引入带来了一些能让它们更易使用的附加语言特性,同样一些包的改进也使那些特性能直接使用。这将能让我们更容易的做一个Java开发者。
Java Magazine在之前发表过lambdas的文章,但写这篇文章时可能比较之前的语法有修改了,并且并不是所有的人都有时间去上面阅读。我将假设读者从没有读过那篇文章。
注意:这篇文章是以即将发布的Java SE 8为基础的,同样的,可能与最终发布版本时有一点区别。因为在最终发布之前语法与语义总会改变。
对于那些想更深入的了解这方面内容的人,我推荐Brian Goetz’论文,比如“State of the Lambda: Libraries Edition” 和在Lambda主页上的其它文章,这些文章都有很好的参考价值。
背景:功能函数
Java一直需要功能性对象(也可以称为功能函数),虽然我们在社区中为淡化其的影响而一直挣扎。在Java的早些年,当我们建立GUI时,我们需要像窗口打开、关闭、按钮按下和滚动条移动这样的响应用户事件的代码块。
在Java 1.0中,抽象窗口工具包(AWT)应用被期待像它的C++前辈一样去扩展窗口类和覆盖选择的事件方法;这被认为是笨拙的和不可行的。所以在Java 1.1 Sun给我们一系列监听接口,每一个接口对应一个或多个GUI事件方法。
CODE = OBJECT
代码=对象
随着Java的成长和成熟,我们发现在很多地方我们把代码块当做对象(数据)不仅很有用并且很必要。
但是为了更简单的去写这些类,必须实现这些接口和接口中相关连的方法。Sun给了我们内部类,其中匿名内部类可以在已存在的类的内部不需要特别命名而去实现一个类。(顺便说一句,监听事件并不是在Java历史中唯一的例子。我们稍后会看到更“核心的”接口,例如:Runnable和Comparator。)
内部类对它们来说不管在语法还是语义上都有一些陌生。例如,决定内部类是静态内部类或实例内部类,并不是由指定的关键字决定的(当然静态内部类,可以用static关键字声明),而是由实例被创建的语境决定的。实际情况中,Java开发者经常在面试中被问到Listing 1中所展示的错误。
Listing 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); // Is this a compile error? InstanceOuter.InstanceInner ii = io.new InstanceInner(); // What does this print? ii.printSomething(); // prints 12 // What about this? StaticOuter.StaticInner si = new StaticOuter.StaticInner(); si.printSomething(); // prints 24 } }
像内部类这样的“特点”一直让Java开发者认为是适合面试而不是其它用途的Java角落里的知识——除非我们用到它。即便如此,大多数时候它们只被用在事件处理上。
Above and Beyond超出本文范围的内容
然而,随着语法和语义的越来截止臃肿,系统仍在运行。随着Java的成长和成熟,我们发现很多地方把代码块当作对象(数据)并不仅仅是有用而且是必要的。在Java SE1.2修订后的安全系统发现传入一个代码块在不同的安全上下文中执行非常有用。Java8 修改后的Collection类发现传入一段代码块顺便去了解如何在排序的集合中排序是非常有用的。Swing发现传一段代码块顺便去决定用户打开文件时展示哪些文件很有用,等等。它起作用——虽然它的语法让人很不喜欢。
但是当函数式编程要进入主流编程时,所有人都放弃了。虽然可行(参考这个非常完整的例子),无论如何,函数式编程在Java中都是棘手的。Java需要成长和加入主流的编程语言,并为定义、传递、存储后执行代码块提供一流的语言支持。
Java8: Lambdas,目标类型和词法作用域(Lexical Scoping)
Java 8 介绍了几种新的语言特性目的是让写这样的代码更加容易——其中最主要的是lambda表达式,通俗称为闭包(原因我们以后更说)或者匿名方法。接下来让我们一条一条解释。
Lambda 表达式。从根本上说,lambda表达式只是简单的实现稍后执行的方法。因此,当我们在Listing 2中定义一个Runnable,这个Runnable是用匿名内部类直接实现(这意味着需要写很多行代码)。但是,Java 8中的lambda允许我们像Listing 3中那样实现。
Listing 2
public class Lambdas { public static void main(String... args) { Runnable r = new Runnable() { public void run() { System.out.println("Howdy, world!"); } }; r.run(); } }
Listing 3
public static void main(String... args) { Runnable r2 = () -> System.out.println("Howdy, world!"); r2.run(); }
这两种方法可以得到相同的结果:一个实现Runnable的对象,其中的run()方法被调用,并输出结果。然而,在底层Java 8并不是仅仅实现了一个Runnable接口的匿名类——其中一些需要Java 7 中介绍的调用动态字节码。我们将不会去深入讨论这方面的内容,但是你要知道这不是“仅仅”实现了一个匿名类接口。
函数式接口。Runnable接口、Callable<T>接口、Comparator<T>接口,和Java中定义的其它大量接口——在Java 8中我们称为函数式接口:它们是只需要实现一个方法去满足需求的接口。这就是为什么它实现起来很简洁的原因,因为这样你可以很确切的知道需要实现哪个方法。
Java 8 的设计者给了我们一个注释,@FunctionalInterface,它被当作接口使用lambdas的一个文档提示,但是编译器不需要这个——它决定了”功能性接口”是从接口的结构而来,而不是从注释。
这一整篇文章,我们将会用Runnale和Comparator<T>接口作为例子,这不是因为它们有什么特别之处,除了它们是单方法接口外。任何开发者任何时间可以定义一个新的接口,像下面的例子那样,它都可以使用lambda实现。
interface Something { public String doit(Integer i); }
Something接口是像Runnable和Comparator<T>那样完全合法的功能性接口;我们将在看一些lambda例子后再分析这个。
语法。Java中的lambda本质上有三部分组成:一些参数加上括号,一个箭头和实体,它可以是一个单独的表达式或一块代码。像Listing 2中的例子那样,run不需要参数并且返回void,所以那个不需要参数和返回值。但是Listing 4中展示的Comparator<T>例子,符合上面的三个条件。Comparator需要两个string并且需要返回integer类型的负值(小于)、正值(大于)和0(相等)。
Listing 4
public static void main(String... args) { Comparator<String> c = (String lhs, String rhs) -> lhs.compareTo(rhs); int result = c.compare("Hello", "World"); }
如果lambda本身需要多个表达式,则表达式可以被当做返回值调用,像其它的Java代码块那样(参考Listing 5)。
Listing 5
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"); }
(像Listing 5列出的花括号中的代码将会在未来几年主导Java留言板和博客。)lambda写代码有几个限制,其中大部分都很直观——不能使用”break”或”continue”跳出lambda,并且如果lambda返回一个值,每一个代码路径都要返回一个值或抛出异常,等等。普通的方法也有类似的规则,所以不要大惊小怪。
推理类型。另一个被其它语言使用的概念是推理类型:编译器应该足够聪明去辨认出这个参数应该是什么类型,而不是强制开发者是重新输入参数。
就像Listing 5中的Comparator的例子。如果目标类型是Comparator<String>,传入lambda中的类型就必须是string;否则代码将不能编译。
在这种情况下在lhs和rhs前面再声明String是完全多余的,多谢Java 8增强了类型推断机制,如Listing 6他们是完全可选的。
Listing 6
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"); }
语言规范中有准确的规则时,需要明确声明lambda正式类型,但在大多数情况下它被当做默认的,而不是需要特别注明的,所以参数类型的声明可能会被完全排除。
Java的lambda语法在Java史中一个有趣的影响是,我们发现不需要分配一个指定类型的引用对象(参考Listing 7)——至少不是没有帮助。
Listing 7
public static void main4(String... args) { Object o = () -> System.out.println("Howdy, world!"); // will not compile }
编译器可能会抱怨Object不是一个功能性接口,尽管真正的原因是编译器并不能理解这个lambda需要实现哪个功能性接口:Runnable或者是其它的?我们可以用一个例子来帮助编译器,如Listing 8。
Listing 8
public static void main4(String... args) { Object o = (Runnable) () -> System.out.println("Howdy, world!"); // now we're all good }
从前lambda语法适用于任何接口,所以一个lambda可以很容易实现一个定制接口,像Listing 9。顺便说一句,原始类型与它们的包装类型在Lambda类型签名中一样。
Listing 9
Something s = (Integer i) -> { return i.toString(); }; System.out.println(s.doit(4));
再一次,这是真正新的东西;Java 8只是应用了Java的长期原则、模式和语法。如果还是明白,就花几分钟时间去探索下代码中的类型推理。
词法作用域(Lexical scoping)。这是新的,对于编译器在lambda和内部类中处理名称的方式。参考在Listing 10内部类的例子。
Listing 10
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(); } }
当我运行Listing 10中的代码,在我们机器上会直接输出“Hello$1@f7ce53”。原因很简单:在匿名Runnable的实现中包括的this和toString是绑定在匿名内部类实现的,因为这是满足要求的最内层范围。
如果我们需要打印出Hello版本的toString,我们不得不明确使用Java规范中内部类的”outerthis”语法,如Listing 11。
Listing 11
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()"; } }
坦白的讲,这是其中一点比起内部类解决的问题,它给我们制造了更多的困惑。当然,直到解释this关键字出现在这不直观的语法中的原因时发现它是有意义的,但是它的意义在于让狡辩者找到借口。
然而,Lambdas是语法作用域,意义是lambda辨认出它定义周围的直接环境作为它的下一层作用域。所以Listing 12中的lambda例子会产生Listing 11中第二个例子的效果,但这种形式语法上更直观。
Listing 12
class Hello { public Runnable r = () -> { System.out.println(this); System.out.println(toString()); }; public String toString() { return "Hello's custom toString()"; } }
顺便说一句,这意味着this不是引用的lambda,这可能在某些情况下很有用——但是这种情况非常少。而且,如果这种情况出现了(例如,也许一个lambda需要返回一个lambda,并且要返回它自身),这里有一个相对简单的方法,我们稍后会讲。
变量捕捉(Variable capture)。lambda被称为是闭包的一部分原因是,一个函数文本(function literal)(比如我们之前写过的)能够“覆盖(Close over)”在作用域内函数文本体之外的引用变量(对于Java,这通常是lambda的方法被定义)。内部类也能这样做,但是所有令Java开发都失望的部分大都关于内部类,实际上,只能从作用域引用在它本身定义的顶部的”final”变量。
Lambda放宽了限制,但是只是放宽了一点:只要引用变量还是”有效的final“,这就是意味着它还是final,这样lambda可以引用它(例如Listing 13)。因为message在main内不会被修改,包括lambda被定义。这就是有效的final,并且,有资格被Runnable lambda存储在r中。(译者注:这里的意思是message不会被修改,而不是不能被修改)
Listing 13
public static void main(String... args) { String message = "Howdy, world!"; Runnable r = () -> System.out.println(message); r.run(); }
从表面上看好像没有什么,但记住lambda语法规则并不改变Java的性质。总的来说,引用在lambda的定义之后,是可以被访问和修改的,例如Listing 14。
Listing 14
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内部类中使用message,这个message就必须是final)。这个在Java社区中仍被视为一个bug或者特性,但目前就是这样,并且,为了避免出错,开发者应该理解Lambda是怎么捕获变量的。(实事上,这种行为并不是新的——这只是重做Java在减少输入这样已有的功能,和从编程器处得到更多的支持。)
方法引用。到目前为止,我们所有的lambda的例子都是匿名的——本质上,lambda需要在它使用的地方定义。这种对单一场景使用非常有帮助,但是对多场景使用用处不大。例如,下面的Person类(这时请忽略封装)。
class Person { public String firstName; public String lastName; public int age; });
如果把一个Person放到SortedSet中,或者它需要以某种形式排序,我们将需要不同的机制来决定Person怎么排序——例如,有时是以firstName,有时会以lastName排序。这就是Comparator<T>的作用:允许我们通过传入Comparator<T>一个实例来决定怎么排序。
SCOPE IT OUT注意
Lambdas是作用域,意思是lambda会辨认出它定义周围的直接环境作为它的下一层作用域。
Lambda能写出比较简单的排序代码,如Listing 15。但是,使用firstName排序Person对象,可能会在之后用到很多次,这现在这样的代码无疑违反了不重复自己(Dont’t Repeat Yourself)原则。
Listing 15
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,像Listing 16。然后,Comparator<T>也像其它静态字段一样被引用,像Listing 17。我确信函数式编程的狂热爱好者非常喜欢这种方式,因为它允许以多种方式组合功能。
Listing 16
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 + "]"; } }
Listing 17
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<T>的方法然后直接使用相反——这正是一个方法引用所允许的(如Listing 18)。注意用::形式,这告诉编译器定义在Person里的compareFirstNames在这里应该被用到,而不是简单的字面方法(method literal)。
Listing 18
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); }
对那些好奇的人来说,这是另一种使用的方法,我们可以使用compareFirstNames方法去创建一个Comparator<Person>实例,像下面这样:
Comparator cf = Person::compareFirstNames;
当然,还能然再简洁,我们还可以通过使用一些新的包特性来完全避免一些语法开销,利用高阶的函数(意思是,更粗略,一个函数传另一些函数)从根本上避免之前的一行一行的代码。
Arrays.sort(people, comparing(
Person::getFirstName));
这就是函数式编程技术为什么那么强大的一部分原因。
虚拟扩展方法。然而,关于接口被提及的一个缺点是,它们没有默认实现,既然当实现是非常明显的时候。例如,假如有一个Relational接口,它定义了一系列假想的关系方法(大于,小于,大于或等于,等等)。只要其中的一个方法被定义,你就会发现其它的方法可以依据这个方法实现。实际上,如果提前知道Comparable<T>中的compare方法,所有的这些方法都可以通过compare方法实现。但是,接口不能有默认行为,并且抽象类也是一个类,Java只能实现单继承。
然而,在Java 8中这样的函数变的很普遍,它变的更加重要的原因是能够指定默认行为没失去接口的“接口性”。因此,Java 8现在介绍虚拟扩展方法(在之前的版本中被称为保守方法),如果没有派生的实现,本质上允许一个接口提供一个默认方法。
回想一下Iterator接口。现在它有三个方法(hasNext,next和remove),每一个都必须定义。但是,在iteration流中“跳跃”到下一个元素可能很有用。并且,因为Iterator的这个方法很容易利用其它三个方法实现,我们在Listing 19中提供了实现。
Listing 19
interface Iterator<T> { boolean hasNext(); T next(); void remove(); void skip(int i) default { for (; i > 0 && hasNext(); i--) next(); } }
有一些可能会在Java社区中引起争议,声明这些是弱化接口的作用,并运用这种形式实现多继承。在某种程度上就是这样,特别是在默认实现的优先级方面(如果一个类继承了多个接口,并且相同的方法有不同的实现的情况)的规则需要大量的研究。
了解更多 |
但是,正如它的名字暗示的一样,虚拟扩展方法提供了一个强大的扩展己存在接口的机制,并且不需要在它的实现类中再去实现该方法。运用这样的机制,Oracle可以为现有的包提供附加的、更强大的实现,而不需要开发者去逐一实现其下的类。没有SkippingIterator类,现在开发都需要去寻找集合去提供支持。实际上,代码不需要修改任何地方,所有的Iterator<T>,不管什么时候写的,它将自动拥有这个行为。
通过虚拟扩展方法,在Collection类中将会发生很多的改变。好的消息是你的Collection类将会得到很多新的方法,更好的消息是你的代码在此期间并不需要做任何改变。不好的消息是我们将在这个系列的另一篇文章中继续讨论。
总结
Lambdas能给Java带来很多改变,包括怎么样写和设计Java代码。其中的一些改变是函数式编程带来的,这将会改变Java程序员写代码的方式——这是个机会也是个挑战。
我们将在另一篇文章中更深入的讨论这些改变给Java库带来的影响,并且我们将会花一些时间去讨论这些新的API、接口和类去设计一些以前由于内部类的原因而不能去实现的方法。
Java 8是一个非常有趣的版本。系好安全带,这将是一次火箭式旅行。
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/140473.html