Java SE 8 Lambda 特性与基本原理

来源:互联网 发布:数据分析的实施过程 编辑:程序博客网 时间:2024/06/05 23:50

Lambda 语言特性与原理

java se 8 edition

        Java语言规范-JSR335 中对Java语言一些新功能的非正式描述这些增强性功能已被OpenJDK 的Lambda项目实现。并且该文章细化了上次发布在2011年九月份的迭代一些JSR关于语言特性变更的正式描述戳JSR-335同时OpenJDK的开发者预览版已经发布(Developer Preview), 一些以往的设计文档查看(OpenJDK project page.),这里(State of the Lambda, Libraries Edition)还有一篇相关文档描述了API中关于JSR335的一些变化。


Lambda 项目力图使Java能够方便的以通用语法的方式将建模代码(modeling code)作为数据。主要的新语言特性包括:

  • Lambda 表达式(通俗的称谓,“闭包”或者“匿名方法”)

  • 方法和构造器引用

  • 扩充目标类型和类型推断

  • 接口中的默认方法和静态方法


下面便是详细的描述与解释。


1.背景


Java主要是一门面向对象的编程语言。在面向对象和函数式语言中,基本的数据值可以被动态地封装到程序的行为中:如面向对象语言中的包含方法的对象,以及函数式语言中的函数。这种相同点也许并不明显,这是因为对一系列单独声明的包含许多域和方法的类的实例化使得java对象往往相对重量级些。

但是一些对象本质上只是作为一个方程确实相当普遍的。 一个典型的用例中,一个java API定义了一个接口,有时被称作‘回调接口’, 希望用户在调用该API时提供该接口的一个实例,举个例子:

public interface ActionListener {     void actionPerformed(ActionEvent e);}


用户通常会实例化一个匿名内部类,而不会为一个只调用一次的接口实例单独声明一个实现了ActionListener的类:

button.addActionListener(new ActionListener() {   public void actionPerformed(ActionEvent e) {     ui.dazzle(e.getModifiers());  }});


许多有用的库都依赖这种模式。对于并行(parallel)API尤为重要,以并行方式执行的代码必须独立于它所运行的线程。并行编程领域是特别有趣的,因为随着摩尔定律不断给我们带来更多的内核,而不是更快的内核,串行(serial)API越来越受限于正在减少的可用的处理能力。

考虑到回调与其他函数式编程风格越来越多的关联性,尽可能的将Java中建模代码作为数据变得轻量些也就尤为重要了。从这方面看,匿名内部类存在以下几个重要缺陷:


  1. 笨重的语法

  2. 变量名称与this引用引起混乱

  3. 不可变的类加载和实例创建语义

  4. 无法访问非final的局部变量

  5. 无法抽象控制流


这个项目解决了许多问题。第1)2)两个问题通过引入新的更加简洁的局部变量规则和表达形式得以解决。第3)个问题通过定义新的更加灵活的语义表达式来避开。第4)个问题通过允许编译器推断最终结果(可以访问最终有效的局部变量)得以改善。

然而,这个项目的目标不是解决所有这些内部类的问题。 因为4)5)都不在这个项目的范围内(尽管可能在将来的语言新特性中得到解决)。


2. 函数式接口(Functional Interface)


尽管匿名内部类存在局限性, 但是它却有着非常好的适合java 类型系统的属性:一个含有函数值的接口类型。这一特点有这么几点便利之处:接口本身就是类型系统的一部分;天生具有运行时表达方式;并且它具有Javadoc注释表示的非正式契约,如可交互的操作,断言(原文:they carry with them informal contracts expressed by Javadoc comments, such as an assertion that an operation is commutative)。


如上面使用的ActionListener接口只有一个方法。许多常用的回调接口都有这样的属性,如Runnable,Comparator。我们给所有这些只有一个名字的接口取了一个名字:函数式接口(functional interfaces)。(之前被叫做SAM类型,意思是“   单一抽象方法”(Single Abstract Method)类型。)


不需要做特殊的工作便可声明一个函数式接口;编译器会通过它的机构进行识别。(识别过程不仅仅是计算声明方法的个数;一个接口或许有多个方法继承自Object,如toString(), 还有可能存在static或者default方法,这些都多余了一个方法的限制)然而,API作者可以使用@FunctionalInterface注解来捕获一个自己想要的函数式接口(而不是碰巧只有一个方法)。在这种情况下编译器会验证该接口是否符合函数式接口的结构要求。


早期存在一个可选的(补充的)创建函数类型的方式,被称作'箭头类型'(arrow type), 需要引入新结构的函数类型。如一个将字符串和对象转换为int的函数类型可以写成(String, Object) -> int.因为以下几个缺点,我们放弃了这种想法:


  • 使类型系统复杂化,更加混合结构与名义上的类型(Java几乎完全是名义上的类型)

  • 使库的风格发生分歧--一些库继续使用回调接口,而另一些使用函数式类型

  • 这种语法不够灵活,尤其是受控异常被包裹在内时。

  • 不可能为每一个不同的函数式类型提供一个运行时表示,也就是说开发人员将更多的接触和受限于类型擦出。举个例子,不可能(或许出人意料)重载m(T->U)和m(X->Y).


因此,我们遵循‘用你所知’的思想路径 --因为当前库中广泛使用了函数式接口,我们将利用这种模式,使得当前库同样可以使用Lambda表达式。

举例说明,下面是Java SE 7中已经存在的非常符合新语言特性的函数式接口, 我们下面的诸多例子中便使用了其中的几个:


  • java.lang.Runnable

  • java.util.concurrent.Callable

  • java.security.PrivilegedAction

  • java.util.Comparator

  • java.io.FileFilter

  • java.beans.PropertyChangeListener


另外, Java SE 8 添加了一个新的 java.util.function包,其中包含了经常被用到的函数式接口,如:


  • Predicate<T> ---- 表示一个布尔值函数

  • Consumer<T> ----  表示一个没有返回值的函数

  • Function<T, R> ---- 表示一个将T转换为R的函数

  • Supplier<T> ---- 表示一个生成新实例的函数

  • UnaryOperator<T> ---- 表示一个操作并返回同一个类型值得函数

  • BinaryOperator<T> ----- 表示一个接受两个同样类型值得参数,并返回一个相同类型值得函数


除了这些基本的“形状”,还有一些像IntSupplier或者LongBinaryOperator这样的基本类型的特殊化。(我门只提供了int,long,和double的特殊化的函数式接口而不是全部基本类型的实现,因为其他基本类型可以通过转化得到),同样的,也有一些多参数量的特殊化的函数式接口,像BiFunction<T,U,R>, 代表一个将(T,U)转化为R的函数。


3. Lambda 表达式


匿名内部类最大的痛楚就是笨重。我们可以称之为“垂直问题”:第一部分中的ActionListener实例用了五行代码才实现一个一个简单的动作。

Lambda表达式是匿名方法,旨在使用轻量级机制代替匿名内部类的机械性来解决“垂直问题”。

下面是几个lambda表达式的例子:

(int x, int y) -> x + y() -> 42 (String s) -> { System.out.println(s); }


第一个表达式包含两个参数x,y,并且返回他们的和。第二没有参数,返回Integer类型的值42。第三个有一个string类型的参数没有返回值,只是将参数值打印到控制台上。


Lambda表达式通常的语法包括一个参数列表,箭头令牌 ->, 以及主体。主体可以是单行表达式,也可以是代码块。对于表达式形式的主体,只是简单的求值并返回。而对于代码块形式的主体会像方法体一样执行,然后return语句将控制权返回给匿名方法的调用者;break和continue不可用于顶层代码中(译注:意思应该是不可以作为主体的return),但在循环体中可以使用;如果主体产生一个结果,每个控制路径必须返回或者抛出异常。


正如上文所述,在lambda表达式非常小的这种常见情况下,对语句进行了优化。举个例子,表达式体的形式去除了相对整个表达式而言占据了大部分语法开销的return关键字。


lambda表达式通常会频繁的出现在嵌套的上下文环境中,如方法调用的参数,或者lambda表达式的返回值中。为了最小化对这种情况的干扰,应避免使用分隔符。然而,当需要将整个表达式分开时它会非常有用,就像其他表达式一样,可以使用圆括号括起来。


下面是几个Lambda表达式出现在声明语句中的例子:

FileFilter java = (File f) -> f.getName().endsWith(".java"); String user = doPrivileged(() -> System.getProperty("user.name")); new Thread(() -> {  connectToService();  sendNotification(); }).start();


4, 目标类型(Target typing)


注意,函数式接口的名字不是lambda表达式的一部分。那么一个lambda表达式代表什么对象呢?它的类型是根据上下文环境进行推断的。举个例子,下面的lambda表达式是一个ActionListener:

ActionListener l = (ActionEvent e) -> ui.dazzle(e.getModifiers());


这样就有一种可能结果就是同样的lambda表达式在不同的环境下可能有不同的类型:

Callable<String> c = () -> "done";PrivilegedAction<String> a = () -> "done";


第一种情况lambda表达式()->"done"表示一个Callable的实例。第二种情况同样的lambda表达式却表示一个PrivilegedAction实例。

编译器会负责推断每一个lambda表达式的类型。它使用lambda表达式所在环境的期望类型,称之为“目标类型”。一个lambda表达式只能出现在一个目标类型是函数式接口的环境中。

当然,没有lambda表达式可以适用于每一个可能的目标类型。编译器会检查lambda表达式所使用的类型与目标类型的方法签名是否一致。就是说,如果以下所有条件得到满足,那么这个lambda表达式便可以赋值给目标类型T:


  • T是一个函数式接口

  • lambda表达式与T方法的参数列表的参数个数与类型都一致

  • lambda表达式的返回值类型与T方法的相兼容

  • lambda表达式抛出的异常与T方法的相兼容


既然函数式接口目标类型已经“知道”了lambda表达式正式的参数都是什么类型,那么也就没有必要重复他们。使用目标类型便可以推断lambda表达式的参数类型:

Comparator<String> c = (s1, s2) -> s1.compareToIgnoreCase(s2);


这里,编译器推断s1,与s2是String。另外,当只有一个类型推断的参数时(一个很常见的情况),参数两侧的圆括号便是可选的了:

FileFilter java = f -> f.getName().endsWith(".java");button.addActionListener(e -> ui.dazzle(e.getModifiers()));


这一增强功能更贴近了一个理想的设计目标:“不要把垂直问题转变成水平问题。”我们希望读者不需要费太大力就能品尝到lambda表达式这块肥肉。


Lambda表达式不是第一个依赖上下文确定类型的表达式:如泛型方法的调用,以及“钻石”构造器的调用同样依赖于所赋值的目标类型进行类型检查:

List<String> ls = Collections.emptyList();List<Integer> li = Collections.emptyList();Map<String,Integer> m1 = new HashMap<>();Map<Integer,String> m2 = new HashMap<>();


5,目标类型上下文


前面我们已经提到Lambda表达式只能出现在具有目标类型的上下文环境中。下面例举的几种情况的上下文含有目标类型:


  • 变量定义

  • 赋值语句

  • 返回语句

  • 数组初始化

  • 方法或构造器参数

  • Lambda表达式主体

  • 条件表达式(?:)

  • Cast表达式


在前三种情况中,目标类型只是简单的分配或者返回。

Comparator<String> c;c = (String s1, String s2) -> s1.compareToIgnoreCase(s2);public Runnable toDoLater() {  return () -> {    System.out.println("later");  };}


数组初始化上下文有点像赋值语句,除了“变量”是数组的组件以及类型来自于数组的类型。

filterFiles(new FileFilter[] {                f -> f.exists(), f -f.canRead(), f -f.getName().startsWith("q")             });


对于方法参数这种情况,事情变得就有点复杂了:目标类型推断就需要与另外两个语言特性,重载决策和类型参数推断相互作用了。


重载决策涉及到对于特定的方法调用找出最好的方法声明。因为不同的方法声明有着不同的签名,这就 影响到作为参数的Lambda表达式的目标类型。编译器会用它所知道的关于Lambda表达式的一切做出选择。如果Lambda表达式是显示的类型(指定参数类型的),编译器将不仅知道参数类型,也会知道方法体内所有的返回表达式的类型。如果Lambda表达式是隐式类型(推断参数类型)重载决策将会忽略Lambda主体,而只考虑Lambda表达式参数个数。


如果对于最好方法的选择是模糊的,转型或者明确的Lambda表达式会对编译器消除歧义提供额外的类型信息。如果一个Lambda表达式的返回类型的目标取决于类型参数推断,那么Lambda表达式主体或许会为编译器进行推断提供类型信息。

List<Person> ps = ...String<String> names = ps.stream().map(p -> p.getName());


在这里,ps是一个List<Person>, 那么ps.stream() 是一个Stream<Person>. map()方法中的R是泛型的,其中map()方法的参数是Function<T, R> , 其中T是Stream的元素类型。(这时T是已知的Person),一旦重载被选定并且Lambda表达式的目标类型已知了,我们就需要推断R;我们对Lambda表达式的主体进行类型检查,并且发现它的返回值是String,因此R便是String,那么map()表达式便有一个Stream<String>的类型。大部分时间,编译器能推断这一切,但是如果卡在了里面,我们可以通过显示的Lambda(给参数p一个显示的类型),将Lambda表达式转型为显示的目标类型如Function<Person, String>,或者为泛型参数R提供显示类型见证(.<String>map(p ->p.getName())来提供额外的类型信息。


Lambda表达式自身为主体提供目标类型,在这种情况下,派生自外部Lambda表达式的目标类型。这使得写一个返回函数的函数变得很方便:

Supplier<Runnable> c = () -> () -> { System.out.println("hi"); };


类似的, 条件表达式可以从上下文中“遗传”目标类型;

Callable<Integer> c = flag ? (() -> 23) : (() -> 42);


最后,如果无法从上下文中推断,转型表达式可以提供一种机制来显示的指定Lambda表达式的目标类型:

// IllegalObject o = () -> { System.out.println("hi"); };Object o = (Runnable) () -> { System.out.println("hi"); };


转型也可以用于解决一个方法声明被一个含有无关的函数式接口类型的方法重载时歧义。

在编译器中目标类型的扩充作用不仅限于Lambda表达式:泛型方法调用和钻石构造器调用都能利用目标类型,下面的声明在Java SE 7 中是不合法的,但在Java SE 8 中可以运行:

List<String> ls =  Collections.checkedList(new ArrayList<>(), String.class);Set<Integer> si = flag ? Collections.singleton(23)                       : Collections.emptySet();


6 , 词法域(Lexical Scoping)


    确定内部类中变量名字(包括this)的意义要比在顶级类中困难的多,并且很容易出错。继承成员--包括类对象中的方法--可能不小心就覆盖了外部类的声明, 未加限定的this引用总是指向外部类自身。

    Lambda表达式更加简单:他们不会从超类中继承任何名字,也不会引入任何新的级别的作用域。相反,他们具有词法作用域,意味着主体中的名字是解释执行的,就像是在封闭的环境中(通过对Lambda表达式形式参数添加名字)。作为一个自然的延伸,this关键字以及对成员的引用与在Lambda表达式的外部类中直接饮用有着相同的意义。

    为了说明这点,下面程序将会在控制台上打印两次“Hello, World!”:

public class Hello {  Runnable r1 = () -> { System.out.println(this); }  Runnable r2 = () -> { System.out.println(toString()); }  public String toString(return "Hello, world!"; }  public static void main(String... args{    new Hello().r1.run();    new Hello().r2.run();  }}


    如果同样的使用匿名内部类,也许打印Hello$1@5b89a773 andHello$2@537a7706会让程序员感到惊讶。

    本地参数化的结构模式如for循环和Catch语句同样符合词法作用域, Lambda表达式参数不可以覆盖任何闭合环境中的本地变量。


7, 变量捕获(Variable captrue)


    在Java SE 7 中,编译器对内部类中封闭环境下的本地变量(捕获变量)引用的检查是非常严格的:如果捕获变量没有声明为final,我们将会得到一个变异错误。现在我们放松了这中限制---对于Lambda表达式和内部类----通过允许捕获有效地final本地变量(effectively final local variable)

    通俗的讲,如果一个本地变量的初始值没有改变我们便称之为有效的final变量。换句话说,声明为final不会导致编译失败。

Callable<String> helloCallable(String name) {  String hello = "Hello";  return () -> (hello + ", " + name);}

    对this的引用,本质上讲是对final局部变量的引用---包括对未限定的字段引用或者方法调用这样的隐式引用。包含这种引用的Lambda主体会捕获this的恰当实例。其他情况下,Lambda不会保留对this的引用。(注:大致意思应该是,如果不访问外围类的变量,就不会保留对外围类的引用)

    这对内存管理带来有益的影响:内部类总是会包含一个对外部类实例的强引用,而Lambda在没有访问外部类成员的情况下是不会保留对外部类的引用的。内部类的这一特征通常会是引起内存泄露的罪魁祸首。

    尽管我们放宽了对变量捕获的语法限制,但是我们依旧不允许访问可变的局部变量,这是因为像这样的语法风格:

int sum = 0;list.forEach(e -> { sum += e.size(); }); // ERROR?

    本质上是串行的;很难写出没有竞态条件的Lambda表达式。除非我们想强制(最好在编译时期)这样一个方程不能逃离它的捕获线程(注:不可多线程访问),但是这一特性或许会适得其反,带来更多的麻烦。Lambda表达式封装的是值,而不是变量。

    另一个不可以捕获可变变量的原因是除了不可变外没有更好的办法解决积累问题,我们把这个问题看做Reduction。java.util.stream 包通用的及专门的(像sum,min,max)集合或其他数据结构上的Reduction.举个例子,我们可以下像这样以串行和并行都安全的方式进行Reduction,而不是使用forEach和可变变量:

int sum = list.stream()              .maptoint(e -> e.size())              .sum();

    sum()方法提供了方便,但是与下面Reduction中更一般的形式是等价的:

int sum = list.stream()              .mapToInt(e -> e.size())              .reduce(0, (x,y) -> x+y);

    Reduction需要一个基数(以防输入是空),以及一个操作符(这里是加法),然后计算下面的表达式

0 + list[0] + list[1] + list[2] + ...

    Reduction同样可以完成其他的操作如minimum,maximum,product等,并且如果操作符是相关联的,很容易安全的并行化。因此,没有支持本质上是串行的并且容易产生数据争用的惯用语法,我们选择提供支持表达并行计算以及不太容易出错的库。


8,方法引用(Method reference)


    Lambda表达式允许我们定义匿名方法并且可以作为函数式接口的实例。通常我们期望使用存在的方法做同一件事情。

    方法引用是一种跟Lambda表达式有同样待遇的表达式(需要目标类型,作为函数式接口的实例),但是不同的是他们不提供方法体,而是引用一个已存在的方法的名称。

    举个例子,考虑一个Person类,我们可以按名字跟年龄进行排序:

class Person {     private final String name;    private final int age;    public int getAge() return age; }    public String getName() return name; }   ...}Person[] people = ...Comparator<Person> byName = Comparator.comparing(p -> p.getName());Arrays.sort(people, byName);


    我们可以使用方法引用来重写这个功能:

Comparator<Person> byName = Comparator.comparing(Person::getName);

    这里,表达式Person::getName可以看做是一个对已命名方法进行简单传参调用并返回结果的Lambda表达式。尽管方法引用可能不会有更加紧凑的语法了(就像本例),但是它表达清晰---我们想调用有有一个名称的方法,并且我们可以直接引用它的名字。


    因为函数式接口的方法的参数类型在隐式的方法调用中作为参数,所以引用方法签名允许操作参数--通过拓展,装箱,分组--就像方法调用一样。

Consumer<Integer> b1 = System::exit;   // void exit(int status)Consumer<String[]> b2 = Arrays::sort;  // void sort(Object[] a)Consumer<String> b3 = MyProgram::main; // void main(String... args)Runnable r = MyProgram::main;          // void main(String... args)


9,多种方法引用


有多重不同的方法引用,每种都有些轻微的语法不同:

  • 静态方法(ClassName::methName)

  • 某对象的实例方法(instanceRef::methName)

  • 某实例的超类方法(super::methName)

  • 某一类型的任意对象的实例方法(ClassName::methName)

  • 类的构造器引用(ClassName::new)

  • 数组的构造器引用(TypeName[]::new)


对于静态方法引用,方法所属的类在::分割符之前,就像Integer::sum。

对于某一对象的实例方法引用,一个代表对象引用的表达式在分隔符之前:

Set<String> knownNames = ...Predicate<String> isKnown = knownNames::contains;


    这里隐式的Lambda表达式会捕获被knownNames引用的String对象,并且Lambda表达式主体会使用该对象作为接受者调用Set.contains方法。

    这种可以引用某一特定对象方法的能力提供了在不同函数式接口类型之间进行转换的方便途径

Callable<Path> c = ...PrivilegedAction<Path> a = c::call;


    对于任意对象的实例方法引用,方法所属对象的类型在::分隔符之前,并且方法调用的接受者是函数式接口的第一个参数:

Function<StringString> upperfier = String::toUpperCase;


    这里,隐式Lambda表达式有一个参数,要被转化为大写的字符串,现在变成了toUpperCase()方法调用的接受者。

    如果实例方法的类是泛型的,可以在::分隔符前提供它的类型参数,或者在大多数情况下,编译器可以根据目标类型进行推断。

    注意:静态方法引用的语法可能会被理解为某个类的实例方法的引用。编译器通过尝试对两种情况进行判断来确定哪一个是我们想要的(记住:实例方法少一个参数)。

    对于所有形式的方法引用,如有必要方法的参数类型会被进行推断,或者在::分隔符后显示的指定。

    构造器只需使用new, 便可以像静态方法那样被引用。

SocketImplFactory factory = MySocketImpl::new;

    如果一个类有多个构造器,那么目标类型的方法签名会被用来选择一个最匹配的构造器, 构造器调用时会使用同样的策略。

    对于内部类来说,没有语法支持显示地提供一个外围类实例构造器的引用。

    如果被实例化的类是泛型的,那么参数类型可以在类的名称后指定,或者像‘钻石’构造器调用那样进行推断。

    还有一个数组的构造器引用的特殊语法,即把数组当做有一个接受int参数的构造器,举个例子:

IntFunction<int[]> arrayMaker = int[]::new;int[] array = arrayMaker.apply(10);  // creates an int[10]


10,默认和静态接口方法


    Lambda表达式跟方法引用为Java语言增添了许多表现力,但是真正完成我们以符合习惯的将代码数据化(make code-as-data)的目标的关键是对标准核心库的调整。

    Java SE 7 中对现有库添加新的功能还是有些困难。尤其是接口一旦发布便是不可改变的了;除非我们同时更新了一个接口所有的实现,否则对接口添加一个新方法会使得现有实现全部挂掉。默认方法(之前被称为virtual extension 方法,或者defender方法)的目的是使得接口能以兼容首发版本的方式进行演化。

    标准集合API显然应该体统新的有好的Lambda操作。举个例子,removeAll方法广义的讲应该可以删除含有任意可以表示为函数式接口Predicate的属性的任何元素。但是这个新方法应该定义到哪呢?我们不能在Collection接口中添加一个抽象的方法---许多已经存在的实现并不会知道这一改变。我们或许可以在Collections工具类中添加静态方法,但这将会使得新的Lambda操作降到二级地位。

    默认方法提供了一种更加面向对象的方式向接口中添加具体的行为。这是一种新的方法:接口方法既可以是abstract的也可以是default的。默认方法有一个被类继承但没有覆盖的实现(详情看下一部分)。函数式接口中的默认方法不影响只有一个抽象方法的限制。举个例子,我们可以有(但是没有真正添加)一个skip方法在Iterator中,像下面这样:

interface Iterator<E{    boolean hasNext();    next();    void remove();    default void skip(int i) {        for (; i > 0 && hasNext(); i--) next();    }}

    鉴于以上Iterator的定义,所有实现了Iterator的类都将继承skip方法。从客户端角度来看,skip只是另一个接口提供的虚方法。在一个没有覆盖skip方法Iterator子类上调用skip方法结果是调用默认实现:多次调用hasNext和next方法。如果一个想要以一个更好的实现覆盖skip方法--举个例子,通过推进一个私有的游标,或者实现线程安全---这一切都是可以的。

    当一个接口继承另一个接口时,它可以为继承自父接口的抽象方法添加defult,还可以提供新的默认方法来覆盖继承自父接口的默认方法,或者重新将一个默认方法声明为抽象的。

    除了允许在接口的默认方法中添加实现代码,Java SE 8 同样引用了在接口中定义静态方法的能力。这就允许某一特定接口的辅助方法可以与接口共存,而不是在另一个类中(通常以接口名称的复数形式命名)。举个例子,Comparator后来有了一个用来生成比较器的静态辅助方法,该方法接受一个提取可比较排序键的函数(Function实例)并且返回一个Comparator实例:

public static <T, U extends Comparable<? super U>> Comparator<T> comparing(Function<TUkeyExtractor{    return (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));}


11,默认方法的继承


    默认方法像其他方法一样可以被继承,大多数情况下,继承会像我们所期望的那样。然而,当类或接口的多个超类型提供了多个相同方法签名的方法时,继承规则会遵循以下两个基本原则来尝试解决这一冲突:

  • 类方法声明优先于接口默认方法。无论类方法声明是具体的还是抽象的,都优先于接口默认方法。(所以defult关键字:默认方法是在类层次结构中什么也没做时的 一个备用方案。

  • 被其他类或接口覆盖的方法会被忽略。这种情况会出现在多个超类型继承自一个共同的祖先时。

    举个例子说明第二个规则如果发挥作用,Collection和List接口提供了不同的removeAll方法的默认实现,并且Queue 继承了Collection的默认实现;在下面的 implements语句中,List声明的默认方法优先于已被Queue继承的Collection的默认方法:

class LinkedList<E> implements List<E>, Queue<E> { ... }

    如果两个独立定义的默认方法冲突,或者一个默认方法与一个抽象方法冲突,将导致编译错误。在这种情况下,程序员必须显示的覆盖超类中的方法。通常,这意味着选择了一个首选的默认方法,并且定义一个方法体调用首选的默认方法。一个对super的增强语法支持对某个特定父接口默认实现的调用:

interface Robot implements ArtistGun {    default void draw() { Artist.super.draw(); }}

    super 前的名称必须引用一个定义了或继承了被调用默认方法的直接父接口。这种形式的方法调用不仅限于简单的消除歧义---还可以用于任何的方法调用,包括类和接口中。

    implements或extends语句中接口声明的顺序,或着某个接口被“第一个”实现了等等,这些在任何情况下都不会影响继承。


12, 融会贯通


    Lambda表达式的语言特性与标准库是被设计成一体的。为了说明这点,我们考虑这样一个任务,对一个People的列表,以LastName排序。

    目前我们可以这样写:

List<Person> people = ...Collections.sort(people, new Comparator<Person>() {    public int compare(Person x, Person y) {        return x.getLastName().compareTo(y.getLastName());    }});

    这是一个非常冗长的方式。

    使用lambda表达式,我们可以使它更加简洁:

Collections.sort(people,                  (Person xPerson y-x.getLastName().compareTo(y.getLastName()));


然而, 更加简洁却意味着不在抽象;这就使得程序员依旧自己实现真正的比较(当排序键是基本类型时会更加糟糕)。对标准库的一点小的改动,如为Comparator接口添加了静态comparing方法,对此会有很大帮助:

Collections.sort(peopleComparator.comparing((Person p-p.getLastName()));

通过编译器对Lambda参数的类型推断,以及静态导入comparing方法还可以进一步缩短代码:

Collections.sort(peoplecomparing(p -p.getLastName()));

上面的Lambda表达式是一个对getLastName()方法的简单转发,我们可以使用方法引用代替Lambda去重用已经存在的方法:

Collections.sort(people, comparing(Person::getLastName));

    像Collections.sort这样的辅助方法由于许多原因而不受欢迎:不够简洁;不能应用到每一种数据结构的List上;它暗中破坏了List接口的价值,因为用户很难通过查看List的文档找到静态的sort方法。

    默认方法提供了更加面向对象的解决方案,我们想List接口中添加了sort()方法:

people.sort(comparing(Person::getLastName));

    这读起来也更像是起初对问题的陈述:按LastName排序people列表。

    如果我们像Comparator接口中添加reversed()默认方法,用来生成一个使用同样排序建但是反序的Comparator,我们就可以很简单的表达降序排列了:

people.sort(comparing(Person::getLastName).reversed());


13,总结


Java SE 8 添加了相对少量的心语言特性---Lambda表达式,方法引用,接口中的默认和静态方法,以及更加普遍的类型推断。但是总起来说,新特性可以使得程序员们使用更少的代码更加简洁清晰的表达自己的意图了,并且使得开发更加 强大,以及更加友好的并行库。



译注:到此对Java SE 8 中的新语言特性及原理也就翻译完成了,至少我是满怀期待。

来个预告的,后续会对“Lambda标准库的概览”进行翻译并发布,希望给众多Javaer们带来帮助。个人水平有限,可能翻译的不是很好,但绝对是认真的揣摩了作者的用意,以及结合Java语言的知识仔细翻译的。


原创粉丝点击