Java8

161 阅读22分钟

《Java8 实战》阅读摘抄,共之后查阅。

Lambda 表达式

Java 演变历程

  • Java 1 具有线程和锁,甚至有一个内存模型——这是当时的最佳做法,但事实证明,不具备专门知识的项目团队很难可靠地使用这些基本模型。
  • Java 5 添加了工业级的构建模块,如线程池和并发集合。
  • Java 7 添加了分支/合并(fork/join)框架,使得并行变得更实用,但仍然很困难(invokedynamic字节码)。
  • Java 8 对并行有了一个更简单的新思路,不过你仍要遵循一些规则。同时提供了Stream API、向方法传递代码的技巧、接口中的默认方法。
  • Java 9 新增了效率提升方面的改进,新增了反应式编程(RxJava、Akka反应式流工具)。
  • Java 10对类型推断做了微调。

Lambda 基本语法风格

//1、表达式——风格的Lambda(返回值是 显示 的)
(parameter) -> expression

//2、块——风格的Lambda (返回值是 隐式 的)
(parameter) -> { statements; }

函数式接口

概念:只定义一个抽象方法的接口,但可以定义多个其他的==虚拟扩展方法==(使用 default 标识的方法)。

注解:@FunctionalInterface

基本函数式接口:

函数式接口函数描述符
PredicateT -> boolean
Consumer T -> void
Function <T,R>T -> R

装箱、拆箱:基本类型转化为引用类型(装箱),引用类型转化为基本类型(拆箱);装箱后是要付出代价的,装箱后的值本质上就是把基本类型包裹起来,并保存在堆里(==装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的基本值==)。

public interface IntPredicate {
    boolean test(int value);
}
//没有自动装箱
IntPredicate evenNumbers = (int i) -> i % 2 == 0;
evenNumbers.test(1000);
//有自动装箱
Predicate<Integer> oddNumbers = (int i) -> i % 2 == 0;
oddNumbers.test(1000);

基本特化类型 函数式接口 见本章最后(基本特化类型列表(常见))。

类型检查

Lambda 的类型是从使用 Lambda 的上下文推断出来的(Lambda 表达式需要的类型称为目标类型)。

Lambda 代码块中之能使用 final 的局部变量(若在 Lambda 使用之后的代码中没有对变量进行修改视为隐式 final)。

//显示定义(具有类型推断)
(Function<String, String>) ((String str) -> { return "hello," + str; });
//隐式定义(没有类型推断)
(str) -> { return "hello," + str; }

方法引用

构建方法引用:

  1. 指向 静态方法 的方法引用(parseInt为静态方法,Integer::parseInt)
  2. 指向任意类型实例方法的方法引用发(String::length)
  3. 指向现存对象或者表达式实例方法的方法引用(temp为局部变量,temp::getValue)

复合 Lambda 表达式

  • 比较器复合(comparing、thenComparing)
//使用静态方法 Comparator.comparing
//根据提取用于比较的键值的 Function 来返回一个 Comparator
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
//逆序(reversed)
inventory.sort(comparing(Apple::getWeight).reversed());
//比较器链(thenComparing)
inventory.sort(comparing(Apple::getWeight)  
         .reversed()  
         .thenComparing(Apple::getCountry));
  • 谓词复合(negate、and、or)
//redApple(现有谓词)
//返回 redApple 的非
Predicate<Apple> notRedApple = redApple.negate();
//且(and)
Predicate<Apple> redAndHeavyApple =  
                 redApple.and(a -> a.getWeight() > 150);
//或(or)
Predicate<Apple> redAndHeavyAppleOrGreen =  
                redApple.and(a -> a.getWeight() > 150) 
                        .or(a -> "green".equals(a.getColor()));
  • 函数复合(andThen、compose)
//andThen 方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2; 
Function<Integer, Integer> h = f.andThen(g); 
int result = h.apply(1);
//compose 方法先把给定的函数用作compose的参数里面给的那个函数,然后再把函数本身用于结果
Function<Integer, Integer> f = x -> x + 1; 
Function<Integer, Integer> g = x -> x * 2; 
Function<Integer, Integer> h = f.compose(g);
int result = h.apply(1);

基本特化类型列表(常见)

函数式接口函数描述符(传入参数 -> 返回类型)基本特化类型
PredicateT -> booleanIntPredicate
----LongPredicate
----DoublePredicate
Consumer T -> voidIntConsumer
----LongConsumer
----DoubleConsumer
Function <T,R>T -> RIntFunction
----IntToDoubleFunction
----IntToLongFunction
----LongFunction
----LongToDoubleFunction
----LongToIntFunction
----DoubleFunction
----DoubleToIntFunction
----DoubleToLongFunction
----ToIntFunction
----ToDoubleFunction
----ToLongFunction
Supplier () -> TBooleanSupplier
----IntSupplier
----LongSupplier
----DoubleSupplier
UnaryOperator T -> TIntUnaryOperator
----LongUnaryOperator
----DoubleUnaryOperator
BinaryOperator (T,T) -> TIntBinaryOperator
----LongBinaryOperator
----DoubleBinaryOperator
BiPredicate <T,U>(T,U) -> boolean-
BiConsumer <T,U>(T,U) -> voidObjIntConsumer
----ObjLongConsumer
----ObjDoubleConsumer
BiFunction <T,U,R>(T,U) -> RToIntBiFunction <T,U>
----ToLongBiFunction <T,U>
----ToDoubleBiFunction <T,U>

流-使用、构建

基本概念

  • 概念:支持 数据处理操作 生成的 元素序列
    • 元素序列:就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。
    • 源:流会使用一个提供数据的源,如集合、数组或输入/输出资源。
    • 数据处理操作:filter、map、reduce、find、match、sort等。
    • 流水线:很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线。
    • 内部迭代:与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。
  • 运行原理:每个元素依次去适配每个数据处理操作之后,在迭代的去处理下个元素。
  • 和迭代器类似,流只能遍历一次(遍历完之后,我们就说这个流已经被消费掉了)。
  • Streams 库的内部迭代可以自动选择一种适合你硬件的数据表示和并行实现。

什么时候使用流(内部迭代、外部迭代)

当需求中要求某一时间点所有值都存在时,使用迭代器(外部迭代);反之,使用流(内部迭代)。

流的组成(短路特性)

List<String> names = menu.stream()  
    .filter(d -> d.getCalories() > 300) 
    .map(Dish::getName)  
    .limit(3)  
    .collect(toList());
  • filter、map和limit可以连成一条流水线,被称为 中间操作
  • collect触发流水线执行并关闭它,被称为 终端操作
中间操作
操作类型返回类型使用的类型/函数式接口函数描述符
filter中间Stream PredicateT -> boolean
map中间Stream Function <T, R>T -> R
limit中间Stream --
sorted中间Stream Comparator (T, T) -> int
distinct中间Stream --
skip中间Stream long-
flatMap中间Stream Function<T, Stream>T -> Stream
takewhile中间Stream PredicateT -> boolean
dropwhile中间Stream PredicateT -> boolean
终端操作
操作类型返回类型使用的类型/函数式接口函数描述符
forEach终端voidConsumerT -> void
count终端long--
collect终端RCollector<T, A, R>-
reduce终端OptionalBinaryOperator(T, T) -> T
anyMatch终端booleanPredicateT -> boolean
noneMatch终端booleanPredicateT -> boolean
allMatch终端booleanPredicateT -> boolean
findAny终端Optional--
findFirst终端Optional--

流的使用

使用谓词筛选(filter)
List<Dish> vegetarianMenu = menu.stream() 
        //接受一个谓词作为参数,并返回一个包括所有符合谓词的元素的流
        .filter(Dish::isVegetarian)  
        .collect(toList());
筛选各异的元素(distinct)
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4); numbers.stream()  
        .filter(i -> i % 2 == 0)  
        //返回一个元素各异的流(根据流所生成元素的hashCode和equals方法实现)
        .distinct()  
        .forEach(System.out::println);
截断流(limit)
List<Dish> dishes = menu.stream()  
        .filter(d -> d.getCalories() > 300)  
        //返回一个不超过给定长度的流
        .limit(3)  
        .collect(toList());
跳过元素(skip)
List<Dish> dishes = menu.stream() 
        .filter(d -> d.getCalories() > 300)  
        //返回一个扔掉了前n个元素的流
        .skip(2)
        .collect(toList()); 

流的切片

使用谓词对流进行切片(takewhile、dropwhile---Java 9)
List<Integer> numbers = Arrays.asList(10, 25, 12, 39, 33, 20, 45); 
numbers.stream()
        //在遭遇到第一个不符合要求的元素时停止处理
        .takewhile(num -> num < 20)
        .collect(Collectors.toList());
numbers.stream()
        //丢弃掉所有 false 的元素,一旦遇到结果为 true 就停止 
        .dropwhile(num -> num < 20)
        .collect(Collectors.toList());

流的映射

映射(map)
List<String> dishNames = menu.stream()  
        //接受一个函数作为参数,
        //这个函数会被应用到每个元素上,并将其映射成一个新的元素
        .map(Dish::getName)  
        .collect(toList());
扁平化(flatamp、Arrays.stream() )
//准备数据
ArrayList<String> words = new ArrayList<>();
words.add("Hello");       words.add("Word");
//遍历列表,将列表中的单词差分为字母,在去重
words.stream()
        .map(word -> word.split(""))
        //Arrays.stream():让每个数组变成一个单独的流
        //flatMap:将各个生成流扁平化(合并)为单个流
        .flatMap(Arrays::stream)
        .distinct()
        .collect(toList());

流的匹配

检查谓词是否至少匹配一个元素(anyMatch)
//anyMatch 流中是否有一个元素能匹配给定的谓词
if(menu.stream().anyMatch(Dish::isVegetarian)){      
    System.out.println("The menu is vegetarian friendly!"); 
} 
检查谓词是否匹配所有元素(allMatch)
boolean isHealthy = menu.stream()  
        //allMatch 确保流中没有任何元素与给定的谓词匹配
        .allMatch(d -> d.getCalories() < 1000);
检查流中元素是否全部不匹配谓词(noneMatch)
boolean isHealthy = menu.stream()  
        //检查流中元素是否全部不匹配谓词
        .noneMatch(d -> d.getCalories() >= 1000);

流的查找(findAny)

Optional<Dish> dish = menu.stream()          
        .filter(Dish::isVegetarian)  
        //返回当前流中的任意元素
        .findAny()
        //如果包含一个值就打印它,否则什么都不做
        .ifPresent(d -> System.out.println(d.getName());
//---------------------------------------------------------
List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> firstValue = someNumbers.stream()  
        .map(x -> x * x)  
        .filter(x -> x % 3 == 0)  
        //找出第一个满足条件的元素
        .findFirst(); //9

流的归约(reduce、min、max、joining() )

//reduce 函数详情
// 第一个参数:初始值(具有默认值)
// 第二个参数:BinaryOperator<T> extends BiFunction<T,T,T>
// 第三个参数:用于将两个结果变量进行合并(只有并行流中才会生效)

//元素求和
int sum = numbers.stream() .reduce(0, (a, b) -> a + b);
//最大值
Optional<Integer> max = numbers.stream().reduce(Integer::max);
//最小值
Optional<Integer> min = numbers.stream().reduce(Integer::min); 
Optional<Integer> min = numbers.stream().min(comparing(Transaction::getValue)); 
//字符串连接
.reduce("",(s1,s2) -> s1 + s2);
.collect(joining());    //joining方法内部用到了StringBuild
//。。。。。。

数值流

Java 8引入了三个原始类型特化流接口来解决这个问题:IntStream、DoubleStream和LongStream,分别将流中的元素特化为int、long和double,从而避免了暗含的装箱成本。

基本类型流特化
//映射到数值流(返回的是一个特化流)
menu.stream()
        //返回一个 IntStream
        .mapToInt(Dish::getCalories)
        //返回一个 LongStream
        .mapToLong(Dish::getCalories)
        //返回一个 DoubleStream
        .mapToDouble(Dish::getCalories)
//转换为对象流
intStream.boxed()
//默认值 OptionalInt、OptionalLong、OptionalDouble
数值范围(range、rangeClosed)
IntStream evenNumbers = 
        //第一个参数:起始值
        //第二个参数:结束值
        //生成 [ 起始值,结束值 ]
        IntStream.rangeClosed(1, 100) 
        //生成 [ 起始值,结束值 )
        IntStream.rangeClosed(1, 100) 
        .filter(n -> n % 2 == 0); 

构建流

由值创建流
Stream<String> stream = 
    //使用静态方法 Stream.of 显式值创建一个流
    Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
//创建一个空流
Stream<Object> empty = Stream.empty();
由数组创建流
int[] numbers = {2, 3, 5, 7, 11, 13}; 
IntStream stream = Arrays.stream(numbers);
由可空对象创建流
Stream.ofNullable(System.getProperty("home"));
由文件生成流(Files.lines)
Stream<String> lines =  
        Files.lines(
            Paths.get("data.txt"),
            Charset.defaultCharset()
        );
由函数生成流:创建无限流
//该流是一个无限流,对每个新生值应用传入的谓词参数
Stream.iterate(0, n -> n + 2) 
        .limit(10)  
        .forEach(System.out::println);
//Java 9 对 iterator 做了增强
//第二个参数:谓词,作为停止的判断条件
Stream.iterate(0, n -> n<100 ,n -> n + 2) 
        .forEach(System.out::println);
//-----------------------------------------------------
//接受一个Supplier<T>类型的Lambda提供新的值
Stream.generate(Math::random)
        .limit(10)
        .forEach(System.out::println);

流-数据收集

基本概念

Collectors实用类提供了很多静态工厂方法,可以方便地创建常见收集器的实例,它们主要提供了三大功能:

  • 将流元素归约和汇总为一个值
  • 元素分组
  • 元素分区

归约、汇总

集合数量(Collectors.counting())
Long num = list.stream().collect(Collectors.counting());
最大最小值(Collectors.maxBy()、Collectors.minBy())
list.stream().collect(
    Collectors.maxBy(
        Comparator.comparing(a -> a.getWeight())
    )
);
汇总
list.stream().collect(
    //该收集器把数量、总和、最大最小值、平均数收集到 IntSummaryStatistics 类中
    Collectors.summarizingLong(FruitInfo::getWeight)
);
//求和
list.stream().collect(Collectors.summingLong(FruitInfo::getWeight));
//平均数
list.stream().collect(Collectors.averagingLong(FruitInfo::getWeight));
连接字符串(joining())
menu.stream()
        .map(Dish::getName)
        //joining在内部使用了StringBuilder来把生成的字符串逐个追加起来
        //可添加参数作为字符串分隔符
        .collect(joining());
reducing 方法
//累积函数
reducing(BinaryOperator<T> op)
//初始值、累计函数
reducing(T identity, BinaryOperator<T> op)
//初始值、转换函数、累计函数
reducing(U identity,
         Function<? super T, ? extends U> mapper,
         BinaryOperator<U> op)

分组、分类

普通分组(单级,Collectors.groupingBy() )
Map<Boolean, List<FruitInfo>> map01 = list.stream()
        .collect(
            Collectors.groupingBy(a -> a.getWeight() > 50)
        );
使用(普通分组)的代码实现
Map<String, List<FruitInfo>> map02 = list.stream()
        .collect(
            Collectors.groupingBy(a ->
                a -> a.getWeight() <= 20 ? "小" : a.getWeight() <= 50 ? "中" : "大")
            )
        );
先分组再处理(保留空分组---分组中全部不符合过滤条件)
list.stream()
    .collect(
        //重构了方法,增加了第二个参数
        Collectors.groupingBy(
            FruitInfo::getColor,
            //1、对分组后的数据进行映射
            Collectors.mapping(
                FruitInfo::getName,
                Collectors.toList()
            )
            //2、对分组后的数据进行过滤
            //Collectors.filtering(
            //    info -> info.getWeight <= 40 ,
            //    Collectors.toList()
            //)
            //3、提取标志列表并合并到当前流中
            //flatMapping(
            //    info -> info.getTags(info.getName()).stream()
            //    Collectors.toSet()
            //)
        )
    );
多级分组
Map<String, Map<String, List<FruitInfo>>> collect = 
list.stream()
    .collect(
        Collectors.groupingBy(
            FruitInfo::getColor,
            //多级分组
            Collectors.groupingBy(info -> info.getWeight() > 15 ? "重" : "轻")
        )
    );
//第二个参数的应用:
//  将收集器返回的结果转换为另一种类型
//      arg01:所要转换的收集器
//      arg02:转换函数
Collectors.collectingAndType( arg01 , arg02 ); 
//  arg0:接收一个函数对流中的元素做转换
//  arg1:将变换的结果对象进行收集(toSet())
//      配合 toCollection() 可以做更多的配置,例如:toCollection( HashSet:new )
mapping( arg0 , arg1 )

分区

分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函数。

分区函数返回一个布尔值,这意味着得到的分组Map的键类型是Boolean。

Map<Boolean, List<Dish>> collect =
    menu.stream().collect(
        //分级函数(是否是素菜)
        //存在第二个参数
        Collectors.partitioningBy(Dish::isVegetarian)
    );
//示例(测试一个数是否为质数)
//  IntStream.range(m,n):返回 [m,n)
IntStream.range(2, candidate)  
    //全都不满足才会返回true
    .noneMatch(i -> candidate % i == 0)

收集器接口(Collect,==自定义收集器==)

仅仅在背后的数据源无序时才会并行处理

//Collect 接口定义(自己实现 toList 方法)
//      T:流中要收集元素的类型
//      A:累加器的类型,在收集过程中用于累积部分结果的对象
//      R:收集操作得到的对象(通常但并不一定是集合)的类型
public interface Collector<T, A, R> { 
    //返回一个空集合(ArrayList)
    Supplier<A> supplier();  
    //对流中的数据进行收集(收集到supplier函数返回的集合中)
    BiConsumer<A, T> accumulator();  
    //对集合中的元素进行转换,转换为指定的类型
    Function<A, R> finisher();  
    //对多个流产生的集合进行合并
    BinaryOperator<A> combiner();  
    //返回一个不可变的 Characteristics 的集合,定义了收集器的行为
    //      UNORDERED:归约结果不受流中项目的遍历和累积顺序的影响
    //      CONCURRENT:accumulator函数可以从多个线程同时调用,且该收集器可以并行归约流
    //              如果收集器没有标为UNORDERED,那它仅在用于无序数据源时才可以并行归约
    //      IDENTITY_FINISH:表明完成器方法返回的函数是一个恒等函数,可以跳过
    Set<Characteristics> characteristics(); 
} 
Collectors 静态工厂方法
工厂方法返回类型作用
toListList把流中所有项目收集到一个List
toSetSet把流中所有项目收集到一个Set,删除重复项
toCollectionCollection把流中所有项目收集到给定的供应源创建的集合
countingLong计算流中元素的个数
summingIntInteger对流中项目的一个整数属性求和
averagingIntDouble计算流中项目Integer属性的平均值
summarizingIntIntSummaryStatistics收集关于流中项目Integer属性的统计值,例如最大、最小、总和与平均值
joiningString连接对流中每个项目调用toString方法所生成的字符串
maxByOptional一个包裹了流中按照给定比较器选出的最大元素的Optional,或如果流为空则为Optional.empty()
minByOptional一个包裹了流中按照给定比较器选出的最小元素的Optional,或如果流为空则为Optional.empty()
reducing归约操作产生的类型从一个作为累加器的初始值开始,利用BinaryOperator与流中的元素逐个结合,从而将流归约为单个值
collectingAndThen转换函数返回的类型包裹另一个收集器,对其结果应用转换函数
groupingByMap<K, List>根据项目的一个属性的值对流中的项目作问组,并将属性值作为结果Map的键
partitioningByMap<Boolean,List>根据对流中每个项目应用谓词的结果来对项目进行分区

流-并行流

并行流内部使用了默认的ForkJoinPool,它默认的线程数量就是你的处理器数量,这个值是由Runtime.getRuntime().availableProcessors()得到的。

可以通过系统属性java.util.concurrent.ForkJoinPool.common. parallelism来改变线程池大小, 例如:System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12")。

并行化的代价:并行化过程本身需要对流做递归划分,把每个子流的归纳操作分配到不同的线程,然后把这些操作的结果合并成一个值(保证在内核中并行执行工作的时间比在内核之间传输数据的时间长)。

//isParallel()判断流是否为并行流
Integer reduce = Stream.iterate(1, i -> ++i)
    .limit(100)
     //将流转换为并行流:
     //      对顺序流调用parallel方法并不意味着流本身有任何实际的变化
     //      在内部实际上就是设了一个boolean标志,表示让调用parallel后进行的所有操作都并行执行
    .parallel()
     //将流转换为顺序流:
     //      两个方法结合起来,就可以更细化地控制在遍历流时哪些操作要并行执行,哪些要顺序执行
     //      最后一次parallel或sequential调用会影响整个流水线
    .sequential()
    .reduce(0, Integer::sum);
    
//1、直接产生原始类型的long数字,没有装箱拆箱的开销
//2、会生成数字范围,很容易拆分为独立的小块
LongStream.rangeClosed(satrt,end);

并行流使用建议(P160)

  1. 并行流并不总是比顺序流快,且并行流有时候会和你的直觉不一致;当你疑惑是否需要并行流时,需要用适当的基准来检查其性能。
  2. 自动装箱和拆箱操作会大大降低性能,Java 8中有原始类型流(IntStream、LongStream、DoubleStream)来避免这种操作,但凡有可能都应该用这些流。
  3. 有些操作本身在并行流上的性能就比顺序流差(limit和findFirst这种依赖元素顺序的;unordered()将流改为无序的---在无序情况下,limit会非常高效)。
  4. 需要考虑流的操作流水线的总计算成本。
  5. 对于较小的数据量,选择并行流几乎从来都不是一个好的决定。
  6. 要考虑流背后的数据结构是否易于分解(例如,ArrayList的拆分效率比LinkedList高得多,因为前者用不着遍历就可以平均拆分,而后者则必须遍历---==PS:用range工厂方法创建的原始类型流也可以快速分解==)。
  7. 考虑终端操作中合并步骤的代价是大是小。

流的数据源和可分解性

数据源类型可分解性
ArrayList极佳
LinkedList
IntStream.range极佳
Stream.iterate
HashSet
TreeSet

分支合并框架(ForkJoinPool)

ForkJoinPool 若使用无参构造则使用JVM能够使用的所有处理器。

工作窃取
  • 理想状态:每个任务都用完全相同的时间完成,让所有的CPU内核都同样繁忙
  • 实际状态:每个子任务所花的时间可能天差地别(划分策略效率低、磁盘访问慢等)
  • 解决方法(工作窃取):每个线程都为分配给它的任务保存一个双向链式队列,每完成一个任务,就会从队列头上取出下一个任务开始执行;若某个线程完成了队列中的所有任务,则随机“窃取”另一个线程未执行的任务,直至所有任务全部完成。
代码示例
public class ForkJoinSumCalculator  
    extends java.util.concurrent.RecursiveTask<Long> {
    //数据源
    private final long[] numbers;  
    //子任务处理的数组的起始和终止位置
    private final int start; 
    private final int end; 
    //不再将任务分解为子任务的数组大小
    public static final long THRESHOLD = 10_000;  
    //公共构造函数用于创建主任务
    public ForkJoinSumCalculator(long[] numbers) {      
        this(numbers, 0, numbers.length); 
    }  
    //私有构造函数用于以递归方式为主任务创建子任务
    private ForkJoinSumCalculator(long[] numbers, int start, int end) {  
        this.numbers = numbers; 
        this.start = start;  
        this.end = end;  
    }  
    
    @Override  
    protected Long compute() {  
        //如果大小小于或等于阈值,顺序计算结果
        int length = end - start;  
        if (length <= THRESHOLD) {  
            return computeSequentially();  
        }  
        //创建一个子任务来为数组的前一半求和
        ForkJoinSumCalculator leftTask =  new ForkJoinSumCalculator(numbers, start, start + length/2);  
        //利用另一个ForkJoinPool线程异步执行新创建的子任务
        leftTask.fork();  
        //创建一个任务为数组的后一半求和
        ForkJoinSumCalculator rightTask =  new ForkJoinSumCalculator(numbers, start + length/2, end);  
        //同步执行第二个子任务,有可能允许进一步递归划分
        Long rightResult = rightTask.compute();  
        //读取第一个子任务的结果,如果尚未完成就等待
        Long leftResult = leftTask.join();  
        //该任务的结果是两个子任务结果的组合
        return leftResult + rightResult; 
    }  
    //在子任务不再可分时计算结果的简单算法
    private long computeSequentially() {  
        long sum = 0;  
        for (int i = start; i < end; i++) {  
            sum += numbers[i];  
        } 
        return sum; 
    } 
} 

流的自动拆分机制(Spliterator)

Spliterator 是 Java8 加入的新接口,叫做“可分迭代器”;和Iterator一样,Spliterator 也用于遍历数据源中的元素,但是它是为了并行执行而设计的。

public interface Spliterator<T> { 
    //类似于普通的Iterator,因为它会按顺序一个一个使用
    //如果还有其他元素要遍历就返回true
    boolean tryAdvance(Consumer<? super T> action);
    //把一些元素划出去分给第二个Spliterator(由该方法返回),让它们两个并行处理
    Spliterator<T> trySplit();  
    //估计还剩下多少元素要遍历
    long estimateSize();  
    //返回一个int,代表Spliterator本身特性集的编码
    int characteristics(); 
} 
拆分过程
  1. 对第一个Spliterator调用trySplit,生成第二个Spliterator。
  2. 对这两个Spliterator调用trysplit,这样总共就有了四个Spliterator。
  3. 不断对Spliterator调用trySplit直到它返回null,表明它处理的数据结构不能再分割。
Spliterator 特性
特 性含 义
ORDERED元素有既定的顺序(例如List),因此Spliterator在遍历和划分时也会遵循这一顺序
DISTINCT对于任意一对遍历过的元素x和y,x.equals(y)返回false
SORTED遍历的元素按照一个预定义的顺序排序
SIZED该Spliterator由一个已知大小的源建立(例如Set),因此estimatedSize()返回的是准确值
NONNULL保证遍历的元素不会为null
IMMUTABLESpliterator的数据源不能修改。这意味着在遍历时不能添加、删除或修改任何元素
CONCURRENT该Spliterator的数据源可以被其他线程同时修改而无需同步
SUBSIZED该Spliterator和所有从它拆分出来的Spliterator都是SIZED

自定义 Spliterator

//规约函数
U reduce(
    //初始值
    U identity,
    //累加器
    BiFunction<U, ? super T, U> accumulator,
    //用来合并多线程计算的结果
    BinaryOperator<U> combiner);
//创建流
//      arg0:流的数据源
//      arg1:true(并行流)、false(顺序流)
StreamSupport.stream( arg0 , arg1 )
    
//====================计算单词个数(顺序)====================
final String SENTENCE =  
        " Nel mezzo del cammin di nostra vita "
        +  "mi ritrovai in una selva oscura"
        +  " ché la dritta via era smarrita ";
Stream<Character> stream = 
        IntStream.range(0, SENTENCE.length())
        .mapToObj(SENTENCE::charAt);
WordCounter wordCounter = stream.reduce(
        new WordCounter(0, true),  
        WordCounter::accumulate,  
        WordCounter::combine
    );
    
//===============处理字符状态,记录单词数量===============
private static class WordCounter {
    //单词数量
    private final int counter;
    //记录遍历的上个单词状态
    //上个字符是空格,这个字母不是空格,则单词数加一
    private final boolean lastSpace;

    public WordCounter(int counter, boolean lastSpace) {
        this.counter = counter;
        this.lastSpace = lastSpace;
    }
    //对单个字符做处理
    public WordCounter accumulate(Character c) {
        if (Character.isWhitespace(c)) {
          return lastSpace ? this : new WordCounter(counter, true);
        }
        else {
          return lastSpace ? new WordCounter(counter + 1, false) : this;
        }
    }
    //合并多个 word count 结果
    public WordCounter combine(WordCounter wordCounter) {
        return new WordCounter(counter + wordCounter.counter, wordCounter.lastSpace);
    }
    //返回结果
    public int getCounter() {
        return counter;
    }
}

//======================并行优化(自定义Spliterator)======================
public class MySpliterator implements Spliterator {

    private final String str;
    private int currentChar = 0;

    public MySpliterator(String str) {
        this.str = str;
    }

    @Override
    public boolean tryAdvance(Consumer action) {
        action.accept(str.charAt(currentChar++));
        return currentChar < str.length();
    }

    @Override
    public Spliterator trySplit() {
        int currentSize = str.length() - currentChar;
        if (currentSize < 10){
            return null;
        }
        for (int i = currentSize / 2 + currentChar; i < str.length(); i++) {
            if (Character.isWhitespace(str.charAt(i))){
                //取走前一部分交给另一个 spliterator 进行拆分
                //由程序继续对后半部分进行拆分
                MySpliterator spliterator = new MySpliterator(str.substring(currentChar,i));
                currentChar = i;
                return spliterator;
            }
        }
        return null;
    }

    @Override
    public long estimateSize() {
        return 10L;
    }

    @Override
    public int characteristics() {
        return ORDERED + SIZED + SUBSIZED + NONNULL + IMMUTABLE;
    }
    
    public static void main(String[] args) {
        Spliterator<Character> spliteratorStream = new MySpliterator(SENTENCE);
        //生成一个并行流
        Stream<Character> stream = StreamSupport.stream(spliteratorStream, true);

        WordCounter reduce = stream
                .reduce(new WordCounter(0, true), WordCounter::accumulate, WordCounter::combine);
        System.out.println(reduce.getCounter());
    }
}

集合工厂(创建集合)

//创建一个固定大小的列表
//      列表元素可以更新但不可以增加或删除
//      增加、删除会抛出 UnsupportedOperationException
Arrays.asList("hello", "world", "how", "are", "you");
//创建一个流,以自定义的形式
Stream.of("how", "are", "you")
        .collect(Collectors.toSet());

List 工厂

为避免不可预知的缺陷,不要在工厂方法中存放 null 元素。

除非你需要进行数据处理,否则尽量使用工厂方法,而非 Stream API。

  • 功能:创建一个只读列表(不可增加、修改、删除)
  • 优化:List.of()中存在 10 个方法分别为 of(E e1 ...... E e9),那么为什么要选择重载方法而不用变参(E... elements)呢?

变参版本函数需要额外分配一个数组,这个数组被封状于列表中,使用变参版本方法,需要你负担分配数组、初始化、垃圾回收的开销;使用定长元素版本(最多 10 个)就没有这部分开销。

Set 工厂

  • 功能:创建一个只读列表(不可增加、修改、删除)
  • 特性:不能使用一个包含重复元素的列表去创建 Set(会抛出 IllegalArgumentException 异常)

Map 工厂

Map.of("k1","v1" ......);
  • 功能:创建一个只读列表(不可增加、修改、删除)
  • 场景:创建不到 10 个键值对的 Map
Map.ofEntries();
Map.ofEntries(
    Map.entry("k1","v1"),
    //......    
    Map.entry("kn","vn")
);
  • 功能:创建一个只读列表(不可增加、修改、删除)
  • 场景:创建大规模(≥ 10)数据量的 Map

List、Set 新增 API

removeIf(Predicate<? super E> filter)
  • 功能:移除集合中匹配指定谓词的元素
  • 范围:List、Set
List<String> arrayList = new ArrayList<>(List.of("how"," ","are"," ","you"));
//去除空格
arrayList.removeIf(str -> str.equals(" "));
arrayList.forEach(System.out::printf);
replaceAll(UnaryOperator operator)
  • 功能:使用一个新的元素替换列表中满足要求的每个元素
  • 范围:List
List<String> arrayList = new ArrayList<>(List.of("how"," ","are"," ","you"));
//转换为大写
arrayList.replaceAll(str -> str.toUpperCase());
arrayList.forEach(System.out::printf);
sort(UnaryOperator operator)
  • 功能:对列表中的元素进行排序
  • 范围:List
List<String> arrayList = new ArrayList<>(List.of("how"," ","are"," ","you"));
//排序
arrayList.sort(String::compareTo);
rrayList.sort(String::compareTo);

Map 新增 API

优化

通常情况下,Map 存放在桶中,若大量返回同一个散列值,HashMap性能会急剧下降,因为桶是由链表实现的,时间复杂度为 n。

现优化为当链表数量超过阈值则转换为树结构,时间复杂度为 log(n)。

forEach(BiConsumer<? super K, ? super V> action)
Map<String, String> map = Map.of("k1","v1","kn","vn");
map.forEach(
    (key,value) -> System.out.println(key + ":" + value + " ")
);
排序

Entry.comparingByKey(Comparator<? super K> cmp)

Entry.comparingByValue(Comparator<? super K> cmp)

将 key 或 value 按照指定规则排序(参数可缺省,默认按照类型的规则排序)

Map<String, String> map = Map.of("k1","v1","kn","vn","k2","v2");
map.entrySet().stream()
    //将 key 按照字母大小排序
    .sorted(Map.Entry.comparingByKey())
    //并行处理,顺序是随机的,效率高
    .forEach(System.out::println);
map.entrySet().stream()
    //将 value 按照字母大小排序
    .sorted(Map.Entry.comparingByValue())
    //顺序处理,顺序是固定的,效率低
    .forEachOrdered(System.out::println);
getOrDefault(Object key, V defaultValue)
  • 功能:当查找的 key 不存在时,返回 defaultValue 避免空指针异常
  • 特殊情况:当 key 存在,但对应的 value 为 null
Map<String, Integer> map = Map.of("k1",1,"kn",5);
Integer num = map.getOrDefault("k3",-1);
System.out.println(num);
计算模式

依据 key 在 Map 中存在或者缺失的状态,有条件的执行某个操作,并存储计算的结果。

  • computeIfAbsent:若指定的 key 没有对应值(没有该 key 或者值为 null),使用该键计算值并添加到 Map 中
  • computeIfPresent:若指定 key 存在,则进行计算,并写入 Map 中
  • compute:使用指定的 key 计算新的值,并将其存入 Map 中
Map<Integer, Integer> map = new HashMap<>(Map.of(1,1,5,5,9,9));
//若 key 存在,计算值,更新 Map
map.computeIfPresent(3,(key,value) -> value*value);
map.forEach((key,value)-> System.out.println(key + ":" + value + " "));
删除模式

删除 Map 中某个键对应特定值的键值对。

Map<Integer, Integer> map = new HashMap<>(Map.of(1,1,5,5,9,9));
//删除一个不存在的 k-v
map.remove(1,2);
map.forEach((key,value)-> System.out.println(key + ":" + value + " "));
//删除一个存在的 k-v
map.remove(1,1);
map.forEach((key,value)-> System.out.println(key + ":" + value + " "));
替换模式
  • repaceAll:通过 BiFunction 替换 Map 中的每个项的值
  • put:若 key 存在,则更新;key 不存在,则插入
Map<String, String> map = new HashMap<>(Map.of("k1","v1","kn","vn","k2","v2"));
//将 value 中的字母转换为大写
map.replaceAll((k,v) -> v.toUpperCase());
map.forEach((key,value)-> System.out.println(key + ":" + value + " "));
merge()

在调用 merge 方法的 Map 中寻找参数中的 key-value 是否存在,若不存在则插入调用 merge 方法的 Map 中,若存在则使用指定的函数处理两个 value。

  • K key,
  • V value,
  • BiFunction<? super V, ? super V, ? extends V> remappingFunction
Map<String, String> map1 = new HashMap<>(Map.of("k1","v1","k2","v2","k3",null));
Map<String, String> map2 = new HashMap<>(Map.of("k3","v3","k2","v-test"));
map1.forEach(
    //将 map1 合并到 map2中,当 key 相同时使用第三个参数处理
    (k,v)-> map2.merge(k,v,(v1,v2) -> v1 + " & " + v2)
);
map1.forEach((key,value)-> System.out.println(key + ":" + value + " "));
System.out.println("-------------------------------");
map2.forEach((key,value)-> System.out.println(key + ":" + value + " "));

ConcurrentHashMap

为了更好的应对高并发的业务场景。ConcurrentHashMap 允许执行并发的添加和操作,内部实现基于分段锁。

与 Hashtable 相比,ConcurrentHashMap 的性能都更好。

归约和索搜

下述所有操作都不会对 ConcurrentHashMap 的状态上锁,他们只是在运行中动态的对对象加锁。执行操作的函数不因该对执行顺序、其它对象、可能在运行中变化的对象有任何依赖。

此外,还需要为所有操作设定一个阈值,如果当前 Map 的规模比指定阈值小,则顺序执行(阈值为1时为最大程度的并发)。

  • ConcurrentHashMap 类支持三种新的操作:
    • forEach:对每个 k-v 执行指定的操作
    • reduce:依据归约函数整合所有 k-v 的计算结果
    • search:对每个 k-v 执行一个函数,直到函数取得一个非空值
  • 上述每种操作支持四种形式的参数,接受函数使用键、值、Map.Entry、k-v对作为参数
    • 使用键:forEachKey、reduceKeys、searchKeys
    • 使用值:forEachValue、reduceValues、searchValues
    • 使用Map.Entry:forEachEntry、reduceEntries、searchEntries
    • 使用k-v对:forEach、reduce、search
ConcurrentHashMap<String,Long> map = new ConcurrentHashMap<String,Long>(Map.of("1",1L,"2",2L));
long parallelismThreshold = 1;
//使用最大程度的并发来计算集合累加的结果
Long aLong = map.reduceValues(parallelismThreshold, Long::sum);
System.out.println(aLong);
计数

mappingCount 方法:以长整型(long)返回 Map 中的映射数目。

Set 视图

keySet:以 Set 的形式返回 ConcurrentHashMap 的一个视图,Map 中的变化会反映在返回的 Set 中,反之亦然。

ConcurrentHashMap<String,String> map = new ConcurrentHashMap<>(Map.of("k1","v1","k2","v2"));
//返回的 set 只支持移除操作(reomove)且会同步到 map 中
ConcurrentHashMap.KeySetView<String, String> set = map.keySet();
System.out.println(set);
map.put("k3","k3");
System.out.println(set);
System.out.println("-------------------------");
System.out.println(map);
set.remove("k1");
System.out.println(map);