简单说,对 Stream 的使用就是实现一个 filter-map-reduce 过程,产生一个最终结果,或者导致一个副作用(side effect)。
Java8中,所有的流操作会被组合到一个 stream pipeline中,这点类似linux中的pipeline概念,将多个简单操作连接在一起组成一个功能强大的操作。一个 stream pileline首先会有一个数据源,这个数据源可能是数组、集合、生成器函数或是IO通道,流操作过程中并不会修改源中的数据;然后还有零个或多个中间操作,每个中间操作会将接收到的流转换成另一个流(比如filter);最后还有一个终止操作,会生成一个最终结果(比如sum)。流是一种惰性操作,所有对源数据的计算只在终止操作被初始化的时候才会执行。
接下来我们看一些stream常用操作。
1、Stream的构造:
1)Collection 和数组:
public static void stream1Test() { Stream<Integer> s1 = Stream.of(1,2,3,4,5); String[] strA = new String[]{"a","b","c"}; Stream<String> s2 = Stream.of(strA); Stream<String> stream2 = Arrays.stream(strA); stream2.forEach(System.out::println); List<String> list1 = Arrays.asList(new String[]{"a1","b2","c3"}); Stream<List<String>> s3 = Stream.of(list1); Stream<String> stream3 = list1.stream(); stream3.forEach(System.out::println); }
主要使用了Stream.of、Arrays.stream和Collection.stream() or Collection.parallelStream()方法来构造数组、list的Stream。
2)三种基本类型 Stream:
需要注意的是,对于基本数值型,目前有三种对应的包装类型 Stream:IntStream、LongStream、DoubleStream。当然我们也可以用 Stream<Integer>、Stream<Long> >、Stream<Double>,但是 boxing 和 unboxing 会很耗时,所以特别为这三种基本数值型提供了对应的 Stream。
Java 8 中还没有提供其它数值型 Stream,因为这将导致扩增的内容较多。而常规的数值型聚合运算可以通过上面三种 Stream 进行。
public static void rangeTest() { IntStream stream = IntStream.range(1, 9); IntStream stream2 = IntStream.rangeClosed(1, 9); IntStream stream3 = IntStream.of(1,2,3,5); stream2.forEach(p -> System.out.println(p)); }
3)使用Random.ints()
import java.util.Random; import java.util.stream.IntStream; public class StreamBuilders{ public static void main(String[] args){ IntStream stream = new Random().ints(1, 10); stream.forEach(p -> System.out.println(p)); } }
4)使用Stream.generate():
import java.util.concurrent.TimeUnit; import java.util.stream.Stream; public class StreamBuilders{ static int i = 0; public static void main(String[] args){ Stream<Integer> stream = Stream.generate(() -> { try{ TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e){ e.printStackTrace(); } return i++; }); stream.forEach(p -> System.out.println(p)); } }
还有很多,这里不一一列出了。
2、Stream转集合/数组类型(Stream结果放到集合/数组中)
1)一般的转换:
public static void stream2list() { Stream<String> stream = Stream.of(new String[]{"a","b","c"}); //array String[] strA = stream.toArray(String[] :: new); //collection List<String> list = stream.collect(Collectors.toList()); ArrayList<String> list1 = stream.collect(Collectors.toCollection(ArrayList :: new)); Set<String> set = stream.collect(Collectors.toSet()); Stack<String> stack = stream.collect(Collectors.toCollection(Stack :: new)); //string String str = stream.collect(Collectors.joining(";")); }
注:一个 Stream 只可以使用一次,上面的代码为了简洁而重复使用了数次。否则会报如下错:
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229) at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499) at j8.StreamTest.groupTest(StreamTest.java:136) at j8.StreamTest.main(StreamTest.java:18)
说明:
- 我们通过Collectors这个类的toList和toSet方法,可以很容易将Stream的结果放到list、set中;也可以通过更通用的Collectors.toCollection(Supplier<C> collectionFactory);方法,将结果放到指定集合中;
- 我们也可以自己制定结果容器的类型Collectors的toCollection接受一个Supplier函数式接口类型参数,可以直接使用构造方法引用的方式;
2)转换成map:
将stream转成map有如下api,我们接下来一一介绍。
这里User对象有四个属性(id,name,sex,age)
public static void stream2Map() { List<User> userList = new ArrayList<User>(){ private static final long serialVersionUID = 1L; { add(new User(1L,"test1",0,12)); add(new User(3L,"test3",1,23)); add(new User(2L,"test2",0,2)); } }; //map Map<Long, User> mapp = userList.stream().collect(Collectors.toMap( User::getId, Function.identity())); System.out.println(mapp); Map<Long, String> map = userList.stream().collect(Collectors.toMap( User::getId, User::getName)); System.out.println(map); }
输出:
{1=User [id=1, name=test1, sex=0, age=12], 2=User [id=2, name=test2, sex=0, age=2], 3=User [id=3, name=test3, sex=1, age=23]} {1=test1, 2=test2, 3=test3}
3)转成map——有重复键
public static void stream2Map2() { List<User> userList = new ArrayList<User>(){ private static final long serialVersionUID = 1L; { add(new User(1L,"test1",0,12)); add(new User(3L,"test3",1,23)); add(new User(2L,"test2",0,2)); add(new User(4L,"test2",0,14)); } }; Map<String, Long> mapp = userList.stream().collect(Collectors.toMap(User::getName, User::getId)); System.out.println(mapp); }
上面我们按照name去建立map,由于有重名的(test2),所以会报一下错误
Exception in thread "main" java.lang.IllegalStateException: Duplicate key 2 at java.util.stream.Collectors.lambda$throwingMerger$0(Collectors.java:133) at java.util.HashMap.merge(HashMap.java:1254) at java.util.stream.Collectors.lambda$toMap$58(Collectors.java:1320) at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169) at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1382) at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481) at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471) at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708) at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499) at j8.StreamTest.stream2Map2(StreamTest.java:96) at j8.StreamTest.main(StreamTest.java:17)
这个错误信息有点误导,应该显示“test2”而不是键的值。
要解决上述重复的关键问题,请传入第三个mergeFunction参数,如下所示:
public static void stream2Map2() { List<User> userList = new ArrayList<User>(){ private static final long serialVersionUID = 1L; { add(new User(1L,"test1",0,12)); add(new User(3L,"test3",1,23)); add(new User(2L,"test2",0,2)); add(new User(4L,"test2",0,14)); } }; Map<String, Long> mapp2 = userList.stream().collect(Collectors.toMap( User::getName, User::getId, (oldValue, newValue) -> oldValue)); System.out.println(mapp2); Map<String, Long> mapp3 = userList.stream().collect(Collectors.toMap( User::getName, User::getId, (oldValue, newValue) -> newValue)); System.out.println(mapp3); }
输出:
{test2=2, test3=3, test1=1} {test2=4, test3=3, test1=1}
4)转成map,并且排序:
public static void stream2Map2() { List<User> userList = new ArrayList<User>(){ private static final long serialVersionUID = 1L; { add(new User(1L,"test1",0,12)); add(new User(3L,"test3",1,23)); add(new User(2L,"test2",0,2)); add(new User(4L,"test2",0,14)); } }; LinkedHashMap<String, Long> mapp4 = userList.stream().collect(Collectors.toMap( User::getName, //key = name User::getId,//vlaue = id (oldValue, newValue) -> newValue,//if same key, take the old key LinkedHashMap::new));//returns a LinkedHashMap, keep order System.out.println(mapp4); }
输出:
{test1=1, test3=3, test2=4}
3、分组操作:
上面会经常使用到Collectors这个类,这个类实际上是一个封装了很多常用的汇聚操作的一个工厂类。
1)现在要按User的name进行分组,如果使用sql来表示就是select * from user group by name; 这时就用到了这个api:
groupingBy(Function<? super T, ? extends K> classifier)接收一个Function类型的变量classifier,classifier被称作分类器,收集器会按着classifier作为key对集合元素进行分组,然后返回Collector收集器对象。
public static void groupTest() { List<User> userList = new ArrayList<User>(){ private static final long serialVersionUID = 1L; { add(new User(1L,"test1",0,12)); add(new User(3L,"test3",1,23)); add(new User(2L,"test2",0,2)); } }; //按照sex分组,select * from user group by sex Map<Integer, List<User>> userMap = userList.stream().collect(Collectors.groupingBy(User :: getSex)); System.out.println(userMap); }
2)如果按name分组后,想求出每组学生的数量:
就需要借助groupingBy另一个重载的方法:
groupingBy(Function<? super T, ? extends K> classifier,Collector<? super T, A, D> downstream)第二个参数downstream还是一个收集器Collector对象,也就是说我们可以先将classifier作为key进行分组,然后将分组后的结果交给downstream收集器再进行处理
//按照sex分组,并计算每组平均年龄 select sex,avg(age) from user group by sex; Map<Integer, Double> map2 = userList.stream().collect(Collectors.groupingBy( User::getSex, Collectors.averagingDouble(User::getAge))); System.out.println(map2);
输出:
{0=[User [id=1, name=test1, sex=0, age=12], User [id=2, name=test2, sex=0, age=2]], 1=[User [id=3, name=test3, sex=1, age=23]]} {0=7.0, 1=23.0}
同样我们可以求count(对应第二个参数为Collectors.counting())、max、min等。
4、分区操作:
1)假设,我们有这样一个需求,分别统计一下男生和女生的信息,这时候符合Stream分区的概念了,Stream分区会将集合中的元素按条件分成两部分结果,key是Boolean类型,value是结果集,满足条件的key是true,我们看下示例。
public static void partitionTest() { List<User> userList = new ArrayList<User>(){ private static final long serialVersionUID = 1L; { add(new User(1L,"test1",0,12)); add(new User(3L,"test3",1,23)); add(new User(2L,"test2",0,2)); } }; //partition Map<Boolean, List<User>> map = userList.stream().collect(Collectors.partitioningBy((u) -> u.getSex() ==0)); System.out.println(map.get(true)); System.out.println(map.get(false)); }
输出:
[User [id=1, name=test1, sex=0, age=12], User [id=2, name=test2, sex=0, age=2]] [User [id=3, name=test3, sex=1, age=23]]
partitioningBy方法接收一个Predicate作为分区判断的依据,满足条件的元素放在key为true的集合中,反之放在key为false的集合中。
2)在假设,我们要统计一下男生和女生的平均年龄信息,这是和分组一样,需要用另外一个重载方法:
partitioningBy(Predicate<? super T> predicate,Collector<? super T, A, D> downstream)第二个参数downstream还是一个收集器Collector对象,也就是说我们可以先按predicate进行分区,然后将分区后的结果交给downstream收集器再进行处理。
Map<Boolean, Double> map2 = userList.stream().collect(Collectors.partitioningBy( (u) -> u.getSex() ==0, Collectors.averagingDouble(User::getAge))); System.out.println(map2.get(true)); System.out.println(map2.get(false));
输出:
7.0 23.0
downstream收集器总结:
使用downstream收集器可以产生非常复杂的表达式,只有在使用groupingBy或者partitioningBy产生“downstream”map时,才使用它们,其它情况下,直接对Stream进行操作便可。
今天的文章
JAVA 8 Stream 2分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/80691.html