Stream(流)是一个来自数据源的元素队列并支持聚合操作
元素是特定类型的对象,形成一个队列。Java中的Stream并不会存储元素,而是按需计算。 数据源 流的来源。可以是集合,数组,I/O channel, 产生器generator 等。 聚合操作 类似SQL语句一样的操作, 比如filter, map, reduce, find, match, sorted等。 和以前的Collection操作不同, Stream操作还有两个基础的特征:
Pipelining: 中间操作都会返回流对象本身。这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
内部迭代:以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。Stream提供了内部迭代的方式, 通过访问者模式(Visitor)实现
java.util.Stream
表示可以在其上执行一个或多个操作的元素序列。流操作是中间或终端。当终端操作返回一个特定类型的结果时,中间操作返回流本身,所以你可以链接多个方法调用。流在源上创建,例如一个 java.util.Collection
像列表或集合(不支持映射)。流操作既可以按顺序执行,也可以并行执行。
我们先来看看顺序流如何工作。首先,我们以字符串列表的形式创建一个示例源代码:
List<String> stringCollection = new ArrayList<>();
stringCollection.add("ddd2");
stringCollection.add("aaa2");
stringCollection.add("bbb1");
stringCollection.add("aaa1");
stringCollection.add("bbb3");
stringCollection.add("ccc");
stringCollection.add("bbb2");
stringCollection.add("ddd1");
Java 8 中的集合已被扩展,因此您可以通过调用
Collection.stream()
或Collection.parallelStream()
来简单地创建流。以下各节介绍最常见的流操作。
stream()
− 为集合创建串行流。
parallelStream()
− 为集合创建并行流。
List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
List<String> filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());
过滤器接受一个谓词来过滤流的所有元素。这个操作是中间的,使我们能够调用另一个流操作(forEach
)的结果。ForEach 接受一个消费者被执行的过滤流中的每个元素。ForEach 是一个终端操作。它是无效的,所以我们不能调用另一个流操作。
stringCollection
.stream()
.filter((s) -> s.startsWith("a"))
.forEach(System.out::println);
// "aaa2", "aaa1"
排序是一个中间操作,返回流的排序视图。元素按自然顺序排序,除非您传递自定义比较器。
stringCollection
.stream()
.sorted()
.filter((s) -> s.startsWith("a"))
.forEach(System.out::println);
// "aaa1", "aaa2"
请记住,排序只会创建流的排序视图,而不会操纵支持的集合的排序。stringCollection
的排序是不变的:
System.out.println(stringCollection);
// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
中间操作映射通过给定函数将每个元素转换为另一个对象。以下示例将每个字符串转换为大写字母字符串。但是您也可以使用 map
将每个对象转换为另一种类型。结果流的泛型类型取决于您传递给 map
的函数的泛型类型。
stringCollection
.stream()
.map(String::toUpperCase)
.sorted((a, b) -> b.compareTo(a))
.forEach(System.out::println);
// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
可以使用各种匹配操作来检查某个谓词是否与流匹配。所有这些操作都是终端并返回布尔结果。
boolean anyStartsWithA =
stringCollection
.stream()
.anyMatch((s) -> s.startsWith("a"));
System.out.println(anyStartsWithA); // true
boolean allStartsWithA =
stringCollection
.stream()
.allMatch((s) -> s.startsWith("a"));
System.out.println(allStartsWithA); // false
boolean noneStartsWithZ =
stringCollection
.stream()
.noneMatch((s) -> s.startsWith("z"));
System.out.println(noneStartsWithZ); // true
Count 是一个终端操作,返回流中元素的个数。
long startsWithB =
stringCollection
.stream()
.filter((s) -> s.startsWith("b"))
.count();
System.out.println(startsWithB); // 3
该终端操作使用给定的功能对流的元素进行缩减。结果是一个 Optional
持有缩小后的值。
Optional<String> reduced =
stringCollection
.stream()
.sorted()
.reduce((s1, s2) -> s1 + "#" + s2);
reduced.ifPresent(System.out::println);
// "aaa1##aaa2##bbb1##bbb2##bbb3##ccc##ddd1##ddd2"
如上所述,流可以是顺序的也可以是并行的。顺序流上的操作在单个线程上执行,而并行流上的操作在多个线程上同时执行。
以下示例演示了通过使用并行流提高性能是多么容易。
首先,我们创建一个较大的独特元素的列表:
int max = 1000000;
List<String> values = new ArrayList<>(max);
for (int i = 0; i < max; i++) {
UUID uuid = UUID.randomUUID();
values.add(uuid.toString());
}
现在我们测量对这个集合进行排序所花费的时间。
long t0 = System.nanoTime();
long count = values.stream().sorted().count();
System.out.println(count);
long t1 = System.nanoTime();
long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
System.out.println(String.format("sequential sort took: %d ms", millis));
// sequential sort took: 899 ms
long t0 = System.nanoTime();
long count = values.parallelStream().sorted().count();
System.out.println(count);
long t1 = System.nanoTime();
long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
System.out.println(String.format("parallel sort took: %d ms", millis));
// parallel sort took: 472 ms
如你所见,两个代码段差不多,但是并行排序快了近 50%。你所需做的仅仅是将 stream()
改为 parallelStream()
。
如前所述,map 不直接支持流。Map 接口本身没有可用的 stream()
方法,但是你可以通过 map.keySet().stream()
、 map.values().stream()
和 map.entrySet().stream()
创建指定的流。
此外,map 支持各种新的、有用的方法来处理常见任务。
Map<Integer, String> map = new HashMap<>();
for (int i = 0; i < 10; i++) {
map.putIfAbsent(i, "val" + i);
}
map.forEach((id, val) -> System.out.println(val));
上面的代码应该是自我解释的:putIfAbsent
阻止我们写入额外的空值检查;forEach
接受消费者为 map 的每个值实现操作。
这个例子展示了如何利用函数来计算 map 上的代码:
map.computeIfPresent(3, (num, val) -> val + num);
map.get(3); // val33
map.computeIfPresent(9, (num, val) -> null);
map.containsKey(9); // false
map.computeIfAbsent(23, num -> "val" + num);
map.containsKey(23); // true
map.computeIfAbsent(3, num -> "bam");
map.get(3); // val33
接下来,我们学习如何删除给定键的条目,只有当前键映射到给定值时:
map.remove(3, "val3");
map.get(3); // val33
map.remove(3, "val33");
map.get(3); // null
另一个有用方法:
map.getOrDefault(42, "not found"); // not found
合并一个 map 的 entry 很简单:
map.merge(9, "val9", (value, newValue) -> value.concat(newValue));
map.get(9); // val9
map.merge(9, "concat", (value, newValue) -> value.concat(newValue));
map.get(9); // val9concat
如果不存在该键的条目,合并或者将键/值放入 map 中;否则将调用合并函数来更改现有值。