文章目录
- 1. Java 8、9、10以及11的变化
- 1.1 为什么变化
- 1.2 Java 为什么还在变
- 1.3 Java 中的函数
- 1.4 流
- 2. 通过行为参数化传递代码
- 2.1 应对不断变化的需求
- 2.2 行为参数化
- 2.3 对付啰嗦
- 2.3.1 匿名类
- 2.3.2 Lambda 表达式
- 2.3.4 List 类型抽象化
- 3. Lambda 表达式
- 3.1 Lambda 管中窥豹
- 3.2 在哪里以及如何使用 Lambda
- 3.2.1 函数式接口
- 3.2.2 函数描述符
- 3.3 Lambda 付诸实践:实现环绕模式
- 3.4 使用函数式接口
- 3.4.1 Predicate
- 3.4.2 Consumer
- 3.4.3 Function
- 3.4.4 基本类型特化
- 3.4.5 异常处理
- 3.5 类型检查、类型推断以及限制
- 3.5.1 类型检查
- 3.5.2 同样的 Lambda,不同的函数式接口
- 3.5.3 类型推断
- 3.5.4 使用局部变量
- 3.6 方法引用
- 3.6.1 构建方法引用
- 3.6.2 构造函数引用
- 3.7 复合 Lambda 表达式
- 3.7.1 比较器复合
- 3.7.2 谓词复合
- 3.7.3 函数复合
- 小总结
- 4. 引入流
- 4.1 流是什么
- 4.2 流与集合
- 4.3 流与集合
- 4.4 流操作
- 4.4.1 中间操作
- 4.4.2 终端操作
- 4.4.3 使用流
- 5. 使用流
- 5.1 筛选
- 5.1.1 filter
- 5.1.2 筛选各异的元素
- 5.2 流的切片
- 5.2.1 使用谓词对流进行切片
- 5.2.2 截断流
- 5.2.3 跳过元素
- 5.3 映射
- 5.3.1 对流中每一个元素应用函数
- 5.3.2 流的扁平化
- 5.4 查找和匹配
- 5.4.1 检查谓词是否至少匹配一个元素
- 5.4.2 检查谓词是否匹配所有元素
- 5.4.3 查找元素
- 5.4.4 查找第一个元素
- 5.5 规约
- 5.5.1 元素求和
- 5.5.2 最大值和最小值
- 5.5.3 状态及界
- 5.6 数值流
- 5.6.1 原始类型流特化
- 5.6.2 数值范围
- 5.7 构建流
- 5.7.1 由值创建流
- 5.7.2 由可空对象创建流
- 5.7.3 由数组创建流
- 5.7.4 由文件生成流
- 5.7.5 由函数生成流:创建无限流
- 6. 用流收集数据
- 6.1 收集器简介
- 6.1.1 收集器用作高级归约
- 6.1.2 预定义收集器
- 6.2 归约和汇总
- 6.2.1 查找流中的最大值和最小值
- 6.2.2 汇总
- 6.2.3 连接字符串
- 6.2.4 广义的归约汇总
- 6.3 分组
- 6.3.1 操作分组的元素
- 6.3.2 多级分组
- 6.3.3 按子组收集数据
- 把收集器的结果转换为另一种类型
- 与 groupingBy 联合使用的其他收集器的例子
- 6.4 分区
- 6.4.1 分区的优势
- 6.4.2 将数字按质数和非质数分区
- 6.5 收集器接口
- 7. 并行数据处理与性能
- 7.1 并行流
- 7.1.1 将顺序流转换为并行流
- 7.1.2 测量流性能
- 8. Collection API 的增强功能
- 8.1 集合工厂
- 8.1.1 List 工厂
- 8.1.2 Set 工厂
- 8.1.3 Map 工厂
- 8.2 使用 List 和 Set
- 8.2.1 removeIf
- 8.2.2 replaceAll
- 8.3 Map
- 8.3.1 forEach
- 8.3.2 排序
- 8.3.3 getOrDefault
- 8.3.4 计算模式
- 8.3.5 删除模式
- 8.3.6 替换模式
- 8.3.7 merge 方法
- 8.4 改进的 ConcurrentHashMap
- 9. 重构、测试和调试
- 9.1 为改善可读性和灵活性重构代码
- 9.1.1 改善代码的可读性
- 9.1.2 从匿名类到 `Lambda` 表达式的转换
- 9.1.3 从 Lambda 表达式到方法引用的转换
- 9.1.4 从命令式的数据处理切换到 Stream
- 9.1.5 增加代码的灵活性
- 9.2 使用 Lambda 重构面向对象的设计模式
- 9.2.1 策略模式
- 9.2.2 模板方法
- 9.2.3 观察者模式
1. Java 8、9、10以及11的变化
1.1 为什么变化
- 为了更好的使用多核CPU
- 为了更好的让并发编程更容易、出错更少
stream
流也可以更好的实现函数式编程
1.2 Java 为什么还在变
Java 起初是使用单核处理器来支持小规模并发,这样可以避免并发编程在多核处理器上发生意外的概率。此外,在当时,将 Java 编译为 JVM 字节码而快速成为当时互联网applet的首选(当时浏览器支持Java Applet,但是现在过时了,都是JS了)
Java 8 可以透明地把输入的不相关部分拿到几个CPU核上分别执行 Stream 操作流水线,这是几乎免费的并行,用不着费劲搞 Thread 了
举例
java">String[] a = {'a', 'aa', 'aaa'};
String[] b = {'b', 'bb', 'bbb'};
假如我们做的操作是每个数字对应下标拼接,然后再转换大小写,再排序,最后输出一个数组
如果不使用流,我们正常的做法是
java">String[] combined = new String[a.length];
for (int i = 0; i < a.length; i++) {combined[i] = a[i] + b[i];
}// Step 2: 转换为小写
for (int i = 0; i < combined.length; i++) {combined[i] = combined[i].toLowerCase();
}// Step 3: 排序
Arrays.sort(combined);
这里顺序是什么?全部拼接完成后,再去转换为小写,再排序,如果用流呢?
java">String[] a = {"a", "aa", "aaaa"};
String[] b = {"b", "bb", "bbbb"};
List<String> list = Arrays.stream(a).map(item -> item.concat(b[Arrays.asList(a).indexOf(item)])).map(String::toLowerCase).sorted().toList();
System.out.println(list);
其实他会帮我们做优化,根本不需要我们去操作,它会形成一个复杂的流水线,相当于三个工人,一个复杂拼接,一个来转换大小写,一个来排序。如果用流,它会在第一个拼接完后,让转换大小写去工作,同时拼接工作也去进行,相当于一个流水线,速度会得到大大提升。
1.3 Java 中的函数
在原来,Java 中的 Integer
String
HashMap
等等,都是对象,所以它们都是有对象引用的,也都是值,所以可以作为方法中的参数,这些也被称为一等值。而类、方法,不属于对象,所以被称为二等值,不能作为方法参数。但是Java8之后,在运行时将方法变为一等值,允许方法作为参数值进行传递。方法作为值也构成了其他几个 Java8 功能(比如 Stream
)的基础。
比如,我们想要查看一个文件夹里面的文件是否为隐藏文件,原来的做法是
java">File[] hiddenFiles = new File(".").listFiles(new FileFilter() { // 因为 Java 只能传递一等值,所以传递一个对象并重写方法@Overridepublic boolean accept(File file) {return file.isHidden();}
});
实际上只是调用了 isHidden
方法进行过滤,引入方法引用后,我们可以这么写
java">File[] hiddenFiles = new File(".").listFiles(File::isHidden);
简洁且易懂
我们可以来看一看函数式编程的强大
加入我们有一个公共库,定义了一个过滤方法
java">static <T> Collection<T> filter(Collection<T> collection, Predicate<T> predicate);
第一个参数我们只要传入 Collection
的子类即可,Predicate
传入过滤条件,然后就可以得到我们想要的结果,这是非常非常方便的,且代码简洁易懂。
但是,为了更好地利用并行,Java 设计师设计了一整套新的类 Stream
1.4 流
Collection
主要用于存储和访问数据,Stream
则主要用于描述对数据的计算
在原来,比如我们想对数据进行筛选,我们需要使用循环进行 if
判断,如果筛选条件复杂,我们需要使用多个 if
语句。我们自己使用循环来管理迭代过程,被称为外部迭代。如果我们使用 Stream
,我们完全不需要操心循环的事,被称为内部迭代。
当数据量非常大时,单个CPU搞不定这么大量的数据(使用 for
循环的操作都是单核处理),而 Stream
可以实现并行操作,并且根本不需要我们去考虑多线程之间的协调,它会让不同CPU做不同的事,以实现高效率并行。
Java 中的并行很难,和 synchronized
相关的都容易出问题,那么 Stream
流是怎么做的呢?
- 库会负责分块,把大的流分成几个小的流,以便并行处理
- 只有在传递给
filter
之类的库方法的方法不会互动(无可变的共享对象)时才能有效、安全地并行工作
因为是并行,所以要求各个元素之间的执行顺序不会影响最终结果
2. 通过行为参数化传递代码
2.1 应对不断变化的需求
由于需求是不断变化的,所以我们需要考虑如何在需求不断变化的同时,尽量实现代码的复用并且尽量不修改代码。
🐯 比如一个果园,我们想挑选绿色苹果
java">enum Color { RED, GREEN }
public static List<Apple> filterGreenApples(List<Apple> inventory){// 遍历,找出绿色苹果List<Apple> result = new ArrayList<>();for (Apple apple: inventory){if(apple.getColor().equals(color)){result.add(apple);}}return result;
}
但是当有新需求,想要红色苹果怎么办,笨方法是再写一个 filterRedApples
,这势必造成了代码的重复,所以我们可以将苹果颜色作为参数
🐯 颜色作为参数
java">enum Color { RED, GREEN }
public static List<Apple> filterApplesByColor(List<Apple> inventory, Color color);
这样我们就可以应对苹果颜色的变化需求。
🐯 重量作为参数
这时,又提出新需求,要重量大于150克的苹果,我们可以重写一个方法来筛选重量
java">public static List<Apple> filterApplesByWeight(List<Apple> inventory, int weight);
如此我们就有两个方法了,一个筛选颜色,一个筛选质量。这违背了软件工程原则,因为只有 if
语句判断不同,其他代码都是一样的。并且,如果我们找到了新的迭代方式能够提升性能,那这两个方法都要修改,工程量会很大。
🐯 属性作为参数
java">public static List<Apple> filterApples(List<Apple> inventory, Color color, int weight, boolean flag){// 判断条件if( (flag && apple.getColor().equals(color)) || (!flag && apple.getWeight() > weight) ){}
}
这种代码糟透了,首先 true
false
不知道什么意思,并且如果还考虑产地、形状,那代码还要修改,参数还需要增加
2.2 行为参数化
我们定义一个谓词接口用于筛选,通过继承该接口来实现不同的需求
java">enum Color { RED, GREEN }
class Apple {Color color;int weight;// ...
}interface ApplePredicate{boolean test (Apple apple);
}class AppleHeavyWeightPredicate implements ApplePredicate{@Overridepublic boolean test(Apple apple) {return apple.getWeight() > 150;}
}class AppleGreenColorPredicate implements ApplePredicate{@Overridepublic boolean test(Apple apple) {return apple.getColor().equals(Color.GREEN);}
}
// filter 方法
public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p){// 循环过滤即可
}
如此,当需求变化时,我们再新建一个类即可。例如找出重量超过150克的红苹果
java">class AppleRedAndHeavyPredicate implements ApplePredicate{@Overridepublic boolean test(Apple apple) {return apple.getColor().equals(Color.RED) && apple.getWeight() > 150;}
}// 调用
filterApples(inventory, new AppleRedAndHeavyPredicate())
2.1 中我们是编写多个方法来进行过滤某种条件的苹果,在 2.2 中我们将过滤方法抽象为 filterApples
,将过滤条件抽象为方法进行传入。我们可以据此编写灵活的 API,然后传入参数。
2.3 对付啰嗦
但是这种写法很罗嗦,浪费时间,因为我们要声明好几个实现 ApplePredicate
接口的类,然后实例化只会使用到一次的 AppleRedAndHeavyPredicate
等对象,只为作为参数来传递
2.3.1 匿名类
java">filterApples(inventory, new ApplePredicate(){public boolean test(Apple a){return RED.equals(a.getColor())}
})
匿名类虽然在某种程度上改善了为一个接口声明好几个实体类的啰嗦问题,但仍不让人满意。在只需要传递一段简单的代码时(此处为选择标准的 boolean 表达式),还是需要创建一个对象,并且实现一个方法来定义一个新的行为
2.3.2 Lambda 表达式
java">List<Apple> result = filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor()));
简洁很多,并且看起来更像问题陈述本身了,其实呢,这个 Lambda 可以看作这个匿名类的简化,比如此处我们要转为 lambda
java">filterApples(inventory, new ApplePredicate(){public boolean test(Apple a){return RED.equals(a.getColor())}
})
实际上我们省略了创建对象的过程,参数 Apple a
即 lambda
中的参数,返回值即 lambda
最终返回的结果
java">(Apple apple) -> RED.equals(apple.getColor())
2.3.4 List 类型抽象化
原来的 filterApples
我们第一个参数类型为 List<Apple>
,现在我们可以抽象化该参数
java">public static <T> List<T> filter(List<T> list, Predicate<T> p){List<T> result = new ArrayList<>();for(T e: list){...}return result;
}// 现在我们可以更灵活的运用了 -> 找出偶数
List<Integer> evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0);
类似于策略设计模式
3. Lambda 表达式
3.1 Lambda 管中窥豹
lambda 即一种简洁的可传递匿名函数
java">Comparator<Apple> comparator = new Comparator<Apple>() {@Overridepublic int compare(Apple o1, Apple o2) {return o1.getWeight().compareTo(o2.getWeight());}
};
// 简洁地自定义一个 comparator 对象
Comparator<Apple> comparator1 = (Apple o1, Apple o2) -> o1.getWeight().compareTo(o2.getWeight());
3.2 在哪里以及如何使用 Lambda
3.2.1 函数式接口
可以在函数式接口上使用 Lambda 表达式。函数式接口就是只定义一个抽象方法的接口,当然接口中可以包含一些默认方法。
Lambda 表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。匿名内部类也可以完成同样的事情,只不过比较笨拙。下面举例 Runnable
java">@FunctionalInterface
public interface Runnable {public abstract void run();
}
java">public static void process(Runnable r){r.run();
}public static void main(String[] args) {Runnable r1 = () -> System.out.println("Hello World 1");Runnable r2 = new Runnable() {@Overridepublic void run() {System.out.println("Hello World 2");}};process(r1);process(r2);process(() -> System.out.println("Hello World 3"));
}
注意:只有函数式接口才能用 Lambda
表达式
3.2.2 函数描述符
Lambda 表达式可以被赋给一个变量,或传递给一个接受函数式接口作为参数的方法,当然这个 Lambda 表达式的签名(返回值类型+参数类型)要和函数式接口的抽象方法一样。
我们知道 Runnable
函数式接口中抽象方法的签名为 () -> void
即参数为空,返回值为空
java">Runnable r1 = () -> System.out.println("Hello World 1");
该 Lambda 的返回值为 System.out.println()
的返回值,System.out
是一个变量,println
为一个方法,该方法返回值为 void
,所以此处是合法的。
@FunctionalInterface
是一种标记型接口,和@Override
一样,如果在接口上标注了该注解,而它不是一个函数式接口的话会返回一个错误消息。当然它不是必须的,因为只要有且仅有一个抽象方法他就是函数式接口,只不过加上是一个比较好的做法。建议加。
3.3 Lambda 付诸实践:实现环绕模式
处理一个资源的过程通常为
- 初始化/准备代码
- 任务
- 清理/结束代码
java">public String processFile() throws IOException {try(BufferedReader br = new BufferedReader(new FileReader("input.txt"))) {return br.readLine();}
}
通常,初始化与清理代码不变,改变的是任务。即对文件执行不同的操作,所以我们需要把 processFile
的行为参数化
第一步:定义一个函数式接口来规定参数类型及返回值
java">@FunctionalInterface
public interface BufferedReaderProcessor {String process(BufferedReader bufferedReader) throws IOException;
}
第二步:使用函数式接口传递行为并执行行为
java">public String processFile(BufferedReaderProcessor brp) throws IOException {try(BufferedReader br = new BufferedReader(new FileReader("input.txt"))) {return brp.process(br); // 执行抽象接口中的方法}
}
第三步:传递 Lambda
使用Lambda描述具体的行为
java">String oneLine = processFile((BufferedReader b) -> b.readLine());
结束!
3.4 使用函数式接口
Java 8的库设计师在 java.util.function
包中引入了几个新的函数式接口
3.4.1 Predicate
在需要表示一个设计类型T的布尔表达式时,可以使用这个接口
java">@FunctionalInterface
public interface Predicate<T> {boolean test(T t);
}
比如我们自己定义一个方法,用来对 List
列表进行过滤
java">public static <T> List<T> filter(List<T> list, Predicate<T> p) {List<T> result = new ArrayList<>();for (T t : list) {if (p.test(t)) {result.add(t);}}return result;
}public static void main(String[] args) throws IOException {// 过滤掉空字符串Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();List<String> nonEmpty = filter(List.of("a", "b", ""), nonEmptyStringPredicate);
}
当然,里面还有 and
,or
等用来实现复合谓词,后面再说
3.4.2 Consumer
如果想要访问类型T的对象,并对其执行某些操作,就可以使用这个接口
java">@FunctionalInterface
public interface Consumer<T> { // (T) -> voidvoid accept(T t);
}
比如我们可以定义一个方法,用来对 List
集合做操作
java">public static <T> void forEach(List<T> list, Consumer<T> c) {for (T t : list) {c.accept(t);}
}public static void main(String[] args) throws IOException {// 输出列表中的每个元素List<Integer> list = Arrays.asList(1, 2, 3);forEach(list, i -> System.out.println(i));}
3.4.3 Function
当需要定义一个表达式,将输入对象的信息映射到输出,可以使用这个接口
java">@FunctionalInterface
public interface Function<T, R> { // (T) -> RR apply(T t);
}
例如
java">public static <T, R> List<R> map(List<T> list, Function<T, R> c) {ArrayList<R> res = new ArrayList<>();for (T t : list) {res.add(c.apply(t));}return res;
}public static void main(String[] args) throws IOException {// 统计字符串列表中字符串的长度System.out.println(map(List.of("a", "bb", "ccc"), (String s) -> s.length()));}
3.4.4 基本类型特化
前面介绍的函数式接口类型都是泛型类型,泛型只能绑定到引用类型,而无法使用基本类型。Java中有自动装箱和拆箱机制,但是在性能方面是要付出代价的。装箱后的值本质上就是把基本类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的基本值。因此Java也提供了一些供基本类型使用的函数式接口。如 IntPredicate
,限定了参数类型为 int
java">@FunctionalInterface
public interface IntPredicate {boolean test(int value);
}
java">IntPredicate evenNumbers = (int i) -> i % 2 == 0;
evenNumbers.test(1000); // 无装箱Predicate<Integer> oddNumbers = (Integer i) -> i % 2 != 0;
oddNumbers.test(1000); // 有装箱
函数式接口 | Predicate | Consumer |
---|---|---|
Predicate<T> | T -> boolean | IntPredicate LongPredicate DoublePredicate |
Consumer<T> | T -> void | IntConsumer LongConsumer DoubleConsumer |
Function<T, R> | T -> R | IntFunction<R> IntToDoubleFunction IntToLongFunction LongFunction<R> LongToDoubleFunction LongToIntFunction DoubleFunction<R> DoubleToIntFunction DoubleToLongFunction ToIntFunction<T> ToDoubleFunction<T> ToLongFunction<T> |
Supplier<T> | () -> T | BooleanSupplier IntSupplier LongSupplier DoubleSupplier |
UnaryOperator<T> | T -> T | IntUnaryOperator LongUnaryOperator DoubleUnaryOperator |
BinaryOperator<T> | (T, T) -> T | IntBinaryOperator LongBinaryOperator DoubleBinaryOperator |
BiPredicate<T, U> | (T, U) -> boolean | |
BiConsumer<T, U> | (T, U) -> void | ObjIntConsumer<T> ObjLongConsumer<T> ObjDoubleConsumer<T> |
BiFunction<T, U, R> | (T, U) -> R | ToIntBiFunction<T, U> ToLongBiFunction<T, U> ToDoubleBiFunction<T, U> |
我们需要 lambda 表达式是什么形式,就使用什么样的接口。
3.4.5 异常处理
如果需要 Lambda 表达式抛出异常,有两种办法
-
定义一个自己的函数式接口,并声明受检异常
比如前面定义过的一个接口java">@FunctionalInterface public interface BufferedReaderProcessor {String process(BufferedReader bufferedReader) throws IOException; }BufferedReaderProcessor p = (BufferedReader br) -> br.readLine();
-
把 lambda 包在一个
try/catch
块中
有些时候我们使用的是Java提供的函数式接口API,比如Function<T,R>
,我们可以显式捕获异常java">Function<BufferedReader, String> f = (BufferedReader b) -> {try {return b.readLine();}catch(IOException e){throw new RuntimeException(e);} };
3.5 类型检查、类型推断以及限制
lambda 表达式可以为函数式接口生成一个实例。但是,lambda 表达式本身并不包含它在实现哪个函数式接口的信息,它是如何知道的呢。
3.5.1 类型检查
lambda 的类型是从使用 Lambda 的上下文推断出来的。接受它传递的方法的参数或接受它的值的局部变量中的 lambda 表达式需要的类型称为目标类型。
java">List<Apple> heavierThan150g = filter(inventory, (Apple apple) -> apple.getWeight() > 150);
过程:
-
找到
filter
方法的声明java">filter(List<Apple> inventory, Predicate<Apple> p)
-
检测到目标类型是
Predicate<Apple>
,得出T
为Apple
-
找到
Predicate<Apple>
接口的抽象方法boolean test(Apple apple)
-
检查抽象方法与 lambda 表达式签名是否匹配
如果 Lambda 表达式抛出一个异常,那么抽象方法所声明的 throws
语句必须与之匹配
3.5.2 同样的 Lambda,不同的函数式接口
如果两个函数式接口中抽象方法的标签都一致,那么可以接收同种形式的 lambda 表达式
java">Callable<Integer> c = () -> 42;
PrivilegedAction<Integer> p = () -> 42;
即同一个 Lambda 可用于多个不同的函数式接口。
虽然两个函数式接口中抽象方法一致,但是并不是说用哪个都行,因为两个接口的功能不同。
java">@FunctionalInterface public interface Callable<V> {/*** Computes a result, or throws an exception if unable to do so.** @return computed result* @throws Exception if unable to compute a result*/V call() throws Exception; }
java">@FunctionalInterface public interface PrivilegedAction<T> {/*** Performs the computation. This method will be called by* {@code AccessController.doPrivileged} after enabling privileges.** @return a class-dependent value that may represent the results of the* computation. Each class that implements* {@code PrivilegedAction}* should document what (if anything) this value represents.* @see AccessController#doPrivileged(PrivilegedAction)* @see AccessController#doPrivileged(PrivilegedAction,* AccessControlContext)*/T run(); }
当然我们自定义的时候可能并不会太复杂
接口名的定义也很重要哦。
🐯 特殊的 void
兼容规则
如果一个 Lambda
的主体是一个语句表达式,它就和一个返回 void
的函数描述符兼容(参数列表必须是一样的)
java">ArrayList<String> list = new ArrayList<>();System.out.println(list.add("a")); // truePredicate<String> p = s -> list.add(s);
Consumer<String> c1 = s -> list.add(s);
Consumer<String> c2 = s -> {return list.add(s);}; // unexpected return value
p
和 c1
没有报错,可以赋值成功,但是 Predicate
返回值类型为 boolean
,Consumer
为 void
。可以得知如果lambda表达式主体为一个语句表达式时,可以兼容返回值为 void
的函数式接口。第三个可以作出验证,因为手动 return
返回的就是 boolean
类型了
🐯 如下代码为什么不能编译
java">Object o = () -> { System.out.println("dog"); };
原因是 Target type of lambda conversion must be an interface
但是我们可以这么做
java">Object o = (Runnable)() -> { System.out.println("dog"); };
啊?Runnable
是一个接口,为什么能被 Object
接收呢?我们可以拆分为如下代码
java">Runnable r = () -> { System.out.println("dog"); }; // 创建了一个实现 Runnable 的匿名类实例
Object o = r; // 可以赋值给 Object 类型的变量,因为 r 是一个对象
lambda 表达式实际上是创建了一个该接口的实现类的一个对象,对象都是 Object
,所以可以的。
底层编译器会通过使用 invokedynamic
指令在运行时生成一个 Runnable
的实例,而不直接生成匿名类。通过这种方式,Java 可以更高效地处理 lambda
表达式,并实现更好的性能。
如果两个不同的函数式接口却有着相同的函数描述符,为了消除这种显式的二义性,可以对 lambda 进行强制类型转换
java">@FunctionalInterface
interface Action{void act();
}public class Main {public static void execute(Runnable runnable) {runnable.run();}public static void execute(Action action){action.act();}public static void main(String[] args) throws IOException {execute((Action) () -> {}); // 强制类型转换为 Action 否则会有二义性}
}
3.5.3 类型推断
Java编译器会从目标类型(即函数式接口)推断出 lambda 的参数类型
java">filter(List<Apple> inventory, Predicate<Apple> p)
// 根据参数推断除 apple 变量为 Apple 类型
List<Apple> greenApples = filter(inventory, apple -> GREEN.equals(apple.getColor()));Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight()); // 根据接口 Comparator 中泛型类型为 Apple 推断出 a1, a2 类型
有时候显式更易读,有时候去掉易读,看程序员选择。
3.5.4 使用局部变量
捕获变量 https://blog.csdn.net/weixin_71246590/article/details/139144066
待理解:Java 中堆和栈的区别以及为什么说访问局部变量时是在访问它的副本
3.6 方法引用
3.6.1 构建方法引用
方法引用就是让你根据已有的方法实现来创建 Lambda 表达式,如
java">inventory.sort(comparing(Apple::getWeight));
可以把方法引用看作针对仅仅涉及单一方法的 Lambda 的语法糖。
方法引用主要有三类:
- 指向静态方法的方法引用,如
Integer::parseInt
- 指向任意类型实例方法的方法引用,如
String::length
,这种实际上是在引用一个对象的方法,而这个对象是 lambda 表达式的一个参数,例如(String s) -> s.toUpperCase()
可以写为String::toUpperCase
- 指向现存对象或表达式实例方法的方法引用,如有一个局部变量
expensive
,有一个方法getValue
,那么() -> expensive.getValue()
可以写为expensive::getValue
🐯 速查表
Lambda | 方法引用 |
---|---|
(args) -> ClassName.staticMethod(args) | ClassName::staticMethod |
(arg0, rest) -> arg0.instanceMethod(rest) arg0 为 ClassName 类型 | ClassName::instanceMethod |
(args) -> expr.instanceMethod(args) | expr::instanceMethod |
一一实现各种类型的方法引用
java">class Dog {// 类静态方法public static String park(String name){return name + "在狗叫";}
}public class Main {// 函数式接口签名为 T -> Tprivate <T> void print(T t, UnaryOperator<T> u){System.out.println(u.apply(t));}public static void main(String[] args) throws IOException {Main main = new Main();// 直接引用main.print("二狗", Dog::park);}
}
- 定义类静态方法,该方法签名即为
T -> T
- 在
main
对象中传入参数("二狗", Dog::park)
,Dog::park
为方法引用,也即是方法实现,所以对应的print
函数式接口参数也要为T -> T
类型。有了具体的方法实现,在print
中去使用该方法
类的实例方法
java">class Dog {private final String name;public Dog(String name) {this.name = name;}// 类实例方法public String park2(){return this.name + "在狗叫";}
}public class Main {private <T, R> void print(T d, Function<T, R> f){System.out.println(f.apply(d));}public static void main(String[] args) throws IOException {Main main = new Main();Dog d = new Dog("二狗");// main.print(d, (Dog i) -> i.park2());// main.print(d, Dog::park2)}
}
-
定义类实例方法
-
为什么要在
print
方法中传递两个参数呢?首先要理解
Dog::park2
对应的 lambda,park2
是实例方法,它需要一个具体的对象去调用,所以它等价于Dog i -> i.park2()
,有具体的对象去调用实例方法。不同于静态方法,静态方法不属于具体的对象,所以Dog::park
直接就可以。注意,lambda 只是一个方法实现,我们并没有往里面传递参数,所以创建了一个
Dog
,作为参数。
现存对象的方法引用
java">class Dog {private final String name;public Dog(String name) {this.name = name;}// T -> Rpublic String park2(String doing){return this.name + doing;}}public class Main {private <T, R> void print(T doing ,Function<T, R> f){System.out.println(f.apply(doing));}public static void main(String[] args) throws IOException {Main main = new Main();Dog d = new Dog("二狗");main.print("在狗叫" ,d::park2);// main.print("在狗叫" ,(String t) -> d.park2(t));}}
- 创建类的实例方法
- 创建一个二狗
d::park2
相当于使用该对象去调用park2
方法,该方法的签名为T -> R
,所以函数式接口为Function
- 传入一个
doing
参数 f.apply(doing)
实际上就是d.park2(doing)
3.6.2 构造函数引用
java">class Dog {private final String name;public Dog(){this.name = "二狗";}public Dog(String name) {this.name = name;}public String getName() {return name;}
}public class Main {public static void main(String[] args) throws IOException {// Supplier<Dog> s = () -> new Dog();Supplier<Dog> s = Dog::new;System.out.println(s.get().getName());// Function<String, Dog> f = (String name) -> new Dog(name);Function<String, Dog> f = Dog::new;System.out.println(f.apply("傻狗").getName());}}
上面创建了一个 lambda 方法来创建对象,和我们定义一个方法是一样的。
java">public Dog createDog(){return new Dog();
}
然后我们使用 Supplier
的 get
方法来执行 lambda 表达式以创建对象。
这个功能有一个有趣的应用,如下
java">static Map<String, Function<Integer, Fruit>> map = new HashMap<>();static {map.put("apple", Apple::new);map.put("orange", Orange::new);map.put("banana", Banana::new);
}public static Fruit giveMeFruit(String fruit, Integer weight){return map.get(fruit).apply(weight);
}
可以根据水果类型及重量来创建一个水果。
我们只需要使用 giveMeFruit
方法即可,想象一下我们原来的做法,这种方法非常好用
java">if(fruit.equals("apple")){return new Apple(weight);
}else if(...)
3.7 复合 Lambda 表达式
3.7.1 比较器复合
java">inventory.sort(Comparator.comparing(Apple::getWeight) // 按重量排序.reversed() // 逆序.thenComparing(Apple::getCountry)); // 对重量相同的苹果再按国家排序
3.7.2 谓词复合
谓词接口包括三个方法: negate
、or
、and
java">Predicate<Apple> redApple = apple -> apple.getColor().equals(Color.RED); // 红色苹果
Predicate<Apple> notRedApple = redApple.negate(); // 非红色苹果Predicate<Apple> redAndHeavyApple = redApple.and(apple -> apple.getWeight() > 150); // 红色苹果且重量大于 150gPredicate<Apple> redAndHeavyAppleOrGreenApple = redApple.and(apple -> apple.getWeight() > 150).or(apple -> apple.getColor().equals(Color.GREEN)); // 红色苹果且重量大于150g或绿色苹果
and
和 or
无优先级的区别,是按照从左到右确定优先级的
java">a.or(b).and(c) // (a || b) && c
a.and(b).or(c) // (a && b) || c
如果想实现类似于 a && (b || c)
,应该写为 a.and(b.or(c))
的形式
3.7.3 函数复合
提供了两个默认方法,compose
和 andThen
java">Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h1 = f.andThen(g);
Function<Integer, Integer> h2 = f.compose(g);System.out.println(h1.apply(2)); // 6
System.out.println(h2.apply(2)); // 5
这里呢 h1
就等价于 g ( f ( x ) ) g(f(x)) g(f(x)) ,先使用 f(x)
得出结果,再将该结果作为 g(x)
的输入
h2
等价于 f ( g ( x ) ) f(g(x)) f(g(x)),从里向外,先使用 g(x)
得出结果,再将结果作为 f(x)
的输入
我们可以利用这些做一个流水线工作(输入一个字符串,字符串不能为 null
,不为空且不为空白字符串,然后去除首尾空白字符)
java">Predicate<String> stringIsNotNull = Objects::nonNull;
Predicate<String> stringIsEmpty = String::isEmpty;
Predicate<String> stringIsBlank = String::isBlank;
// 不为 null, 不为空字符串, 不为空白字符串
Predicate<String> checkString = stringIsNotNull.and(stringIsEmpty.negate()).and(stringIsBlank.negate());
Function<String, String> stringWithOutWhiteSpace = String::trim;String s = null;
if(checkString.test(s))s = stringWithOutWhiteSpace.apply(s);
System.out.println(s);
小总结
-
函数式接口用来表明一个抽象方法的标签(参数类型加返回值),当然,也可以抛出异常
-
lambda
表达式用来表示某个函数式接口的具体实现方式 -
方法引用相当于一个快捷的
lambda
表达式,如类的静态方法、类的实例方法、对象的实例方法三种引用方式
java">public Fruit createFruit(String name, Function<String, Fruit> f){} // 参数1是我们指定的水果名称,需要自己传, 第二个参数是方法的 T->R 类型方法的实现
注意,当我们将 lambda
传入给参数时,只是给定了方法实现,并没有去执行,且真正执行需要的参数还需要我们给,调用 f.apple(name)
传入参数并去执行
4. 引入流
4.1 流是什么
Stream API
代码特点
- 声明式,更简洁,更易读
- 可复合,更灵活
- 可并行,性能更好
SQL 查询语句就是声明式查询,不需要指定查询的具体方式,只需要给定想要结果即可。
SELECT name FROM dishes WHERE calorie < 400
Java 中也可以如此做
java">List<String> lowCaloricDishesName = menu.parallelStream().filter(d -> d.getCalories() < 400).sorted(Comparator.comparing(Dishes::getCalories)).map(Dish::getName).collect(toList());
- 代码是声明性方式写的,只需要说明想要完成什么,而不是说明如何实现一个操作(不用利用循环和
if
语句),即指令型编程方式 - 可以实现并行提高查询效率而不用自己书写代码。
filter
、sorted
、map
和collect
等操作是与具体线程模型无关的高层次构件,所以它们的内部实现可以是单线程的,也可以透明地充分利用多核架构!在实践中,我们也无须为了数据处理任务并行而考虑线程和锁,Stream API
已经做好了
第4节和第5节用到的数据如下
java">class Dish {private final String name;private final boolean vegetarian;private final int calories;private final Type type;public Dish(String name, boolean vegetarian, int calories, Type type) {this.name = name;this.vegetarian = vegetarian;this.calories = calories;this.type = type;}public int getCalories() {return calories;}public String getName() {return name;}public Type getType() {return type;}public boolean isVegetarian() {return vegetarian;}@Overridepublic String toString() {return name;}public enum Type {MEAT, FISH, OTHER}
}List<Dish> menu = Arrays.asList(new Dish("pork", false, 800, Dish.Type.MEAT),new Dish("beef", false, 700, Dish.Type.MEAT),new Dish("chicken", false, 400, Dish.Type.MEAT),new Dish("french fries", true, 530, Dish.Type.OTHER),new Dish("rice", true, 350, Dish.Type.OTHER),new Dish("season", true, 120, Dish.Type.OTHER),new Dish("pizza", true, 550, Dish.Type.OTHER),new Dish("prawns", false, 300, Dish.Type.FISH),new Dish("salmon", false, 450, Dish.Type.FISH)
);
4.2 流与集合
- 集合是数据,流是计算
- 集合使用迭代器是显式迭代,流的迭代操作是在后台进行的,属于内部迭代
4.3 流与集合
- 集合与流之间的差异就在于什么时候进行计算。
集合是一个内存中的数据结构,集合中的每个元素都得先算出来才能添加到集合中,大小是确定的
流是一个延迟创建的集合,filter
,map
只是操作的描述,只有在消费者要求的时候才会计算值(forEach
、collect
) - 流只能遍历一次,遍历完一次后,就被消费掉了,只能重新从数据源那里再次获得一个新的流使用。
- 流与集合另一个关键区别在于它们遍历数据的方式。
集合是外部迭代,自己管理(循环和if
以及并发)
流是内部迭代,Stream
自动选择一种适合我们硬件的数据表示和并行实现,以更优化的顺序进行处理
集合是数据,可以从集合中创建一个流,流更像是 SQL 语句,从数据集合中进行筛选查询。数据库查询也是声明式的,我们只给我们想要的结果,过程不管。
下面一个示例,我们可以看看流的好处
java">List<String> names = menu.stream().filter(dish -> {System.out.println("filtering:" + dish.getName());return dish.getCalories() > 300;}).map(dish -> {System.out.println("mapping:" + dish.getName());return dish.getName();}).limit(3).collect(toList());
System.out.println(names);// filtering:pork
// mapping:pork
// filtering:beef
// mapping:beef
// filtering:chicken
// mapping:chicken
// [pork, beef, chicken]
由结果可以看出,filter
和 map
是合并到同一次遍历中了(称为循环合并)。它内部,并不是先筛选,再取名字,再去取前三个,而是做了性能优化的。
4.4 流操作
4.4.1 中间操作
诸如 filter
或 sorted
等中间操作会返回另一个流,这可以让多个操作连接起来形成一个查询。除非在流水线上触发一个终端操作,否则中间操作不会执行任何处理。
4.4.2 终端操作
终端操作会从流的流水线生成结果,其结果是任何不是流的值。如 List
、Integer
等
4.4.3 使用流
- 一个数据源
- 一个中间操作链形成一条流水线
- 一个终端操作,执行流水线并生成结果
5. 使用流
外部迭代只能用单一线程挨个迭代,但如果使用内部迭代,StreamAPI
可以在背后进行很多优化。
5.1 筛选
5.1.1 filter
谓词筛选
java">List<Dish> vegetarianMenu = menu.stream().filter(Dish::isVegetarian).collect(toList()); // 筛选出所有素菜
5.1.2 筛选各异的元素
流支持 distinct
方法,它返回一个元素各异(根据流所生成元素的 hashCode
和 equals
方法实现)的流
java">List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);numbers.stream().filter(i -> i % 2 == 0).distinct().forEach(System.out::println); // 2 4
5.2 流的切片
5.2.1 使用谓词对流进行切片
🐯 takeWhile
:在遭遇第一个不符合要求的元素时停止处理
java">List<Dish> list = menu.stream().takeWhile(dish -> dish.getCalories() >= 700).toList(); // [pork, beef]
直到遇到第一个热量不大于 700 的菜肴停止处理。该情况适合于已经按热量排好序的菜单,使用 filter
可以遍历拿到所有热量大于 700 的菜肴,但是会遍历列表所有元素,而如果菜单是按热量排好序的,只需要从前面几个取即可,不需要完全遍历。
🐯 dropWhile
:是对 takeWhile
的补充,它会从头开始,丢弃所有谓词结果为 true
的元素,在遇到第一个为 false
的元素,返回该元素及剩余所有元素。
java">List<Dish> list = menu.stream().dropWhile(dish -> dish.getCalories() > 320).toList(); // [season, pizza, prawns, salmon]
如果明确地知道数据源是排序的,那么 takeWhile
和 dropWhile
方法通常比 filter
高效的多
5.2.2 截断流
流支持 limit(n)
方法,该方法返回另一个不超过给定长度的流。如果流是有序的,则最多返回前 n
个元素。
java">List<Dish> list = menu.stream().filter(dish -> dish.getCalories() > 320).limit(3).toList(); // [pork, beef, chicken]
如果 n
给的过大,超过列表长度,则也不会报错,只不过取到的元素个数到不了 n
。
limit
也可以用在无序流上,比如源是一个 Set
。这种情况下, limit
的结果不会以任何顺序排列
5.2.3 跳过元素
流支持 skip(n)
方法,返回一个扔掉了前 n
个元素的流。如果流中元素不足 n
个,则返回同一个空流。 limit
和 skip
是互补的
java">List<Dish> list = menu.stream().filter(dish -> dish.getCalories() > 320).skip(3).toList(); // [french fries, rice, pizza, salmon]
5.2.2 和 5.2.3 两者的结果就是所有卡路里大于 320 的菜肴。
5.3 映射
5.3.1 对流中每一个元素应用函数
流支持 map
方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素。
java">List<String> list = menu.stream().map(Dish::getName) // 去所有菜肴的名称.toList();
map
方法的参数为一个 Function
函数式接口
❓ Dish
类中的 getName()
不是没有参数吗 ,怎么能被 Function
接口接收呢?
getName
是类的实例方法,见 3.6.1,它的实际形式为 (arg0) -> arg0.instanceMethod() == ClassName::instanceMethod
,arg0
实际为该对象引用
5.3.2 流的扁平化
flatMap
方法可以把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。即将流的流转为流
java">Stream<Stream<String>> --> Stream<String>
Arrays.stream()
可以接收一个数组并产生一个流
java">List<String> words = Arrays.asList("Modern", "Java", "In", "Action");List<String> uniqueCharacters = words.stream().map(word -> word.split("")) // Stream<String[]>.map(Arrays::stream) // Stream<Stream<String>>.flatMap(i -> i) // Stream<String>.distinct().collect(toList());
这里 flatMap
只用来合并流,实际上我们可以简化为,将 Arrays::stream
方法引用传入 flatMap
java">List<String> words = Arrays.asList("Modern", "Java", "In", "Action");List<String> uniqueCharacters = words.stream().map(word -> word.split("")) // Stream<String[]>.flatMap(Arrays::stream) // Stream<String>.distinct().collect(toList());
5.4 查找和匹配
5.4.1 检查谓词是否至少匹配一个元素
anyMatch
回答流中是否有一个元素能匹配给定的谓词,它返回一个 boolean
,是一个终端操作(只要返回的不是流,那就是终端操作)
java">if(menu.stream().anyMatch(Dish::isVegetarian)) { // 看菜单里是否有素食可选择System.out.println("The menu is (somewhat) vegetarian friendly!!");
}
5.4.2 检查谓词是否匹配所有元素
allMatch
它会查看流中的元素是否都能匹配给定的谓词
java">boolean isHealthy = menu.stream().allMatch(dish -> dish.getCalories() < 1000); // 是否都满足这个条件
noneMatch
确保流中没有任何元素与给定的谓词匹配
java">boolean isHealthy = menu.stream().noneMatch(dish -> dish.getCalories() >= 1000); // 是否都不满足这个条件
对于流而言,某些操作,如(allMatch
、anyMatch
、noneMatch
、findFirst
和 findAny
)不用处理整个流就能得到结果。只要找到一个元素,就可以有结果。同样 limit
也是短路操作。
5.4.3 查找元素
findAny
用于返回当前流中的任意元素,有序流中会返回第一个元素,无序流中不确定,尤其是在并行流中。
java">Optional<Dish> dish = menu.stream().filter(Dish::isVegetarian).findAny();dish.ifPresent(i -> System.out.println(i.getName())); // french friesOptional<Dish> dish = menu.parallelStream() // 并行流.filter(Dish::isVegetarian).findAny();dish.ifPresent(i -> System.out.println(i.getName())); // 变为了 season
5.4.4 查找第一个元素
有些流由一个出现顺序来指定流中项目出现的逻辑顺序(排列好的数据流,如 List)。对于这种流,如果想要找到第一个元素,可以使用 findFirst
java">Optional<Dish> dish = menu.stream().filter(Dish::isVegetarian).findFirst();dish.ifPresent(i -> System.out.println(i.getName())); // french fries
🐯 findFirst
和 findAny
的区别
-
无论流是否有序、是否在并行流中,
findFirst()
都会保证返回流中的第一个元素,即结果总是确定的。这意味着它可能无法充分利用并行处理的性能优势,因为它需要确保顺序一致性。 -
在并行流中,
findAny()
会返回任意一个元素,因此能够更好地利用并行流的性能优势。它会返回第一个被任何线程找到的元素,结果是非确定性的。
如果不关心返回的元素是哪个,可以使用 findAny
,它在并行流时限制较少。
5.5 规约
5.5.1 元素求和
java">List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);Integer sum = numbers.stream().reduce(0, (a, b) -> a + b); // 对列表求和
Integer sum = numbers.stream().reduce(0, Integer::sum);
reduce
第一个参数为初始值,第二个为 BinaryOperator<T>
即 (T, T) -> T
🐯 这里实际上的操作是如何进行的呢?
0
赋值给a
a
加上列表第一个元素,即1
,也是b
a
变为1
,去加列表第二个元素,即2
,也是b
- …
做的累加操作
reduce
还可以不给定初始值(不建议),但是会返回一个 Optional
对象
java">Optional<Integer> sum = numbers.stream().reduce(Integer::sum);
当流中没有任何元素时,reduce
操作无法返回其和,所以可能不存在,所以返回值设为 Optional
5.5.2 最大值和最小值
求最大值、最小值,可以利用 reduce
java">Optional<Integer> sum = numbers.stream().reduce(Integer::max); // 最小值使用 Integer::min
sum.ifPresent(i -> System.out.println("max number:" + i));numbers.stream().min(Comparator.comparingInt(i -> i)) // 利用 min, max 方法.ifPresent(System.out::println);
计算流中元素个数:count()
或 map + reduce
java">long count = numbers.stream().count();long count = numbers.stream().map(d -> 1).reduce(0, Integer::sum); // 两种方式
5.5.3 状态及界
🐯 区分有状态与无状态
有状态操作指的是在处理流的元素时,需要存储中间状态信息以正确执行操作的流操作。这些操作在处理流时不仅仅依赖于当前元素,还需要知道前面已经处理过的元素或需要等待整个流的数据才能继续。比如 reduce
需要记录累积结果,distinct
需要记录哪些元素已经存在过,skip
、limit
还要记录已经跳过或取了多少个元素。
无状态的比如 map
,filter
,我总是对每个元素进行操作,操作后就不会再管了,各个元素之间没有任何关联或影响。
在并行流中,有状态的操作比无状态的操作更难有效地并行处理,且会导致性能开销的增加,因为要协调多个线程之间的状态来保证正确性。
🐯 区分有界和无界
有界流就是流有限,无界流就是流无限。目前这个概念定义还不清楚,个人理解如下
流如果无界,那么对于某些操作,如排序 sorted
,去重 distinct
是无法进行的。它们只能操作有界流
对于 findAny
findFirst
则不害怕无界流。
5.6 数值流
我们在使用 reduce
方法计算流中某些值的总和时,可以写成如下形式
java">int calories = menu.stream().map(Dish::getCalories).reduce(0, Integer::sum);
但是这里暗含一个装箱成本。每个 Integer
都要拆箱成一个原始类型,再进行求和,如果可以直接使用 sum
方法,岂不是更好
java">int calories = menu.stream().map(Dish::getCalories).sum();
但是这是不可以的,因为 map
得到的流为 Stream<Dish>
,对其进行加和是没有任何意义的。所以 Stream
接口不会提供 sum
方法。
5.6.1 原始类型流特化
在 Java8
中引入了 IntStream
、DoubleStream
、LongStream
,分别将流中的元素特化为 int
、long
和 double
,从而避免了暗含的装箱成本。引入的主要原因是解决 int
和 Integer
之间的效率差异
🐯 映射到数值流
java">int sum = menu.stream().mapToInt(Dish::getCalories) // IntStream 类型.sum();
如果流为空,则 sum
默认返回 0
。IntStream
还支持其他的方便方法,如 max
、min
、average
等。
🐯 转换回对象流
java">IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();
有时候需要访问 Stream
接口中定义的更广义的操作。比如我想利用 IntStream
中的每个值去生成一个对象,但是 IntStream
中 map
的参数为 (int) -> int
类型,返回值必须为 int
,这就限制了我们的操作,所以需要转为 Stream<Integer>
再去操作。
继承关系,都继承自 BaseStream
java">BaseStream<T, S>├── Stream<T>├── IntStream├── LongStream└── DoubleStream
各自的 filter
,map
等方法都是自己定义的,没有继承和实现。
🐯 默认值 OptionalInt
前面介绍了 Optional
类。对应的也有 OptionalInt
、OptionalDouble
、OptionalLong
。前面的 sum()
方法,如果没有元素默认返回 0
。但是对于 min()
和 max()
方法不适用,它会返回一个 OptionalInt
java">OptionalInt maxCalories = menu.stream().mapToInt(Dish::getCalories).max();int max = maxCalories.orElse(1);
5.6.2 数值范围
Java8 中引入了两个可以用于 IntStream
和 LongStream
的静态方法,帮助生成这种范围:range
和 rangeClosed
。这两个方法都是第一个参数接收起始值,第二个参数接收结束值。但 range
不包含结束值,rangeClosed
包含结束值。
java">IntStream evenNumbers = IntStream.rangeClosed(1, 1000).filter(i -> i % 2 == 0); // [1, 100] 所有的偶数
5.7 构建流
5.7.1 由值创建流
可以使用静态方法 Stream.of
,通过显式值创建一个流。它可以接收任意数量的参数
java">Stream<String> stream = Stream.of("Modern", "Java", "In", "Action"); // 类型必须一致
也可以使用 empty()
得到一个空流
java">Stream<String> emptyStream = Stream.empty();
5.7.2 由可空对象创建流
Java9 提供了一个新方法可以由一个可空对象创建流。
java">String homeValue = System.getProperty("home"); // home 可以为 null
Stream<String> homeValueStream = homeValue == null ? Stream.empty() : Stream.of(homeValue);
借助于 Stream.ofNullable
可以使代码更简洁
java">Stream<String> values = Stream.ofNullable(System.getProperty("home")); // 为空则创建空流
这种模式搭配 flatMap
处理由可空对象构成的流时尤其方便
java">Stream<String> values = Stream.of("config", "home", "user").flatMap(key -> Stream.ofNullable(System.getProperty(key))); // Stream.ofNullable 会返回一个流
flatMap
将流进行扁平化。
5.7.3 由数组创建流
可以使用静态方法 Arrays.stream
从数组创建一个流。它接受一个数组作为参数。
java">int[] numbers = {2, 3, 5, 7, 11, 13};
IntStream stream = Arrays.stream(numbers); // 生成 IntStream 流
- 如果数组为
int
double
long
类型会生成对应的IntStream
、DoubleStream
、LongStream
- 如果为对象类型,则为
Stream<T>
5.7.4 由文件生成流
Java 中用于处理文件等 I/O 操作的 NIO API(非阻塞 I/O)已更新,以便利用 Stream API。java.nio.file.Files
中的很多静态方法都会返回一个流。例如,一个很有用的方法是 Files.lines
,它会返回一个由指定文件中的各行构成的字符串流。
java">long uniqueWords = 0;
try(Stream<String> lines =Files.lines(Paths.get("data.txt"), Charset.defaultCharset())){ // 项目根路径下的 data.txtuniqueWords = lines.flatMap(i -> Arrays.stream(i.split(" "))).distinct().count();}catch (IOException e){System.out.println("open error");
}
流会自动关闭,因此不需要执行额外的 try-finally
操作。
调用 Files.lines
打开一个 I/O 资源,这些资源使用完毕后必须被关闭,否则会发生资源泄露。在过去,必须显式地声明一个 finally
块来完成这些回收工作。Stream
接口通过实现 AutoCloseable
接口,很方便地解决了这一问题。
5.7.5 由函数生成流:创建无限流
Stream.iterate
和 Stream.generate
两个静态方法可以创建所谓的无限流。一般需要结合 limit(n)
来对这种流加以限制。
🐯 迭代
java">Stream.iterate(0, n -> n + 2).limit(10).forEach(System.out::println);
iterate
参数一为一个初始值,参数二为一个应用在每个产生的新值上的 Lambda(UnaryOperator类型),后一个值取决于前一次值。这种流是无限的,这也是流和集合之间的一个关键区别,流可以是无限的,但是集合不能,集合必须是有限的。通常我们使用 limit
方法来显式限制流的大小。
Java9 对 iterate
方法进行了增强,它现在可以支持谓词操作了
java">Stream.iterate(0, n -> n < 100, n -> n + 4).forEach(System.out::println);
第二个参数是一个谓词,它决定了迭代调用何时终止。当然,takeWhile
也可以达到同样的效果,它能对流执行短路操作
java">Stream.iterate(0, n -> n + 4).takeWhile(n -> n < 100).forEach(System.out::println);
但是对于 filter
不可行,因为 filter
在这里只能对无限流进行过滤,无限流并不能被截断。
java">Stream.iterate(0, n -> n + 4).filter(n -> n < 100).forEach(System.out::println); // 卡炸
🐯 生成
generate
方法也可按需生成一个无限流。它接受一个 Supplier<T>
类型的 Lambda 提供新的值。
java">Stream.generate(Math::random).limit(5).forEach(System.out::println); // 生成5个 [0, 1) 的随机数
此处 generate
指向的供应源(Math::random
的方法引用)是无状态的,它不会在任何地方记录任何值,以备以后计算使用。但供应源不一定是无状态的。可以创建存储状态的供应源,它可以修改状态,并为流生成下一个值使用,但是这在并行代码中使用有状态的供应源是不安全的。下面是 generate
方法生成斐波那契数列
java">IntSupplier fib = new IntSupplier() {private int pre = 0;private int cur = 1;@Overridepublic int getAsInt() {int oldPre = this.pre;this.pre = this.cur;this.cur = oldPre + this.cur;return oldPre;}
};IntStream.generate(fib).limit(10).forEach(System.out::println);
此处 getAsInt
在调用时会改变对象的状态,由此在每次调用时产生新的值。相比之下,iterate
的方法是纯粹不变的,它没有修改现有的状态,而是访问,在每次迭代时创建新的元组。
java">Stream.iterate(new int[]{0, 1},t -> new int[]{t[1], t[0] + t[1]}) // 没有修改状态,而是访问、创建新的元组.limit(10).forEach(System.out::println);
我们应该始终采用不变的方法,以便并行处理流,并保证最终结果正确。所以上面那个 generate
生成斐波那契数列的写法尽量不要使用,用这个 iterate
的这种写法。
6. 用流收集数据
6.1 收集器简介
groupingBy
是生成一个 Map
,它的键是桶,值则是桶中那些元素的值。
6.1.1 收集器用作高级归约
java">transactions.stream().collect(Collectors.groupingBy(Transaction::getCurrency));
对流调用 collect
方法将对流中的元素触发一个归约操作(由 Collector
来参数化),Collector
接口中方法的实现决定了如何对流执行归约操作。Collectors
实用类提供了很多静态工厂方法,可以方便地创建常见收集器的实例。
java">List<Transaction> transactions = transactionStream.collect(Collectors.toList());
collect
用来表示归约操作,其参数是一个Collector
接口的实现,最终对元素的归约,都是它来处理的Collectors
工具类中提供了很多方法实现,这些方法的返回类型都是Collector
类型,可以用于归约
6.1.2 预定义收集器
即 Collectors
类提供的工厂方法创建的收集器。它们主要提供了三大功能
- 将流元素归约和汇总为一个值
- 元素分组
- 元素分区
6.2 归约和汇总
但凡要把流中的所有的项目合并成要给结果时就可以用 collect
方法。这个结果可以是任何类型,可以复杂如代表一棵树的多级映射,或是简单如一个整数。
java">transactions.stream().collect(Collectors.counting()) // 统计流中元素transactions.stream().count(); // 更直接的方法
6.2.1 查找流中的最大值和最小值
可以使用 Collectors.maxBy
和 Collectors.minBy
来计算流中的最大值或最小值。这两个收集器接受一个 Comparator
参数来比较流中的元素。
java">// 找到菜单中热量最大的菜肴
Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);Optional<Dish> maxCalorieDish = menu.stream().collect(Collectors.maxBy(dishCaloriesComparator));
也可以直接使用 stream
流提供的方法更方便
java">Optional<Dish> maxCalorieDish = menu.stream().max(dishCaloriesComparator);
6.2.2 汇总
对流中的对象的某个字段进行求和或平均值等称为汇总操作
Collectors
类提供了一个工厂方法:Collectors.summingInt
。它可以接受一个把对象映射为求和所需 int
的函数,并返回一个收集器。该收集器在传递给普通的 collect
方法后即执行我们需要的汇总操作。
java">// 计算菜单总热量
Integer totalCalories = menu.stream().collect(Collectors.summingInt(Dish::getCalories));// 也可以这么写
int totalCalories = menu.stream().mapToInt(Dish::getCalories).sum();
summingInt
这个收集器会在遍历流时把每一道菜都映射为其热量,然后把这个数字累加到一个累加器。summingLong
和 summingDouble
也是用于求和
此外还有 averagingInt
、averagingLong
和 averagingDouble
可以计算数值的平均数
java">// 计算平均热量
double avgCalories = menu.stream().collect(Collectors.averagingInt(Dish::getCalories));
不过很多时候,你可能想要得到两个或更多这样的结果,而且你希望只需一次操作就可以完成。这时,可以使用 summarizingInt
工厂方法返回的收集器。通过一次操作就可以数出菜单中元素的个数,并得到菜肴热量总和、平均值、最大值和最小值
java">IntSummaryStatistics menuCalorieStatistics = menu.stream().collect(Collectors.summarizingInt(Dish::getCalories));menuCalorieStatistics.getAverage();
6.2.3 连接字符串
joining
工厂方法返回的收集器会把对流中每一个对象应用 toString
方法得到的所有字符串连接成一个字符串
java">String shortMenu = menu.stream().map(Dish::getName).collect(Collectors.joining());
joining
在内部使用了 StringBuilder
来把生成的字符串逐个追加起来,速度快。
对啦,还记得吗,我们之前在定义 Dish
时,toString
方法重写是只返回菜肴名称,所以此处我们还可以这么写
java">String shortMenu = menu.stream().collect(Collectors.joining()); // 直接 joining
结果是相同的。此外,joining
工厂方法有一个重载版本可以接受元素之间的分界符。
java">String shortMenu = menu.stream().map(Dish::getName).collect(Collectors.joining(", "));
// pork, beef, chicken, french fries, rice, season, pizza, prawns, salmon
结果就好看的多
6.2.4 广义的归约汇总
Collectors.reducing
工厂方法是所有这些特殊情况的一般化,前面所说的这些都可以使用 reducing
方法实现,只不过前面这些可读性更高且方便程序员使用。
java">Integer totalCalories = menu.stream().collect(Collectors.reducing(0, Dish::getCalories, (i, j) -> i + j));
三个参数
- 归约操作的起始值,也是流中没有元素时的返回值
- 转换函数
Function
类型,此处将菜肴转换成一个表示其热量的int
- 第三个参数是一个
BinaryOperator
,将两个项目累积成一个同类型的值。这里它就是对两个int
求和
同样它也有单参数形式,即只需要提供一个 BinaryOperator
java">Optional<Dish> collect = menu.stream().collect(Collectors.reducing((i, j) -> i.getCalories() > j.getCalories() ? i : j));
它也可以看作三参数方法的特殊情况,它把流中第一个项目作为起点,把恒等函数(即一个函数仅仅是返回其输入参数)作为一个转换函数。也正应为单参数方法中没有起始值,所以收集器没有起点,所以返回值类型为 Optional
来保证流为空时不会出现问题。
❓ reduce
收集与 reducing
归约的区别
P125
总体来说,reducing
方法就是利用累积函数(第三个参数),把一个初始化为起始值(第一个参数)的累加器,和把转换函数(第二个参数)应用到流中每个元素上得到的结果不断迭代合并起来。
6.3 分组
一个常见的数据库操作是根据一个或多个属性对集合中的项目进行分组。如下,按照菜肴类型分类。
java">Map<Dish.Type, List<Dish>> dishByType = menu.stream().collect(Collectors.groupingBy(Dish::getType));
// {OTHER=[french fries, rice, season, pizza], MEAT=[pork, beef, chicken], FISH=[prawns, salmon]}
groupingBy
方法传递一个 Function
,它提取了流中每一道 Dish
的 Type
,我们称这个 Function
为分类函数,因为它用来把流中的元素分成不同的组。分组操作的结果是一个 Map
,把分组函数返回的值作为映射的键,把流中所有具有这个分类值的项目的列表作为对应的映射值。
如果分类函数不一定像方法引用那样可用,想要分类的条件可能比简单的属性访问器更复杂,我们可以把这个逻辑写成 Lambda
表达式
java">// 根据热量进行分类
Map<String, List<Dish>> dishesByCaloricLevel = menu.stream().collect(Collectors.groupingBy(dish -> {if (dish.getCalories() <= 400) return "DIET";else if (dish.getCalories() <= 700) return "NORMAL";else return "FAT";})
);
6.3.1 操作分组的元素
与 groupingBy
搭配食用的三个方法
filtering
mapping
flatMapping
例如我们想找到热量大于 500 的菜肴并按照类型分类
java">Map<Dish.Type, List<Dish>> stream = menu.stream().filter(i -> i.getCalories() > 500) // 先过滤.collect(Collectors.groupingBy(Dish::getType)); // 再分组
// {MEAT=[pork, beef], OTHER=[french fries, pizza]}
确实可以正常工作,但是也伴有缺陷,即没有类型是 FISH
的菜肴,这个键在结果映射中完全消失了。
为了解决这个问题,我们可以使用 Collectors
提供的 filtering
方法,把过滤谓词挪到第二个 Collector
中
java">Map<Dish.Type, List<Dish>> stream = menu.stream().collect(Collectors.groupingBy(Dish::getType,Collectors.filtering(dish -> dish.getCalories() > 500, Collectors.toList())));
// {OTHER=[french fries, pizza], FISH=[], MEAT=[pork, beef]}
前者可以认为是先过滤再分组,后者可认为是先分组再过滤,所以即使为空也有 FISH
。
filtering
作为 groupingBy
的第二个参数
filtering
的第二个参数可以用来指定收集方式,此处是 toList
,也可以指定为 toSet
,那返回类型即 Map<Dish.Type, Set<Dish>>
Collectors
类还有 mapping
方法,它接受一个映射函数和另一个 Collector
函数作为参数,作为参数的 Collector
会收集对每个元素执行该映射函数的运行结果。
java">Map<Dish.Type, List<String>> stream = menu.stream().collect(Collectors.groupingBy(Dish::getType,Collectors.mapping(Dish::getName, Collectors.toList()))); // 对每个菜品执行 getName 并放入 List
// {OTHER=[french fries, rice, season, pizza], FISH=[prawns, salmon], MEAT=[pork, beef, chicken]}
将对应的标签结果列表归并为一层,使用 flatMapping
java">// 为每个菜肴添加标签
Map<String, List<String>> dishTags = new HashMap<>();
dishTags.put("pork", Arrays.asList("greasy", "salty"));
dishTags.put("beef", Arrays.asList("salty", "roasted"));
dishTags.put("chicken", Arrays.asList("fried", "crisp"));
dishTags.put("french fries", Arrays.asList("fried", "crisp"));
dishTags.put("rice", Arrays.asList("light", "natural"));
dishTags.put("season", Arrays.asList("fresh", "natural"));
dishTags.put("pizza", Arrays.asList("tasty", "salty"));
dishTags.put("prawns", Arrays.asList("tasty", "roasted"));
dishTags.put("salmon", Arrays.asList("delicious", "fresh"));Map<Dish.Type, Set<String>> stream = menu.stream().collect(Collectors.groupingBy(Dish::getType,Collectors.flatMapping(dish -> dishTags.get(dish.getName()).stream(), Collectors.toSet())));
// {MEAT=[salty, greasy, roasted, fried, crisp], FISH=[roasted, tasty, fresh, delicious], OTHER=[salty, natural, light, tasty, fresh, fried, crisp]}
6.3.2 多级分组
实现多级分组,可以由双参数版本的 groupingBy
工厂方法创建的收集器。
一级分类 Dish.Type
,二级分类 CaloricLevel
,可以继续向第二个参数中传递 groupingBy
以获取多级分组
java">Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel = menu.stream().collect(Collectors.groupingBy(Dish::getType,Collectors.groupingBy(dish -> {if (dish.getCalories() <= 400) return CaloricLevel.DIET;else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;else return CaloricLevel.FAT;})));// {FISH={DIET=[prawns], NORMAL=[salmon]}, OTHER={DIET=[rice, season], NORMAL=[french fries, pizza]}, MEAT={FAT=[pork], DIET=[chicken], NORMAL=[beef]}}
6.3.3 按子组收集数据
groupingBy
的第二个收集器可以是任意(Collector
)类型,而并不一定是另一个 groupingBy
(前面也用过了 filtering
、mapping
、flatMapping
)。例如要数一数每类菜有多少个,可以传递 counting
收集器。
java">Map<Dish.Type, Long> typesCount = menu.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.counting())
);
// {FISH=2, OTHER=4, MEAT=3}
普通的单参数 groupingBy(f)
其中 f
是分类函数,实际上是 groupingBy(f, toList())
的简便写法。
🐯 Collectors
类中大部分方法返回值都是 Collector
类型,可以作为 groupingBy
的第二个参数。
java">// 找出每个类别中热量最高的菜肴
Map<Dish.Type, Optional<Dish>> maxCaloriesType = menu.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)))
);// {OTHER=Optional[pizza], MEAT=Optional[pork], FISH=Optional[salmon]}
把收集器的结果转换为另一种类型
可以使用 Collectors.collectingAndThen
工厂方法返回的收集器,这个工厂方法接受两个参数——要转换的收集器以及转换函数,并返回另一个收集器。这个收集器相当于旧收集器的一个包装,collect
操作的最后一步就是将返回值用转换函数做一个映射。
java">Map<Dish.Type, Dish> mostCaloricByType = menu.stream().collect(Collectors.groupingBy(Dish::getType, // 分类函数Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)), // 包装后的收集器Optional::get // 转换函数)));
// {FISH=salmon, OTHER=pizza, MEAT=pork}
也就是对收集结果再加一个转换函数而已。
与 groupingBy 联合使用的其他收集器的例子
通过 groupingBy
工厂方法的第二个参数传递的收集器将会对分到同一组中的所有流元素执行进一步归约操作。例如,对每种菜肴类型的热量求和。
java">Map<Dish.Type, Integer> totalCaloriesByType = menu.stream().collect(Collectors.groupingBy(Dish::getType,Collectors.summingInt(Dish::getCalories))
);
可以将 groupingBy
与 mapping
收集器结合起来,对流中的每个元素做一个转换然后再收集起来
java">Map<Dish.Type, Set<CaloricLevel>> caloricLevelsByType = menu.stream().collect(Collectors.groupingBy(Dish::getType,Collectors.mapping(dish -> {if (dish.getCalories() <= 400) return CaloricLevel.DIET;else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;else return CaloricLevel.FAT;}, Collectors.toSet()))); // 放入 Set
但是对于返回的 Set
是什么类型并没有任何保证,可以通过 toCollection
进行更多的控制。
java">Map<Dish.Type, Set<CaloricLevel>> caloricLevelsByType = menu.stream().collect(Collectors.groupingBy(Dish::getType,Collectors.mapping(dish -> {if (dish.getCalories() <= 400) return CaloricLevel.DIET;else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;else return CaloricLevel.FAT;}, Collectors.toCollection(HashSet::new)))); // HashSet 类型
6.4 分区
分区是分组的特殊情况:由一个谓词作为分类函数,它称为分区函数,分区函数返回一个布尔值,所以最多可分为两组,true
一组,false
一组。
java">Map<Boolean, List<Dish>> partitionedMenu = menu.stream().collect(Collectors.partitioningBy(Dish::isVegetarian));// {false=[pork, beef, chicken, prawns, salmon], true=[french fries, rice, season, pizza]}
partitionedMenu.get(true); // 返回为素食的菜肴
当然你用 filter + collect
也能做到,但一次只能选择符合条件的菜肴,而不是分为两份。
java">menu.stream().filter(Dish::isVegetarian).collect(toList());
6.4.1 分区的优势
分区的好处在于保留了分区函数返回 true
或 false
的两套流元素列表。partitioningBy
工厂方法有一个重载版本,可以传递第二个收集器。
java">Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType = menu.stream().collect(Collectors.partitioningBy(Dish::isVegetarian,Collectors.groupingBy(Dish::getType)));// {false={MEAT=[pork, beef, chicken], FISH=[prawns, salmon]}, true={OTHER=[french fries, rice, season, pizza]}}
结果为一个二级分类 Map
由 partitioningBy
返回的 Map
实现其结构更紧凑,也更高效,这是因为它只包含两个键:true
或 false
。实际上,它的内部实现就是一个特殊的 Map
,只有两个字段。
6.4.2 将数字按质数和非质数分区
java">// 判断是否为质数
public static boolean isPrime(int candidate){int candidateRoot = (int) Math.sqrt(candidate);return IntStream.rangeClosed(2, candidateRoot).noneMatch(i -> candidate % i == 0);
}
// [2, n] 中的数字进行分区
public static Map<Boolean, List<Integer>> partitionPrimes(int n){return IntStream.rangeClosed(2, n).boxed().collect(Collectors.partitioningBy(Main::isPrime));
}
6.5 收集器接口
Collectors
类中大部分静态方法都返回的是 Collector
接口类型,所以理解它很重要。
Collector
接口定义
java">public interface Collector<T, A, R> {Supplier<A> supplier();BiConsumer<A, T> accumulator();BinaryOperator<A> combiner();Function<A, R> finisher();Set<Characteristics> characteristics();
}
T
是流中收集的项目的泛型A
是累加器的类型,累加器是在收集过程中用于累积部分结果的对象,如ArrayList
R
是收集操作得到的对象(通常但并不一定是集合)的类型
接口中前四个方法都会返回一个会被 collect
方法调用的函数,第五个方法 characteristics
则提供了一系列特征,也就是一个提示列表,告诉 collect
方法在执行归约操作的时候可以应用哪些优化(比如并行化)。
这里我们可以实现一个 ToListCollector<T>
类,将 Stream<T>
中的所有元素收集到一个 List<T>
里,它的签名如下:
java">public class ToListCollector<T> implements Collector<T, List<T>, List<T>>
🐯 建立新的结果容器:supplier
方法
supplier
方法必须返回一个结果为空的 Supplier
,也就是一个无参数函数,在调用时它会创建一个空的累加器实例,供数据收集过程使用。
java">public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {@Overridepublic Supplier<List<T>> supplier() {// return ArrayList::new;return () -> new ArrayList<>();}@Overridepublic BiConsumer<List<T>, T> accumulator() {// return List::addreturn (list, t) -> list.add(t);}@Overridepublic BinaryOperator<List<T>> combiner() {return (list1, list2) -> {list1.addAll(list2);return list1;};}@Overridepublic Function<List<T>, List<T>> finisher() {return Function.identity();}@Overridepublic Set<Characteristics> characteristics() {return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH, Characteristics.CONCURRENT));}
}
— 看不懂,看课本吧,自己理解 P143
7. 并行数据处理与性能
7.1 并行流
对收集源调用 parallelStream
方法就能将集合转换为并行流。并行流就是一个把内容拆分成多个数据块,用不同线程分别处理每个数据块的流。例如,计算 1~n 的和
java">// 顺序流
public long sequentialSum(long n) {return Stream.iterate(1L, i -> i + 1).limit(n).reduce(0L, Long::sum);
}
// 迭代
public long iterativeSum(long n) {long result = 0;for(long i = 1; i <= n; i++) {result += i;}return result;
}
7.1.1 将顺序流转换为并行流
对顺序流调用 parallel
方法,可以将流转换为并行流,让前面的函数式归约过程(也就是求和)并行执行
java">public long parallelSum(long n) {return Stream.iterate(1L, i -> i + 1).limit(n).parallel() // 流转为并行流.reduce(0L, Long::sum);
}
实际上,对顺序流调用 parallel
方法并不意味着流本身有任何实际的变化。它其实仅仅在内部设置了一个 boolean
标志,表示你想让调用 parallel
之后进行的所有操作都并行执行。类似地,只需要对并行流调用 sequential
方法就可以把它变成顺序流。
java">stream.parallel().filter(...).sequential().map(...).parallel() // 最后一次调用的.reduce();
但是仅最后一次的 parallel
或 sequential
会影响整个流水线,该实例中,最后会并行执行,因为最后一次调用的是 parallel
。
7.1.2 测量流性能
其实这里会让你惊讶一下的,迭代求和速度最快,顺序流比迭代慢了45倍左右,而并行流比迭代慢了130倍左右。
原因
- 顺序流中有拆箱的消耗
- 并行流中
iterate
很难进行并行,因为iterate
实际上是一种顺序操作,分割为并行操作时会增加开销。
— 后面有时间再看并行吧
8. Collection API 的增强功能
8.1 集合工厂
Java9 引入了一些新的方法,可以很简便地创建由少量对象构成的 Collection
。例如 Arrays.asList
java">List<String> friends = Arrays.asList("Raphael", "Olivia", "Thibaut");
friends.set(0, "dog");
friends.add("ergou"); // java.lang.UnsupportedOperationException
不过该方法是创建了一个固定大小的列表,列表中的元素可以更新,但不能增加或者删除。尝试添加会抛出异常,因为通过工厂方法创建的 Collection
的底层是大小固定的可变数组。
Java 中还未提供类似于 Arrays.asSet()
这种工厂方法,我们可以通过别的方法实现类似的效果
java">Set<String> friends = new HashSet<>(Arrays.asList("Raphael", "Olivia", "Thibaut"));
或者通过流的方式
java">Set<String> friends = Stream.of("Raphael", "Olivia", "Thibaut").collect(Collectors.toSet());
这两种方案并非完美,背后都有不必要的对象分配,且最终得到的是可变的 Set
哦。
Java9 通过增强 CollectionAPI
,另辟蹊径地增加了对集合常量的支持。
8.1.1 List 工厂
java">List<String> friends = List.of("Raphael", "Olivia", "Thibaut");
该方式创建的集合列表是一个只读列表,不能修改、添加、删除。可以防止集合被意外地修改。
为了避免不可预知的缺陷,同时以更紧凑的方式存储内部数据,不要在工厂方法创建的列表中存放 null
元素。不允许放入 null
,否则会抛出 NullPointerException
异常
🐯 查看 List.of
方法会发现重载了十几个,只是参数个数不同,为什么?
可变参数方法内部会创建一个数组来存储传递的参数,这种数组的创建和填充会引入一些额外的开销。而重载方法则避免了这一额外的开销。
- 重载方法(0 到 10 个参数)可以避免创建临时数组:
- 对于
List.of()
这种常用的工厂方法,传递 0 到 10 个参数的情况很常见。通过提供多个重载的List.of()
方法,可以避免为少量元素创建临时数组。 - 例如,当调用
List.of("A", "B", "C")
时,重载方法的实现直接处理这三个参数,而不需要将它们放入一个数组。这在性能上更高效,尤其是在需要频繁创建小列表时。
- 对于
- 可变参数(超过 10 个参数时)会创建临时数组:
- 当传递超过 10 个参数时,Java 会使用可变参数(varargs),这会创建一个数组来存储所有参数。这个数组的创建和填充比重载方法略有性能开销(分配数组、初始化以及垃圾回收),但由于超过 10 个参数的情况较少见,因此在设计中将这种开销限制在特殊场景中。
8.1.2 Set 工厂
Set.of
创建列表元素的不可变 Set
集合
java">Set<String> friends = Set.of("Raphael", "Olivia", "Thibaut");
如果试图创建一个包含重复元素的 Set
,会受到 IllegalArgumentException
异常。
8.1.3 Map 工厂
共提供了两种方式
- 需要同时传递键和值
java">Map<String, Integer> friends =Map.of("Raphael", 30, "Olivia", 25, "Thibaut", 26);
- 当键值对规模较大时,可以考虑
Map.ofEntries
的工厂方法,使用Map.Entry<K, V>
对象作为参数。
java">Map<String, Integer> friends =Map.ofEntries(Map.entry("Raphael", 30), Map.entry("Olivia", 25), Map.entry("Thibaut", 26));
8.2 使用 List 和 Set
for-each
循环底层使用迭代器实现,只能用于访问集合,不能修改、删除或添加,普通 for
循环通过索引 i
来进行增删改查,迭代器循环时只能进行删除、修改或访问,不能添加。
Java 8 中引入了以下方法
removeIf
移除集合中匹配指定谓词的元素。实现了List
和Set
的所有类都提供了该方法(事实上,这个方法继承自Collection
接口)replaceAll
用于List
接口中,它使用一个函数(UnaryOperator
)替换元素sort
也用于List
接口中,对列表自身的元素进行排序
以上方法都作用于调用对象本身。这一点和流的操作有很大不同,流的操作会生成一个新的结果。之所以要添加这些新方法,是因为集合的修改繁琐而且容易出错。
8.2.1 removeIf
比如想删除交易记录中以数字打头的引用代码的交易
java">for(Transaction transaction: transactions){if(Character.isDigit(transaction.getReferenceCode().charAt(0))){transactions.remove(transaction);}
}
这段代码可能导致 ConcurrentModificationException
。原因是因为在底层实现上,for-each
循环使用了一个迭代器对象,所以代码执行会像下面这样:
java">for(Iterator<Transaction> iterator = transactions.iterator(); iterator.hasNext();){Transaction transaction = iterator.next();if(Character.isDigit(transaction.getReferenceCode().charAt(0)){transactions.remove(transaction); // java.util.ConcurrentModificationException}
}
错误原因可参考 Java 核心卷I
第九章集合
需要修改为
java">for(Iterator<Transaction> iterator = transactions.iterator(); iterator.hasNext();){Transaction transaction = iterator.next();if(Character.isDigit(transaction.getReferenceCode().charAt(0)){iterator.remove(); // 通过迭代器删除}
}
但如此以来代码就变得复杂,所以可以使用 removeIf
方法
java">transactions.removeIf(transaction -> Character.isDigit(transaction.getReferenceCode().charAt(0));
其实这么写也不会出错,前面出错的原因是应为 for-each
循环底层是利用了迭代器
java">for(int i = 0; i < transactions.size(); i++){Transaction transaction = transactions.get(i);if(transaction.getYear() == 2011){transactions.remove(transaction);}
}
8.2.2 replaceAll
可以使用一个新的元素替换列表中满足要求的每个元素。用流会生成一个新的列表,但不会修改原列表。
java">for(ListIterator<Transaction> iterator = transactions.listIterator(); iterator.hasNext(); ) {Transaction transaction = iterator.next();iterator.set(new Transaction(raoul, transaction.getYear(), 111)); // 也很麻烦
}
可以使用 replaceAll
java">transactions.replaceAll(t -> new Transaction(raoul, t.getYear(), 111));
8.3 Map
Java8 在 Map
接口中引入了几个默认方法。
8.3.1 forEach
一直以来,访问 Map
中的键值都比较麻烦,需要使用 Map.Entry<K,V>
迭代器访问 Map
集合
java">for (Map.Entry<String, Integer> mapEntry : map.entrySet()) {mapEntry.getKey();mapEntry.getValue();
}
现在可以使用 forEach
来接受一个 BiConsumer
类型的参数
java">map.forEach((k, v) -> System.out.println(k + ": " + v));
8.3.2 排序
对 Map
中的键或值排序
java">Map<String, Integer> animal = new HashMap<>();
animal.put("dog", 22);
animal.put("tiger", 21);
animal.put("cat", 23);animal.entrySet().stream().sorted(Map.Entry.comparingByKey()) // 按照 key 排序.forEachOrdered(System.out::println);
这里使用 forEachOrdered
来进行输出
- 当使用
.stream()
串行流时,其实使用forEach
和forEachOrdered
输出的顺序都是相同的,即key
排序后的顺序 - 当使用
.parallelStream()
并行流时,使用forEachOrdered
可以保证并行情况下仍按序排列,但是forEach
就不能保证了
8.3.3 getOrDefault
如果要查找的键在 Map
中并不存在,就会收到一个空引用,因此需要检查返回值以避免空指针异常
java"> Map<String, Integer> animal = new HashMap<>();
animal.put("dog", 22);
animal.put("tiger", 21);
animal.put("cat", 23);Integer v = animal.getOrDefault("animal", -1);
System.out.println(v); // -1
你要是默认值就用 null
那就没办法避免了。
8.3.4 计算模式
根据键在 Map
中存在或者缺失的状况,有条件地执行某个操作,并存储计算的结果
computeIfAbsent
如果指定的键没有对应的值(没有该键或对应值为空),那么使用该键计算新的值,并将其添加到Map
中computeIfPresent
如果指定的键在Map
中存在,就计算该键的新值,并将其添加到Map
中compute
使用指定的键计算新的值,并将其存储到Map
中
computeIfAbsent
java">Map<String, Integer> animal = new HashMap<>();
animal.put("dog", 22);
animal.put("tiger", 21);
animal.put("cat", 23);Integer v = animal.computeIfAbsent("animal", t -> {System.out.println(t); // animalreturn 15;
});
System.out.println(v); // 15
如果 Map
的结构为 Map<K, List<V>>
形式,我们在向某个键里加入值时还需要判断键值是否为空
java">// Map<String, List<String>>
List<String> movies = friendsToMovies.get("Raphael");
if(movies == null){movies = new ArrayList<>();friendsToMovies.put("Raphael", movies);
} // 需要判断
movies.add("Star Wars");
但如果有了 computeIfAbsent
就方便多了
java">friendsToMovies.computeIfAbsent("Raphael", name -> new ArrayList<>()).add("Star Wars"); // 接着对值列表 List 操作
computeIfPresent
如果该值存在就计算新值
compute
无论是否为空,直接计算新值
java">Integer v = animal.compute("dog", (t, u) -> {System.out.println(t); // dogSystem.out.println(u); // 22return 15;
});Integer v = animal.compute("animal", (t, u) -> { // t 为键, u 为值System.out.println(t); // animalSystem.out.println(u); // nullreturn 15;
});
8.3.5 删除模式
删除指定键值对对应的条目
java">String key = "dog";
Integer value = 22;
if(animal.containsKey(key) && Objects.equals(animal.get(key), value)) { // 键值都相等时再删除animal.remove(key);return true;
}else {return false;
}
其实挺麻烦,可以一句话实现删除
java">String key = "dog";
Integer value = 22;
animal.remove(key, value); // 返回 boolean 类型来表示是否删除
8.3.6 替换模式
replaceAll
通过 BiFunction
替换 Map
中每个项的值
repalce
如果键存在,就可以通过该方法替换 Map
中该键对应的值
java">Map<String, Integer> animal = new HashMap<>();
animal.put("dog", 22);
animal.put("tiger", 21);
animal.put("cat", 23);animal.replaceAll((key, value) -> value + 1);
System.out.println(animal); // {cat=24, tiger=22, dog=23}
animal.replace("dog", 1); // 该键存在就替换
animal.replace("tiger", 22, 99); // key 和 oldValue 都相等才替换为 newValue
System.out.println(animal); // {cat=24, tiger=99, dog=1}
8.3.7 merge 方法
合并两个 Map
java">Map<String, String> family =Map.ofEntries(Map.entry("Dog", "二狗"), Map.entry("Cat", "猫咪"));
Map<String, String> friends =Map.of("Tiger", "虎哥");Map<String, String> everyone = new HashMap<>(family); // family 是不可变map,所以需要新建一个map用于合并
everyone.putAll(friends);
System.out.println(everyone); // {Dog=二狗, Tiger=虎哥, Cat=猫咪}
只要 Map
中不含有重复的键,这段代码就没有问题(比如 friends
中也有一个 Dog
,那合并的时候会把 family
中的 Dog
覆盖),如果想要在合并时对值有更加灵活的控制,需要考虑使用 merge
方法。
工作机制
java">map.merge(key, value, remappingFunction);
-
如果
key
不存在于Map
中,则将key
和value
直接插入。 -
如果
key
存在,则调用remappingFunction
将旧值与新值进行合并,并将合并结果作为新的值存入Map
。 -
如果
remappingFunction
返回null
,则Map
会删除该key
。
如果值为 null
,则不进行合并,而是直接用新值代替,如果合并后值为 null
,则直接删除该键
这样我们可以利用该策略进行初始化检查
java">Map<String, Long> moviesToCount = new HashMap<>();
String movieName = "James Bound";Long count = moviesToCount.get(movieName);
if(count == null){moviesToCount.put(movieName, 1L);
}else {moviesToCount.put(movieName, count + 1);
}
// 可以简化为
moviesToCount.merge(movieName, 1L, (key, count) -> count + 1);
❓ 如何对 Map
使用谓词判断并删除
java">Map<String, Long> moviesToCount = new HashMap<>();
String movieName = "James Bound";moviesToCount.merge(movieName, 1L, (key, count) -> count + 1L);
System.out.println(moviesToCount); // {James Bound=1}moviesToCount.entrySet().removeIf(entry -> entry.getValue() < 10);
System.out.println(moviesToCount); // {}
8.4 改进的 ConcurrentHashMap
9. 重构、测试和调试
9.1 为改善可读性和灵活性重构代码
9.1.1 改善代码的可读性
- 使用
Lambda
表达式取代匿名类 - 用方法引用重构
Lambda
表达式 - 用
Stream API
重构命令式的数据处理
9.1.2 从匿名类到 Lambda
表达式的转换
采用匿名类,代码会更简洁,可读性更好
匿名类和 Lambda
表达式中的 this
和 super
的含义是不同的。在匿名类中,this
代表的类自身,但是在 Lambda
中,它代表的是包含类。其次,匿名类可以屏蔽包含类的变量,而 Lambda
表达式不行,如下
java">int a = 10;
Runnable r1 = () -> {int a = 2; // Variable 'a' is already defined in the scope
};Runnable r2 = new Runnable(){public void run(){int a = 2; // 没问题System.out.println(a);}
}
在涉及重载的上下文里,将匿名类转换为 Lambda
表达式可能导致代码更加晦涩。因为匿名类的类型是在初始化时确定的,而 Lambda
的类型取决于它的上下文。参考 3.5.2,需要进行显式的类型转换来表明 Lambda
表达式的类型。
9.1.3 从 Lambda 表达式到方法引用的转换
Lambda
表达式适合用于传递代码片段的场景。不过,为了改善代码的可读性,尽量使用方法引用。
比如前面第 6 章用过的一个分类代码
java">Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel = menu.stream().collect(Collectors.groupingBy(Dish::getType,Collectors.groupingBy(dish -> {if (dish.getCalories() <= 400) return CaloricLevel.DIET;else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;else return CaloricLevel.FAT;})));// 可以重构为
public class Dish{...public CaloricLevel getCaloricLevel(){if (this.getCalories() <= 400) return CaloricLevel.DIET;else if (this.getCalories() <= 700) return CaloricLevel.NORMAL;else return CaloricLevel.FAT;}
}
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel =menu.stream().collect(groupingBy(Dish::getCaloricLevel));
此外,对于同一个需求,我们应使用可读性更高的代码,比如计算所有菜肴的热量和
java">int totalCalories = menu.stream().map(Dish::getCalories).reduce(0, (c1, c2) -> c1 + c2);
// 可读性不如下面的
int totalCalories =menu.stream().collect(Collectors.summingInt(Dish::getCalories));
int totalCalories =menu.stream().mapToInt(Dish::getCalories).sum();
9.1.4 从命令式的数据处理切换到 Stream
建议将所有迭代器这种数据处理方式都转为 Stream API
方式,一是意图会更清晰,二是并行更容易
java">List<String> dishNames = new ArrayList<>();
for(Dish dish: menu){if(dish.getCalories() > 300){dishNames.add(dish.getName());}
}
// 整改如下
menu.parallelStream().filter(d -> d.getCalories() > 300).map(Dish::getName).collect(toList());
9.1.5 增加代码的灵活性
9.2 使用 Lambda 重构面向对象的设计模式
9.2.1 策略模式
java">interface ValidationStrategy {boolean execute(String s);
}
// 一种验证策略
class IsAllLowerCase implements ValidationStrategy{@Overridepublic boolean execute(String s) {return s.matches("[a-z]+");}
}
// 一种验证策略
class IsNumeric implements ValidationStrategy{@Overridepublic boolean execute(String s) {return s.matches("\\d+");}
}public class Validator {private final ValidationStrategy strategy;public Validator(ValidationStrategy strategy) {this.strategy = strategy;}// 这个不错public boolean validate(String s){return strategy.execute(s);}public static void main(String[] args) {// 验证Validator validator = new Validator(new IsAllLowerCase());System.out.println(validator.validate("abc"));}
}
使用 Lambda
重构后我们可以不使用接口继承了,只定义接口
java">// 函数接口
interface ValidationStrategy {boolean execute(String s);
}public class Validator {private final ValidationStrategy strategy;public Validator(ValidationStrategy strategy) {this.strategy = strategy;}public boolean validate(String s){return strategy.execute(s);}public static void main(String[] args) {// 在使用时传递验证方式Validator validator = new Validator(s -> s.matches("[a-z]+"));System.out.println(validator.validate("abc"));}
}
该方式也会更灵活,避免一种验证方式一个类这种僵化的模板代码
9.2.2 模板方法
模板代码就是在你希望使用某个算法,但是需要对其中的某些行进行改进,才能达到希望的效果时是非常有用的
比如,有一个在线银行,输入一个用户账号,然后查询该账号的信息,最后银行进行一些反馈(发放福利或推广文件等)
java">abstract class OnlineBanking {public void processCustomer (int id){Customer c = Database.getCustomerWithId(id);makeCustomerHappy(c);}abstract void makeCustomerHappy(Customer c);
}
不同的支行可以继承该类并重写 makeCustomerHappy
方法。
使用 Lambda
重构,可以使用两个参数,将 makeCustomerHappy
方法的实现用第二个参数进行传递
java">class OnlineBanking {public void processCustomer (int id, Consumer<Customer> makeCustomerHappy){Customer c = Database.getCustomerWithId(id);makeCustomerHappy.accept(c);}
}// 调用
new OnlineBanking().processCustomer(1337, (Customer c) -> System.out.println("hello"));