目录
- 第 5 章 使用流
- 5.1 筛选和切片
- 5.1.1 用谓词筛选(filter)
- 5.1.2 筛选各异的元素(distinct)
- 5.1.3 截短流(limit)
- 5.1.4 跳过元素(skip)
- 5.2 映射(map,flatMap)
- 5.2.1 对流中每一个元素应用函数(map)
- 5.2.2 流的扁平化(flatMap)
- 5.3 查找和匹配
- 5.3.1 检查谓词是否至少匹配一个元素(anyMatch)
- 5.3.2 检查谓词是否匹配所有元素(allMatch、noneMatch)
- 5.3.3 查找元素(findAny)
- 5.3.4 查找第一个元素(findFirst)
- 5.4 归约
- 5.4.1 元素求和(reduce)
- 5.4.2 最大值和最小值(reduce)
- 5.4.3 统计个数(reduce、count)
- 5.5 付诸实践
- 代码清单5-1 找出2011年的所有交易并按交易额排序(从低到高)
- 代码清单5-2 交易员都在哪些不同的城市工作过
- 代码清单5-3 查找所有来自于剑桥的交易员,并按姓名排序
- 代码清单5-4 返回所有交易员的姓名字符串,按字母顺序排序
- 代码清单5-5 有没有交易员是在米兰工作的
- 代码清单5-6 打印生活在剑桥的交易员的所有交易额
- 代码清单5-7 所有交易中,最高的交易额是多少
- 代码清单5-8 找到交易额最小的交易
- 5.6 数值流
- 引入背景:
- 5.6.1 原始类型流特化(mapToInt、mapToDouble、mapToLong)
- 5.6.2 数值范围(rangeClosed,range)
- 5.6.3 数值流应用:勾股数
- 5.7 构建流
- 5.7.1 由值创建流(Stream.of)
- 5.7.2 由数组创建流(Arrays.stream)
- 5.7.3 由文件生成流(Files.lines)
- 5.7.4 由函数生成流:创建无限流(Stream.iterate、Stream.generate)
- 5.7.5 由集合创建流(list.stream())
第 5 章 使用流
5.1 筛选和切片
5.1.1 用谓词筛选(filter)
Streams接口支持filter方法。该操作会接受一个谓词作为参数,并返回一个包括所有符合谓词的元素的流。
Stream<T> filter(Predicate<? super T> predicate);
List<Dish> vegetarianMenu = menu.stream().filter(Dish::isVegetarian) ←─方法引用检查菜肴是否适合素食者.collect(toList());
5.1.2 筛选各异的元素(distinct)
流还支持一个叫作distinct的方法,它会返回一个元素各异(根据流所生成元素的hashCode和equals方法实现)的流
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream().filter(i -> i % 2 == 0).distinct().forEach(System.out::println);
5.1.3 截短流(limit)
流支持limit(n)方法,该方法会返回一个不超过给定长度的流。
Stream<T> limit(long maxSize);
List<Dish> dishes = menu.stream().filter(d -> d.getCalories() > 300).limit(3).collect(toList());
5.1.4 跳过元素(skip)
流还支持skip(n)方法,返回一个扔掉了前n个元素的流。如果流中元素不足n个,则返回一个空流。
Stream<T> skip(long n);
List<Dish> dishes = menu.stream().filter(d -> d.getCalories() > 300).skip(2).collect(toList());
5.2 映射(map,flatMap)
一个非常常见的数据处理套路就是从某些对象中选择信息。比如在SQL里,你可以从表中选择一列。Stream API也通过map和flatMap方法提供了类似的工具。
5.2.1 对流中每一个元素应用函数(map)
流支持map方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的细微差别在于它是“创建一个新版本”而不是去“修改”)。
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
List<Integer> dishNameLengths = menu.stream().map(Dish::getName).map(String::length).collect(toList());
5.2.2 流的扁平化(flatMap)
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
给定单词列表["Hello","World"],你想要返回列表["H","e","l", "o","W","r","d"]。
List<String> uniqueCharacters =words.stream().map(w -> w.split("")) ←─将每个单词转换为由其字母构成的数组.flatMap(Arrays::stream) ←─将各个生成流扁平化为单个流.distinct().collect(Collectors.toList());
一言以蔽之,flatmap方法把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。
为巩固对于map和flatMap的理解,如下测试:
给定[1, 2, 3, 4, 5],应该返回[1, 4, 9, 16, 25]。List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squares =numbers.stream().map(n -> n * n).collect(toList());
例如,给定列表[1, 2, 3]和列表[3, 4],应该返回[(1, 3), (1, 4), (2, 3), (2, 4), (3, 3), (3, 4)]。List<Integer> numbers1 = Arrays.asList(1, 2, 3);
List<Integer> numbers2 = Arrays.asList(3, 4);
List<int[]> pairs =numbers1.stream().flatMap(i -> numbers2.stream().map(j -> new int[]{i, j})).collect(toList());
如何扩展前一个例子,只返回总和能被3整除的数对呢?例如(2, 4)和(3, 3)是可以的。List<Integer> numbers1 = Arrays.asList(1, 2, 3);
List<Integer> numbers2 = Arrays.asList(3, 4);
List<int[]> pairs =numbers1.stream().flatMap(i ->numbers2.stream().filter(j -> (i + j) % 3 == 0).map(j -> new int[]{i, j})).collect(toList());
5.3 查找和匹配
5.3.1 检查谓词是否至少匹配一个元素(anyMatch)
anyMatch方法可以回答“流中是否有一个元素能匹配给定的谓词”。比如,你可以用它来看看菜单里面是否有素食可选择:
boolean anyMatch(Predicate<? super T> predicate);
if(menu.stream().anyMatch(Dish::isVegetarian)){System.out.println("The menu is (somewhat) vegetarian friendly!!");
}
5.3.2 检查谓词是否匹配所有元素(allMatch、noneMatch)
allMatch方法的工作原理和anyMatch类似,但它会看看流中的元素是否都能匹配给定的谓词。
boolean allMatch(Predicate<? super T> predicate);
boolean isHealthy = menu.stream().allMatch(d -> d.getCalories() < 1000);
和allMatch相对的是noneMatch。它可以确保流中没有任何元素与给定的谓词匹配。
boolean noneMatch(Predicate<? super T> predicate);
boolean isHealthy = menu.stream().noneMatch(d -> d.getCalories() >= 1000);
anyMatch、allMatch和noneMatch这三个操作都用到了我们所谓的短路,这就是大家熟悉的Java中&&
和||
运算符短路在流中的版本。
短路操作:
allMatch、anyMatch、noneMatch、findFirst、findAny、limit
5.3.3 查找元素(findAny)
findAny方法将返回当前流中的任意元素。
Optional<T> findAny();
Optional<Dish> dish =menu.stream().filter(Dish::isVegetarian).findAny();
5.3.4 查找第一个元素(findFirst)
Optional<T> findFirst();
List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> firstSquareDivisibleByThree =someNumbers.stream().map(x -> x * x).filter(x -> x % 3 == 0).findFirst(); // 9
为什么会同时有findFirst和findAny呢?
答案是并行。找到第一个元素在并行上限制更多。如果你不关心返回的元素是哪个,请使用findAny,因为它在使用并行流时限制较少。
5.4 归约
“计算菜单中的总卡路里”或“菜单中卡路里最高的菜是哪一个”。此类查询需要将流中所有元素反复结合起来,得到一个值,比如一个Integer。这样的查询可以被归类为归约操作(将流归约成一个值)。用函数式编程语言的术语来说,这称为折叠(fold),因为你可以将这个操作看成把一张长长的纸(你的流)反复折叠成一个小方块,而这就是折叠操作的结果。
5.4.1 元素求和(reduce)
有初始值
T reduce(T identity, BinaryOperator<T> accumulator);
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
int sum = numbers.stream().reduce(0, Integer::sum); ←─可以使用方法引用让这段代码更简洁
reduce操作对这种重复应用的模式做了抽象。
reduce接受两个参数:
一个初始值,这里是0;
一个BinaryOperator来将两个元素结合起来产生一个新值,这里我们用的是lambda (a, b) -> a +b,Lambda反复结合每个元素,直到流被归约成一个值。
无初始值(reduce)
Optional<T> reduce(BinaryOperator<T> accumulator);
Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));
为什么它返回一个Optional呢?
考虑流中没有任何元素的情况。reduce操作无法返回其和,因为它没有初始值。这就是为什么结果被包裹在一个Optional对象里,以表明和可能不存在。
5.4.2 最大值和最小值(reduce)
用归约计算最大值和最小值:
Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> min = numbers.stream().reduce(Integer::min);
Optional<Integer> min = numbers.stream().reduce((x, y) -> x < y ? x : y);
5.4.3 统计个数(reduce、count)
int count = menu.stream().map(d -> 1).reduce(0, (a, b) -> a + b);
long count = menu.stream().count();
归约方法的优势与并行化
reduce的好处在于,这里的迭代被内部迭代抽象掉了,这让内部实现得以选择并行执行reduce操作。而外部迭代式求和例子要更新共享变量sum,这不是那么容易并行化的。如果你加入了同步,很可能会发现线程竞争抵消了并行本应带来的性能提升!这种计算的并行化需要另一种办法:将输入分块,分块求和,最后再合并起来
可变的累加器模式对于并行化来说是死路一条 !!!
流操作:无状态和有状态
诸如map或filter等操作会从输入流中获取每一个元素,并在输出流中得到0或1个结果。这些操作一般都是无状态的:它们没有内部状态。
但诸如reduce、sum、max等操作需要内部状态来累积结果。在上面的情况下,内部状态很小。在我们的例子里就是一个int或double。不管流中有多少元素要处理,内部状态都是有界的。相反,诸如sort或distinct等操作一开始都和filter和map差不多——都是接受一个流,再生成一个流(中间操作),但有一个关键的区别。从流中排序和删除重复项时都需要知道先前的历史。例如,排序要求所有元素都放入缓冲区后才能给输出流加入一个项目,这一操作的存储要求是无界的。要是流比较大或是无限的,就可能会有问题(把质数流倒序会做什么呢?它应当返回最大的质数,但数学告诉我们它不存在)。我们把这些操作叫作有状态操作。
5.5 付诸实践
代码清单5-1 找出2011年的所有交易并按交易额排序(从低到高)
代码清单5-1 找出2011年的所有交易并按交易额排序(从低到高)List<Transaction> tr2011 =transactions.stream().filter(transaction -> transaction.getYear() == 2011) ←─给filter传递一个谓词来选择2011年的交易.sorted(comparing(Transaction::getValue)) ←─按照交易额进行排序.collect(toList()); ←─将生成的Stream中的所有元素收集到一个List中
代码清单5-2 交易员都在哪些不同的城市工作过
代码清单5-2 交易员都在哪些不同的城市工作过List<String> cities =transactions.stream().map(transaction -> transaction.getTrader().getCity()) ←─提取与交易相关的每位交易员的所在城市.distinct() ←─只选择互不相同的城市.collect(toList());Set<String> cities =transactions.stream().map(transaction -> transaction.getTrader().getCity()).collect(toSet());
代码清单5-3 查找所有来自于剑桥的交易员,并按姓名排序
代码清单5-3 查找所有来自于剑桥的交易员,并按姓名排序List<Trader> traders =transactions.stream().map(Transaction::getTrader) ←─从交易中提取所有交易员.filter(trader -> trader.getCity().equals("Cambridge")) ←─仅选择位于剑桥的交易员.distinct() ←─确保没有任何重复.sorted(comparing(Trader::getName)) ←─对生成的交易员流按照姓名进行排序.collect(toList());
代码清单5-4 返回所有交易员的姓名字符串,按字母顺序排序
代码清单5-4 返回所有交易员的姓名字符串,按字母顺序排序
String traderStr =transactions.stream().map(transaction -> transaction.getTrader().getName()) ←─提取所有交易员姓名,生成一个Strings构成的Stream.distinct() ←─只选择不相同的姓名.sorted() ←─对姓名按字母顺序排序.reduce("", (n1, n2) -> n1 + n2); ←─逐个拼接每个名字,得到一个将所有名字连接起来的String以上解决方案效率不高(所有字符串都被反复连接,每次迭代的时候都要建立一个新的String对象)。String traderStr =transactions.stream().map(transaction -> transaction.getTrader().getName()).distinct().sorted().collect(joining()); ←─使用joining(其内部会用到StringBuilder)
代码清单5-5 有没有交易员是在米兰工作的
代码清单5-5 有没有交易员是在米兰工作的
boolean milanBased =transactions.stream().anyMatch(transaction -> transaction.getTrader().getCity().equals("Milan")); ←─把一个谓词传递给anyMatch,检查是否有交易员在米兰工作
代码清单5-6 打印生活在剑桥的交易员的所有交易额
代码清单5-6 打印生活在剑桥的交易员的所有交易额
transactions.stream().filter(t -> "Cambridge".equals(t.getTrader().getCity())) ←─选择住在剑桥的交易员所进行的交易.map(Transaction::getValue) ←─提取这些交易的交易额.forEach(System.out::println); ←─打印每个值
代码清单5-7 所有交易中,最高的交易额是多少
代码清单5-7 所有交易中,最高的交易额是多少
Optional<Integer> highestValue =transactions.stream().map(Transaction::getValue) ←─提取每项交易的交易额.reduce(Integer::max); ←─计算生成的流中的最大值
代码清单5-8 找到交易额最小的交易
代码清单5-8 找到交易额最小的交易
Optional<Transaction> smallestTransaction =transactions.stream().reduce((t1, t2) -> t1.getValue() < t2.getValue() ? t1 : t2); ←─通过反复比较每个交易的交易额,找出最小的交易Optional<Transaction> smallestTransaction =transactions.stream().min(comparing(Transaction::getValue));
5.6 数值流
引入背景:
nt calories = menu.stream().map(Dish::getCalories).reduce(0, Integer::sum);
这段代码的问题是,它有一个暗含的装箱成本。每个Integer都必须拆箱成一个原始类型,再进行求和
5.6.1 原始类型流特化(mapToInt、mapToDouble、mapToLong)
Java 8引入了三个原始类型特化流接口来解决这个问题:IntStream、DoubleStream和LongStream,分别将流中的元素特化为int、long和double,从而避免了暗含的装箱成本。这些特化的原因并不在于流的复杂性,而是装箱造成的复杂性——即类似int和Integer之间的效率差异。
1. 映射到数值流
将流转换为特化版本的常用方法是mapToInt、mapToDouble和mapToLong。
int calories = menu.stream() ←─返回一个Stream<Dish>.mapToInt(Dish::getCalories) ←─返回一个IntStream.sum();请注意,如果流是空的,sum默认返回0。IntStream还支持其他的方便方法,如max、min、average等。
2. 转换回对象流
一旦有了数值流,你可能会想把它转换回非特化流
IntStream intStream = menu.stream().mapToInt(Dish::getCalories); ←─将Stream 转换为数值流
Stream<Integer> stream = intStream.boxed(); ←─将数值流转换为Stream
3. 默认值OptionalInt
对于三种原始流特化,也分别有一个Optional原始类型特化版本:OptionalInt、OptionalDouble和OptionalLong。
OptionalInt maxCalories = menu.stream().mapToInt(Dish::getCalories).max();nt max = maxCalories.orElse(1); ←─如果没有最大值的话,显式提供一个默认最大值
5.6.2 数值范围(rangeClosed,range)
IntStream evenNumbers = IntStream.rangeClosed(1, 100) ←─表示范围[1, 100].filter(n -> n % 2 == 0); ←─一个从1到100的偶数流System.out.println(evenNumbers.count()); ←─从1 到100 有50个偶数IntStream evenNumbers = IntStream.range(1, 100) ←─表示范围[1, 100).filter(n -> n % 2 == 0); System.out.println(evenNumbers.count()); ←─从1 到99 有49个偶数
5.6.3 数值流应用:勾股数
Stream<int[]> pythagoreanTriples =IntStream.rangeClosed(1, 100).boxed().flatMap(a -> IntStream.rangeClosed(a, 100).filter(b -> Math.sqrt(a*a + b*b) % 1 == 0).mapToObj(b ->new int[]{a, b, (int)Math.sqrt(a * a + b * b)}));Stream<double[]> pythagoreanTriples2 =IntStream.rangeClosed(1, 100).boxed().flatMap(a -> IntStream.rangeClosed(a, 100).mapToObj(b -> new double[]{a, b, Math.sqrt(a*a + b*b)}) ←─产生三元数.filter(t -> t[2] % 1 == 0)); ←─元组中的第三个元素必须是整数pythagoreanTriples.limit(5).forEach(t ->System.out.println(t[0] + ", " + t[1] + ", " + t[2]));
5.7 构建流
5.7.1 由值创建流(Stream.of)
Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);
Stream<String> emptyStream = Stream.empty(); ←─使用empty得到一个空流
5.7.2 由数组创建流(Arrays.stream)
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum(); ←─总和是4
5.7.3 由文件生成流(Files.lines)
long uniqueWords = 0;
try (Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset())) { ←─流会自动关闭uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" "))) ←─生成单词流.distinct() ←─删除重复项.count(); ←─数一数有多少各不相同的单词
} catch (IOException e) {←─如果打开文件时出现异常则加以处理
}
5.7.4 由函数生成流:创建无限流(Stream.iterate、Stream.generate)
1. 迭代
iterate方法生成了一个所有正偶数的流
Stream<Integer> stream = Stream.iterate(0, n -> n + 2).limit(10);
stream.forEach(System.out::println);
斐波纳契元组序列
Stream<int[]> stream = Stream.iterate(new int[]{0, 1}, t -> new int[]{t[1], t[0] + t[1]}).limit(20);
stream.forEach(t -> System.out.println("(" + t[0] + "," + t[1] + ")"));
2. 生成
生成一个流,其中有五个0到1之间的随机双精度数
Stream<Double> stream = Stream.generate(Math::random).limit(5);
stream.forEach(System.out::println);
生成一个全是1的无限流
IntStream ones = IntStream.generate(() -> 1);
5.7.5 由集合创建流(list.stream())
list.stream()
-----------------------------------------------------------------------------读书笔记摘自 书名:Java 8实战 作者:[英] Raoul-Gabriel Urma [意] Mario Fusco [英] Alan M