Java8的一些新特性

初探Java8的一些“新”特性

随着Java9的发布,我愈加地感觉连这个Java8的最重要的新特性都没摸清楚的我越来越面临被淘汰的危险。为了避免这样的境地,我速度入门了一波Java的Stream API,大致作为Java8的一些“新”特性的最初的一点点探索。

lambda表达式到底是什么

lambda表达式又被人们称作匿名函数,它是一个只有一行(?)的没有名字的函数。一个普通的函数拥有了返回值、命名、参数以及代码块四个部分,然而lambda表达式省略了其中的前两个。其基本语法如下所示:

1
(args) -> {codes}

上面中的两个括号都是可以省略掉的。这东西就是所谓的lambda表达式,然而我们今天要提到的当然不只是这一点点。

Java lambda的应用场景

参考Java 8里面lambda的最佳实践

使用() -> {} 替代匿名类

这一点是我最早使用的,因为也是最简单的一个。在很早之前IDEA就开始把匿名类自动在编辑器里面变成了这个模样,当时便了解了一下。很显然的一点是,这个匿名内部类所需要实现的方法只能有一个,然后这个方法即便就是这个这个lambda表达式所表示的函数。

1
2
3
button.addActionListener(e -> {
System.out.println("Hello lambda.");
});

使用内循环

我在学习的时候看到一个观点,说Java的容器有一个设计缺陷就是内部性不好(?)。比如说,如果想要遍历整个容器,只能从外部使用一个迭代器或者一个临时变量来进行操作。不过现在有了内循环这么一个工具,就可以说是解决了这个问题。

更好的事情是,当你使用了内循环之后,具体地循环方式对于你来说已经是透明的了。Java可能会串行遍历、可能会分割成几块遍历更可能是将其分配到CPU上的多核上遍历(这便是函数式编程的优点),你都不需要在意,你只知道我把整个List循环遍历了一次。

1
2
3
4
5
IntStream intStream1 = Arrays.stream(new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9});
IntStream intStream2 = Arrays.stream(new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9});
intStream1.parallel()
.forEach(System.out::println);
intStream2.forEach(System.out::println);

对于上面两个流,我用了forEach内部循环,最后输出结果是不一样——第一个流是乱序的,确实说明了它是并行发生的(是否到了核心上面我还没去试,但是从耗时反而变长了的效果来看,有很大可能是分配到了核上了的)。

函数式编程

此处是一个重点,后面慢慢讲

Stream API

此处也是一个重点,后面慢慢讲

Stream API

参考Java 8 中的 Streams API 详解

Java8里的Stream和之前的InputStream是完全不一样的概念,它实际上是对集合的一种增强。它提供了高效的聚合操作或者大批量的数据操作方式,也提供了串行模式以及并发模式两种模式,能够很方便地写出高性能的并发程序。对于我们来说,可以说是非常方便了。

什么是Stream

Stream不是一种数据结构,也不会保存数据。它在某种意义上来说是一种更高级的迭代器,一种能够解决某些更复杂操作的Iterator——能够完成用户所提供的函数的操作。Stream正如其名,它像流水一样,有一个起始点、一个终点、不可被复制、不可被重新开始并且是可以无限的。

管道流的一个标准流程

简单说,对 Stream 的使用就是实现一个 filter-map-reduce 过程,产生一个最终结果,或者导致一个副作用(side effect)。

如何创建一个流

只列两个似乎比较经常使用的就好。。

  • 从容器和数组中
1
2
3
Collection.stream()
Collection.parallelStream()
Arrays.stream(T array) or Stream.of()
  • 从BufferedReader中
1
java.io.BufferedReader.lines()

对流的操作

Intermediate:一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。

Terminal:一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。

short-circuiting:这类操作会将无限的流转换为有限的流。当操作一个无限大的 Stream,而又希望在有限时间内完成操作,则在管道内拥有一个 short-circuiting 操作是必要非充分条件。

整个操作过程将会在最后Terminal过程发生之后才会执行,他们会融合在一起在一次遍历中完成。在某种理解层面上来说,所有的中间操作就像是一条流水线上的很多机器手,在生产开始之前可以随便往上面放操作方法,只有在装置启动了之后才会使得整个流经过一次所有机器手的修饰。

常见的操作如下:

  • Intermediate:
    map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered

  • Terminal:
    forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator

  • Short-circuiting:
    anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit

map

map的作用是将一个流里面的所有元素映射成另一中元素(要注意的是Stream定义的时候是有范型类型的限制的,所以不能自由的变幻对象类型),然后继续流下去。

1
2
3
IntStream intStream1 =
Arrays.stream(new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9})
.map(e -> e + 1);

经过这样的映射之后,所有元素都被变换了。

flatMap

flatMap是一个一对多的映射,会把流里面的元素进行扁平化,简单的来说如下所示:

1
2
3
4
5
6
7
8
Stream<List<Integer>> inputStream = Stream.of(
Arrays.asList(1),
Arrays.asList(2, 3),
Arrays.asList(4, 5, 6)
);
Stream<Integer> outputStream =
inputStream.
flatMap((childList) -> childList.stream());

可以看到的是,Stream里面本来有两层结构,被flat化之后变成了只有一层Integer结构的Stream。

filter

filter是过滤器,它会对流里面的数据进行测试,即通过一个函数判断是否为真,如果是假的元素将会被丢弃。

1
2
3
4
IntStream intStream1 =
Arrays.stream(new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9})
.map(e -> e + 2)
.filter(e -> e > 5);

forEach

forEach会遍历整个流,并且对其调用后面的lambda表达式,并且不可被break或者return来改变这个遍历的流程。一般来说,不考虑其性能差距。

1
2
intStream1.parallel()
.forEach(System.out::println);

因为它是一个Terminal操作,一旦使用就会消耗掉这个Stream。如果想要多次使用的话,可以使用peek来做为替代。

reduce

reduce操作是将所有的元素全部进行某项操作串联在一起的一种操作,其将会根据第二个参数——一个二元操作符类来进行计算,将第一个种子数在流中的每一个都进行该操作,最后将会返回这个流的类型是这个流里面的元素。

1
2
3
4
5
6
7
8
9
10
11
private class Opt1 implements BinaryOperator<String> {
@Override
public String apply(String s, String s2) {
return s + " " + s2;
}
}
String result = reader.lines()
.map(s -> Arrays.stream(s.split(" ")))
.flatMap(streams -> streams)
.reduce("", new Opt1());

limit/skip

limit和skip的返回值分别是该流的前n个元素,skip是返回的该流的后(size - n)个元素——即跳过了之前的n个元素。不过在有一种情况下是不会起到Short-circuiting操作的,就是把它放在sorted之后,因为系统并不会知道sorted之后的流是什么样子的。最后的结果是sorted之后的流的limit之后的流。

1
2
3
4
5
6
7
IntStream intStream1 = Arrays.stream(new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9})
.limit(5);
IntStream intStream2 = Arrays.stream(new int[]{1, 5, 3, 6, 7, 4, 2, 8, 9})
.sorted()
.limit(5);
intStream1.forEach(System.out::println);
intStream2.forEach(System.out::println);

min/max/distinct

min/max/distinct都是在O(n)的时间内进行的最小、最大以及类似于进行集合的操作。

1
2
3
4
5
6
7
8
9
List<String> words = br.lines()
.flatMap(line -> Stream.of(line.split(" ")))
.filter(word -> word.length() > 0)
.map(String::toLowerCase)
.distinct()
.sorted()
.collect(Collectors.toList());
br.close();
System.out.println(words);

Match

match操作包括allMatch, noneMatch, anyMatch,从命名上就可以看出每一个各自的功能。它们都根据所传入的predicate来进行判断。例子略。

Stream.generate生成一个流

通过实现一个Supplier接口的对象来完成一个流的生成,由于生成的这个流是无限的,所以我们需要在操作之前先对其使用limit操作,获得有限长度的流之后才能对其进行相应的操作。

1
2
3
IntStream intStream3 = IntStream.generate(random::nextInt)
.limit(10)
.sorted();

function包

参考Java 8函数式接口functional interface的秘密

Java8提供了很多API用于完成函数式编程的目标,我认为大部分有用的都在function这个包。这个包即是所有在之前Stream中可能会用到的函数的接口,实现了它们即可以任意的实现流操作的任意操作。

  • Predicate – 传入一个参数,返回一个bool结果, 方法为boolean test(T t)
  • Consumer – 传入一个参数,无返回值,纯消费。 方法为void accept(T t)
  • Function – 传入一个参数,返回一个结果,方法为R apply(T t)
  • Supplier – 无参数传入,返回一个结果,方法为T get()
  • UnaryOperator – 一元操作符, 继承Function,传入参数的类型和返回类型相同。
  • BinaryOperator – 二元操作符, 传入的两个参数的类型和返回类型相同, 继承BiFunction

除此之外,在包里还有很多其他的更细的继承的接口,就不一一列举了。

在此之外,这些接口还有(不一定都有)两个compose、andThen接口,它们用作将函数进行链接。它们唯一的区别就是链接的顺序是不同的,可以从接口的命名中很容易看出这一点来。

参考Java 8: Composing functions using compose and andThen

结语

个人水平太低,到这里就感觉已经能够将如上的东西全部应用到日常编程之中了,于是也就没有继续往下走的更深。在我自己看来,java8提供的这些新的API并不是万能的。因为从我目前的学习程度来看,为了达到其能够提高代码可读性的目的,它们不可能对过于复杂的命令进行操作,否则会背道而驰。

同时函数式编程中闭包的概念使得在很多情况下不能够满足程序在流程发生中对当前元素的内部进行某些修改,因为java仍然是基于对象、类这一套的,如果每一个闭包之后都需要用新的对象来代替之前的对象,我感觉不是很好。(不过似乎效率其实很好?)总之,我觉得这东西值得好好利用。

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2018 Alex's Blog All Rights Reserved.

Yifeng Tang hält Urheberrechtsansprüche.