经典伴读_java8实战_Stream高级

经典伴读系列文章,想写的不是读书笔记,也不是读后感,自己的理解加上实际项目中运用,让大家3-4天读懂这本书
请添加图片描述
预备知识:

六、用流收集数据

收集器

1、收集器Collector
问题:要求按照年份分组展示所有交易信息,
(1)准备实体和测试数据

    /**
     * 交易
     */
    public static class Transaction {
        private long id; //交易编号
        private String trader; //交易员
        private double value; //交易金额
        private int year; //交易时间

        public Transaction(long id, String trader, int year, double value) {
            this.id = id;
            this.trader = trader;
            this.year = year;
            this.value = value;
        }
private static List<Transaction> getAllTransaction() { //测试数据
        List<Transaction> transactions = Arrays.asList(
                new Transaction(1, "brian", 2011, 300),
                new Transaction(2, "raoul", 2012, 1000),
                new Transaction(3, "raoul", 2011, 400),
                new Transaction(4, "mario", 2012, 710),
                new Transaction(5, "mario", 2012, 700),
                new Transaction(6, "alan", 2012, 950)
        );
        return transactions;
    }

(2)使用传统迭代

		List<Transaction> trans = getAllTransaction();
        Map<Integer, List<Transaction>> transMap = new HashMap<>();
        for (Transaction tran : trans) {
            List<Transaction> groupTrans = transMap.get(tran.getYear());
            if (groupTrans == null) {
                groupTrans = new ArrayList<>();
                transMap.put(tran.getYear(), groupTrans);
            }
            groupTrans.add(tran);
        }
        System.out.println("分组结果:" + transMap);

如果突然拿到这段没有注释的程序,是不是只能在脑袋里执行循环代码,才能明白程序作用。有没有一眼就可以看出代码意图的方式?

(3)分组收集
静态导入所有收集器,可以更加简洁的体现出函数意图。

import static java.util.stream.Collectors.*;

只需向流提出按照交易年份分组的要求:groupingBy(Transaction::getYear)),不关心实现细节,就能得到分组结果。这下看起来可舒服多了。

        List<Transaction> trans = getAllTransaction();
        Map<Integer, List<Transaction>> transMap 
                = trans.stream().collect(groupingBy(Transaction::getYear)); 
        System.out.println("分组结果:" + transMap);

分组结果:{2011=[Transaction{id=1, trader=‘brian’, value=300.0, year=2011}, Transaction{id=3, trader=‘raoul’, value=400.0, year=2011}], 2012=[Transaction{id=2, trader=‘raoul’, value=1000.0, year=2012}, Transaction{id=4, trader=‘mario’, value=710.0, year=2012}, Transaction{id=5, trader=‘mario’, value=700.0, year=2012}, Transaction{id=6, trader=‘alan’, value=950.0, year=2012}]}

收集(collect)是流的终端操作,用于将流中的每个元素按照一定规则汇总成一个对象,这不正是归约(reduce)么?不错,collect就是一种高级归约,它的参数Collector就是汇总的规则,被称为收集器。我们可以使用内置的收集器,处理预定义的汇总规则,如:Collectors.groupby将流分组,Collectors.toList流转为列表等。当然也可以自定义收集器。

2、收集collect
对收集器有了基本认识后,继续学习更多的内置收集器之前,有个无法绕开的问题,到底什么是收集collect?先看下collect方法签名。(觉得复杂同学可以先跳过这节)

<R, A> R collect(Collector<? super T, A, R> collector);

这里发现一件有趣事情,收集器Collector并不是一个函数式接口,它不止有一个抽象方法,也就是说汇总的过程由多个步骤组成,和reduce果然不同。惊不惊喜,意不意外。

public interface Collector<T, A, R> {
    Supplier<A> supplier();
    BiConsumer<A, T> accumulator();
    Function<A, R> finisher(); 
    BinaryOperator<A> combiner(); 
	Set<Characteristics> characteristics(); 

虽然无法立即知道这些方法的含义,但至少感觉到一丝模版模式的味道,接下来看下collect如何编排这些步骤,首先我们得知道流是可以并行汇总的,类似分布式计算(后面在讲),这里先说当流顺序汇总时,逻辑步骤如下:(只用到前三个函数)
在这里插入图片描述
参考上图,我们查看收集器Collector的抽象方法:

public interface Collector<T, A, R> {

T:流中要收集的元素类型
A:累加器类型,CPU中累加器是一种寄存器,用来储存计算产生的中间结果,这里的累加器是一个结果容器,在收集过程中存放部分结果的对象。A就是这个结果容器的类型。
R:收集操作最后要返回的对象类型。

Supplier<A> supplier();

创建结果容器(累加器),返回值的函数描述符是() -> A

BiConsumer<A, T> accumulator();

将元素添加到结果容器,到底以什么规则汇总就在这里,是收集的核心步骤,
返回值的函数描述符是(A a, T t) -> void

Function<A, R> finisher();

将结果容器类型转换为返回值类型,返回值的函数描述符是(A a) -> R

BinaryOperator<A> combiner(); 

并行汇总时,合并两个结果容器(累加器),返回值的函数描述符是(A a1, A a2) -> A

Set<Characteristics> characteristics();

返回收集器的特征集,这些特征Characteristics是枚举类型,包括三个:

  • CONCURRENT表示accumulator函数支持多线程调用,且收集器可以并行归约。
  • UNORDERED表示归约结果无序。注意支持并行的收集器必须支持无序或者是无序数据源 。
  • IDENTITY_FINISH表示finisher函数返回的是恒等函数identity function,输入参数是什么,返回值就是什么。也就是说可以不执行finisher函数,直接跳过,将最后一次累加器中的值当做结果返回。

Collectors(注意带s)中有现成的特征集常量,可惜不是public,无法直接使用,但是可以过来拷贝呀。

    static final Set<Collector.Characteristics> CH_CONCURRENT_ID
            = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.CONCURRENT,
                                                     Collector.Characteristics.UNORDERED,
                                                     Collector.Characteristics.IDENTITY_FINISH));
    static final Set<Collector.Characteristics> CH_CONCURRENT_NOID
            = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.CONCURRENT,
                                                     Collector.Characteristics.UNORDERED));
    static final Set<Collector.Characteristics> CH_ID
            = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));
    static final Set<Collector.Characteristics> CH_UNORDERED_ID
            = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.UNORDERED,
                                                     Collector.Characteristics.IDENTITY_FINISH));
    static final Set<Collector.Characteristics> CH_NOID = Collections.emptySet();

到此收集器Collector总算弄明白了,我们模仿Collector.toList()方法创建一个自定义收集器,将流转换为列表。(暂不支持并发)

List<String> traders = trans.stream()
                .map(Transaction::getTrader) //映射交易员
                .collect(new Collector<String, List<String>, List<String>>() {
            @Override
            public Supplier<List<String>> supplier() {
                return ArrayList::new; //构造方法引用,等同于 new ArrayList();
            }

            @Override
            public BiConsumer<List<String>, String> accumulator() {
                return List::add; //内部方法引用,等同于 (a, t) -> a.add(t)
            }

            @Override
            public BinaryOperator<List<String>> combiner() {
                return (a1, a2) -> {
                    //注意抛异常的位置,不在combiner中,而是在返回值里。
                    throw new UnsupportedOperationException("暂不支持并发流");
                };
            }

            @Override
            public Function<List<String>, List<String>> finisher() {
                return Function.identity(); //等同于 t -> t
            }

            @Override
            public Set<Characteristics> characteristics() {
                //如果返回Collections.unmodifiableSet(
                // EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));
                // 则finisher函数跳过不执行
                return Collections.emptySet();
            }
        });
        System.out.println("交易员:" + traders);

交易员:[brian, raoul, raoul, mario, mario, Alan]

内置收集器

Collectors(注意带s)就像一个工厂,里面有一堆静态方法,用于创建各种各样的内置收集器Collector,让我们先将常用方法分类:(暂时不包含并发相关的收集器)

  • 汇总到集合:toList,toSet,toMap等
  • 归约为一个值:joining,counting,minBy,maxBy,summingInt,averagingInt,summarizingInt,reduce等
  • 分组和分区:groupingBy,partitioningBy

注意:书中将summingInt和averagingInt称为汇总,实际网上对于规约和汇总并没有明确区分,为了容易理解,这里我将返回集合的操作称为汇总,返回一个值的操作叫做归约。

汇总到集合

需要记忆,实际项目中非常常用。

		List<String> traderList = trans.stream()
                .map(Transaction::getTrader).distinct()
                .collect(Collectors.toList());
        System.out.println("转List:" + traderList);
        
        Set<String> traderSet = trans.stream()
                .map(Transaction::getTrader)
                .collect(toSet());
        System.out.println("转Set(去重且无序):" + traderSet);
        
        Map<Long, Transaction> transMap = trans.stream()
                .collect(toMap(Transaction::getId, Function.identity())); //identity是恒等函数等同于t->t
        System.out.println("转Map(值为对象):" + transMap);
        
        Map<Long, String> transTraderMap = trans.stream()
                .collect(toMap(Transaction::getId, Transaction::getTrader)); //identity是恒等函数等同于t->t
        System.out.println("转Map(值为对象属性):" + transTraderMap);

转List:[brian, raoul, mario, alan]
转Set(去重且无序):[raoul, alan, mario, brian]
转Map(值为对象):{1=Transaction{id=1, trader=‘brian’, value=300.0, year=2011}, 2=Transaction{id=2, trader=‘raoul’, value=1000.0, year=2012}, 3=Transaction{id=3, trader=‘raoul’, value=400.0, year=2011}, 4=Transaction{id=4, trader=‘mario’, value=710.0, year=2012}, 5=Transaction{id=5, trader=‘mario’, value=700.0, year=2012}, 6=Transaction{id=6, trader=‘alan’, value=950.0, year=2012}}
转Map(值为对象属性):{1=brian, 2=raoul, 3=raoul, 4=mario, 5=mario, 6=alan}

归约为一个值

在上一篇讲解归约reduce操作时,介绍过内置归约操作stream().xxx(),如:sum(),count(),max(),min(),average(),这一篇的内置收集器Collectors.xxx()中也有它们,如:counting(),minBy(),maxBy(),summingInt(),averagingInt(),甚至还有归约reducing()收集器。注意和stream().reduce()方法名不同。如果不考虑并行流情况下,它们都没有太大区别,

		double sum = trans.stream().map(Transaction::getValue)
                .collect(reducing(0.0, Double::sum));
        System.out.println("规约操作求和:" + sum);
        sum = trans.stream().map(Transaction::getValue)
                .reduce(0.0, Double::sum);
        System.out.println("收集操作求和:" + sum);

summarizingInt是它们的合体。joining用于拼接列表较常用。


		DoubleSummaryStatistics statistics = trans.stream().collect(summarizingDouble(Transaction::getValue));
        System.out.println("统计:" + statistics);
        
        String traderNames = trans.stream().map(Transaction::getTrader)
                .collect(Collectors.joining(", "));
        System.out.println("List转String(逗号分隔):" + traderNames);

List转String(逗号分隔):brian, raoul, raoul, mario, mario, alan
统计:DoubleSummaryStatistics{count=6, sum=4060.000000, min=300.000000, average=676.666667, max=1000.000000}

分组

groupby收集器可以复合(嵌入)多种收集器,进行多级分组,分区和归约。

1、复合summingDouble,用于分组求和。
问题:要求按照年份统计交易销售额。
使用SQL:

	SELECT year, sum(value) FROM transactions GROUP BY year

使用Stream:

        Map<Integer, Double> valuesByYearMap = trans.stream()
                .collect(groupingBy(
                                Transaction::getYear,
                                summingDouble(Transaction::getValue))
                );
        System.out.println("按年份统计营业额:" + valuesByYearMap);

按年份统计营业额:{2011=700.0, 2012=3360.0}

这里使用两个参数的groupby,第2个参数为Collector,嵌入summingDouble在已收集的交易列表List基础上,销售额求和。

public static <T, K, A, D>
    Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
                                          Collector<? super T, A, D> downstream)

2、复合mapping,用于映射后再收集。
问题:按年份统计交易员。

		Map<Integer, Set<String>> traderByYearMap = trans.stream().collect(
               groupingBy(Transaction::getYear, 
                       mapping(Transaction::getTrader, toSet())));
        System.out.println("按照年份统计交易员:" + traderByYearMap);

按照年份统计交易员:{2011=[raoul, brian], 2012=[raoul, alan, mario]}

groupby嵌入mapping,mapping方法第2个参数还是Collector,说明映射之后可以再收集,如:映射交易员后,可以再次收集到Set后返回。

	public static <T, U, A, R>
    Collector<T, ?, R> mapping(Function<? super T, ? extends U> mapper,
                               Collector<? super U, A, R> downstream)

3、复合collectingAndThen,用于收集完之后做类型转换。
问题:按照年份统计最大交易额。

		Map<Integer, Double> maxValueByYearMap = trans.stream().collect(
                groupingBy(Transaction::getYear, 
                        collectingAndThen(
                                maxBy(Comparator.comparingDouble(Transaction::getValue)),
                                t -> t.getValue())));
        System.out.println("按照年份统计最大交易额:" + maxValueByYearMap);

按照年份统计最大交易额:{2011=400.0, 2012=1000.0}

groupby嵌入collectingAndThen,collectingAndThen第2个参数是Function,说明了收集后可以执行一步操作,如:选出的最大值是Optional类型,这时需要获取他的交易额double类型返回。

	public static<T,A,R,RR> Collector<T,A,RR> collectingAndThen(Collector<T,A,R> downstream,
                                                                Function<R,RR> finisher)

4、复合groupby,用于多级分组。
问题:要求同时按照年份和交易员统计交易销售额。
使用SQL:

	SELECT year,trader, sum(value) FROM transactions GROUP BY year, trader

使用Stream:

        Map<Integer, Map<String, Double>> valuesByYearAndTraderMap 
                = trans.stream().collect(groupingBy(
                        Transaction::getYear, 
                        groupingBy(
                                Transaction::getTrader, 
                                summingDouble(Transaction::getValue))));
        System.out.println("按年份,交易员统计营业额:" + valuesByYearAndTraderMap);

按年份,交易员统计营业额:{2011={raoul=400.0, brian=300.0}, 2012={raoul=1000.0, alan=950.0, mario=1410.0}}

groupby嵌入groupby,再次分组,Map中还有Map,如:先按照年份分组,如果年份相同,再按照交易员分组。

分区

问题:以2012年为界,选出2012以前和以后(包含2012)的最高交易额。

        Map<Boolean, Transaction> partitioningMap =
                trans.stream().collect(
                        partitioningBy(t -> t.getYear() >= 2012,
                                collectingAndThen(maxBy(
                                        Comparator.comparingDouble(Transaction::getValue)),
                                        Optional::get)));
        System.out.println("2012年前后分区选出最大的交易:"partitioningMap);

2012年前后分区选出最大的交易:{false=Transaction{id=3, trader=‘raoul’, value=400.0, year=2011}, true=Transaction{id=2, trader=‘raoul’, value=1000.0, year=2012}}

partitioningBy分区收集器是一种特殊的分组收集器,区别在于结果Map的key值为Boolean,也就是只能将一个列表分为是或不是两组。

public static <T, D, A>
    Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate,
                                                    Collector<? super T, A, D> downstream)

七、并行数据处理与性能

终于到并行流,不知道你现在是不是也这么想。

顺序流执行过程

		long sum = Arrays.asList(1,2,3,4,5).stream()
                .reduce(0, (a, t) -> {
                    System.out.print(a + "+" + t + "=" + (a + t) + ", ");
                    return a + t;
                });
        System.out.println("sum:" + sum);

0+1=1, 1+2=3, 3+3=6, 6+4=10, 10+5=15, sum:15

其中a为累加器,t为流中的元素,顺序流执行过程如下:
在这里插入图片描述

并行流执行过程

		long sum = Arrays.asList(1,2,3,4,5).stream().parallel()
                .reduce(0, (a, t) -> {
                    System.out.print(a + "+" + t + "=" + (a + t) + ", ");
                    return a + t;
                });
        System.out.println("sum:" + sum);

0+2=2, 0+4=4, 0+5=5, 0+1=1, 0+3=3, 1+2=3, 4+5=9, 3+9=12, 3+12=15, sum:15

顺序流调用parallel()就变成了并行流,简洁的背后封装了并行算法,并行流执行过程如下:
(1)首先将流中的元素分成很多数据块,这个过程叫做Fork。
(2)然后用不同线程分别处理每个数据块获取块结果。
(3)最后将所有的块结果合并起来得到最终的结果,这个过程叫做Join。
在这里插入图片描述

测试流性能

我们使用了并行流后,性能是否真的变快了,变快了多少?
准备测试框架:

	public static <T> void test(Function<T, T> f, T n, String title) {
        new Thread(() -> {
            long begin = System.nanoTime();
            T t = f.apply(n);
            System.out.println("结果:" + t); //输出方法返回,如求和sum
            long used =  System.nanoTime() - begin;
            System.out.println(title + "耗时:" + used + "ns");
        }).start();
    }

开始测试:

	//顺序流求和
    public static long sequentialSum(long n) {
        long sum = 0;
        for (long i = 0; i < n; i++) {
            sum += i;
        }
        return sum;
    }

    //并行流iterate求和
    public static long parallelIterateSum(long n) {
        return  LongStream.iterate(0, t -> t+1).limit(n)
                .parallel()
                .reduce(0l, Long::sum);
    }

    //并行求range求和
    public static long parallelRangeSum(long n) {
        return LongStream.range(0, n)
                .parallel()
                .reduce(0l, Long::sum);
    }

    public static void main(String[] args) {
        long n = 100000l;
        //ParallelStreamTest是当前类名
        test(ParallelStreamTest::sequentialSum, n, "sequentialSum");
        test(ParallelStreamTest::parallelIterateSum, n, "parallelIterateSum");
        test(ParallelStreamTest::parallelRangeSum, n, "parallelRangeSum");
    }

随着n的增大,测试结果有所不同,可以看到两个现象:
(1)无论n如何变化,parallelRangeSum都小于parallelIterateSum耗时。同样使用数值流,都没有装箱拆箱的过程,为什么速度相差很大?
答:这是因为在使用iterator方法生成的流是一个无法确定大小的流spliteratorUnknownSize。分成了8个数据块n[1024]到n[8192]生成,然后再合并在一起。由于不知道limit方法需要截取多少元素,所以只能先尽量多的生成数据。相比起来,使用range或rangeClosed方法则是一次性生成固定数量的元素,自然要快很多。(建议不要在实际项目中生成无限流,如:Stream.iterator,Stream.generate)

(2)只有当计算次数n上亿时,parallelRangeSum才会小于sequentialSum耗时,由此可知并行流大多数情况下都不如顺序流快,如果只做简单运算确实如此,但如果我们把单次计算耗时增加50ms时,会发现n就算只有5,parallelRangeSum也会小于sequentialSum耗时。说明并行流适用于单次计算时间稍长的场景。

正确使用并行流

并行流内部默认使用ForkJoinPool实现,它默认线程数就是处理器的数量,既然是多线程就有线程安全的问题,最常见的就是多线程访问共享变量。

//自定义累加器对象
    public static class Accumulator {
        private long total = 0; //非线程安全
        public void add(long value) {
            total += value;
        }
    }

    //错误并行求和
    public static long wrongParallelSum(long n) {
        Accumulator accumulator = new Accumulator();
        LongStream.range(0l, n).parallel().forEach(accumulator::add);
        return accumulator.total;
    }

    public static void main(String[] args) {
        long n = 100;
        //两次求和结果不同
        test(ParallelStreamTest::wrongParallelSum, n, "wrongParallelSum1");
        test(ParallelStreamTest::wrongParallelSum, n, "wrongParallelSum2");
    }

结果:4939
结果:4837
wrongParallelSum1耗时:8269332ns
wrongParallelSum2耗时:7755148ns

为什么每次并行执行结果都不同?
多线程间共享类的实例变量(private也一样共享),并且+=,++,–等需要多步的操作都不是原子操作,编译时会生成多条字节码(或汇编命令),在多线程情况下,有可能交错执行。导致出现非预期的结果。

如何解决?
(1)加锁synchronized

		public synchronized void add(long value) {
            total += value;
        }

(2)使用原子类AtomicLong

	//自定义累加器对象
    public static class Accumulator {
        private AtomicLong total = new AtomicLong(0); 
        public void add(long value) {
            total.addAndGet(value);
        }

        public long get() {
            return total.get();
        }
    }

加锁会降低并行流的效率,因此要么使用原子类,要么最好避免修改共享变量。

并行流中的reduce和collect

1、reduce

		double s = trans.stream().parallel() //并行流
                .map(Transaction::getValue)
                .reduce(0.0, Double::sum, (u1, u2) -> u1 + u2); //等同于Double::sum
        System.out.println("销售总额:" + s);

reduce归约操作三参方法:

	<U> U reduce(U identity,
                 BiFunction<U, ? super T, U> accumulator,
                 BinaryOperator<U> combiner);

2、collect

	List<String> traders = trans.stream().parallel()
                .map(Transaction::getTrader)
                .collect(ArrayList<String>::new, (li, t) -> li.add(t), (l1, l2) -> {
                    l1.addAll(l2);
                });
        System.out.println("交易人员:" + traders);

collect收集操作三参方法:

	<R> R collect(Supplier<R> supplier,
                  BiConsumer<R, ? super T> accumulator,
                  BiConsumer<R, R> combiner);

注意:这两个方法看起来像,实际相差很大,如:

  • 第1个参数都是初始值(或容器),区别在于reduce的U identity是一个实际的初始值,而collect的Supplier supplier则是一个用来获取初始值的函数。
  • 第2个参数都是累加器,区别在于reduce中是BiFunction的函数类型是(U u, T t) -> U,而collect中是BiConsumer的函数类型是(R r, T t)-> void。后者没有返回值。
  • 第3个参数都是合并函数,区别在于reduce中是BinaryOperator的函数描述符是 (U u1, U u2) -> U,collect中是BiConsumer的函数描述符是 (R r1, R r2) -> void。后者没有返回值。

3、同是累加器,当使用并行流修改共享变量时,会不会也有线程安全问题?

	//重写累加器对象
    public static class Accumulator {
        private long total = 0; //非线程安全

        //为了满足recude参数BIFunction,BinaryOperator,添加返回值
        public Accumulator add(long value) {
            total+=value;
            return this;
        }
        public long get() {
            return total;
        }
    }
    
	//并行流reduce
    public static long parallelReduce(long n) {
        Accumulator s = LongStream.rangeClosed(1, n).parallel().boxed().reduce(new Accumulator(),
                 (Accumulator a, Long t) -> {
                     System.out.println("累加器地址:" + System.identityHashCode(a));
                     a.add(t);
                     return a;
                 },
                 (Accumulator a1, Accumulator a2) -> {
                        a1.add(a2.get());
                        return a1;
                 });
        return s.get();
    }

    //并行流collect
    public static Long parallelCollect(long n) {
        Accumulator s = LongStream.rangeClosed(1, n).parallel().boxed()
                .collect(Accumulator::new,  (Accumulator a, Long t) -> {
                            System.out.println("累加器地址:" + System.identityHashCode(a));
                            a.add(t);
                        },(Accumulator a1, Accumulator a2) -> {
                            a1.add(a2.get());
                        });
        return  s.get();
    }

测试代码1:

		long n = 100;
        test(ParallelStreamTest::parallelReduce, n, "parallelReduce");

累加器地址:1251915859
累加器地址:1251915859

累加器地址:1251915859
累加器地址:1251915859
累加器地址:1251915859
结果:55868495446784
parallelReduce耗时:17425825ns

测试代码2:

		long n = 100;
        test(ParallelStreamTest::parallelCollect, n, "parallelCollect");

累加器地址:1479325164
累加器地址:531367754

累加器地址:1596367378
累加器地址:1978686776
累加器地址:1596367378
结果:5050
parallelCollect耗时:12979936ns

从输出结果得出结论:
(1)如果并行流修改的是容器(内部有共享变量),如List,Set,Map等,适合用collect。在同一个数据块内使用同一个累加器(顺序执行),不同的数据块之间会生成新的累加器(并行执行),如此保证线程安全。
(2)如果并行流修改的是值(不可变类型),如String,Integer,Long,Double等,适合用reduce。所有数据块共用一个累加器(返回之后再传入),但由于是不可变类型的值,参数传递会生成新值,也做到相对线程安全。

至此流的高级应用基本讲完,还有关于并行流的实现原理,如:Fork/Join框架以及Spliterator的使用,以及如何利用他们模拟实现一个并行流的内容,放在最后一篇。《经典伴读_java8实战_一网打尽》

未完待续

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值