Java Tutorials-12-新特性(Java8~12)

Java 8

Lambda

“Lambda 表达式”(lambda expression)是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。Lambda表达式可以表示闭包(注意和数学传统意义上的不同)。
闭包的概念: 可以把闭包简单理解成”定义在一个函数内部的函数体”,并且在内部函数体中能访问在外部函数中定义的变量

lambda的语法为: expression = (variable) -> action, 例如

  • Runnable r = () -> { log.info"HelloWorld";}
  • int sum = (x,y) -> x+y;
  • 等号的右边即是一个lambda表达式

Lambda表达式要点总结

  • lambda表达式可以用于以下几个情况:
    • 有单个抽象方法的类, 比如一个方法接收Runnable、Comparable或者 Callable 接口,都有单个抽象方法,可以传入lambda表达式;
    • 使用了 @FunctionalInterface 注释的函数式接口,比如java.util.function包下面的Predicate、Function、Consumer 或 Supplier, BinaryOperator
      • 例如ArrayList的forEach(Consumer<E> action)方法的形参是Consumer类型, 可以接受一个lambda表达式做实参;
      • 例如Collection的stream()返回一个Stream, Stream类的filter(), map() 的形参分别是Predicate和Function;
  • lambda表达式内可以使用方法引用,仅当该方法不修改lambda表达式提供的参数。例如list.forEach(System.out::println)
  • Lambda表达式在Java中又称为闭包或匿名函数
  • Lambda方法在编译器内部被翻译成私有方法,并派发 invokedynamic 字节码指令来进行调用。使用 javap -p 或 javap -c -v 命令来看一看lambda表达式生成的字节码。大致应该长这样: private static java.lang.Object lambda$0(java.lang.String);
  • lambda内部可以使用静态、非静态和局部变量,这称为lambda内的变量捕获。
  • lambda表达式有个限制,那就是只能引用 final 或 final 局部变量,这就是说不能在lambda内部修改定义在域外的变量,读取是可以的但不能修改。
    int factor = 2;
    primes.forEach(element -> { System.out.println(factor*element); });

创建匿名类

例1:

new Thread(new Runnable() {
@Override
public void run() {
...
}
}).start();

// Lambda写法:
new Thread(
() -> System.out.println("Lambda thread")
).start();

例2: 你们最讨厌的Comparator接口

Comparator<Score> byName = new Comparator<Score>() {
@Override
public int compare(Score o1, Score o2) {
return o1.getName().compareTo(o2.getName());
}
};
Collections.sort(list, byName);

// Lambda写法:
Comparator<Score> byName =
(Score o1, Score o2) -> o1.getName().compareTo(o2.getName());

表达式迭代 forEach

List list = Arrays.asList("Lambdas", "Default Method", "Stream API", "Date and Time API");

list.forEach((e) -> System.out.println(e));
// 或者使用Java 8的方法引用:
list.forEach(System.out::println);

map() & reduce()

// 为每个订单的价格加上12$的税, 并求和
List costBeforeTax = Arrays.asList(100, 200, 300, 400, 500);
double bill = costBeforeTax.stream().map((cost) -> cost + 0.12*cost).reduce((sum, cost) -> sum + cost).get();
System.out.println("Total : " + bill);

函数式接口

什么是函数式接口? 简单说就是只拥有一个抽象方法的接口,如Runnable

Function功能型函数式接口

java.util.function.Function<T,R> 相当于仅含有一个方法的接口类, 这个方法接收一个参数T, 返回类型R.
在Java8中, 这种接口类可以用一个lambda表达式来表示.
Function只有一个方法apply, 该方法接收一个参数并返回一个值:

Function<Integer, Integer> func = x -> x*2;
Integer ii = func.apply(100);

除了👆上面这种形式, 在Java8中还增加了::, 称为”方法引用操作符”, 对象::方法将返回一个函数接口(function interface),
我们可以使用它来引用类的方法. 例如:

class MyMath{
public double square(double num){
return Math.pow(num , 2);
}
}

MyMath myMath = new MyMath();
Function<Double, Double> square = myMath::square; // 声明一个函数式接口实例, 相当于把square方法抽取出来, 增加给这个实例
double ans = square.apply(23.0);

注意被::引用的方法需要符合“函数式接口” (一个输入参数一个返回值)

Predicate断言型函数式接口

java.util.function.Predicate<T> 相当于一个”接收一个输入参数T, 返回boolean的lambda表达式”类型 :

Predicate<Integer> pred = x -> x>5;
boolean ret = pred.test();

使用::方法引用操作符:

Set<String> set = new HashSet<>();
set.addAll(Arrays.asList("one","two","three"));
Predicate<String> pred = set::contains;
boolean exists = pred.test("one");

Predicate.test()的更多例子:

List languages = Arrays.asList("Java", "Scala", "C++", "Haskell", "Lisp");
filter(languages, (str)->str.startsWith("J"));
filter(languages, (str)->str.endsWith("a"));
filter(languages, (str)->true);
filter(languages, (str)->false);
filter(languages, (str)->str.length() > 4);

public static void filter(List names, Predicate predicate) {
for(String name: names) {
if(predicate.test(name)) {
System.out.println(name + " ");
}
}
}

Predicate.and(), or(), xor()的例子:

List languages = Arrays.asList("Java", "Scala", "C++", "Haskell", "Lisp");
Predicate<String> startsWithJ = (n) -> n.startsWith("J");
Predicate<String> fourLetterLong = (n) -> n.length() == 4;
languages.stream()
.filter(startsWithJ.and(fourLetterLong))
.forEach((n) -> System.out.print("nName, which starts with 'J' and four letter long is : " + n));

Consumer消费型函数式接口

@todo

Supplier供给型函数式接口

@todo

Stream API

stream并不是某种数据结构,它只是数据源的一种视图。这里的数据源可以是一个数组,Java容器或I/O channel等。正因如此要得到一个stream通常不会手动创建,而是调用对应的工具方法,比如:

  • 调用Collection.stream()或者Collection.parallelStream()方法
  • 调用Arrays.stream(T[] array)方法
  • Map类容器无法直接用stream(), 但可以使用map.entrySet().stream()获得流

常见的stream接口继承关系如图:

流(Stream)的特性

大部分情况下Stream是容器调用Collection.stream()方法得到的,但Stream和Collections有以下不同:

  • 无存储。stream不是一种数据结构,它只是某种数据源的一个视图,数据源可以是一个数组,Java容器或I/O channel等。
  • 为函数式编程而生。对stream的任何修改都不会修改背后的数据源,比如对stream执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新stream。
  • 惰式执行。stream上的操作并不会立即执行,只有等到用户真正需要结果的时候才会执行。
  • 可消费性。stream只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成。

中间操作 & 结束操作

对stream的操作分为为两类,中间操作(intermediate operations)和结束操作(terminal operations),二者特点是:

  • 中间操作总是会惰式执行,调用中间操作只会生成一个标记了该操作的新stream,仅此而已。
  • 结束操作会触发实际计算,计算发生时会把所有中间操作积攒的操作以pipeline的方式执行,这样可以减少迭代次数。计算完成之后stream就会失效。

下表汇总了Stream接口的部分常见方法:

operator function
中间操作 concat() distinct() filter() flatMap() limit() map() peek() skip() sorted() parallel() sequential() unordered()
结束操作 allMatch() anyMatch() collect() count() findAny() findFirst() forEach() forEachOrdered() max() min() noneMatch() reduce() toArray()

中间操作

filter

filter(): 函数原型为Stream<T> filter(Predicate<? super T> predicate),作用是返回一个只包含满足predicate条件元素的Stream。

predicate 可以看成是返回boolean的lambda表达式

下面例子中, filter方法接收一个predicate类型的参数:

// 保留长度等于3的字符串
Stream<String> stream = Stream.of("Java", "Scala", "C++", "Haskell", "Lisp");
stream.filter(str -> str.length()==3)
.forEach(str -> System.out.println(str));

下面例子中, filter接收的参数是list2::contains, 被引用的方法(这里的contain方法)需符合“predicate”原型:

// 求list1和list2的交集
List<T> intersect = list1.stream()
.filter(list2::contains)
.collect(Collectors.toList());
distinct

distinct(): 函数原型为Stream<T> distinct(),作用是返回一个去除重复元素之后的Stream。

stream.distinct()
.forEach(str -> System.out.println(str));
limit & skip

limit(n)/skip(n): limit 返回 Stream 的前面 n 个元素;skip 则是扔掉前 n 个元素

sort

sorted(): 排序函数有两个,一个是用自然顺序排序,一个是使用自定义比较器排序,函数原型分别为Stream<T> sorted()Stream<T> sorted(Comparator<? super T> comparator)

stream().sorted((x, y) -> x-y ).collect(Collectors.toList());

// 使用 Map::Entry.comparingByValue和Comparator提供的方法:
unsortedMap.entrySet().stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
map

map(): 对当前Stream所有元素执行mapper操作, 返回新的Stream

stream.map(str -> str.toUpperCase())
.forEach(str -> System.out.println(str));
flatMap

flatMap(): “摊平”

// 把List<Int> 摊平成 Int
Stream<List<Integer>> stream = Stream.of(Arrays.asList(1,2), Arrays.asList(3, 4, 5));
stream.flatMap(list -> list.stream())
.forEach(i -> System.out.println(i));

结束操作

结束操作包括collect, reduce, forEach等, 分别用于聚合和遍历.

forEach

forEach是结束操作, 会立刻执行, 执行结束后Stream失效.
方法定义为void forEach(Consumer<? super E> action),作用是对容器中的每个元素执行action指定的动作,也就是对元素进行遍历。
通常我们在使用forEach时, 也会用来做合并操作

  • 使用Stream.forEach()迭代

    Stream<String> stream = Stream.of("Java", "Scala", "C++", "Haskell", "Lisp");
    stream.forEach(str -> System.out.println(str));
  • 在forEach中进行合并:

    // Combine map1 and map2
    // Map.merge()用于相同k的合并
    Map<String,Integer> mergedMap = new HashMap(map1);
    map2.forEach((k,v) -> mergedMap.merge(k,v, Integer::Sum));
reduce()

规约操作(reduction operation)又被称作折叠操作(fold),是通过某个连接动作将所有元素汇总成一个汇总结果的过程。元素求和、求最大值或最小值、求出元素总个数、将所有元素转换成一个列表或集合,都属于规约操作。
Stream类库有两个通用的规约操作reduce()collect(),也有一些为简化书写而设计的专用规约操作,比如sum()max()min()count()等。
其原型为:
Optional<T> reduce(BinaryOperator<T> accumulator)

reduce()最常用的场景就是从一组值中生成一个值,reduce()的方法定义有三种重写形式:

  1. Optional<T> reduce(BinaryOperator<T> accumulator): 返回的类型Optional表示(一个)值的容器,使用它可以避免null值的麻烦。

    // 找出最长的单词
    Optional<String> longest = stream.reduce((s1, s2) -> s1.length()>=s2.length() ? s1 : s2);
    System.out.println(longest.get());
  2. T reduce(T identity, BinaryOperator<T> accumulator):

    int[] array = {23,43,56,97,32};
    // 求所有元素的和:
    Integer sum = Arrays.stream(array).reduce(0, (a, b) -> a+b);
    // 等价于:
    Integer sum = Arrays.stream(array).reduce(0, Integer::sum);
  3. <U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner):
    它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合。

    // 求单词长度之和
    Stream<String> stream = Stream.of("Java", "Scala", "C++", "Haskell", "Lisp");
    Integer lengthSum = stream.reduce(0, // 初始值
    (sum, str) -> sum+str.length(), // 累加器
    (a, b) -> a+b); // 部分和拼接器,并行执行时才会用到
  4. 更多reduce()的例子:

    // 求最小值, 有起始值
    double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min);

    // 求和,sumValue = 10, 有起始值
    int sumValue = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);

    // 求和,sumValue = 10, 无起始值
    int sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get();

    // 字符串连接,有起始值, concat = "ABCD"
    String concat = Stream.of("A", "B", "C", "D").reduce("", String::concat);

    // 字符串连接,有起始值, 有filter操作, concat = "ace"
    String concat = Stream.of("a", "B", "c", "D", "e", "F").
    filter(x -> x.compareTo("Z") > 0).
    reduce("", String::concat);
collect()

Stream.collect()方法和类Collectors一起使用, 常用于把一个Stream的结果收集进容器里,
考虑一下将一个Stream转换成一个容器(或者Map)需要做哪些工作?我们至少需要两样东西:

  1. 目标容器是什么?是ArrayList还是HashSet,或者是个TreeMap。
  2. 新元素如何添加到容器中?是List.add()还是Map.put()。
  3. 如果并行的进行规约,还需要告诉collect() 多个部分结果如何合并成一个。

结合以上分析,collect()方法定义为

<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);

三个参数依次对应上述三条分析。不过每次调用collect()都要传入这三个参数太麻烦,收集器Collectors就是对这三个参数的简单封装,所以collect()的另一定义为<R,A> R collect(Collector<? super T,A,R> collector)

一些例子:

  • 将Stream规约成List

    Stream<String> stream = Stream.of("Java", "Scala", "C++", "Haskell", "Lisp");
    List<String> list = stream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);// 方式1
    List<String> list = stream.collect(Collectors.toList());// 方式2
    System.out.println(list);
  • 将Stream转换成List 或Set

    Stream<String> stream = Stream.of("Java", "Scala", "C++", "Haskell", "Lisp");
    List<String> list = stream.collect(Collectors.toList());
    Set<String> set = stream.collect(Collectors.toSet());
  • Stream转换成map & map排序:

    // Stream转换成map:
    // Function.identity()返回一个输出跟输入一样的Lambda表达式对象,等价于形如t -> t形式的Lambda表达式。
    Map<Integer, String> map = stream.collect(Collectors.toMap(Function.identity(), String::length));

    // map排序 & 取TopN:
    // 对Entry的流进行排序, 然后生成有序的LinkedHashMap:
    Map<String ,Long> sortedMap = unsortedMap.entrySet().stream()
    .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
    .limit(topN)
    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
    (oldValue, newValue) -> oldValue,
    LinkedHashMap::new));
  • 合并Map1, Map2

    Map<String, Integer> mx = Stream.of(m1, m2)
    .map(Map::entrySet) // converts each map into an entry set
    .flatMap(Collection::stream) // converts each set into an entry stream, then
    // "concatenates" it in place of the original set
    .collect(
    Collectors.toMap( // collects into a map
    Map.Entry::getKey, // where each entry is based
    Map.Entry::getValue, // on the entries in the stream
    Integer::max // such that if a value already exist for
    // a given key, the max of the old
    // and new value is taken
    )
    );
  • 拼接字符串

    Stream<String> stream = Stream.of("Java", "Scala", "C++", "Haskell", "Lisp");
    String mergedString = stream.filter(string -> !string.isEmpty()).collect(Collectors.joining(", "));
  • 上述代码能够满足大部分需求,但由于返回结果是接口类型,我们并不知道类库实际选择的容器类型是什么,有时候我们可能会想要人为指定容器的实际类型,这个需求可通过Collectors.toCollection(Supplier<C> collectionFactory)方法完成。

    // 使用toCollection()指定规约容器的类型
    ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));// (3)
    HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));// (4)

Stream的底层实现

  • stream(): Stream只是一个接口,并没有操作的缺省实现。最主要的实现是ReferencePipeline,而它的一些具体实现又是由AbstractPipeline完成的
  • parrallelStream(): 底层使用的是ForkJoinPool, 比较适合使用计算密集型并且没有I/O的任务

新的Data & Time

@ref: Java 8 新特性概述

Java 9

@ref: Java 9 新特性概述

Java 10

@ref: Java 10 新特性介绍