JDK 8 stream 是什么
本文所说的stream 跟java IO 里的stream有点区别, 虽然都是流的意思, 但本文所写的stream 是指 JDK8 API的新成员(还有lambda 和 try-with-resource) , 它允许以生命的方式处理数据集合。
特点
1. 代码简洁
函数式编程写出的代码简洁且而且可读性不差, 摆脱for循环
2. 多核友好 (parallelStream())
java函数式编程使得并行程序开发更加简单, 也就是将相对于for 循环性能提高了。
流程
1. 第一步, 把集合转换为流stream
2. 第二步, 操作stream流
stream流在管道中经过中间操作(intermediate operation)的处理, 最后由最终操作(terminal operation)得到前面管道处理的结果。
操作符
两种: 中间操作符 和 终止操作符
中间操作符
流方法 | Description | Example |
---|---|---|
filter | 设置条件过滤集合中的元素 | List strList = Arrays.asList(“abc”,“cde”,“BBB”,“ccc”,“ecdd”,“bbbbb1”); List filteredStrList=strList.stream().filter(x->x.contains(“c”)).collect(Collectors.toList()); filteredStrList.forEach(x->log.info(x)); |
distinct | 根据条件去重, 如果是集合是一些对象, 则相对应的类要重写hashcode()和equal(), 不支持对对象某个属性去重,如果要实现根据某个属性去重则必须用filter(distinctByKey(x->x.getId()) | List strList = Arrays.asList(“abc”,“cde”,“BBB”,“ccc”,“ecdd”,“bbbbb1”,“cde”,“BBB”,“ccc”); List distinctedStrList = strList.stream().distinct().collect(Collectors.toList()); |
limit | 会返回一个不超过指定长度的流,用于分页, 相当于sql的top() | List strList = Arrays.asList(“abc”,“cde”,“BBB”,“ccc”,“ecdd”,“bbbbb1”,“cde”,“BBB”,“ccc”); List limitedStrList = strList.stream().limit(3).collect(Collectors.toList()); |
skip | Returns a stream consisting of the remaining elements of this stream after discarding the first n elements of the stream. If this stream contains fewer than n elements then an empty stream will be returned. 用于除掉前n个元素 | List strList = Arrays.asList(“abc”,“cde”,“BBB”,“ccc”,“ecdd”,“bbbbb1”); List filteredStrList = strList.stream().filter(x->x.contains(“c”)).collect(Collectors.toList()); |
map | 对集合里每个元素里进行处理 | List strList = Arrays.asList(“abc”,“cde”,“BBB”,“ccc”,“ecdd”,“bbbbb1”,“cde”,“BBB”,“ccc”); List mapStrList = strList.stream().map(x->x + “!!”).collect(Collectors.toList()); |
flatMap | 流的扁平化处理, 集合里的集合元素转换成1个合并后的流, 和map的返回值对比请 | 参考下面的例子 |
sorted | 根据规则排序, 注意这里是stream的sort, 注意和List.sort()的区别 https://editor.csdn.net/md/?articleId=126576104 | List sortedUserList = userList.stream().sorted(Comparator.comparing(MyUser::getName).reversed() .thenComparing(MyUser::getId)).collect(Collectors.toList()); |
根据对象元素某个属性去重.
上面提到了, distinct()只会根据对象本身的Hashcode 和equal 去重. 如果想根据对象某个属性去重.
实际场景:
在Hibernate Envers(record level audit 框架)的 AuditReader中会用到。去重以获得pk列表。
这时我们需要定义1个distinctbyKey的方法:
private static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
Map<Object,Boolean> seen = new ConcurrentHashMap<>();
return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
}
而利用Fitler 和 distinctBykey 方法来去重, 下面是例子
public void example(){
List<MyUser> userList = new ArrayList<>();
userList.add(new MyUser(3L,"Jack"));
userList.add(new MyUser(3L,"Bill"));
userList.add(new MyUser(4L,"Alice"));
List<MyUser> disUserList = userList.stream().distinct().collect(Collectors.toList());
log.info(String.valueOf(disUserList));
List<MyUser> disUserList2 = userList.stream().filter(distinctByKey(x->x.getId())).collect(Collectors.toList());
log.info(String.valueOf(disUserList2));
}
flatMap 和 map 的区别
我们首先编写1个工具方法, 把字符串转成字符List的流:
class Tools{
public static Stream<Character> getCharListStream(String str){
List<Character> characterList = new ArrayList<>();
for (char x : str.toCharArray()){
characterList.add(x);
}
return characterList.stream();
}
}
我们先看用map的情况
/**
* map(Tools::getCharListStream) return a Stream<Stream<Character>> Object
*/
@Slf4j
class StreamMap2 {
public void example1(){
List<String> strList = Arrays.asList("abc","cde","BBB","ccc","ecdd","bbbbb1","cde","BBB","ccc");
Stream<Stream<Character>> mapStrStream = strList.stream().map(Tools::getCharListStream);
List<Stream<Character>> mapStrStreamList = mapStrStream.collect(Collectors.toList());
List<List<Character>> mapStrListList = strList.stream().map(Tools::getCharListStream).collect(Collectors.toList())
.stream().map(x->x.collect(Collectors.toList())).collect(Collectors.toList());
log.info(String.valueOf(mapStrStream));
log.info(String.valueOf(mapStrStreamList));
log.info(String.valueOf(mapStrListList));
}
}
可以当我们用map和使用上面的工具类去转换String 到 字符串数组时, 返回的是1个Stream<Stream<Character>> 对象。
也就是字符串数组流的数组 的流
但是我们可以将里面的Stream<Charactor>对象利用Collect()函数转换成List<Character>。然后输出
输出
20:47:45.826 [main] INFO com.home.javacommon.study.collectionstream.StreamMap2 - java.util.stream.ReferencePipeline$3@1ee0005
20:47:45.827 [main] INFO com.home.javacommon.study.collectionstream.StreamMap2 - [java.util.stream.ReferencePipeline$Head@75a1cd57, java.util.stream.ReferencePipeline$Head@3d012ddd, java.util.stream.ReferencePipeline$Head@6f2b958e, java.util.stream.ReferencePipeline$Head@1eb44e46, java.util.stream.ReferencePipeline$Head@6504e3b2, java.util.stream.ReferencePipeline$Head@515f550a, java.util.stream.ReferencePipeline$Head@626b2d4a, java.util.stream.ReferencePipeline$Head@5e91993f, java.util.stream.ReferencePipeline$Head@1c4af82c]
20:47:45.827 [main] INFO com.home.javacommon.study.collectionstream.StreamMap2 - [[a, b, c], [c, d, e], [B, B, B], [c, c, c], [e, c, d, d], [b, b, b, b, b, 1], [c, d, e], [B, B, B], [c, c, c]]
十分合理
第1个对象 flatMapStrStream 输出只是它的hashcode
第1个数组mapStrStreamList 里面的元素是Stream<Charactor> 对象, 输出的是Hascode 列表
第2个数组里面的mapStrListList 里面的元素是List<Character> 对象, 输出的是直观的字符串数组了。
我们再看看FlatMap
首先, FlatMap里面的参数必须是1个流对象的函数型接口对象, 而不是1个简单的函数型接口对象(Map的参数可以是。
也就是将, 如果不可以直接当Map来使用。
下面的代码是错误的
由于我们上面工具类方法返回的类型是Stream<Charater>, 则是适用的。
例子:
@Slf4j
/**
* flatMap(Tools::getCharListStream) return a Stream<Character> Object (merged multiple Stream<Characters> to a single one
*/
class StreamFlatMap1 {
public void example1(){
List<String> strList = Arrays.asList("abc","cde","BBB","ccc","ecdd","bbbbb1","cde","BBB","ccc");
//List<Character> flatMapStrSreamList= strList.stream().flatMap(x->x.concat("sdf")).collect(Collectors.toList());
Stream<Character> flatMapStrStream= strList.stream().flatMap(Tools::getCharListStream);
List<Character> flatMapStrList = flatMapStrStream.collect(Collectors.toList());
log.info(String.valueOf(flatMapStrStream));
log.info(String.valueOf(flatMapStrList));
}
}
可以见到 flatMap返回的是1个Stream<Charactor> 的对象, 而不是map函数的List<Stream<Charactor>>
再看输出:
20:47:45.828 [main] INFO com.home.javacommon.study.collectionstream.StreamFlatMap1 - java.util.stream.ReferencePipeline$7@cac736f
20:47:45.829 [main] INFO com.home.javacommon.study.collectionstream.StreamFlatMap1 - [a, b, c, c, d, e, B, B, B, c, c, c, e, c, d, d, b, b, b, b, b, 1, c, d, e, B, B, B, c, c, c]
也就是将, flatMap把里面的内容合并了在返回1个合并后数组的流…
map返回一个值, flatMap必须返回一个流, 多个值
这就是区别, 也就是把原来的数组降维了, 就是所谓的扁平化…
终止操作符
流方法 | Description | Example |
---|---|---|
anyMarch | 返回boolean, 检查Stream是否存在至少1个符合条件的元素 | List strList = Arrays.asList(“abc”,“cde”,“BBB”,“ccc”,“ecdd”,“bbbbb1”,“cde”,“BBB”,“ccc”); boolean isThere = strList.stream().map(x->x + “!!”).anyMatch(x->x.length() < 5); log.info(String.valueOf(isThere)); |
allMarch | 返回boolean, 检查Stream是否每个元素都满足提供的条件 | List strList = Arrays.asList(“abc”,“cde”,“BBB”,“ccc”,“ecdd”,“bbbbb1”,“cde”,“BBB”,“ccc”); boolean isThere = strList.stream().map(x->x + “!!”).allMatch(x->x.length() > 5); log.info(String.valueOf(isThere)); |
noneMarch | 返回boolean, 检查Stream是否每个元素都不满足条件 | List strList = Arrays.asList(“abc”,“cde”,“BBB”,“ccc”,“ecdd”,“bbbbb1”,“cde”,“BBB”,“ccc”); boolean isThere = strList.stream().map(x->x + “!!”).anyMatch(x->x.length() < 5); log.info(String.valueOf(isThere)); |
findFrist | 返回stream里的第1个元素 | List strList = Arrays.asList(“abc”,“cde”,“BBB”,“ccc”,“ecdd”,“bbbbb1”,“cde”,“BBB”,“ccc”); Optional first = strList.stream().map(x->x + “!!”).filter(x->x.length() > 5).findFirst(); log.info(String.valueOf(first.get())); |
findAny | 返回stream里的第任意1个元素,在串型stream等于findFirst, 一般用于parallelStream | List strList = Arrays.asList(“abc”,“cde”,“BBB”,“ccc”,“ecdd”,“bbbbb1”,“cde”,“BBB”,“ccc”); Optional any = strList.parrallelstream().map(x->x + “!!”).filter(x->x.length() > 5).findAny(); log.info(String.valueOf(any.get())); |
forEach | 遍历每一个元素,执行操作, 优点类似于中间操作符的Map,但是相对于Map,ForEach是有序的, 对比于外部的for循环性能会更高。跟List的forEach很类似 | List strList = Arrays.asList(“abc”,“cde”,“BBB”,“ccc”,“ecdd”,“bbbbb1”,“cde”,“BBB”,“ccc”); strList.stream().map(x->x + “!!”).filter(x->x.length() > 5).forEach(x->log.info(x)); |
collect | 将流转换返回集合,而且可以转换为其他形式的流(例如list->set去重)厉害了 | List strList = Arrays.asList(“abc”,“cde”,“BBB”,“ccc”,“ecdd”,“bbbbb1”,“cde”,“BBB”,“ccc”); Set strSet = strList.stream().collect(Collectors.toSet());; log.info(String.valueOf(strSet)); |
reduce | 别被单词字面意思误导, reduce用于对集合中所有元素进行1个总的操作(例如所有元素相加/平均…), 右面reduce方法第一个参数identity是计算结果都初始值,接口方法参数的acc代表计算结果, item表示元素 | List strList = Arrays.asList(“abc”,“cde”,“BBB”,“ccc”,“ecdd”,“bbbbb1”,“cde”,“BBB”,“ccc”); String sumStr = strList.stream().reduce(“sum:”, (acc, item)-> acc.concat(item)); |
count | 返回元素结果, 其实用集合的size()一样的… | Arrays.asList(“abc”,“cde”,“BBB”,“ccc”,“ecdd”,“bbbbb1”,“cde”,“BBB”,“ccc”).stream().count() |
max | 获得最大的元素, 必须提供Comparator参数 | MyUser maxUser = userList.stream().max(Comparator.comparing(MyUser::getId).thenComparing(MyUser::getName)).get(); |
collect toMap 的方法
其实这个方法一般用于把1个对象数组 创建1个对象的MAP, 其中\指定对象某个属性作为key。
例子:
public void example1(){
List<MyUser> userList = new ArrayList<>();
userList.add(new MyUser(4L,"Alice"));
userList.add(new MyUser(3L,"Jack"));
userList.add(new MyUser(3L,"Bill"));
userList.add(new MyUser(2L,"Ted"));
Map<Long,MyUser> myUserMap = userList.stream().collect(Collectors.toMap(MyUser::getId, v->v, (a,b)->a));
log.info(String.valueOf(myUserMap));
}
toMap()中第1个参数 MyUser::getId, 就是指定对象中哪个属性作为key
v->v 表示以对象本身作为值, 当然这里很多操作空间(见下1个例子)。 而且可以用Function.identity() 来代替v->v
源码:
/**
* Returns a function that always returns its input argument.
*
* @param <T> the type of the input and output objects to the function
* @return a function that always returns its input argument
*/
static <T> Function<T, T> identity() {
return t -> t;
}
(a,b)->a, 表示如果某两个对象的key属性重复, 则取前1个or后1个对象作为值。
如果没有这个参数, 如果数字出现两个具有重复key的对象, 则会抛出下面的异常。
Exception in thread "main" java.lang.IllegalStateException: Duplicate key MyUser(id=3, name=Jack)
输出:
{2=MyUser(id=2, name=Ted), 3=MyUser(id=3, name=Jack), 4=MyUser(id=4, name=Alice)}
进阶了, 如果对象中没有属性适合做key,如何在toMap()方法里,用自增的数字生成key?
参考:https://stackoverflow.com/questions/51047522/how-to-auto-increment-the-key-of-a-hashmap-using-collectors-and-stream-in-java-8
需求: 下面构建了4个User对象的数组userList, 其中 getId()的属性有重复, 并不适合做key。
如何利用stream的toMap方法来构造个新的Map, 以1,2,3,4… 自增数字作为key。
List<MyUser> userList = new ArrayList<>();
userList.add(new MyUser(4L,"Alice"));
userList.add(new MyUser(3L,"Jack"));
userList.add(new MyUser(3L,"Bill"));
userList.add(new MyUser(2L,"Ted"));
Step 1. 构造1个 指定size 的LongStream 对象.
IntStream intStream = IntStream.range(0, userList.size());
上面的代码是构造1个IntStream, 从0开始, 数量是userList的元素个数(4),
range()方法,创建一个以1为增量步长,从startInclusive(包括)到endExclusive(不包括)的有序的数字流。
至于不是用LongStream, 毕竟List的元素上线就是Integer 上线, list 用get(Integer i) 来获得元素。
Step 2. 利用boxed(装箱)函数获得1个stream对象。
Stream iStream = intStream.boxed();
boxes()的源代码:
@Override
public final Stream<Integer> boxed() {
return mapToObj(Integer::valueOf);
}
这样我们的得到1个基于Integer 0,1,2,3 的流, 然后基于这个流去而不是UserList去创建1个Map。
Map<Integer, MyUser> userMap = iStream.collect(Collectors.toMap(k->k, k->userList.get(k)));
这样我们就实现了需求了。
也明白我们的IntStream 为何要从0开始, 因为userList第一元素就是userList.get(0), 如何用1就必须写成userList.get(k-1)了。
当然我们也可以整合一下代码:
public void example2(){
List<MyUser> userList = new ArrayList<>();
userList.add(new MyUser(4L,"Alice"));
userList.add(new MyUser(3L,"Jack"));
userList.add(new MyUser(3L,"Bill"));
userList.add(new MyUser(2L,"Ted"));
Map<Integer, MyUser> userMap = IntStream.range(0, userList.size()).boxed().collect(Collectors.toMap(Function.identity(), userList::get));
log.info(String.valueOf(userMap));
}
代码越来越高大上了。。