Java 8 中的 Streams API 详解

1. 为什么需要 Stream

Stream 作为 Java 8 的一大亮点,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念。它也不同于 StAX 对 XML 解析的 Stream,也不是 Amazon Kinesis 对大数据实时处理的 Stream。Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。所以说,Java 8 中首次出现的 java.util.stream 是一个函数式语言+多核时代综合影响的产物。

2. 对比java7和java8中的取值/排序实现

java7:

List<Transaction> groceryTransactions = new Arraylist<>();
// 选择交易类型为杂货的
for(Transaction t: transactions){
 if(t.getType() == Transaction.GROCERY){
 groceryTransactions.add(t);
 }
}
// 根据交易值进行排序
Collections.sort(groceryTransactions, new Comparator(){
 public int compare(Transaction t1, Transaction t2){
 return t2.getValue().compareTo(t1.getValue());
 }
});
// 构建list,存储所有的id
List<Integer> transactionIds = new ArrayList<>();
for(Transaction t: groceryTransactions){
 transactionsIds.add(t.getId());
}

java8:

List<Integer> transactionsIds = transactions.parallelStream().
 filter(t -> t.getType() == Transaction.GROCERY).
 sorted(comparing(Transaction::getValue).reversed()).
 map(Transaction::getId).
 collect(toList());

3. 流的构成

当我们使用一个流的时候,通常包括三个基本步骤:

获取一个数据源(source)→ 数据转换→执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道,如下图所示。

图 1. 流管道 (Stream Pipeline) 的构成
在这里插入图片描述

有多种方式生成 Stream Source:

  1. 从 Collection 和数组
    Collection.stream()
    Collection.parallelStream()
    Arrays.stream(T array) or Stream.of()

  2. 从 BufferedReader
    java.io.BufferedReader.lines()

  3. 静态工厂
    java.util.stream.IntStream.range()
    java.nio.file.Files.walk()

  4. 自己构建
    java.util.Spliterator

  5. 其它
    Random.ints()
    BitSet.stream()
    Pattern.splitAsStream(java.lang.CharSequence)
    JarFile.stream()

流的操作类型分为两种:

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

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

在对于一个 Stream 进行多次转换操作 (Intermediate 操作),每次都对 Stream 的每个元素进行转换,而且是执行多次,这样时间复杂度就是 N(转换次数)个 for 循环里把所有操作都做掉的总和吗?其实不是这样的,转换操作都是 lazy 的,多个转换操作只会在 Terminal 操作的时候融合起来,一次循环完成。我们可以这样简单的理解,Stream 里有个操作函数的集合,每次转换操作就是把转换函数放入这个集合中,在 Terminal 操作的时候循环 Stream 对应的集合,然后对每个元素执行所有的函数。

一个流操作的示例

int sum = widgets.stream()
.filter(w -> w.getColor() == RED)
 .mapToInt(w -> w.getWeight())
 .sum();

4. 流的使用详解

4.1 流的构造与转换

下面提供最常见的几种构造 Stream 的样例。

// 1. Individual values
Stream stream = Stream.of("a", "b", "c");
// 2. Arrays
String [] strArray = new String[] {"a", "b", "c"};
stream = Stream.of(strArray);
stream = Arrays.stream(strArray);
// 3. Collections
List<String> list = Arrays.asList(strArray);
stream = list.stream();

清单 6. 流转换为其它数据结构

// 1. Array
String[] strArray1 = stream.toArray(String[]::new);
// 2. Collection
List<String> list1 = stream.collect(Collectors.toList());
List<String> list2 = stream.collect(Collectors.toCollection(ArrayList::new));
Set set1 = stream.collect(Collectors.toSet());
Stack stack1 = stream.collect(Collectors.toCollection(Stack::new));
// 3. String
String str = stream.collect(Collectors.joining()).toString();

一个 Stream 只可以使用一次,上面的代码为了简洁而重复使用了数次。

4.2 流的操作

接下来,当把一个数据结构包装成 Stream 后,就要开始对里面的元素进行各类操作了。常见的操作可以归类如下。

  1. Intermediate:

    1. map (mapToInt, flatMap 等) 映射
    2. filter 过滤
    3. distinct 去重
    4. sorted 排序
    5. peek、
    6. limit、
    7. skip、
    8. parallel、
    9. sequential、
    10. unordered
  2. Terminal:

    1. forEach、
    2. forEachOrdered、
    3. toArray、
    4. reduce、
    5. collect、
    6. min、 max、 count、
    7. anyMatch、 allMatch、 noneMatch、
    8. findFirst、 findAny、 iterator
  3. Short-circuiting:

    1. anyMatch、 allMatch、 noneMatch、
    2. findFirst、 findAny、 limit

我们下面看一下 Stream 的比较典型用法。

  1. map / flatMap
// map
List<String> output = wordList.stream()
	.map(String::toUpperCase)
	.collect(Collectors.toList());
	
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
List<Integer> squareNums = nums.stream()
	.map(n -> n * n)
	.collect(Collectors.toList());
	
// flatMap 把 input Stream 中的层级结构扁平化,就是将最底层元素抽出来放到一起,最终 output 的新 Stream 里面已经没有 List 了,都是直接的数字。
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());
	
  1. filter 对原始 Stream 进行某项测试,通过测试的元素被留下来生成一个新 Stream。
// 留下偶数
Integer[] sixNums = {1, 2, 3, 4, 5, 6};
Integer[] evens =
Stream.of(sixNums).filter(n -> n%2 == 0).toArray(Integer[]::new);

// 把单词挑出来
List<String> output = reader.lines().
 flatMap(line -> Stream.of(line.split(REGEXP))).
 filter(word -> word.length() > 0).
 collect(Collectors.toList());
  1. forEach

forEach 方法接收一个 Lambda 表达式,然后在 Stream 的每一个元素上执行该表达式。

打印姓名(forEach 和 pre-java8 的对比)
// Java 8
roster.stream()
	.filter(p -> p.getGender() == Person.Sex.MALE)
	.forEach(p -> System.out.println(p.getName()));
// Pre-Java 8
for (Person p : roster) {
	if (p.getGender() == Person.Sex.MALE) {
	System.out.println(p.getName());
	}
}

forEach 是 terminal 操作,因此它执行后,Stream 的元素就被”消费”掉了,你无法对一个 Stream 进行两次 terminal 运算。下面的代码是错误的:

stream.forEach(element -> doOneThing(element));
stream.forEach(element -> doAnotherThing(element));
  1. peek
    相反,具有相似功能的 intermediate 操作 peek 可以达到上述目的。如下是出现在该 api javadoc 上的一个示例。
//peek 对每个元素执行操作并返回一个新的 Stream
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());

forEach 不能修改自己包含的本地变量值,也不能用 break/return 之类的关键字提前结束循环。

  1. findFirst
    这是一个 termimal 兼 short-circuiting 操作,它总是返回 Stream 的第一个元素,或者空。
    这里比较重点的是它的返回值类型:Optional。这也是一个模仿 Scala 语言中的概念,作为一个容器,它可能含有某值,或者不包含。使用它的目的是尽可能避免 NullPointerException。

Optional 的两个用例

public static void print(String text) {
 // Java 8
	Optional.ofNullable(text).ifPresent(System.out::println);
 // Pre-Java 8
	if (text != null) {
		System.out.println(text);
		}
	}

Stream 中的 findAny、max/min、reduce 等方法等返回 Optional 值。还有例如 IntStream.average() 返回 OptionalDouble 等等。

  1. reduce

这个方法的主要作用是把 Stream 元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合。从这个意义上说,字符串拼接、数值的 sum、min、max、average 都是特殊的 reduce。例如 Stream 的 sum 就相当于

Integer sum = integers.reduce(0, (a, b) -> a+b); 
// 或
Integer sum = integers.reduce(0, Integer::sum);

也有没有起始值的情况,这时会把 Stream 的前面两个元素组合起来,返回的是 Optional。

reduce用例:

	// 1. 字符串连接,concat = "ABCD"
    String concat = Stream.of("A", "B", "C", "D").reduce("", String::concat);
    System.out.println(concat); // ABCD
    // 2. 求最小值,minValue = -3.0
    double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min);
    System.out.println(minValue);
    // 3. 求和,sumValue = 10, 有起始值
    int sumValue = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);
    System.out.println(sumValue);
    // 4. 求和,sumValue = 10, 无起始值
    sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get();
    System.out.println(sumValue);
    // 过滤,字符串连接,concat = "ace"
    concat = Stream.of("a", "B", "c", "D", "e", "F").
            filter(x -> x.compareTo("Z") > 0).
            reduce("", String::concat);
    System.out.println(concat); // ace

上面代码例如第一个示例的 reduce(),第一个参数(空白字符)即为起始值,第二个参数(String::concat)为 BinaryOperator。这类有起始值的 reduce() 都返回具体的对象。而对于第四个示例没有起始值的 reduce(),由于可能没有足够的元素,返回的是 Optional,请留意这个区别。

  1. limit/skip
    limit 返回 Stream 的前面 n 个元素;skip 则是扔掉前 n 个元素(它是由一个叫 subStream 的方法改名而来)。
public void testLimitAndSkip() {
	List<Person> persons = new ArrayList();
	for (int i = 1; i <= 10000; i++) {
		Person person = new Person(i, "name" + i);
		persons.add(person);
	}
	List<String> personList2 = persons.stream().
	map(Person::getName).limit(10).skip(3).collect(Collectors.toList());
	System.out.println(personList2);
}
private class Person {
	public int no;
	private String name;
	public Person (int no, String name) {
	this.no = no;
	this.name = name;
	}
	public String getName() {
	System.out.println(name);
	return name;
	}
}

输出结果为:

[name4, name5, name6, name7, name8, name9, name10]
  1. min/max/distinct

min 和 max 的功能也可以通过对 Stream 元素先排序,再 findFirst 来实现,但前者的性能会更好,为 O(n),而 sorted 的成本是 O(n log n)。同时它们作为特殊的 reduce 方法被独立出来也是因为求最大最小值是很常见的操作。

// 找出最长一行的长度!
BufferedReader br = new BufferedReader(new FileReader("c:\\SUService.log"));
int longest = br.lines().
	mapToInt(String::length).
	max().
	getAsInt();
br.close();
System.out.println(longest);

// 找出全文的单词,转小写,并排序
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
Stream 有三个 match 方法,从语义上说:

allMatch:Stream 中全部元素符合传入的 predicate,返回 true
anyMatch:Stream 中只要有一个元素符合传入的 predicate,返回 true
noneMatch:Stream 中没有一个元素符合传入的 predicate,返回 true
它们都不是要遍历全部元素才能返回结果。例如 allMatch 只要一个元素不满足条件,就 skip 剩下的所有元素,返回 false。

List<Person> persons = new ArrayList();
persons.add(new Person(1, "name" + 1, 10));
persons.add(new Person(2, "name" + 2, 21));
persons.add(new Person(3, "name" + 3, 34));
persons.add(new Person(4, "name" + 4, 6));
persons.add(new Person(5, "name" + 5, 55));
boolean isAllAdult = persons.stream().
	allMatch(p -> p.getAge() > 18);
System.out.println("All are adult? " + isAllAdult);
boolean isThereAnyChild = persons.stream().
	anyMatch(p -> p.getAge() < 12);
System.out.println("Any child? " + isThereAnyChild);

5. 进阶:自己生成流

Stream.generate

通过实现 Supplier 接口,你可以自己来控制流的生成。这种情形通常用于随机数、常量的 Stream,或者需要前后元素间维持着某种状态信息的 Stream。把 Supplier 实例传递给 Stream.generate() 生成的 Stream,默认是串行(相对 parallel 而言)但无序的(相对 ordered 而言)。由于它是无限的,在管道中,必须利用 limit 之类的操作限制 Stream 大小。

Random seed = new Random();
Supplier<Integer> random = seed::nextInt;
Stream.generate(random).limit(10).forEach(System.out::println);
//Another way
IntStream.generate(() -> (int) (System.nanoTime() % 100)).
limit(10).forEach(System.out::println);

Stream.generate() 还接受自己实现的 Supplier。例如在构造海量测试数据的时候,用某种自动的规则给每一个变量赋值;或者依据公式计算 Stream 的每个元素值。这些都是维持状态信息的情形。

6. Stream 的特性可以归纳为

  1. 不是数据结构

  2. 它没有内部存储,它只是用操作管道从 source(数据结构、数组、generator function、IO channel)抓取数据。

  3. 它也绝不修改自己所封装的底层数据结构的数据。例如 Stream 的 filter 操作会产生一个不包含被过滤元素的新 Stream,而不是从 source 删除那些元素。

  4. 所有 Stream 的操作必须以 lambda 表达式为参数

  5. 不支持索引访问

  6. 你可以请求第一个元素,但无法请求第二个,第三个,或最后一个。不过请参阅下一项。

  7. 很容易生成数组或者 List

  8. 惰性化

  9. 很多 Stream 操作是向后延迟的,一直到它弄清楚了最后需要多少数据才会开始。

  10. Intermediate 操作永远是惰性化的。

  11. 并行能力

  12. 当一个 Stream 是并行化的,就不需要再写多线程代码,所有对它的操作会自动并行进行的。

  13. 可以是无限的

  14. 集合有固定大小,Stream 则不必。limit(n) 和 findFirst() 这类的 short-circuiting 操作可以对无限的 Stream 进行运算并很快完成。

Reference:

https://developer.ibm.com/zh/articles/j-lo-java8streamapi/

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值