简单说,对 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进行操作便可。