项目中我们可能会遇到需要合并两个或者以上的list第一种方法是使用flatMap将他们打平再收集,第二种是reduce直接做归约操作。我们重点看第二种的实现。
实现:

List<Integer> list = Lists.newArrayList(1, 2,3);
        List<Integer> afterList = list.stream()
                .map(integer -> Lists.newArrayList(integer + 10))
                .reduce(Lists.newArrayList(), (list1, list2) -> {
                    System.out.println("这里1" + list1);
                    System.out.println("这里2" + list2);
                    list1.addAll(list2);
                    return list1;
                });
        System.out.println(afterList);

输出结果:

这里1[]
这里2[11]
这里1[11]
这里2[12]
这里1[11, 12]
这里2[13]
[11, 12, 13]

使用串行流这个做法能够很好地解决我们的问题,那么如果这个数量太大了我们可能就会考虑使用并行流,那么我们看看并行流的情况:

List<Integer> list = Lists.newArrayList(1, 2,3);
        List<Integer> afterList = list.parallelStream()
                .map(integer -> Lists.newArrayList(integer + 10))
                .reduce(Lists.newArrayList(), (list1, list2) -> {
                    System.out.println("这里1" + list1);
                    System.out.println("这里2" + list2);
                    list1.addAll(list2);
                    return list1;
                });
        System.out.println(afterList);

输出结果:

这里1[]
这里1[]
这里1[]
这里2[13]
这里2[12]
这里2[11]
这里1[13, 12]
这里2[13, 12, 11]
这里1[13, 12, 11, 13, 12, 11]
这里2[13, 12, 11, 13, 12, 11]
[13, 12, 11, 13, 12, 11, 13, 12, 11, 13, 12, 11]

这个结果应该是相当意外的。why?怎么会重复这么多次?我查了好多的资料发现都没找到我想要的结果,最后我想到了会不会是因为内存地址都指向同一个,所以导致这个问题。为了验证这个想法,我加了打印hashCode的方法(这里我没有直接取 list.hashCode,因为Lists.newArrayList这里面重写了hashCode)

AtomicInteger combineCount = new AtomicInteger(0);
List<Integer> list = Lists.newArrayList(1, 2, 3);
List<Integer> afterList = list.parallelStream()
        .map(integer -> Lists.newArrayList(integer + 10))
        .reduce(Lists.newArrayList(), (list1, list2) -> {
            combineCount.incrementAndGet();
            System.out.println(String.format("thread name:%s", Thread.currentThread()
                    .getName()));
            System.out.println("这里1" + list1 + " 内存地址" + System.identityHashCode(list1));
            System.out.println("这里2" + list2 + " 内存地址" + System.identityHashCode(list2));
            list1.addAll(list2);
            return list1;
        });
System.out.println(afterList);

输出结果:

thread name:main
thread name:ForkJoinPool.commonPool-worker-9
thread name:ForkJoinPool.commonPool-worker-2
这里1[] 内存地址1798286609
这里1[] 内存地址1798286609
这里2[12] 内存地址2036958521
这里1[] 内存地址1798286609
这里2[11] 内存地址508100523
这里2[13] 内存地址1865623252
thread name:ForkJoinPool.commonPool-worker-2
这里1[12, 11, 13] 内存地址1798286609
这里2[12, 11, 13] 内存地址1798286609
thread name:ForkJoinPool.commonPool-worker-2
这里1[12, 11, 13, 12, 11, 13] 内存地址1798286609
这里2[12, 11, 13, 12, 11, 13] 内存地址1798286609
[12, 11, 13, 12, 11, 13, 12, 11, 13, 12, 11, 13]

我们可以看到,开了三个线程在跑,我们可以注意到“这里1”打印出的hashCode始终是一致的,也就是Lists.newArrayList()(我们就叫他newList)被new一个初始值之后,后续的操作都是在往这个初始值里add,所以三个线程执行完第一步之后newList就被赋予了3个值,所以第四次打印出来的是3个值。我们可以看一下reduce这个归约的图

reduce归约流程图
reduce归约流程图

并行过程中每一个线程都会执行自己那个线程里的归约,得到最后值我们可以认为每一个线程的最终值被存放在一个中间容器里,当完成每个线程里的操作就要把不同线程的中间容器的值进行做归约,所以如上的三个线程需要做两次归约(线程1+线程2得到线程12合并数据,合并数据再和线程3做归约),所以也就是第四次【11,12,13】两个做一次归约,得到了【12, 11, 13, 12, 11, 13】,再做最后一次得到了四倍的结果。
所以如果想要输出结果不是这样重复的结果,就需要new一个全新的List:

tomicInteger combineCount = new AtomicInteger(0);
        List<Integer> list = Lists.newArrayList(1, 2, 3);
        List<Integer> afterList = list.parallelStream()
                .map(integer -> Lists.newArrayList(integer + 10))
                .reduce(Lists.newArrayList(), (list1, list2) -> {
                    ArrayList<Integer> list3 = Lists.newArrayList();
                    list3.addAll(list1);
                    list3.addAll(list2);
                    return list3;
                });
        System.out.println(afterList);

输出结果:[12,13,11]
符合我们的要求!

2019.8.25修改:

收集与归约

在上一章和本章中讨论了很多有关归约的内容。你可能想知道,Stream接口的collect 和reduce方法有何不同,因为两种方法通常会获得相同的结果。例如,你可以像下面这样使用reduce方法来实现toListCollector所做的工作:

        Stream<Integer> stream = Arrays.asList(1, 2, 3, 4, 5, 6)
                .stream();
        List<Integer> numbers = stream.reduce(new ArrayList<Integer>(), (List<Integer> l, Integer e) -> {
            l.add(e);
            return l;
        }, (List<Integer> l1, List<Integer> l2) -> {
            l1.addAll(l2);
            return l1;
        });

这个解决方案有两个问题:一个语义问题和一个实际问题。语义问题在于,reduce方法旨在把两个值结合起来生成一个新值,它是一个不可变的归约。与此相反,collect方法的设计就是要改变容器,从而累积要输出的结果。这意味着,上面的代码片段是在滥用reduce方法,因为它在原地改变了作为累加器的List。你在下一章中会更详细地看到,以错误的语义使用reduce方法还会造成一个实际问题:这个归约过程不能并行工作,因为由多个线程并发修改同一个数据结构可能会破坏List本身。在这种情况下,如果你想要线程安全,就需要每次分配一个新的List,而对象分配又会影响性能。这就是collect方法特别适合表达可变容器上的归约的原因,更关键的是它适合并行操作,本章后面会谈到这一点。

从《Java8实战》书中,我们可以得到我们使用reduce去聚合多个List的行为其实是不合理的,因为需要不断去分配一个新的List来存储数据,应该采用reducing去实现这个功能。关于reducing《Java8实战》总结(2) 流(Stream)

List<Integer> list = Lists.newArrayList(1, 2,3);
        List<Integer> afterList = list.parallelStream()
                .map(integer -> Lists.newArrayList(integer + 10))
                .collect(Collectors.reducing(Lists.newArrayList(),(left,right)->{
                    left.addAll(right);
                    return left;
                }));

参考:https://segmentfault.com/q/1010000004944450