1.1 从迭代到流的操作

  • 迭代:for, while
  • 流:stream()。优点:1)代码易读;2)性能优化
public class CountingLongWords {
public static void main(String[] args) throws IOException {
var contents = new String(Files.readAllBytes(
Paths.get("./gutenberg/alice30.txt")), StandardCharsets.UTF_8);
List<String> words = List.of(contents.split("\\PL+")); // 方式一:迭代循环
long count = 0;
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); // 并行流操作
count = words.parallelStream().filter(w -> w.length() > 12).count();
System.out.println(count);
}
}

流 和 集合的差异:

  • 1) 流不存储其元素;
  • 2) 流的操作不会修改其数据源;
  • 3) 流的操作是尽可能惰性执行的;

1.2 流的创建

  • stream(): turn any collection into a stream
  • stream.of(): 1) turn any array of objects into a stream, Stream.of(array);  2) 参数为值序列,如下面的例子
  • stream.empty(): makes an empty stream
  • stream.generate(): 创建无限流的方式一
  • stream.iterate(): 创建无限流的方式二
  • splitAsStream(): breaking a string into a stream of tokens
// 静态方法 Stream.of() 构建流,参数为值序列
Stream<String> song = Stream.of("gently", "down", "the", "stream"); // 创建不包含任何元素的流
Stream<String> silence = Stream.empty(); // 创建无限流方式一:generate():获取常量值的流
Stream<String> echos = Stream.generate(() -> "Echo");
// 创建无限流:获取随机数的流
Stream<Double> randoms = Stream.generate(Math::random); // 创建无限流方式二:iterate() 产生一个序列。一个“种子”值+一个函数,并会反复地将该函数应用到之前的结果上。第一个元素 BigInteger.ONE 是种子,第二个元素是 f(seed),即 1,下一个元素是 f(f(seed)),即 2
Stream<BigInteger> integers = Stream.iterate(BigInteger.ONE, n-> n.add(BigInteger.ONE)); // splitAsStream() 会按照某个正则表达式来分割一个 CharSequence 对象
Stream<String> wordsAnotherWay = Pattern.compile("\\PL+").splitAsStream(contents); // 静态方法 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<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() 的函数映射到流的每一个元素。
  • 如果流中有一个空串,结果可能有异常。所以,推荐结合使用,即: stream.filter().map()
  • flatMap() :1)扁平化处理流;2)to compose Optional-valued functions.
public class GenerateNewStream {
public static void main(String[] args) {
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"); Stream<String> longWords = words.stream().filter(w -> w.length() > 20);
System.out.println(longWords.collect(Collectors.toList()));
// [Lili is a teacher in Fudan University] // 2.1 map() 实现转换流中的值:
// 2.1.1 带有方法引用的 map 来实现
Stream<String> lowercaseWords = words.stream().map(String::toLowerCase);
System.out.println(lowercaseWords.collect(Collectors.toList()));
// [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));
System.out.println(firstLetters.collect(Collectors.toList()));
// [P, A, L, S] // 2.2 使用 map 时,会有一个函数应用到每个元素上,并且其结果是包含了应用该函数后所产生的所有结果的流
// map: 获得一个流的 list
Stream<Stream<String>> result = words.stream().map(w -> codePoints(w)); // 2.3 2层列表流的打印
result.forEach(e-> System.out.print(e.collect(Collectors.toList()) + ", "));
     System.out.println();
/*
[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));
System.out.println(flatResult.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]
} // 将字符串转换成字符串流
public static Stream<String> codePoints(String s) {
var result = new ArrayList<String>();
int i = 0;
while (i < s.length()) {
int j = s.offsetByCodePoints(i, 1);
result.add(s.substring(i, j));
i = j;
}
return result.stream();
}
}

1.4 抽取子流和组合流:

  • limit()
  • skip()
  • dropWhile()
  • takeWhile()
  • concat()
public class ExtractAndCombineStream {
public static void main(String[] args) throws IOException {
// 1. limit(n) 裁剪。 1)原流长度 < n,按原流结束; 2)原流长度 >n,裁剪为 n
Stream<Double> randoms = Stream.generate(Math::random).limit(10);
System.out.println("randoms: " + randoms.collect(Collectors.toList()));
// randoms: [0.12221378038694952, 0.45301879816627766, 0.08100225598387067, 0.6827275759321462, 0.44389989789763085, 0.5066289414264251, 0.7360846954646125, 0.10375208303094441, 0.7383847599165345, 0.4160284944965318] var contents = new String(Files.readAllBytes(
Paths.get("./gutenberg/alice30.txt")), StandardCharsets.UTF_8);
Stream<String> words1 = Stream.of(contents.split("\\PL+")).limit(10);
System.out.println("words1: " + words1.collect(Collectors.toList()));
// words1: [, This, is, the, Project, Gutenberg, Etext, of, Alice, in] // 2. skip(n) 丢弃前 n 个元素
Stream<String> words2 = Stream.of(contents.split("\\PL+")).skip(1).limit(10);
System.out.println("words2: " + words2.collect(Collectors.toList()));
// words2: [This, is, the, Project, Gutenberg, Etext, of, Alice, in, Wonderland] // 3. dropWhile(): 条件为真时丢弃元素,并产生一个由第一个使该条件为假的字符开始的元素构成的流
String str = "2023Hello0120Java";
Stream<String> withoutInitialDigits = codePoints(str).dropWhile(s -> "0123456789".contains(s));
System.out.println("withoutInitialDigits: " + withoutInitialDigits.collect(Collectors.toList()));
// withoutInitialDigits: [H, e, l, l, o, 0, 1, 2, 0, J, a, v, a] // 4. takeWhile(): 获取所有字符为真的元素,直到条件为假时
Stream<String> initialDigits = codePoints(str).takeWhile(s -> "0123456789".contains(s));
System.out.println("initialDigits: " + initialDigits.collect(Collectors.toList()));
// initialDigits: [2, 0, 2, 3] // 5. 静态方法 concat(): 将两个流连接起来
Stream<String> combined = Stream.concat(codePoints("Hello"), codePoints("World"));
System.out.println("combined: " + combined.collect(Collectors.toList()));
// combined: [H, e, l, l, o, W, o, r, l, d]
} // 将字符串转换成字符串流
public static Stream<String> codePoints(String s) {
var result = new ArrayList<String>();
int i = 0;
while (i < s.length()) {
int j = s.offsetByCodePoints(i, 1);
result.add(s.substring(i, j));
i = j;
}
return result.stream();
}
}

1.5 其他的流转换:

  • distinct()
  • sorted()
  • peek()
public class OtherStreamTransfer {
public static void main(String[] args) throws IOException {
// distinct(): 剔除重复元素
Stream<String> uniqueWords = Stream.of("merrily", "merrily", "merrily", "gently").distinct();
System.out.println(uniqueWords.collect(Collectors.toList()));
// [merrily, gently] // sorted():对流进行排序
var contents = new String(Files.readAllBytes(
Paths.get("./gutenberg/alice30.txt")), StandardCharsets.UTF_8);
List<String> words = List.of(contents.split("\\PL+"));
Stream<String> longestFirst = words.stream().limit(10).sorted(Comparator.comparing(String::length).reversed());
System.out.println(longestFirst.collect(Collectors.toList()));
// [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 简单约简:从流到单个值

  • count()
  • max()
  • min()
  • findFirst()
  • findAny()
  • anyMatch()
public class SimpleTerminalOperation {
public static void main(String[] args) throws IOException {
var contents = new String(Files.readAllBytes(
Paths.get("./gutenberg/alice30.txt")), StandardCharsets.UTF_8);
List<String> words = List.of(contents.split("\\PL+")); // 1. count():统计元素数量
System.out.println("words count: " + words.stream().count());
// words count: 29075
// --> recommended: words.size()
System.out.println("words count: " + (long) words.size());
// words count: 29075 // 2. max(): 返回最大值
Optional<String> largest = words.stream().max(String::compareToIgnoreCase);
System.out.println("largest: " + largest.orElse(""));
// largest: zip // 3. min(): 返回最小值
Optional<String> smallest = words.stream().skip(1).limit(10).min(String::compareToIgnoreCase);
System.out.println("smallest: " + smallest.orElse(""));
// smallest: Alice // 4. findFirst(): 返回非空集合中的第一个值,通常与 filter() 组合使用
Optional<String> startWithQFF = words.stream().filter(s -> s.startsWith("Q")).findFirst();
System.out.println("startWithQFF: " + startWithQFF);
// startWithQFF: Optional[Quick] // 5. findAny():使用任意的匹配
Optional<String> startWithQFA = words.stream().filter(s -> s.startsWith("Q")).findAny();
System.out.println("startWithQFA: " + startWithQFA);
// startWithQFA: Optional[Quick] // 6. anyMatch() + parallelStream() 并行流
boolean aWordStartsWithQ = words.parallelStream().anyMatch(s -> s.startsWith("Q"));
System.out.println("aWordStartsWithQ: " + aWordStartsWithQ);
// aWordStartsWithQ: true
}
}

1.7 Optional 类型

Optional<T> 对象是一种包装器对象,1)包装了类型 T 的对象,称值存在; 2)没有包装对象。

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("myapp.default"));
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());

1.7.4 不适合使用 Optional 值的方式

// 1. 不能再 Optional 上调用 get 方法。下面的 get() 在值存在时获取包装的元素,不存在时将抛出 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 值的函数

// 1. 假设: f() 可以产生 Optional<T> 对象,且 类型 T 有一个可以产生 Optional<U> 对象的方法 g。
s.f().g(); // error: 无法将它们组合起来,因为 s.f() 的类型是 Optional<T>,不是 T

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:
Optional<Double> result = inverse(x).flatMap(MyMath::squareRoot); // 调用方式2:
Optional<Double> result = Optional.of(-4).flatMap(Demo::inverse).flatMap(Demo::squareRoot);

1.7.7 将 Optional 转换为流

stream() 会将一个 Optional<T> 对象转换为 0 个或 1 个元素的 Stream<T> 对象。

// 假设有一个用户 ID 流和如下方法:
Optional<User> lookup(String id) // 方式1: 下面方法会过滤无效 ID,但要慎用 isPresent() 和 get()
Stream<String> ids = ...;
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);

1.8 收集结果:将流结果放入集合

  • iterator():老式迭代器
  • forEach() / forEachOrdered()
  • toArray():放入数组
  • 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[] numbers = Stream.iterate(0, n -> n + 1).limit(10).toArray();
System.out.println("object array: " + Arrays.toString(numbers)); // 3.2 创建正确类型的数组,可以将 类型传递到数组构造器中, stream.toArray(Integer[]::new)
Integer[] numbers3 = Stream.iterate(0, n -> n + 1)
.limit(10)
.toArray(Integer[]::new);
System.out.println("Integer array: " + Arrays.toString(numbers3));

4. toCollect() 收集流到另一个目标中

// 4.1 转换到 List
List<String> result = stream.collect(Collectors.toList()); // 4.2 转换到 Set
Set<String> result = stream.collect(Collectors.toSet()); // 4.3 控制获得的集的种类
TreeSet<String> result = stream.collect(Collectors.toCollection(TreeSet::new)); // 4.4 通过连接操作来收集流中的所有字符串
String result = stream.collect(Collectors.joining()); // 4.5 元素间增加分隔符,可以将分隔符传递给 joining()
String result = stream.collect(Collectors.joining(", "));

// 4.6 如果流中包含了字符串以外的对象,需要先将其转换为字符串
String result = stream.map(Object::toString).collect(Collectors.joining(", "));

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. 第二个函数是实际的元素
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]
// 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,则需要引入第四个引元(构造器)

idToPerson = people().collect(
Collectors.toMap(
Person::getId,
Function.identity(),
(existingValue, newValue) -> {throw new IllegalStateException();},
TreeMap::new));

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. 将具有相同特性的值群聚成组,可用 groupingBy() 来实现
Map<String, List<Locale>> countryToLocales = locales.collect(
Collectors.groupingBy(
Locale::getCountry));
System.out.println("countryToLocales: " + countryToLocales);
// 1.2. 查找指定国家对应的所有地点
List<Locale> swissLocales = countryToLocales.get("CH");
System.out.println("swissLocales: " + swissLocales);
// 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() 更高效
locales = Stream.of(Locale.getAvailableLocales());
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
Stream<Locale> locales = Stream.of(Locale.getAvailableLocales()); 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():
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() 是一种从流中计算某个值的通用机制, 其简单的形式将接受一个二元函数,并从前两个元素开始应用它。

List<Integer> values = List.of(1, 10, 15, 60, 90);

// type 1: 定义 Optional<T>,防止流为空
Optional<Integer> sum = values.stream()
.reduce((x, y) -> x+ y);
System.out.println("sum: " + sum);
// sum: Optional[176] // type 2: 加入玄元值 e
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 // 方式二: 映射为数字流并计算总和,更为方便
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.*; /**
* @author: Bruce He
* @Date: 2023/1/10 14:02
* @Description: section: 1.13 基本类型流, page: 34, program list: 1-7
* 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 = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); // 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, 100).boxed();
System.out.println(integers.collect(Collectors.toList()));
/*
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
...
91, 92, 93, 94, 95, 96, 97, 98, 99]
*/ integers = IntStream.range(0, 100).boxed();
// mapToInt() 将对象流转换为基本类型流
IntStream is5 = integers.mapToInt(Integer::intValue);
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();
Stream<String> parallelWords = Stream.of(wordArray).parallel();
public class ParallelStreams {
public static void main(String[] args) throws IOException {
var contents = new String(Files.readAllBytes(
Paths.get("./gutenberg/alice30.txt")), StandardCharsets.UTF_8);
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. 用长度将字符串分组,然后分别计数。此时,并行计算是安全的
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} // Downstream order not deterministic。same result if run again
Map<Integer, List<String>> result = wordList.parallelStream().collect(
Collectors.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() 使用了共享的并发映射表

读后笔记 -- Java核心技术(第11版 卷 II ) Chapter1 Java 8 的流库的更多相关文章

  1. 【Java】-NO.16.EBook.4.Java.1.007-【疯狂Java讲义第3版 李刚】- Java基础类

    1.0.0 Summary Tittle:[Java]-NO.16.EBook.4.Java.1.007-[疯狂Java讲义第3版 李刚]-  Java基础类 Style:EBook Series:J ...

  2. java核心技术(第十版卷一)笔记(纯干货!)

    这是我读过的第三本关于java基础的书.第一本<<java从入门到精通>>这本书让我灵识初开.第二本<<java敏捷开发>>这本书则是有一次被一位师傅批 ...

  3. Java核心技术卷一基础知识-第3章-Java的基本程序设计结构-读书笔记

    第3章 Java的基本程序设计结构 本章内容: 一个简单的Java应用程序 字符串 注释 输入输出 数据类型 控制流 变量 大数值 运算符 数组 本章主要讲述程序设计相关的基本概念(如数据类型.分支以 ...

  4. Java核心技术卷一基础知识-第2章-Java程序设计环境-读书笔记

    第2章 Java程序设计环境 本章内容: 安装Java开发工具箱 使用集成开发环境 选择开发环境 运行图形化应用程序 使用命令行工具 建立并运行applet本章主要介绍如何安装Java开发工具箱(JD ...

  5. JAVA核心技术I---JAVA基础知识(文件系统及java文件基本操作)

    一:文件概述 文件系统是由OS(操作系统)管理的 文件系统和Java进程是平行的,是两套系统 文件系统是由文件夹和文件递归组合而成 文件目录分隔符 –Linux/Unix 用/隔开 –Windows用 ...

  6. java核心技术第十版 笔记

    1.java区分大小写 2.类名是以大写字母开头 (驼峰) 3.http://docs.oracle.com/javase/specs  java语言规范 4. /* */ 注释不能嵌套 5. Jav ...

  7. 《Java核心技术 卷II 高级特性(原书第9版)》

    <Java核心技术 卷II 高级特性(原书第9版)> 基本信息 原书名:Core Java Volume II—Advanced Features(Ninth Edition) 作者: ( ...

  8. Java核心技术·卷 II(原书第10版)分享下载

    Java核心技术·卷 II 内容介绍 Java领域最有影响力和价值的著作之一,由拥有20多年教学与研究经验的资深Java技术专家撰写(获Jolt大奖),与<Java编程思想>齐名,10余年 ...

  9. 《Java核心技术卷I》观赏指南

    Tomxin7 如果你有想看书的计划,但是还在纠结哪些书值得看,可以简单看看"观赏指南"系列,本文会简单列出书中内容,给还没有买书的朋友提供一个参考. 前言 秋招过去很久了,虽然在 ...

  10. Java核心技术卷阅读随笔--第4章【对象与类】

    对 象 与 类 4.1 面向对象程序设计概述 面向对象程序设计(简称 OOP) 是当今主流的程序设计范型, 它已经取代了 20 世纪 70 年代的" 结构化" 过程化程序设计开发技 ...

随机推荐

  1. ctf 菜鸟杯

    web签到 首先最里面的是ctfshow-QQ群:,而他需要进行cookie传参,因此我们需要在cookie传入CTFshow-QQ群=a,然后就要以POST方式传入a的值,我们传入a=b,而b是以G ...

  2. 我在京东做研发 | 从好玩到好用,爆火ChatGPT前沿分享

    | 嘉宾:吴友政,京东集团高级总监.京东科技语音语言算法部负责人. 2006年中科院自博士毕业后,先后在日本国立信息通信研究机构.英国爱丁堡大学.索尼中国研究院从事自然语言处理相关研究工作,主要聚焦自 ...

  3. 从零开始使用阿里云OSS服务(白嫖)

    阿里云对象存储服务OSS使用记录 1 新人免费开通OSS服务 访问 阿里云官网,登录账号(个人新用户账号),首页搜索 对象存储OSS,点击下方的直达. 点击立即开通,此时会在一个新网页中完成开通 完成 ...

  4. Connect-The-Dots

    Connect-The-Dots 目录 Connect-The-Dots 1 信息收集 1.1 端口扫描 1.2 后台目录扫描 1.2.1 目录分析 2 目标服务安全检测 2.1 ftp检测 2.2 ...

  5. 梅毒感染者能否应用TNF抑制剂

    对于伴发的未经控制的任何严重感染,都不适合使用TNF抑制剂.在1998年国际上首个TNF抑制剂获批治疗类风湿关节炎(RA)以来,这就是广大临床医生和风湿性疾病患者的共识.在临床实践中,需要权衡药物的利 ...

  6. Canvas:绘制多边形

    前言 CanvasRenderingContext2D 没有提供绘制多边形的函数,所以只能由我们自己来实现绘制多边形的函数.以六边形为基础,需要用到三角函数:sin 和 cos. 点 A 坐标 (一) ...

  7. Java处理正则匹配卡死(正则回溯问题)

    目录 背景 项目现场问题 问题跟踪 优化方案 处理正则问题 使用子线程来匹配正则实现 监控线程实现 最优选择方案 参考文章 正则匹配卡死怎么来的? 背景 背景:这次问题的背景是项目上遇到了,在使用正则 ...

  8. Ubuntu中用普通方法无法添加自启动

    参考了https://www.cnblogs.com/a5idc/p/13752839.html 如何知道你使用的是哪个init系统?你可以使用这个命令来知道与PID 1(系统上运行的第一个进程)相关 ...

  9. C#判断useragent是pc还是手机端

    public static bool CheckAgent(string userAgent) { bool flag = false; string[] keywords = { "And ...

  10. Nginx 同一个域名自动识别 pc h5

    首先设置环境变量 我们先设置变量,通过判断来改变变量的值(注: 我写在server中) set $is_mobile false; # 初始值 if ( $http_cookie ~* "A ...