读后笔记 -- Java核心技术(第11版 卷 II ) Chapter1 Java 8 的流库
1.1 从迭代到流的操作
- 迭代:for, while
- 流:stream()。优点:1)代码易读;2)性能优化
var contents = new Files.readString(Paths.get("./gutenberg/alice30.txt")); // contents 是 String 类型 List<String> words = List.of(contents.split("\\PL+")); long count = 0; // 方式一:for 循环过滤、统计 for (String w : words) { if (w.length() > 12) count++; } System.out.println(count); // 方式二:流处理。三个阶段的操作管道 // 1. 创建一个流:stream()/parallelStream() => 2. 指定将初始流转换为其他流的中间操作,可能包含多步。 如 filter() => 3. 应用终止操作,从而产生结果。如 count() count = words.stream().filter(w -> w.length() > 12).count(); System.out.println(count); // 方式三:流以并行方式处理。更多 并行流的信息,看 1.14 并行流 count = words.parallelStream().filter(w -> w.length() > 12).count(); System.out.println(count);
流 和 集合的差异:
- 1) 流不存储其元素;
- 2) 流的操作不会修改其数据源;
- 3) 流的操作是尽可能惰性执行的;
- 4) 流使用完后,将不再可用。如果需要,再次创建;
1.2 流的创建
- stream(): turn any collection into a stream
- stream.of(): 1) turn any array of objects into a stream, Stream.of(array); 2) 参数为值序列,如下面的例子
- Arrays.stream(array, from, to):将数组的一部分转成流
- stream.empty(): makes an empty stream
- stream.generate(): 创建无限流的方式一
- stream.iterate(): 创建无限流的方式二
- splitAsStream(): breaking a string into a stream of tokens
// 1. collection.stream(): 将集合转流,参见 section 1.1 的用例
// 2. 静态方法 Stream.of() 构建流,
// 2.1 参数为值序列 Stream<String> song = Stream.of("gently", "down", "the", "stream"); // 2.2 创建不包含任何元素的流 Stream<String> silence = Stream.empty();
// 3. 将数组的一部分转为流
String[] array = {"gently", "down", "the", "stream"};
Steam<String> subArray = Arrays.stream(arry, 1, 3); // 4.1 创建无限流方式一:generate():获取常量值的流 Stream<String> echos = Stream.generate(() -> "Echo"); // 创建无限流:获取随机数的流 Stream<Double> randoms = Stream.generate(Math::random); // 4.2 创建无限流方式二:iterate() 产生一个序列。一个“种子”值+一个函数,并会反复地将该函数应用到之前的结果上。第一个元素 BigInteger.ONE 是种子,第二个元素是 f(seed),即 1,下一个元素是 f(f(seed)),即 2 Stream<BigInteger> integers = Stream.iterate(BigInteger.ZERO, n-> n.add(BigInteger.ONE));
// 4.3 创建有限流:
var limit = new BigInteger("8");
Stream<BigInteger> integers2 = Stream.iterate(BigInteger.ZERO, n-> n.compareTo(limit) < 0, n -> n.add(BigInteger.ONE)); // 5. splitAsStream() 会按照某个正则表达式来分割一个 CharSequence 对象 Stream<String> wordsAnotherWay = Pattern.compile("\\PL+").splitAsStream(contents); // 6. 静态方法 Files.lines() 会返回一个包含了文件中所有行的 Stream. Use inside try-with-resources to make sure file is closed. try (Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) { show("lines", lines); }
// 将 Iterable<T> 转换成 Stream<T> Iterable<Path> iterable = FileSystems.getDefault().getRootDirectories(); // 如果持有的 Iterable 对象不是集合,可以通过下面的调用将其转换为一个流 Stream<Path> rootDirectories = StreamSupport.stream(iterable.spliterator(), false);
1.3 流转换为其他的流:filter、map 和 flatMap 方法
- filter():接受一个谓词,引元是 Predicate<T>,即从 T 到 boolean 的函数。过滤并产生一个新的流;
- map() :流作为输入,参数是可以是 方法 或 lambda 表达式。将 map() 的函数映射到流的每一个元素;
- 如果流中有一个空串,结果可能有异常。所以,推荐结合 filter 一起使用,即: stream.filter().map()
- flatMap() :1)扁平化处理流;2)to compose Optional-valued functions.
List<String> words = List.of("Paul is a good boy", "Amy is a girl", "Lili is a teacher in Fudan University", "", "Sam is a manager"); // 1. filter 的引元是 Predicate<T>,即从 T 到 boolean 的函数。过滤并产生一个新的流 Stream<String> longWords = words.stream().filter(w -> w.length() > 16); // longWords.getClass().getName(): java.util.stream.ReferencePipeline$2 System.out.println(longWords.collect(Collectors.toList())); // [Paul is a good boy, Lili is a teacher in Fudan University] // 2.1 map() 实现转换流中的值: // 2.1.1 带有方法引用的 map 来实现 Stream<String> lowercaseWords = words.stream().map(String::toLowerCase); // [paul is a good boy, amy is a girl, lili is a teacher in fudan university, sam is a manager] // 2.1.2 通过 map() 里的方法使用 lambda 表达式来代替。加上 filter 过滤可以解决字符串有空串,否则可能产生异常 Stream<String> firstLetters = words.stream().filter(w -> w.length() > 0).map(s -> s.substring(0, 1)); // [P, A, L, S] // 2.2 使用 map 时,会有一个函数应用到每个元素上,并且其结果是包含了应用该函数后所产生的所有结果的流 // map: 获得一个流的 list // Stream<Stream<String>> result = words.stream().map(w -> codePoints(w)); // 对应的 lambda 表达式写法为 Stream<Stream<String>> result = words.stream().map(GenerateNewStream::codePoints); // 2.3 2 层列表流的打印。另外,从result 的类型定义也可以看出,Stream<Stream<String>> 两层定义 result.forEach(e-> System.out.print(e.collect(Collectors.toList()) + ", ")); /* [P, a, u, l, , i, s, , a, , g, o, o, d, , b, o, y], [A, m, y, , i, s, , a, , g, i, r, l], [L, i, l, i, , i, s, , a, , t, e, a, c, h, e, r, , i, n, , F, u, d, a, n, ,\
U, n, i, v, e, r, s, i, t, y], [], [S, a, m, , i, s, , a, , m, a, n, a, g, e, r] */ // 3. flatMap: 将流 list 摊平成单个流 // Stream<String> flatResult = words.stream().flatMap(w -> codePoints(w)); // 对应的 lambda 表达式写法为 Stream<String> flatResult = words.stream().flatMap(GenerateNewStream::codePoints); // [P, a, u, l, , i, s, , a, , g, o, o, d, , b, o, y, A, m, y, , i, s, , a, , g, i, r, l, L, i, l, i, , i, s, , a, , t, e, a, c, h, e, r, , i, n, , F, u, d, a, n, ,\
U, n, i, v, e, r, s, i, t, y, S, a, m, , i, s, , a, , m, a, n, a, g, e, r]// 将字符串转换成字符串流 public static Stream<String> codePoints(String s) { var result = new ArrayList<String>(); int i = 0; while (i < s.length()) { // 不能直接使用 int j = i +1; 有些字符占2个码元,如 𝕆 (\uD835\uDD46),详细见 Volume I P48 int j = s.offsetByCodePoints(i, 1); result.add(s.substring(i, j)); i = j; } return result.stream(); }
1.4 抽取子流和组合流:
- limit()
- skip()
- dropWhile()
- takeWhile()
- concat()
// 1. limit(n) 裁剪。 1)原流长度 < n,按原流结束; 2)原流长度 >n,裁剪为 n Stream<Double> randoms = Stream.generate(Math::random).limit(10); // randoms.getClass().getName(): java.util.stream.SliceOps$1 // [0.12221378038694952, 0.45301879816627766, 0.08100225598387067, ..., 0.4160284944965318] // 2. skip(n) 丢弃前 n 个元素 Stream<String> words2 = Stream.of(contents.split("\\PL+")).skip(1).limit(10); // contents 变量源于 section 1.1 的用例 // [This, is, the, Project, Gutenberg, Etext, of, Alice, in, Wonderland] // 3. dropWhile(): 条件为真时丢弃元素,并产生一个由第一个使该条件为假的字符开始的元素构成的流 String str = "2023Hello0120Java"; Stream<String> withoutInitialDigits = codePoints(str).dropWhile("0123456789"::contains); // [H, e, l, l, o, 0, 1, 2, 0, J, a, v, a] // 4. takeWhile(): 获取所有字符为真的元素,直到条件为假时 Stream<String> initialDigits = codePoints(str).takeWhile("0123456789"::contains); // codePoints() 见 1.3 示例 // [2, 0, 2, 3] // 5. 静态方法 concat(): 将两个流连接起来 Stream<String> combined = Stream.concat(codePoints("Hello"), codePoints("World")); // [H, e, l, l, o, W, o, r, l, d]
1.5 其他的流转换:
- distinct():去除重复项
- sorted():必须要传入一个比较器;
- peek():yields a stream with the same elements at the original, calling a funciton on each retrieved element;
// distinct(): 剔除重复元素 Stream<String> uniqueWords = Stream.of("merrily", "merrily", "merrily", "gently").distinct(); // [merrily, gently] // sorted():对流进行排序 var contents = Files.readString(Paths.get("./gutenberg/alice30.txt")); List<String> words = List.of(contents.split("\\PL+")); Stream<String> longestFirst = words.stream().limit(10).sorted(Comparator.comparing(String::length).reversed()); // [Gutenberg, Project, Etext, Alice, This, the, is, of, in, ] // peek(): 产生一个流,元素与原流中的元素相同,但在每次获取一个元素时,都会调用一个函数。调试很方便。 Object[] powers = Stream.iterate(1.0, p -> p * 2).peek(e -> System.out.println("Fetching " + e)).limit(5).toArray(); System.out.println(Arrays.toString(powers)); /* Fetching 1.0 Fetching 2.0 Fetching 4.0 Fetching 8.0 Fetching 16.0 [1.0, 2.0, 4.0, 8.0, 16.0] */
1.6 简单约简 (simple reductions):从流到单个值
- count()
- max()、min()、findFirst()、findAny():返回一个 Optioanl<T> 类型
- anyMatch()
var contents = Files.readString(Paths.get("./gutenberg/alice30.txt")); List<String> words = List.of(contents.split("\\PL+")); // 1. count():统计元素数量 System.out.println("words count: " + words.stream().count()); // 推荐使用 (long) words.size() // words count: 29075
// 2. max(): 返回最大值 Optional<String> largest = words.stream().max(String::compareToIgnoreCase);
System.out.println("largest: " + largest);
// largest: Optinal[zip] System.out.println("largest: " + largest.orElse("")); // largest: zip
// 4. findFirst(): 返回非空集合中的第一个值,通常与 filter() 组合使用 Optional<String> startWithQFF = words.stream().filter(s -> s.startsWith("Q")).findFirst();// Optional[Quick]
// 6. anyMatch() + parallelStream() 并行流 boolean aWordStartsWithQ = words.parallelStream().anyMatch(s -> s.startsWith("Q"));// true
1.7 Optional 类型
Optional<T> 对象是一种包装器对象,被当作更安全的方式,来替换 类型 T 的引用,这种引用的结果:1)引用某个对象;2) null。 但仅在正确使用的情形下才会更安全。
1.7.1 获取 Optional 值 Grab the data
有效使用 Optional 策略一:值存在时使用该值,不存在时使用默认值或异常
- orElse(defaultValue):使用默认值
- orElseGet(() -> ...) :使用代码计算默认值
- orElseThrow( SomeException::new) :抛出异常
- orElseThrow(); // same as get, throws NoSuchElementException, Java 10
String result = optionalString.orElse(""); String result = optionalString.orElseGet(() -> System.getProperty("user.dir")); String result = optionalString.orElseThrow(IllegalStateException::new);
1.7.2 消费 Optional 值 If/then/else with lambdas
有效使用 Optional 策略二:值只有在存在的情况下才消费
- ifPresent(value -> ...) : 值存在时处理,否则不做任何动作
- ifPresentOrElse(value -> ..., () -> ...) // Java 9
// 1. 值存在时传递给函数,否则不做任何动作 optionalValue.ifPresent(v -> Process v); // 2. 仅值存在时添加到某个集中 optionalValue.ifPresent(v -> results.add(v)); // optionalValue.ifPresent(results::add); // 3. 值存在时执行一种操作,不存在时执行另一操作 optionalValue.ifPresentOrElse( v -> System.out.println("Found " + v), () -> logger.warning("No match"));
1.7.3 管道化 Optional 值
有效使用 Optional 策略三:保持 Optional 完整,使用 map() 来转换 Optional 内部的值 Transform/substitute:
anotherOpt = opt.filter(value -> ...).map( value -> ...).or(() -> ...);
// 1. 转换值,如 optionalString 为空,则 transformed 也为空 Optional<String> transformed = optionalString.map(String::toUpperCase); // 2. 将结果添加到列表,如值为空,则什么也没处理 optionalValue.map(results::add); // 3. or() 将 空 Optional 进行 替换。下例中,不为空时为 optionalString,为空时为 lambda 表达式的计算结果 Optional<String> result = optionalString.or(() -> alternatives.stream().findFirst());
示例:
Optional<String> startWithQ = words.stream().filter(s -> s.startsWith("QQ")).findFirst().or(
() -> words.stream.filter(s -> s.startsWith("Q")).findFirst());
1.7.4 不适合使用 Optional 值的方式
// 1. 不能在 Optional 上调用 get 方法。下面的 get() : 1) 在值存在时获取包装的元素,2) 不存在时将抛出 NoSuchElementException 异常 Optional<T> optionalValue = ...; optionalValue.get().someMethod(); 其并不比下面的安全 T value = ...; value.someMethod(); // 2. ifPresent() 会报告某个 Optional<T> 对象是否具有值,但 if (optionalValue.isPresent()) optionalValue.get().someMethod(); 并不比下面的容易处理 if (value != null) { value.someMethod();}
总结,Optional 类型正确使用的提示:
- 1. Optional 类型的变量永远不为 null;
- 2. 不要使用 Optional 类型的域。其代价是多出一个对象。在类的内部,使用 null 表示缺失的域更易操作;
- 3. 不要在集合中放置 Optional 对象,且不要将它们用作 map 的键。应直接 收集 其中的值
- Don't create an Optional to make a null check: Optional.ofNullable(result).ifPresentOrElse(...); // Way too complex
较好的方式:
- 1. 选择一个替代值;
- 2. 只消费确实存在的值
1.7.5 创建 Optional 值
- Optional.of()
- Optional.empty()
public static Optional<Double> inverse(Double x) { return x == 0 ? Optional.empty() : Optional.of( 1 / x); }
1.7.6 用 flatMap 构建 Optional 值的函数
假设: f() 可以产生 Optional<T> 对象,且 类型 T 有一个可以产生 Optional<U> 对象的方法 g
1. s.f().g(); // error: 无法将它们组合起来,因为 s.f() 的类型是 Optional<T>,不是 T
2. Optional<U> result = s.f().flatMap(T::g); // correct,如果 s.f() 值存在, g 就可以应用到上面;否则 返回一个空 Optional<U>
=> Three possibilites:
1). If s.f() is Optional.of(t) and t.g() is Optional(r), that's the result.
2). If s.f() is Optional.of(t) and t.g() is empty, the result is empty.
3). If s.f() is empty, the result is empty.
Similar to flatMap on streams if you think of an Optional as a stream of size zero or one.
如果有更多产生 Optional 值的方法或 lambda 表达式,那么可以通过 flatMap 将它们链接起来,从而构建由这些步骤构成的管道,只有所有步骤都成功,该管道才会成功。
public static Optional<Double> inverse(Double x) { return x == 0 ? Optional.empty() : Optional.of(1 / x); } public static Optional<Double> squareRoot(Double x) { return x < 0 ? Optional.empty() : Optional.of(Math.sqrt(x)); } // 调用方式1:用 flatMap 构建 Optional 的值的函数。这里的 flatMap() 方式调用的是 Optional 类的方法 Optional<Double> result = inverse(x).flatMap(Demo::squareRoot); // Demo 是 squareRoot 方法所在类的类名 // 调用方式2:flatMap() 组合多个方法 Optional<Double> result = Optional.of(-4).flatMap(Demo::inverse).flatMap(Demo::squareRoot);
1.7.7 将 Optional 转换为流
stream() 会将一个 Optional<T> 对象转换为 0 个或 1 个元素的 Stream<T> 对象。
// 应用: 假设有一个用户 ids 流 和 如下方法: Stream<String> ids = ...;
Optional<User> lookup(String id) // 方式1: 下面方法会过滤无效 ID,但要慎用 isPresent() 和 get() Stream<User> users = ids.map(Users::lookup) .filter(Optional::isPresent) .map(Optional::get); // 方式2: 使用 flatMap() 更好地处理。Use Stream.flatMap to drop empty results。 // 1) ids.map(User:lookup) 会得到一个 Optional<T> 的一个流,实际结果可能是一个包含实际结果的 Optional,也可能是 空 Optional // 2) 对 Optional::stream 调用 flatMap,所有空值将会被丢弃,只留下非空的值 Stream<User> users = ids.map(Users::lookup) .flatMap(Optional::stream); // 这里的 flatMap() 是调用 Stream 接口的方法
1.8 收集结果:将流结果放入集合
- iterator():老式迭代器
- forEach() / forEachOrdered()
- toArray():放入数组,将返回 Object[];如果需要正确的类型,则需要传入数组构造器
- collect(): passes stream elements to a Collector. Collectors has many factory methods for collectors.
1. iterator() 收集结果
Iterator<Integer> iter = Stream.iterate(0, n -> n + 1).limit(10).iterator(); while (iter.hasNext()) { System.out.print(iter.next() + ", "); }
2. forEach() 输出
stream.forEach(System.out::println);
3. toArray() 获得由流的元素构成的数组
// 3.1 stream.toArray() 创建数组,无参数,默认是 Object[] Object[] numbers = Stream.iterate(0, n -> n + 1).limit(10).toArray();
// 3.2 创建正确类型的数组,可以将 类型传递到数组构造器中, stream.toArray(Integer[]::new) Integer[] numbers3 = Stream.iterate(0, n -> n + 1) .limit(10) .toArray(Integer[]::new);
4. toCollect() 收集流到另一个目标中
List<String> result = stream.collect(Collectors.toList()); // 4.1 转换到 List Set<String> result = stream.collect(Collectors.toSet()); // 4.2 转换到 HashSet TreeSet<String> result = stream.collect(Collectors.toCollection(TreeSet::new)); // 4.3 控制获得的集的种类 String result = stream.collect(Collectors.joining()); // 4.4 通过连接操作来收集流中的所有字符串 String result = stream.collect(Collectors.joining(", ")); // 4.5 元素间增加分隔符,可以将分隔符传递给 joining()
String result = stream.map(Object::toString).collect(Collectors.joining(", ")); // 4.6 如果流中包含了字符串以外的对象,需要先将其转换为字符串
5. 约简流的结果
// 5. 将流的结果约简为总和、数量、平均值、最大值或最小值,可以使用 summarizing(Int|Long|Double) 方法中的某一个 IntSummaryStatistics summary = stream.collect(Collectors.summarizingInt(String::length)); double averageWordLength = summary.getAverage(); double maxWordLength = summary.getMax();
1.9 收集到映射表中:流结果放入映射
1. Collectors.toMap() 有两个函数引元,它们用来产生映射表的键和值。
// 1.1 第二个函数是值 Map<Integer, String> idToName = people.collect( Collectors.toMap(Person::getId, Person::getName)); // 2. 第二个函数是实际的元素,使用 Function.identity() Map<Integer, Person> idToPerson = people.collect( Collectors.toMap(Person::getId, Function.identity()));
2. 引入第三个函数,解决键冲突问题
// 构建一个映射表,存储了所有可用 locale 中的语言,其中,每种语言在默认 locale 中的名字(如 "German")为键,而本地化的名字(如 "Deutsch")为值 // 我们不关心同一种语言是否可能出现2次(如 德国和瑞士都使用德语),因此只记录第一项 Stream<Locale> locales = Stream.of(Locale.getAvailableLocales()); Map<String, String> languageNames = locales.collect( Collectors.toMap( Locale::getDisplayLanguage, l -> l.getDisplayLanguage(l), (existingValue, newValue) -> existingValue)); // 引入第三个函数解决键冲突
3. 要获得 Map<String, Set<String>> 这样的情形,如一个国家相关的语言集
// 如果需要了解给定国家的所有语言,就需要一个 Map<String, Set<String>>,如 Switzerland 的值集 [French, German, Italian](Better solution in section 1.10) // 1) 首先,为每种语言都存储一个单例集; // 2) 对已有集和新集进行并操作; locales = Stream.of(Locale.getAvailableLocales()); Map<String, Set<String>> countryLanguageSets = locales.collect( Collectors.toMap( Locale::getDisplayCountry, l -> Set.of(l.getDisplayLanguage()), // 单列集 (a, b) -> { // 已有集和新集进行合并操作 Set<String> union = new HashSet<>(a); union.addAll(b); return union; }));
4. 如果要获得 TreeMap,则需要引入第四个引元(构造器)
Map<Integer, Person> idToPerson = people().collect( Collectors.toMap( Person::getId, // 1st Function.identity(), // 2nd (existingValue, newValue) -> {throw new IllegalStateException();}, // 3rd TreeMap::new)); // 4th
1.10 群组和分区
- groupingBy():produces a list for a key(缺点:只能为所有键生成一个列表。如果不需要列表,则 需要使用到 1.11 的下游收集器处理)
- partitioningBy()
// 针对 1.9 第3个应用,需要为每个映射表的值都生成单例集,然后指定将现有值与新值合并。 // => (Better Solution) groupingBy:将具有相同特性的值聚集成组 Stream<Locale> locales = Stream.of(Locale.getAvailableLocales()); // section: 1.10 群组和分区 // 1.1 将具有相同特性的值群聚成组,可用 groupingBy() 来实现。查看 groupingBy() 的返回类型,可以得出前面接受变量的类型定义 Map<String, List<Locale>> countryToLocales = locales.collect( Collectors.groupingBy( Locale::getCountry)); // {..., AF=[ps_AF_#Arab, uz_AF, uz_AF_#Arab, fa_AF, ps_AF], ...}
// 1.2. 查找指定国家对应的所有地点
List<Locale> swissLocales = countryToLocales.get("CH");
// swissLocales: [wae_CH_#Latn, de_CH, pt_CH, rm_CH_#Latn, gsw_CH, fr_CH, rm_CH, it_CH, wae_CH, en_CH, gsw_CH_#Latn]
// 2. 当分类函数是断言函数(即返回 boolean 值的函数),流的元素可分为两个列表:该函数返回 true 的元素和其他元素。此时,使用 partitioningBy() 比 groupingBy() 更高效 Map<Boolean, List<Locale>> englishAndOtherLocales = locales.collect( Collectors.partitioningBy(l -> l.getLanguage().equals("en"))); List<Locale> englishLocales = englishAndOtherLocales.get(true);
1.11 下游收集器
groupingBy()/partitioningBy() 会产生一个映射表,它的每个值都是一个列表。使用 ”下游收集器“可以处理这些列表
Use static import of java.util.stream.Collectors.* to make the expressions easier to read.
// 1. toSet():将 groupingBy() 的列表,转换成 set。
// 这里的 groupingBy() 方法和 section 1.10 的 groupingBy() 不是一个方法 Map<String, Set<Locale>> countryToLocaleSet = locales.collect( groupingBy(Locale::getCountry, toSet())); // 2. Java 提供了多种可将收集到的元素约简为数字的收集器: // 2.1 counting():对每个国家有多少个 locale 进行计数 Map<String, Long> countryToLocaleCounts = locales.collect( groupingBy(Locale::getCountry, counting())); // 2.2 summing(Int|Long|Double) 会接受一个函数作为引元,将该函数应用到下游元素中,并产生它们的和 Map<String, Integer> stateToCityPopulation = cities.collect( // 计算城市流中每个州的人口总和 groupingBy(City::getState, summingInt(City::getPopulation))); // 2.3 maxBy() 和 minBy() 会接受一个比较器,并分别产生下游元素中的最大值和最小值 Map<String, Optional<String>> stateToLongestCityName = cities.collect( // 计算每个州中名称最长的城市 groupingBy(City::getState, mapping(City::getName, maxBy(Comparator.comparing(String::length))))); // 3. collectingAndThen 收集器会在收集器后面添加一个最终处理步骤 Map<Character, Integer> stringCountsByStartingLetter = strings.collect( groupingBy(s -> s.charAt(0), collectingAndThen(toSet(), Set::size))); // 4. mapping 收集器正好相反,它会将函数应用到收集到的每个元素,并将结果传递到下游收集器
Stream<String> strings = Stream.of("Alan", "Heber", "Helen", "Alex", "Bob");
Map<String, Set<Integer>> stringLengthsByStartingLetter = strings.collect( groupingBy(s -> s.charAt(0), mapping(String::length, toSet()))); // 5. 收集某国所有的语言到一个集中的更佳方案 Map<String, Set<String>> countryToLanguages = locales.collect( groupingBy(Locale::getDisplayCountry, mapping(Locale::getDisplayLanguage, toSet()))); // 6. 如果群组和映射函数的返回值为 int, long 或 double,那么就可以将元素收集到汇总统计对象中,如通过 summarizingInt():这是个2层字典,可以通过 get() 逐步调用想要的值 Map<String, IntSummaryStatistics> stateToCityPopulationSummary = cities.collect( groupingBy(City::getState, summarizingInt(City::getPopulation))); // 7. filtering 收集器将一个过滤器应用到每个组上. // Java 9 Map<String, Set<City>> largestCitiesByState = cities.collect( groupingBy(City::getState, filtering(c -> c.getPopulation() > 500000, toSet())));
1.12 约简操作
reduce() 是一种从流中计算某个值的通用机制, 其简单的形式将接受一个二元函数,并从前两个元素开始应用它。
// 应用1:统计列表的和
// 分析:将列表转换为流,然后使用 reduce()
List<Integer> values = List.of(1, 10, 15, 60, 90); // 方案一: 定义 Optional<T>,防止流为空。 reduce() 返回一个 Optional<T> Optional<Integer> sum = values.stream().reduce(Integer::sum); System.out.println("sum: " + sum); // sum.get() 可以获取值 176 // sum: Optional[176] // 方案二: 加入玄元值 e 后,就不需要处理 Optional 类了 Integer sum2 = values.stream().reduce(0, Integer::sum); System.out.println("sum2: " + sum2); // sum2: 176
// 应用 2. 统计字符串流的长度: // 分析:有两种类型: 流的元素具有 String 类型,而累积结果是 Integer /* 方式一: 1)函数一:需要“累积器”函数 (total, word) -> total + word.length(),该函数被反复调用,产生累积的总和; 2)函数二:需要将结果合并 */ Stream<String> names = Stream.of("Alan", "Heber", "Helen", "Alex", "Bob"); int result = names.reduce(0, (total, word) -> total + word.length(), Integer::sum); System.out.println("result: " + result); // result: 21 // 方式二: (better)映射为数字流并计算总和,更为方便 names = Stream.of("Alan", "Heber", "Helen", "Alex", "Bob"); int result2 = names.mapToInt(String::length).sum(); System.out.println("result2: " + result2); // result2: 21
1.13 基本类型流
针对基本类型 如 int 都是通过 Stream<Integer> 这样的包装器操作,低效。流库中有专门的类型来处理这些基本类型:
- IntStream : int, short, char, byte, boolean
- DoubleStream: double, float
- LongStream: long
import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.util.Arrays; import java.util.stream.*; /** * 1. 创建基本类型流的两种方式:例如 IntStream * 1.1 IntStream.of() 和 Arrays.stream(values, from, to) // values is an int[] array * 1.2 静态方法 generate() 和 iterate() * 1.3 静态方法 range() 和 rangeClosed() * 1.4 CharSequence 接口的 CodePoints() 和 chars() * 2. mapToInt, mapToLong, mapToDouble 可以将 对象流 转换成 基本类型流 * 3. boxed() 将 基本类型流 转换为 对象流 */ public class PrimitiveTypeStreams { public static void show(String title, IntStream stream) { final int SIZE = 10; int[] firstElements = stream.limit(SIZE + 1).toArray(); System.out.print(title + ": "); for (int i = 0; i < firstElements.length; i++) { if (i > 0) System.out.print(", "); if (i < SIZE) System.out.print(firstElements[i]); else System.out.print("..."); } System.out.println(); } public static void main(String[] args) throws IOException { // 1. 创建 IntStream // 1.1 IntStream.of() IntStream is11 = IntStream.of(1, 10, 100, 1000); show("is11", is11); // is11: 1, 10, 100, 1000 // 1.2 Arrays.stream(value, from, to). value is an int[] Array. int[] values = {10, 20, 30, 40, 50, 60}; IntStream is12 = Arrays.stream(values, 1, 4); show("is12", is12); // is12: 20, 30, 40 // 1.3 静态方法 generate() 生成 IntStream is1 = IntStream.generate(() -> (int) (Math.random() * 100)); show("is1", is1); // is1: 80, 22, 23, 38, 80, 74, 65, 67, 26, 8, ... // 1.5 静态方法 range() IntStream is2 = IntStream.range(5, 10); show("is2", is2); // is2: 5, 6, 7, 8, 9 // 1.6 静态方法 rangeClosed() IntStream is3 = IntStream.rangeClosed(5, 10); show("is3", is3); // is3: 5, 6, 7, 8, 9, 10 // 1.7 CodePoints() var sentence = "\uD835\uDD46 is the set of octonions."; System.out.println(sentence); // 𝕆 is the set of octonions. IntStream codes = sentence.codePoints(); System.out.println(codes.mapToObj(c -> String.format("%X ", c)).collect(Collectors.joining())); // 1D546 20 69 73 20 74 68 65 20 73 65 74 20 6F 66 20 6F 63 74 6F 6E 69 6F 6E 73 2E Path path = Paths.get("./gutenberg/alice30.txt"); var contents = Files.readString(path); // 2. mapToInt, mapToLong, mapToDouble 可以将 对象流 转换成 基本类型流 // Stream.of() 生成流 -> mapToInt() 将字符串流转换成 基本类型流 IntStream Stream<String> words = Stream.of(contents.split("\\PL+")); IntStream is4 = words.mapToInt(String::length); show("is4", is4); // is4: 0, 4, 2, 3, 7, 9, 5, 2, 5, 2, ... // 3. boxed() 将 基本类型流 转换为 对象流 Stream<Integer> integers = IntStream.range(0, 15).boxed(); System.out.println(integers.collect(Collectors.toList())); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] integers = IntStream.range(0, 15).boxed(); // mapToInt() 将对象流转换为基本类型流 IntStream is5 = integers.mapToInt(Integer::intValue); // IntStream 是一个基本类型流,这里将 对象流 integers 转换为 基本类型流 is5 show("is5", is5); // is5: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... } }
基本类型流 和 对象流的方法类似,但主要差异有:
- toArray() 会返回基本类型数组(int[], long[], double[]);
- 产生可选结果的方法会返回一个 OptionalInt, OptionalLong, OptionalDouble。这些类与 Optional 类相似,但具有 getAsInt, getAsLong, getAsDouble 方法,而不是 get 方法;
- 具有分别返回 sum, average, max, min 方法。 对象流没有这些方法;
- summaryStatistics() 会产生一个类型为 IntSummaryStatistics, LongSummaryStatistics, DoubleSummaryStatistics 对象;
1.14 并行流
并行流的重要要求:操作是无状态的,并且可以任意顺序执行(如数组的相加、相乘可以并行,但相减相除不可以)。
1. Turn any collection or stream into a parallel stream:
Stream<String> parallelWords = words.parallelStream(); // Collection.parallelStream() 从任何集合中获取一个并行流 Stream<String> parallelWords = Stream.of(wordArray).parallel(); // parallel() 将任意的顺序流转换为并行流
示例:
var contents = Files.readString(Paths.get("./gutenberg/alice30.txt")); List<String> wordList = List.of(contents.split("\\PL+")); // Beware of race conditions. 并行时,共享的数组会竞争,导致每次计算的结果可能不一致 var shortWords = new int[10]; wordList.parallelStream().forEach(s -> { if (s.length() < 10) shortWords[s.length()]++; }); System.out.println(Arrays.toString(shortWords)); // [1, 1658, 4114, 6060, 5098, 3203, 1927, 1612, 782, 517] // => Instead, use a stream pipeline. 用长度将字符串分组,然后分别计数。那么,并行计算将是安全的
// 解决方案一:使用管道,groupingBy -> parallelStream() Map<Integer, Long> shortWordCounts = wordList.parallelStream() .filter(s -> s.length() < 10) .collect(groupingBy(String::length, counting())); System.out.println(shortWordCounts); // {0=1, 1=1746, 2=4753, 3=7423, 4=6033, 5=3543, 6=2071, 7=1720, 8=805, 9=536} // 方式二:Collections.groupingByConcurrent() 使用了共享的并发映射表。为了从并行中获益,映射表中的顺序不会与流中的顺序相同
// same result if run again Map<Integer, List<String>> result = wordList.parallelStream().collect( groupingByConcurrent(String::length)); System.out.println(result.get(14)); // [Multiplication, contemptuously, affectionately, contemptuously, disappointment, electronically, electronically, electronically]
// 方式三:使用独立于排序的下游收集器 Map<Integer, Long> wordCounts = wordList.parallelStream().collect( groupingByConcurrent(String::length, counting())); System.out.println(wordCounts); // {0=1, 1=1746, 2=4753, 3=7423, 4=6033, 5=3543, 6=2071, 7=1720, 8=805, 9=536, 10=234, 11=139, 12=44, 13=18, 14=8, 15=1}
并行流处理的原则:
- 1)因为开销大,仅非常大的数据集才使用;
- 2)只有底层的数据源可以被有效地分割为多个部分,才有意义;
- 3)并行流使用的线程池可能会因为文件 I/O 或 网络访问被阻塞而饿死;
Collectors.groupByConcurrent() 使用了共享的并发映射表
Collectors.groupingBy() 和 Collectors.groupingByConcurrent() 的详细介绍,请参考 http://news.sangniao.com/p/161385798