java 8新特性

java8新特性

了解 Java 8 中的 lambda 表达式



一、背景:函数式对象

java一直需要函数式对象,在早起,我们需要代码块响应用户事件,例如窗口打开和关闭,按钮按下和滚动条移动。在 Java 1.0 中,Abstract Window Toolkit (AWT) 应用程序需要像它们的 C++ 前辈一样扩展窗口类并覆盖选择的事件方法;这被认为是笨拙和行不通的。因此,在 Java 1.1 中,Sun 为我们提供了一组“侦听器”接口,每个接口都有一个或多个与 GUI 中的事件相对应的方法。但是为了更容易编写必须实现这些接口及其相应方法的类,Sun 给了我们内部类,包括在现有类的主体中编写这样一个类的能力,而无需指定名称——无处不在的匿名内部类。内部类在语法和语义方面都有些陌生。例如,内部类要么是静态内部类要么是实例内部类,这不取决于任何特定的关键字(尽管静态内部类可以使用static关键字显式声明为这样),而是取决于创建实例的词法上下文. 实际上,这意味着 Java 开发人员在编程面试中经常会遇到诸如 代码 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);

    // 这是编译错误吗?
    InstanceOuter.InstanceInner ii = io.new InstanceInner();

    // 这打印的是什么?
    ii.printSomething(); //  打印 12

    // 这打印的是什么?
    StaticOuter.StaticInner si = new StaticOuter.StaticInner();
    si.printSomething(); //  打印  24
  }
}

诸如内部类之类的“特性”常常让 Java 开发人员相信,此类功能最好归于语言的极端情况,适合编程面试而不是其他太多——除非他们需要它们。即便如此,在大多数情况下,它们的使用纯粹是出于事件处理的原因。

二、Lambda、目标类型和词法范围

1.Lambda 表达式

Java 8 引入了一些新的语言特性,旨在使编写此类代码块变得更加容易——关键特性是lambda 表达式,也通俗地称为闭包(原因我们将在后面讨论)或匿名方法

代码 2 和 代码 3

public class Lambdas {
  public static void main(String... args) {
    Runnable r = new Runnable() {
      public void run() {
        System.out.println("Howdy, world!");
      }
    };
    r.run();
  }
}

public static void main(String... args) {
    Runnable r2 = () -> System.out.println("Howdy, world!");
    r2.run();
  }


这两种方法具有相同的效果:一个Runnable-implementing 对象,其run()方法被调用以将某些内容打印到控制台。然而,在底层,Java 8 版本所做的不仅仅是生成一个实现Runnable接口的匿名类——其中一些与invoke dynamic Java 7 中引入的字节码有关

2.功能接口

上述 Runnable接口类的Callable接口,该Comparator接口,以及中已定义的其他接口一大堆Java的是什么样的Java 8调用functional interface:它是只需要一个方法,以满足该接口的要求,要实现一个接口. 这就是语法实现其简洁性的方式,因为对于 lambda 试图定义的接口方法没有歧义。

Java 8 的设计者选择给我们一个注解,@FunctionalInterface作为文档提示,接口被设计为与 lambdas 一起使用,但编译器不需要这样做——它从结构中确定“功能接口”接口,而不是来自注释。
代码 4

interface Something {
  public String doit(Integer i);
}

3.句法

lambda 基本上由三部分组成:带括号的参数集、箭头和主体,主体可以是单个表达式或 Java 代码块

代码 5

public static void main(String... args) {
    Comparator<String> c = 
      (String lhs, String rhs) -> lhs.compareTo(rhs);
    int result = c.compare("Hello", "World");
  }

Comparator接受两个字符串并返回一个整数,其值为负(对于“小于”)和正(对于“大于”) , 和零(表示“相等”)。
如果 lambda 的主体需要多个表达式,则表达式返回的值可以通过return关键字返回,就像任何 Java 代码块一样 如下代码 6。

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");
  }

对于可以在 lambda 的主体中执行的操作有一些限制,其中大部分都非常直观——一个 lambda 体不能“中断”或“继续”出 lambda,如果 lambda 返回一个值,每个代码路径都必须返回一个值或抛出异常,依此类推。这些规则与标准 Java 方法的规则大致相同。

4.类型推断

如果目标类型是 a Comparator,则传递给 lambda 的对象必须是字符串(或某个子类型);否则,代码首先不会编译。在这种情况下,和参数String前面的声明完全是多余的,而且由于 Java 8 增强的类型推断功能,它们完全是可选的 如下 代码7

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");
  }

Java 的 lambda 语法的一个有趣的副作用是,在 Java 的历史上,我们第一次发现一些不能分配给类型引用的东西Object(参见代码 8)——至少在没有帮助的情况下是这样。

public static void main4(String... args) {
    Object o = () -> System.out.println("Howdy, world!");
      // 不会编译
  }

编译器会抱怨这Object不是一个函数式接口,尽管真正的问题是编译器不能完全弄清楚这个 lambda 应该实现哪个函数式接口:Runnable还是别的什么?我们可以一如既往地帮助编译器进行强制转换。如代码9

public static void main4(String... args) {
    Object o = (Runnable) () -> System.out.println("Howdy, world!");
  }

lambda 语法适用于任何接口,因此推断为自定义接口的 lambda 也将同样容易推断。原始类型与其在 lambda 类型签名中的包装类型一样可行,Java 8 只是将 Java 长期存在的原则、模式和语法应用于新功能。代码10

Something s = (Integer i) -> { return i.toString(); };
    System.out.println(s.doit(4));

5 语法范围

然而,编译器在lambda函数体中对待名称(标识符)的方式与在内部类中对待名称的方式是如何的呢?
代码 11

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();
  }
}
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()";
  }
}
class Hello {
  public Runnable r = () -> {
      System.out.println(this);
      System.out.println(toString());
    };

  public String toString() {
    return "Hello's custom toString()";
  }
}

代码11中的第一段 匿名实现中的关键字this和调用都绑定到匿名内部类实现上,因为那是满足表达式的最内层作用域。如果我们想要打印出Hello的版本toString,我们必须使用thisJava 规范内部类部分中的“外部”语法明确限定它,就如第二段,坦率地说,这是一个内部类只是造成的混乱比它们解决的更多的领域。诚然,只要解释了this关键字出现在这种相当不直观的语法中的原因,它就有点道理了,但它的道理与政治家的特权也有道理一样。然而,Lambdas是词法作用域,这意味着 lambda 将其定义周围的直接环境识别为下一个最外层作用域。因此第三段比第二段语法更直观。

6 变量捕获

将 lambda 称为闭包的部分原因是函数字面量(例如我们一直在编写的内容)可以“关闭”封闭范围内函数字面量主体之外的变量引用(在这种情况下Java 的,通常是定义 lambda 的方法)。内部类也可以做到这一点,但在所有让 Java 开发人员感到沮丧的关于内部类的主题中,内部类只能引用封闭范围中的“最终”变量这一事实接近顶部。
Lambda 放宽了这一限制,但只是放宽了一点:只要变量引用是“有效最终的”,这意味着它除了名称之外的所有内容都是最终的,lambda 可以引用它(参见代码12)。因为message在main包含被定义的 lambda的方法的范围内永远不会被修改,它实际上是最终的,因此有资格从Runnable存储在 r 中的lambda 中引用。

public static void main(String... args) {
    String message = "Howdy, world!";
    Runnable r = () -> System.out.println(message);`在这里插入代码片`
    r.run();
  }

虽然从表面上看这可能听起来没什么,但请记住,lambda 语义规则不会改变整个 Java 的性质——在 lambda 定义很久之后,引用另一侧的对象仍然可以访问和修改,如代码13所示。

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 社区眼中,这究竟是 bug 还是特性还有待观察,但事实就是如此,开发人员最好了解 lambda 变量捕获的工作原理,以免出现意外的 bug。(事实上​​,这种行为并不新鲜——它只是通过更少的击键次数和编译器的更多支持来重铸 Java 的现有功能。)

7 方法参考

到目前为止,我们检查过的所有 lambda 表达式都是匿名字面量——本质上是在使用时定义 lambda 表达式。这对于一次性行为非常有用,但是当在多个地方需要或想要这种行为时,它并没有多大帮助。例如,考虑以下Person类。(暂时忽略缺乏适当的封装。)
当 Person放入 SortedSet或需要按某种形式的列表排序时,我们希望有不同的机制来对Person实例进行排序——例如,有时按名字,有时按姓氏。这是Comparator为了允许我们通过传入Comparator实例来定义强加的排序。
Lambdas 当然可以更轻松地编写排序代码,如代码13 所示。但是,按Person名字排序实例可能需要在代码库中多次完成,并且多次编写这种算法显然违反了不要重复自己 (DRY) 原则。

class Person { //新建一个类
  public String firstName;
  public String lastName;
  public int age;
});

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自身的成员,如代码 14 所示。Comparator然后可以像引用任何其他静态字段一样被引用。而且,说实话,函数式编程狂热者会更喜欢这种风格,因为它允许以各种方式组合功能。

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 + "]";
  }
}

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然后直接使用它——这正是方法引用所允许的(代码15)。注意双冒号的方法命名风格,它告诉编译器应该在此处使用compareFirstNames定义于的方法Person,而不是方法文字。

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);
  }

对于那些好奇的人来说,另一种方法是使用该compareFirstNamest方法创建一个Comparator实例 比如 :

Comparator cf = Person::compareFirstNames;

而且,为了更简洁,我们可以通过使用一些新的库功能来完全避免一些语法开销,编写以下代码,它使用了一个高阶函数(意思是,粗略地说,一个函数传递函数)基本上避免了所有先前的代码,而支持一行就地使用:

Arrays.sort(people, comparing(Person::getFirstName));

这在一定程度上就是为什么 lambda 以及它们附带的函数式编程技术如此强大的原因。

8 虚拟扩展方法

经常提到的关于接口的缺点之一是它们没有默认实现,即使该实现非常明显。例如,考虑一个虚构的Relational接口,它定义了一系列方法来模拟关系方法(大于、小于、大于或等于等)。一旦定义了这些方法中的任何一个,就很容易看出如何根据第一个方法定义其他方法。事实上,如果提前知道方法的定义,所有这些都可以根据 aComparable的compare方法定义compare。但是接口不能有默认行为,抽象类仍然是一个类,占据任何潜在子类的一个实现继承槽。
然而,在 Java 8 中,随着这些函数文字变得越来越普遍,能够指定默认行为而不失去接口的“接口性”变得更加重要。因此,Java 8 现在引入了虚拟扩展方法(在之前的草案中曾经被称为防御者方法),基本上允许接口指定方法的默认行为,如果派生实现中没有提供。
考虑一下Iterator界面。目前,它具有三个方法(hasNext、next和remove),并且每个方法都必须定义。但是“跳过”迭代流中的下一个对象的能力可能会有所帮助。并且因为Iterator的实现很容易根据其他三个来定义,所以我们可以提供它,如代码16 所示。

interface Iterator<T> {
  boolean hasNext();
  T next();
  void remove();

  void skip(int i) default {
    for (; i > 0 && hasNext(); i--) next();
  }
}

顾名思义,虚拟扩展方法提供了一种强大的机制来扩展现有接口,而不会将扩展降级为某种二等状态。使用这种机制,Oracle 可以为现有库提供额外的、强大的行为,而无需开发人员跟踪不同种类的类。SkippingIterator对于那些支持它的集合,开发人员现在不需要降级到任何类。事实上,任何地方的代码都不必更改,所有的Iterators,无论何时编写,都会自动具有这种跳过行为。
Collection类中发生的绝大多数更改都是通过虚拟扩展方法实现的。

总结

Lambda 将为 Java 带来很多变化,无论是在 Java 代码的编写方式还是它的设计方式方面。其中一些受函数式编程语言启发的变化将改变 Java 程序员对编写代码的看法——这既是机遇也是麻烦。
我们将在本系列的下一篇文章中更多地讨论这些变化对 Java 库的影响,我们将花一点时间讨论这些新的 API、接口和类如何开启一些新的设计由于内部类语法的笨拙,以前不实用的方法。
Java 8 将是一个非常有趣的版本。系好安全带,这将是一次火箭飞船之旅。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值