《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
基本函数式接口:
| 函数式接口 | 函数描述符 |
|---|---|
| Predicate | T -> 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; }
方法引用
构建方法引用:
- 指向 静态方法 的方法引用(parseInt为静态方法,Integer::parseInt)
- 指向任意类型实例方法的方法引用发(String::length)
- 指向现存对象或者表达式实例方法的方法引用(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);
基本特化类型列表(常见)
| 函数式接口 | 函数描述符(传入参数 -> 返回类型) | 基本特化类型 |
|---|---|---|
| Predicate | T -> boolean | IntPredicate |
| -- | -- | LongPredicate |
| -- | -- | DoublePredicate |
| Consumer | T -> void | IntConsumer |
| -- | -- | LongConsumer |
| -- | -- | DoubleConsumer |
| Function <T,R> | T -> R | IntFunction |
| -- | -- | IntToDoubleFunction |
| -- | -- | IntToLongFunction |
| -- | -- | LongFunction |
| -- | -- | LongToDoubleFunction |
| -- | -- | LongToIntFunction |
| -- | -- | DoubleFunction |
| -- | -- | DoubleToIntFunction |
| -- | -- | DoubleToLongFunction |
| -- | -- | ToIntFunction |
| -- | -- | ToDoubleFunction |
| -- | -- | ToLongFunction |
| Supplier | () -> T | BooleanSupplier |
| -- | -- | IntSupplier |
| -- | -- | LongSupplier |
| -- | -- | DoubleSupplier |
| UnaryOperator | T -> T | IntUnaryOperator |
| -- | -- | LongUnaryOperator |
| -- | -- | DoubleUnaryOperator |
| BinaryOperator | (T,T) -> T | IntBinaryOperator |
| -- | -- | LongBinaryOperator |
| -- | -- | DoubleBinaryOperator |
| BiPredicate <T,U> | (T,U) -> boolean | - |
| BiConsumer <T,U> | (T,U) -> void | ObjIntConsumer |
| -- | -- | ObjLongConsumer |
| -- | -- | ObjDoubleConsumer |
| BiFunction <T,U,R> | (T,U) -> R | ToIntBiFunction <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 | Predicate | T -> 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 | Predicate | T -> boolean |
| dropwhile | 中间 | Stream | Predicate | T -> boolean |
终端操作
| 操作 | 类型 | 返回类型 | 使用的类型/函数式接口 | 函数描述符 |
|---|---|---|---|---|
| forEach | 终端 | void | Consumer | T -> void |
| count | 终端 | long | - | - |
| collect | 终端 | R | Collector<T, A, R> | - |
| reduce | 终端 | Optional | BinaryOperator | (T, T) -> T |
| anyMatch | 终端 | boolean | Predicate | T -> boolean |
| noneMatch | 终端 | boolean | Predicate | T -> boolean |
| allMatch | 终端 | boolean | Predicate | T -> 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 静态工厂方法
| 工厂方法 | 返回类型 | 作用 |
|---|---|---|
| toList | List | 把流中所有项目收集到一个List |
| toSet | Set | 把流中所有项目收集到一个Set,删除重复项 |
| toCollection | Collection | 把流中所有项目收集到给定的供应源创建的集合 |
| counting | Long | 计算流中元素的个数 |
| summingInt | Integer | 对流中项目的一个整数属性求和 |
| averagingInt | Double | 计算流中项目Integer属性的平均值 |
| summarizingInt | IntSummaryStatistics | 收集关于流中项目Integer属性的统计值,例如最大、最小、总和与平均值 |
| joining | String | 连接对流中每个项目调用toString方法所生成的字符串 |
| maxBy | Optional | 一个包裹了流中按照给定比较器选出的最大元素的Optional,或如果流为空则为Optional.empty() |
| minBy | Optional | 一个包裹了流中按照给定比较器选出的最小元素的Optional,或如果流为空则为Optional.empty() |
| reducing | 归约操作产生的类型 | 从一个作为累加器的初始值开始,利用BinaryOperator与流中的元素逐个结合,从而将流归约为单个值 |
| collectingAndThen | 转换函数返回的类型 | 包裹另一个收集器,对其结果应用转换函数 |
| groupingBy | Map<K, List> | 根据项目的一个属性的值对流中的项目作问组,并将属性值作为结果Map的键 |
| partitioningBy | Map<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)
- 并行流并不总是比顺序流快,且并行流有时候会和你的直觉不一致;当你疑惑是否需要并行流时,需要用适当的基准来检查其性能。
- 自动装箱和拆箱操作会大大降低性能,Java 8中有原始类型流(IntStream、LongStream、DoubleStream)来避免这种操作,但凡有可能都应该用这些流。
- 有些操作本身在并行流上的性能就比顺序流差(limit和findFirst这种依赖元素顺序的;unordered()将流改为无序的---在无序情况下,limit会非常高效)。
- 需要考虑流的操作流水线的总计算成本。
- 对于较小的数据量,选择并行流几乎从来都不是一个好的决定。
- 要考虑流背后的数据结构是否易于分解(例如,ArrayList的拆分效率比LinkedList高得多,因为前者用不着遍历就可以平均拆分,而后者则必须遍历---==PS:用range工厂方法创建的原始类型流也可以快速分解==)。
- 考虑终端操作中合并步骤的代价是大是小。
流的数据源和可分解性
| 数据源类型 | 可分解性 |
|---|---|
| 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();
}
拆分过程
- 对第一个Spliterator调用trySplit,生成第二个Spliterator。
- 对这两个Spliterator调用trysplit,这样总共就有了四个Spliterator。
- 不断对Spliterator调用trySplit直到它返回null,表明它处理的数据结构不能再分割。
Spliterator 特性
| 特 性 | 含 义 |
|---|---|
| ORDERED | 元素有既定的顺序(例如List),因此Spliterator在遍历和划分时也会遵循这一顺序 |
| DISTINCT | 对于任意一对遍历过的元素x和y,x.equals(y)返回false |
| SORTED | 遍历的元素按照一个预定义的顺序排序 |
| SIZED | 该Spliterator由一个已知大小的源建立(例如Set),因此estimatedSize()返回的是准确值 |
| NONNULL | 保证遍历的元素不会为null |
| IMMUTABLE | Spliterator的数据源不能修改。这意味着在遍历时不能添加、删除或修改任何元素 |
| 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);