从这一篇文章开始,我们会由浅入深,全面的学习stream API的最佳实践(结合我的使用经验),本想一篇写完,但写着写着发现需要写的内容太多了,所以分成一个系列慢慢来说。给大家分享我的经验的同时,也促使我复习每一个细节,大家共同进步。
Java 8新增了一个API叫做Stream ,Stream的英文可以理解为流动的液体,可能很多人一听脑子里的第一印象就是流式计算,不自觉地就心生畏惧,感觉非常的高深莫测。其实这就是一个辅助处理集合数据的工具类,工具的更新必然带来的是生产力的提升,这里的生产力代表的就是整洁优雅的代码,更高的灵活度,更好的性能。相信各类的技术文章(包括博客和书籍)已经写过无数遍了。比如下面摘录《Java 8实战》关于流的描述:
流是Java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。就现在来说,你可以把它们看成遍历数据集的高级迭代器。此外,流还可以透明地并行处理,你无需写任何多线程代码了!
这段话的表述个人感觉类似于抓手、赋能、心智之类的PPT黑话,看着挺高级的,也能懂一些,但也不是很懂,反正如果对于不知道Stream的人,并不能建立直接的理解。
所以流到底是什么呢?是一个接口。让我们看看它的声明:
public interface Stream<T> extends BaseStream<T, Stream<T>> {
Stream<T> filter(Predicate<? super T> predicate);
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
void forEach(Consumer<? super T> action);
...
}
就是个接口,然后这个借口有一些抽象方法:filter,map,forEach等等。我们可以看到有些方法返回了新的Stream,有些直接是void。这个接口用来干什么用呢?处理集合数据。为什么这么说?看下面一个Collection接口的方法:
public interface Collection<E> extends Iterable<E> {
...
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
}
那么所有继承了Collection的接口都可以直接创建Stream,然后再执行Stream里面的操作。所以这么看下来,首先得承认书中的表述是高度抽象且精炼的,这是书籍该做的事情。但从易于理解的角度,我觉得可以说是简洁高效安全的处理集合数据的工具类。如下图所示,Stream是一个中间过程。
Stream随着Java 8的发布已经8年多了,在我有限的职业生涯里,碰到的一些职场新人依然有些人觉得使用for或者iterator来遍历集合更易读易懂。但如果他真正了解Stream所蕴含的能力后,应该会转变想法。下面简单介绍一下Stream都提供了什么样的能力。
可以看到,可以操作stream的对象基本为List或者Array.
这可能是用的最多的功能。对应的方法为:
Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1")
.filter(x -> x.startsWith("a"))
.distinct()
.skip(1)
.limit(3)
.forEach(System.out::println);
}
// output:
a3
a4
a1
这里主要是map,map代表了一种对应关系,即地图坐标与实际地点的对应关系,我们有了经纬度就可以准确的找到地址,这个例子可以很形象的解释map命名的由来和功能。
同样,举个简单的例子:
Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1")
.filter(x -> x.startsWith("a"))
.map(String::toUpperCase)
.forEach(System.out::println);
//output
A2
A3
A4
A2
A1
List<String> list = Stream.of("Hello", "world!")
.map(s -> s.split(""))
.flatMap(Arrays::stream)
.collect(Collectors.toList());
System.out.println(list);
//output
[H, e, l, l, o, w, o, r, l, d, !]
这里的能力可以认为是一个加强版的contains方法,具备多种查找匹配能力。
这里需要解惑的是findAny与findFirst的区别,因为这两个都是找到满足条件的元素就返回,但findFirst会在限制并行流的计算,会严格按照集合中元素的顺序来依次查找。findAny就不会有这个限制。如果非并行计算场景,这二者并无区别。
下面依旧举简单的例子说明:
boolean b1 = Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1")
.anyMatch(x -> x.startsWith("a"));
System.out.println(b1);
//output: true
String s2 = Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1")
.filter(x -> x.startsWith("a"))
.findFirst()
.get();
System.out.println(s2);
//output: a2
String s3 = Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1")
.filter(x -> x.startsWith("a"))
.findAny()
.get();
System.out.println(s3);
//output: a2
//换成并行流
String s4 = Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1")
.parallel()
.filter(x -> x.startsWith("a"))
.findFirst()
.get();
System.out.println(s4);
//output: a2
String s5 = Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1")
.parallel()
.filter(x -> x.startsWith("a"))
.findAny()
.get();
System.out.println(s5);
//output: a4
归约是一个比较复杂的数学理论,通常是用于将一个未知的问题转换成另一些已知问题,同时这些已知的问题和未知的问题存在某种关联。这里不做详细探讨。在Stream API有一些方法就是用的类似的归约的思想,将大的集合计算分解成小的函数计算并最终合成结果。
T reduce(T identity, BinaryOperator<T> accumulator);
Optional<T> reduce(BinaryOperator<T> accumulator);
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
这样的方法签名如同天书,先看一个简单的例子:
Integer i = Stream.of(1, 4, 6, 7, 9).reduce(1, (sum, i) -> sum + i);
System.out.println(i);
其中reduce我传了2个参数:
List<String> props = Lists.newArrayList("profile=native", "debug=true", "logging=warn", "interval=500");
Map<String, String> map = props.stream()
.map(kv -> {
String[] ss = kv.split("=", 2);
Map<String, String> m = Maps.newHashMap();
m.put(ss[0], ss[1]);
return m;
})
.reduce(new HashMap<>(), (m, kv) -> {
m.putAll(kv);
return m;
});
map.forEach((k, v) -> System.out.println(k + " = " + v));
//output:
logging = warn
interval = 500
debug = true
profile = native
第一个map执行完之后返回了多个小map这里使用reduce进行一个map的累加:
本文介绍了stream是什么、创建stream的方法、stream的一些基本API的能力和reduce方法的使用。作为stream最佳实践的开篇,先从stream的基础开始写,后续会逐步深入并总结我个人使用下来的最佳实践,希望大家持续关注,共同学习。