JAVA 8 Stream 2

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


猜你喜欢

转载自blog.csdn.net/liuxiao723846/article/details/81045426