美文网首页
[译] Java 8 中的新特性

[译] Java 8 中的新特性

作者: 风雪围城 | 来源:发表于2017-12-08 00:19 被阅读0次

距离 java 8 发布,都快四年了。现在回过头来,系统地整理一下零零散散的知识点。
本章是对Java 8 中新特性的一些总结。原文链接

接口改进

现在我们可以在接口中定义静态方法了。比如,在 java.util.Comparator 中就定义了静态方法 naturalOrder:

public static <T extends Comparable<? super T>>
Comparator<T> naturalOrder() {
    return (Comparator<T>)
        Comparators.NaturalOrderComparator.INSTANCE;
}

另外,现在在接口中可以定义 default 方法。例如, java.lang.Iterable 中的 forEach 方法:

public default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

在过去,这基本上是不可能的。
在Java 8 中,大量的 default 方法被添加进核心的 JDK 接口中,后面我会进一步讨论他们。

为什么不同使用 default 方法重写 equals 、hashCode、toString 方法?

接口中并不能通过默认实现(default implementation)的方式重写Object类中的任何方法。这就意味着不能通过这种方式重写equals 、 hashCode 以及 toString方法。
一开始看起来这有点奇怪,Brian Goetz在 Lambda项目中的邮件列出了四个冗长的理由,这里我只讲一个,因为这一个理由足够说服我:
所有接口实例本身都是Object,这使得它原本就有关于equals/hashCode/toString 等方法的 non-default 实现,因此,再提供一个default版本的实现就显得毫无用户。

函数接口( Functional interfaces)

在 Java 8 中引入的一个核心概念就是函数接口。一个接口是函数接口,就意味着,它有且仅有一个抽象方法。
正如 java.lang.Runnable 就是一个函数接口。唯一的抽象方法就是 run 方法。
接口中的 default 方法并不是抽象的,所有函数接口中可以定义任意多的 default 方法。
一个新的注解 @FunctionalInterface 也被引入了,用于标注函数接口。类似于@Override 注解,它声明这个接口是一个函数接口,如果不是,它会拒绝编译。

lamdbas

关于函数接口,一个非常有价值的性质在于,它可以被通过 lambdas表达式的方式进行实例化。下面将给出一些例子:
在左边使用逗号分隔的具有指定类型的输入列表,在右边是带有返回值的代码块:

(int x, int y) -> { return x + y; }

左边是可以推断出类型的以逗号分隔的输入列表,右边是返回值:

(x, y) -> x + y

左边是可以推断出类型的单个参数,右边是返回值

x-> x*x 

没有输入,但是有返回值:

()->x 

左边是可以推断出类型的单个参数,右边是代码块,但是没有返回值:

x->{System.out.println(x);}

静态方法引用:

String::valueOf

非静态方法引用:

Object::toString

捕获方法引用(capturing method reference):

x::toString

构造器引用:

ArrayList::new

事实上,你可以把方法应用看做是lamdba的速记形式。
方法引用和lambda表达式是等价的。
String::valueOf 等价于 x->String.valueOf(x);
Object::toString 等价于 x->x.toString();
x::toString 等价于 ()->x.toString();
ArrayList::new 等价于 ()-> new ArrayList();
当然,方法是可以被重载的。对于同一个方法名,类中可能有多个不同参数的方法。一个 ArrayList::new 可以指向它三个构造器中的任意一个,具体的指向,取决于到底使用哪个函数接口。
lambda表达式和函数接口在“形状”是匹配的。也就是说,它们的输入列表、输出列表以及声明的检查期异常都是一样的。
比如说

Comparator<String> c = (a, b) -> Integer.compare(a.length(),
                                                 b.length());

Comparator接口中的compare方法有两个 String 类型的输入,返回一个 int 值,这和 lambda 表达式的左右输入、返回是一致的。
在比如说:

Runnable r = () -> { System.out.println("Running!"); }

Runnable 中的 run 方法,没有输入,没做返回这和它的 lambda表达式也应该是一致的。

捕获式和非捕获式 lambda

所谓捕获,是指 lambda表达式中访问了在 lambda 函数体非静态的变量或者对象。举个栗子,下面 lambda 捕获了变量x:

int x = 5;
return y -> x + y;

为了使这种 lambda 声明变得合理,它所捕获的变量必须是“有效的 final”类型。即,要么被标记为 final,或者在之后不可以被赋值。
非捕获的 lambda 通常会比捕获 lambda 效率高,虽然并没有任何证据指明这一点(据我所知)。(译注:前者通常比后者的依赖少)。比如,下面的 isFunActivity 依赖于 this,因为 isFun 是对象方法。

class FunDetector {
      private boolean isFun(Activity activity) { ... }
 
       public Predicate<Activity> isFunAcitivity() {
        return activity -> isFun(activity);
      }
    }
lambda 不能做什么

有些特性,是 lambda 并没有提供。
非 final 变量的捕获 如果外部变量会赋新值,那就不应该被用在 lambda 中。当然,final 修饰不是必须的,但是肯定不能再 lambda 之后修改,否则代码不会被编译。

int count = 0;
List<String> strings = Arrays.asList("a", "b", "c");
strings.forEach(s -> {
    count++; // 错误:不能修改 count
});

异常透明度 如果一个编译期异常被 lambda 抛出来了,那么,它所对应的函数接口必须声明了这个异常可以被抛出来,否则,异常是不会向上传播的。下面这段代码不会被编译:

void appendAll(Iterable<String> values, Appendable out)
        throws IOException { // doesn't help with the error
    values.forEach(s -> {
        out.append(s); //错误:无法抛出IO异常,因为它所对应的Consumer.accept 没有允许,所以,forEach是不会收到抛出的异常的
    });
}

控制流(break,return) 在上面的 forEach 栗子中,如果想在 lambda 中返回true,并停止循环,这样是不行的

final String secret = "foo";
boolean containsSecret(Iterable<String> values) {
    values.forEach(s -> {
        if (secret.equals(s)) {
            ??? // want to end the loop and return true, but can't
        }
    });
}
为什么抽象方法不能通过 lambda 实例化

在一个抽象类中,哪怕只有一个抽象方法,都不能使用 lambda 进行实例化。
两个原因,其一是,这么做,隐藏了构造方法的执行;另外一个原因,是 lambda 未来的优化方向。以后, lambda 可能不需要被计算进一个实例化对象中。

java.util.function

这里有一些通用的函数接口被广泛使用,这些接口都是新增的。这里列举一些:

  • Function<T,R> - T是输入,R作为输出
  • Predicate<T> - T作为输入,输出boolean
  • Consumer<T> - T作为输入,执行一些动作,但是没有输出
  • Supplier<T> - 没有输入,返回 T
  • BinaryOperator<T> - 由两个T作为输入,返回一个T。该接口对于“reduce”操作很有用。
    出于性能考虑,在使用基础类型作为输入输出时,为了避免装包解包,会提供专用的函数接口,比如:
  • intConsumer - 使用一个int类型作为输入,执行一些操作,而不做返回。

java.util.stream

新的 java.util.stream 包提供了工具集以支持面向函数的编程。通常,我们会从一个 collection 集合中获取 stream:

Stream<T> stream = collection.stream();

stream 就像一个 iterator.类比水流,一个 stream 只能被遍历一次,一次就用完了。 Stream 可能是无线流。
stream 可以是串行的或者并行的 。串行流执行在一个线程中,并行流执行在多个线程中。
这里,给出一些实例:

int sumOfWeights = blocks.stream().filter(b -> b.getColor() == RED)
                                  .mapToInt(b -> b.getWeight())
                                  .sum();

stream 提供了丰富的 API 来转换流和执行动作。这些操作包括:中间操作(intermediate)和终止操作(terminal)。

  • 中间操作:一个中间操作会允许在流的基础上做进一步操作,然后接着返回这个流。就像上面的 filter 和 map,就是中间操作,它们返回当前 stream,以允许操作链上的处理继续进行。
  • 终止操作:一个终止操作意味在在这个stream会最后被调用。一旦终止操作被调用,stream 就被“消费”完了,不能再被使用。上面的 sum 方法就是一个终止操作。

通常,处理 stream 会按照下面的步骤进行调用:

  1. 从数据源获得一个 stream;
  2. 执行一个或者多个中间操作;
  3. 执行一个终止操作。

这里有两个通用属性需要考虑:

  • 状态(Stateful ):中间操作分为有状态的和无状态的。通常有状态的要比无状态的实现代价更高。(译注:有状态意味着要解决更多依赖问题)
  • 短路(short-circuiting):终止操作分为短路和非短路的。所谓短路,意味着允许流处理提前结束,而不需要检查所有元素。

中间操作:

  • filter : 排除所有不匹配 Predicate 的元素;
  • map :通过Function 对所有元素进行一一映射;
  • flatMap :通过flatMap,可以使用一个Stream替换某个值,然后把所有的Stream合并起来。map操作将值替换一个新的值,但是有时候我们想替换为一个新的Stream对象,更常见的是把多个Stream和合并为一个Stream.下面假设order是购物清单
//函数原型
<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)
orders.flatMap(order -> order.getLineItems().stream())...
  • peek :对当当前流中的每一个元素做一些操作。
Stream.of("one", "two", "three", "four")
         .filter(e -> e.length() > 3)
         .peek(e -> System.out.println("Filtered value: " + e))
         .map(String::toUpperCase)
         .peek(e -> System.out.println("Mapped value: " + e))
         .collect(Collectors.toList());
  • distinct :通过 .equals 行为排除所有相同的元素。这是一个有状态的操作。
  • sorted :通过输入进去的 Comparator,确保流中的元素是有序的。这是一个有状态的操作。
  • limit :确保序列操作只能访问查阅到最大的数量。这是一个有状态的、短路操作。
  • skip :跳过前面 n 个元素。

终止操作:

  • forEach : 遍历 stream 中的每个元素,进行操作。
  • toArray :把 stream 中的元素转为数组。
  • reduce : 通过 BinaryOperator 将 stream 中的元素聚合成 1 个;(译注:这种聚合是针对不可变类型数据的聚合,将问题变成了f(g(x,y))的问题,这里,g(x,y)即是上面的BinaryOperator。下面我以代码举个例子)
Stream.of(1, 2, 3, 4).reduce(100, (sum, item) -> sum +item);
上述代码中的reduce过程
  • collect :将流中的元素聚合到一些容器中,比如Collection或者Map(译注:以下示例来自官方文档)。
List<String> asList = stringStream.collect(Collectors.toList());

Map<String, List<Person>> peopleByCity
         = personStream.collect(Collectors.groupingBy(Person::getCity));
  • min :根据 Comparator 找到流中的最小元素。
  • max :根据 Comparator 找到流中的最大元素。
  • count :算出流中元素的个数;
  • anyMatch :根据 Predicate 查找时候至少有一个元素是匹配的。这是一个短路操作,意味着一旦找到一个,就停止寻找过程。
  • allMatch :根据 Predicate 判断是否所有元素都满足匹配,这也是一个短路操作,一旦发现有一个不匹配,就停止这个过程。
  • noneMatch :查找是否没有元素匹配。
  • findFirst :查找流中的第一个元素,这是一个短路操作。
  • findAny :查找一些元素。通常它会比 findFirst 效率高些。 这是一个短路操作。

这些中间操作,都是懒加载的。也就是说,只有终止操作才能开始这个流对元素的处理过程。
Stream 会尽量做更少的工作以提升效率。它会做一些优化处理,比如当它确定元素已经是顺序时,sorted() 会被省略执行。在一些操作例如 limit(x) 或者 substream(x,y)中,stream 通常会避免执行一些不必要的 map 操作(译注:显然它没必要对流中的所有操作进行map,如果中间操作中有 map 的话)。
回到并行流(parallel stream)的概念,很重要的一点是,它将消耗更多的性能,同时,并不能保证结果的一致性。所以,在使用前,需要注意这些问题:这是排序问题吗?函数是无状态的吗?数据量太大、操作太复杂而只能使用并行机制吗?等等等等。

小结

先到这里吧,以上,都是比较常用的功能。后面,还有些关于集合新增函数、同步API、IO这些,与我不是特别常用[捂脸],立个flag,有时间接着写。
让我觉得最好用的就是 lambda 表达式的使用。同时还有 stream 的概念,面向函数式编程,我觉得最大的优势,就是简洁、可读性高。

相关文章

  • [译] Java 8 中的新特性

    距离 java 8 发布,都快四年了。现在回过头来,系统地整理一下零零散散的知识点。本章是对Java 8 中新特性...

  • Java11的新特性

    Java语言特性系列 Java5的新特性 Java6的新特性 Java7的新特性 Java8的新特性 Java9的...

  • Java15的新特性

    Java语言特性系列 Java5的新特性 Java6的新特性 Java7的新特性 Java8的新特性 Java9的...

  • Java12的新特性

    Java语言特性系列 Java5的新特性 Java6的新特性 Java7的新特性 Java8的新特性 Java9的...

  • 【转】Java 8 新增的 Stream 类学习

    声明:本篇转自《【译】Java 8的新特性—终极版》中的 Streams 相关部分。这里只是为了方便查阅学习。其他...

  • Java8常用的新特性总结

    一、Java8常用的新特性总结 1.1.Java8常用特性总览 1.2.lambda表达式 在Java8中引入了一...

  • 关于java8的学习(一)

    Java 8 新特性 官网java8介绍地址菜鸟教程关于java8的介绍 Java 8 里面加了很多的新特性,在这...

  • Java 8 新特性——检视阅读

    Java 8 新特性——检视阅读 参考 Java 8 新特性——菜鸟 Oracle 公司于 2014 年 3 月 ...

  • Java 8 新特性——实践篇

    Java 8 新特性——实践篇 参考Java8新特性[https://www.bilibili.com/video...

  • Java 8 新特性

    Java 8 新特性 声明:java8新特性系列为个人学习笔记,参考地址点击这里,侵删!! Java 8 (又称为...

网友评论

      本文标题:[译] Java 8 中的新特性

      本文链接:https://www.haomeiwen.com/subject/bnphixtx.html