文章目录
Java8的流库
通过使用流,可以说明想要完成什么任务,而不是说明如何去实现它。将操作的调度留给具体实现去解决。
1.1从迭代到流的操作
long count = words.stream().filter(w -> w.length() > 12).count();
仅将
stream
修改为parallelStream
就可以让流库以并行方式来执行。
流遵循了"做什么而非怎么做"的原则。
虽然表面上看起来和集合很类似,都可以转换和获取数据。但是,它们之间存在着显著的差异:
- 流并不存储其元素。这些元素可能存储在底层的集合中,或者是按需生成的。
- 流的操作不会修改其数据源。例如,
filter
方法不会从流中移除元素,而是会生成一个新的流,其中不包含被过滤掉的元素。- 流的操作是尽可能惰性执行的。这意味着直至需要其结果时,操作才会执行。
操作流时的典型流程:
- 创建一个流。
- 指定将初始流转换为其他流的中间操作。可能包含多个步骤。
- 应用终止操作,从而产生结果。这个操作会强制执行之前的惰性操作。从此之后,这个流就再也不能用了。
public class CountLongWords {
public static void main(String[] args) throws IOException {
var contents = new String(Files.readAllBytes(Paths.get("../gutenberg/alice30.txt")), StandardCharsets.UTF_8);
var words = List.of(contents.split("\\PL+"));
long count = 0;
for (String w : words) {
if (w.length() > 12) {
count++;
}
}
System.out.println(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流的创建
如果有一个数组,那么可以使用静态的
Stream.of
方法:
Stream<String> words = Stream.of(contents.split("\\PL+"));
// split returns a String[] array
of
方法具有可变长参数,因此可以构建具有任意数量引元的流:
Stream<String> song = Stream.of("gently", "down", "the", "stream");
使用
Arrays.stream(array, from, to)
可以用数组中的一部分元素来创建一个流。
为了创建不包含任何元素的流,可以使用静态的Stream.empty
方法:
Stream<String> silence = Stream.empty();
// Generic type <String> is inferred; same as Stream.<String>empty()
Stream
接口有两个用于创建无限流的静态方法。
generate
方法会接受一个不包含任何引元的函数(或者从技术上讲,是一个Supplier<T>
接口的对象)。无论何时,只要需要一个流类型的值,该函数就会被调用以产生一个这样的值。例如,获得一个常量值的无限流:
Stream<String> echos = Stream.generate(() -> "Echo");
或者获取一个随机数的无限流:
Stream<Double> randoms = Stream.generate(Math::random);
如果要产生像
0 1 2 3 ...
这样的无限序列,可以使用iterate
方法。它会接受一个种子值以及一个函数(从技术上讲,是一个UnaryOperator<T>
),并且会反复地将该函数应用到之前的结果上。例如:
Stream<BigInteger> integers = Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE));
// 该序列中的第一个元素是种子BigInteger.ZERO,第二个元素是f(seed),即1,
// 下一个元素是f(f(seed)),即2,以此类推
如果要产生一个有限序列,则需要添加一个谓词来描述迭代应该如何结束:
var limit = new BigInteger("10000000");
Stream<BigInteger> integers = Stream.iterate(BigInteger.ZERO,
n -> n.compareTo(limit) < 0,
n -> n.add(BigInteger.ONE));
只要该谓词拒绝了某个迭代生成的值,这个流即结束。
Stream.ofNullable
方法会用一个对象来创建一个非常短的流。如果该对象为null
,那么这个流的长度就为0;否则,这个流的长度为1,即只包含该对象。这个方法与flatMap
相结合时最有用。
Java API中有大量方法都可以产生流。例如,
Pattern
类有一个splitAsStream
方法,它会按照某个正则表达式来分割一个CharSequence
对象。例如,将一个字符串分割为一个个的单词:
Stream<String> words = Pattern.compile("\\PL+").splitAsStream(contents);
Scanner.tokens
方法会产生一个扫描器的符号流。另一种从字符串中获取单词流的方式是:
Stream<String> words = new Scanner(contents).tokens();
静态的
Files.lines
方法会返回一个包含了文件中所有行的Stream
:
try (Stream<String> lines = Files.lines(path)) {
// process lines
}
如果持有的
Iterable
对象不是集合,那么可以将其转换为一个流:
StreamSupport.stream(Iterable.spliterator(), false);
如果持有的是
Iterator
对象,并且希望得到一个由它的结果构成的流,那么可以使用:
StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), false);
至关重要的是,在执行流的操作时,并没有修改流背后的集合。记住,流并没有收集其数据,数据一直存储在单独的集合中。如果修改了该集合,那么流操作的结果就会变成未定义的。JDK文档称这种要求为不干涉性。
准确地讲,因为中间的流操作是惰性的,所以在终止操作得以执行时,集合有可能已经发生了变化。例如,尽管不推荐下面这段代码,但是它仍旧可以工作:
List<String> wordList = ...;
Stream<String> words = wordList.stream();
wordList.add("END");
long n = words.distinct().count();
但是下面的代码是错误的:
Stream<String> words = wordList.stream();
words.forEach(s -> if (s.length() < 12) wordList.remove(s));
// ERROR--interference
public class CreatingStreams {
public static <T> void show(String title, Stream<T> stream) {
final int SIZE = 10;
List<T> firstElements = stream.limit(SIZE + 1).collect(Collectors.toList());
System.out.print(title + ": ");
for (int i = 0; i < firstElements.size(); i++) {
if (i > 0) {
System.out.print(", ");
}
if (i < SIZE) {
System.out.print(firstElements.get(i));
} else {
System.out.print("...");
}
}
System.out.println();
}
public static void main(String[] args) throws IOException {
Path path = Paths.get("/Users/kirito/program/java/study/java_study/core_java_volume_two_11/src/main/resources/alice30.txt");
var contents = new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
Stream<String> words = Stream.of(contents.split("\\PL+"));
show("words", words);
Stream<String> song = Stream.of("gently", "down", "the", "stream");
show("song", song);
Stream<String> silence = Stream.empty();
show("silence", silence);
Stream<String> echos = Stream.generate(() -> "Echo");
show("echos", echos);
Stream<Double> randoms = Stream.generate(Math::random);
show("randoms", randoms);
Stream<BigInteger> integers = Stream.iterate(BigInteger.ONE, n -> n.add(BigInteger.ONE));
show("integers", integers);
Stream<String> wordsAnotherWay = Pattern.compile("\\PL+").splitAsStream(contents);
show("wordsAnotherWay", wordsAnotherWay);
try (Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) {
show("lines", lines);
}
Iterable<Path> iterable = FileSystems.getDefault().getRootDirectories();
Stream<Path> rootDirectories = StreamSupport.stream(iterable.spliterator(), false);
show("rootDirectories", rootDirectories);
Iterator<Path> iterator = Paths.get("/usr/share/dict/words").iterator();
Stream<Path> pathComponents = StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), false);
show("pathComponents", pathComponents);
}
}
1.3filter
、map
和flatMap
方法
流的转换会产生一个新的流,它的元素派生自另一个流中的元素。
filter
的引元是Predicate<T>
,即从T
到boolean
的函数。
通常,想要按照某种方式来转换流中的值,此时,可以使用map
方法并传递执行该转换的函数。例如,将所有单词转换为小写:
Stream<String> lowercaseWords = words.stream().map(String::toLowerCase);
// 也可以使用lambda表达式
Stream<String> firstLetters = words.stream().map(s -> s.substring(0, 1));
在使用
map
时,会有一个函数应用到每个元素上,并且其结果是包含了应用该函数后所产生的所有结果的流。
假设有一个函数,它返回的不是一个值,而是包含了众多值的流。例如,将字符串转换为字符串流,即一个个的编码点:
// 这个方法可以正确地处理需要用两个char值来表示的unicode字符
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();
}
// 返回值是流["b", "o", "a", "t"]
codePoints("boat");
// 假设将方法映射到一个字符串流上,那么会得到一个包含流的流,就像[..., ["y", "o", "u", "r"], ["b", "o", "a", "t"], ...]
Stream<Stream<String>> result = words.stream().map(w -> codePoints(w));
为了将其摊平为单个流
[..., "y", "o", "u", "r", "b", "o", "a", "t", ...]
,可以使用flatMap
方法(产生一个流,它是通过将函数应用于当前流中的所有元素所产生的结果连接到一起而获得的,注意,这里的每个结果都是一个流):
Stream<String> flatResult = words.stream().flatMap(w -> codePoints(w));
// Calls codePoints on each word and flattens the results
1.4抽取子流和组合流
调用
stream.limit(n)
会返回一个新的流,它在n个元素之后结束(如果原来的流比n短,那么就会在该流结束时结束)。这个方法对于裁剪无限流的尺寸特别有用:
Stream<Double> randoms = Stream.generate(Math::random).limit(100);
调用
stream.skip(n)
正好相反,它会丢弃(跳过)前n个元素。
stream.takeWhile(predicate)
调用会在谓词为真时获取流中的所有元素,然后停止。例如,收集字符串中的所有数字元素:
Stream<String> intialDigits = codePoints(str).takeWhile(s -> "0123456789".contains(s));
dropWhile
方法的做法正好相反,它会在条件为真时丢弃元素,并产生一个由第一个使该条件为假的元素开始的所有元素构成的流:
List<Integer> list = List.of(1, 2, 3, 4, 5, 6);
var integerStream = list.stream().dropWhile(e -> e < 3);
// 3, 4, 5, 6
可以用
Stream
类的静态concat
方法将两个流连接起来:
Stream<String> combined = Stream.concat(codePoints("Hello"), codePoints("World"));
// Yields the stream ["H", "e", "l", "l", "o", "W", "o", "r", "l", "d"]
1.5其他的流转换
distinct
方法会返回一个流,它的元素是从原有流中产生的,即原来的元素按照同样的顺序剔除重复元素后产生的。这些重复元素并不一定是毗邻的。
Stream<String> uniqueWords = Stream.of("merrily", "merrily", "merrily", "gently").distinct();
// Only one "merrily" is retained
对于流的排序,有多种
sorted
方法的变体可用。其中一种用于操作Comparable
元素的流,而另一种可以接受一个Comparator
。例如,对字符串排序,最长的字符串排在最前面:
Stream<String> longestFirst = words.stream().sorted(Comparator.comparing(String::length).reversed())
当然,在对集合排序时可以不使用流。但是,当排序处理是流管道的一部分时,
sorted
方法就会显得很有用。
peek
方法会产生另一个流,它的元素与原来流中的元素相同,但是在每次获取一个元素时,都会调用一个函数。这对于调试来说很方便。
Object[] powers = Stream.iterate(1.0, p -> p * 2)
.peek(e -> System.out.println("Fetching: " + e))
.limit(20)
.toArray();
// 当实际访问一个元素时(调用toArray方法时),就会打印出来一条消息。通过这种方式,
// 可以验证iterate返回的无限流是被惰性处理的
1.6简单约简
约简是一种终结操作,它们会将流约简为可以在程序中使用的非流值。例如,
count
方法会返回流中元素的数量。
其他的简单约简还有max
和min
,它们分别返回最大值和最小值。需要注意的是,这些方法返回的是一个类型Optional<T>
的值,它要么在其中包装了答案,要么表示没有任何值(因为流碰巧为空)。Optional
类型是一种表示缺少返回值的更好的方式。
Optional<String> largest = words.max(String::compareToIgnoreCase);
System.out.println("largest: " + largest.orElse(""));
findFirst
返回流中的第一个元素,如果这个流为空,会产生一个空的Optional
对象。它通常在与filter
组合使用时很有用。
Optional<String> startsWithQ = words.filter(s -> s.startsWith("Q")).findFirst();
如果不强调使用第一个匹配,而是使用任意的匹配都可以,那么就可以使用
findAny
方法。这个方法在并行处理流时很有效,因为流可以报告任何它找到的匹配而不是被限制为必须报告第一个匹配。
Optional<String> startsWithQ = words.parallel().filter(s -> s.startsWith("Q")).findAny();
如果只想知道是否存在匹配,那么可以使用
anyMatch
。这个方法会接受一个断言引元,因此不需要使用filter
。
boolean aWordStartsWithQ = words.parallel().anyMatch(s -> s.startsWith("Q"));
还有
allMatch
和noneMatch
方法,它们分别在所有元素和没有任何元素匹配谓词的情况下返回true
。这些方法也可以通过并行运行而获益。
1.7Optional
类型
Optional<T>
对象是一种包装器对象,要么包装了类型T
的对象,要么没有包装任何对象。对于前者,称这种值是存在的。Optional<T>
类型被当作一种更安全的方式,用来替代类型T
的引用,这种引用要么引用某个对象,要么为null
。但是,它只有在正确使用的情况下才会更安全。
1.7.1获取Optional
值
有效地使用
Optional
的关键是要使用这样的方法:它的值不存在的情况下会产生一个可替代物,而只有在值存在的情况下才会使用这个值。
String result = optionalString.orElse("");
// The wrapped string, or "" if none
还可以调用代码来计算默认值:
String result = optionalString.orElseGet(() -> System.getProperty("myapp.default"));
或者可以在没有任何值时抛出异常:
String result = optionalString.orElseThrow(IllegalStateException::new);
// Supply a method that yields an exception object
1.7.2消费Optional
值
另一条使用可选值的策略是只有在其存在的情况下才消费该值。
ifPresent
方法会接受一个函数。如果可选值存在,那么它会被传递给该函数。否则,不会发生任何事情。
optionalValue.ifPresent(v -> results.add(v));
// 等价于
optionalValue.ifPresent(results::add)
如果想在可选值存在时执行一种动作,在可选值不存在时执行另一种动作,可以使用
ifPresentOrElse
:
optionalValue.ifPresentOrElse(v -> System.out.println("Found " + v),
() -> logger.warning("No match"));
1.7.3管道化Optional
值
另一种有用的策略是保持
Optional
完整,使用map
方法来转换内部的值:
// 如果optionalString为空,那么transformed也为空
Optional<String> transformed = optionalString.map(String::toUpperCase);
// 将一个结果添加到列表中,如果optionalValue为空,则什么也不会发生
optionalValue.map(results::add);
与
Stream
接口的map
方法类似。可以直接将可选值想象成尺寸为0或1的流。结果的尺寸也是0或1,并且在后一种情况中,函数会应用于其上。
类似的,可以使用
filter
方法来只处理那些在转换它之前或之后满足某种特定属性的Optional
值。如果不满足该属性,那么管道会产生空的结果:
Optional<String> transformed = optionalString.filter(s -> s.length() >= 8)
.map(String::toUpperCase);
也可以用
or
方法将空Optional
替换为一个可替代的Optional
。这个可替代值将以惰性方式计算。
Optional<String> result = optionalString.or(() -> {
// Supply an Optional
})
如果当前
Optional
不为空,则产生当前的Optional
;否则计算lambda表达式,并使用计算出来的结果。
1.7.4不适合使用Optional
值的方式
如果没有正确地使用
Optional
值,那么相比以往得到某物或null
的方式,并没有得到任何好处。
get
方法会在Optional
值存在的情况下获得其中包装的元素,或者在不存在的情况下抛出一个NoSuchElementException
异常。因此:
Optional<T> optionalValue = ...;
optionalValue.get().someMethod();
并不比下面的方式更安全:
T value = ...;
value.someMethod();
isPresent
方法会报告某个Optional<T>
对象是否具有值,但是:
if (optionalValue.isPresent()) {
optionalValue.get().someMethod();
}
并不比下面的方式更容易处理:
if (value != null) {
value.someMethod();
}
有关
Optional
类型正确用法的提示:
Optional
类型的变量永远都不应该为null
。- 不要使用
Optional
类型的域。因为其代价是额外多出来一个对象。在类的内部,使用null
表示缺失的域更易于操作。- 不要在集合中放置
Optional
对象,并且不要将它们用作map
的键。应该直接收集其中的值。
1.7.5创建Optional
值
如果想要编写方法来创建
Optional
对象,那么有多个方法可以用于此目的,包括Optional.of(result)
和Optional.empty()
。例如:
public static Optional<Double> inverse(Double x) {
return x == 0 ? Optional.empty() : Optional.of(1 / x);
}
ofNullable
方法被用来作为可能出现的null
值和可选值之间的桥梁。Optional.ofNullable(obj)
会在obj
不为null
的情况下返回Optional.of(obj)
,否则会返回Optional.empty()
。
1.7.6用flatMap
构建Optional
值的函数
假设有一个可以产生
Optional<T>
对象的方法f
,并且目标类型T
具有一个可以产生Optional<U>
对象的方法g
。如果它们都是普通的方法,那么可以通过调用s.f().g()
来将它们组合起来。但是这种组合无法工作,因为s.f()
的类型为Optional<T>
,而不是T
。因此,需要调用:
Optional<U> result = s.f().flatMap(T::g);
// 如果s.f()的值存在,那么g就可以应用到它上面。否则,就会返回一个空Optional<U>
很明显,如果有更多可以产生
Optional
值的方法或lambda表达式,那么就可以重复此过程。可以直接将对flatMap
的调用链接起来,从而构建这些步骤构成的管道,只有所有步骤都成功,该管道才会成功。例如:
// 考虑前一节中安全的inverse方法,假设还有一个安全的平方根
public static Optional<Double> squareRoot(Double x) {
return x < 0 ? Optional.empty() : Optional.of(Math.sqrt(x));
}
// 计算倒数的平方根
Optional<Double> result = inverse(x).flatMap(Demo::squareRoot);
// 或者,可以选择下面的方式
Optional<Double> result = Optional.of(-4.0).flatMap(Demo::inverse).flatMap(Demo::squareRoot);
// 无论是inverse方法还是squareRoot方法返回Optional.empty(),整个结果都会为空
1.7.7将Optional
转换为流
stream
方法会将一个Optional<T>
对象转换为一个具有0个或1个元素的Stream<T>
对象,这会使返回Optional
结果的方法变得很有用。
假设有一个用户id流和下面的方法:
Optional<User> lookup(String id);
现在想要在获取用户流时,跳过那些无效的id。可以先过滤,然后将
get
方法应用于剩余的id:
Stream<String> ids = ...;
Stream<User> users = ids.map(Users::lookup) // Stream<Optional<User>>
.filter(Optional::isPresent) // Stream<Optional<User>>
.map(Optional::get); // Stream<User>
但是这样就需要使用之前警告过要慎用的
isPresent
和get
方法。下面的调用显得更优雅:
Stream<User> users = ids.map(Users::lookup) // Stream<Optional<User>>
.flagMap(Optional::stream); // Stream<User>
每一个对
stream
的调用都会返回一个具有0个或1个元素的流。flatMap
方法将这些方法组合在一起,这意味着不存在的用户会直接被丢弃。
但是许多方法都会在没有任何有效结果的情况下返回
null
。假设Users.classicLookup(id)
会返回一个User
对象或者null
,而不是Optional<User>
,当然可以过滤掉null
值:
Stream<User> users = ids.map(Users::classicLookup).filter(Objects::nonNull);
但是如果更喜欢
flatMap
的方式,可以使用下面的代码:
Stream<User> users = ids.flagMap(id -> Stream.ofNullable(Users.classicLookup(id)));
或者是下面的代码:
Stream<User> users = ids.map(Users::classicLookup).flatMap(Stream::ofNullable);
Stream.ofNullable(obj)
这个调用在obj
为null
时,会产生一个空的流,否则会产生一个只包含obj
的流。
public class OptionalTest {
public static void main(String[] args) throws IOException {
var contents = Files.readString(Paths.get("/Users/kirito/program/java/study/java_study/core_java_volume_two_11/src/main/resources/alice30.txt"));
List<String> wordList = List.of(contents.split("\\PL+"));
Optional<String> optionalValue = wordList.stream().filter(s -> s.contains("fred")).findFirst();
System.out.println(optionalValue.orElse("No word") + " contains fred");
Optional<String> optionalString = Optional.empty();
String result = optionalString.orElse("N/A");
System.out.println("result: " + result);
result = optionalString.orElseGet(() -> Locale.getDefault().getDisplayName());
System.out.println("result: " + result);
try {
result = optionalString.orElseThrow(IllegalStateException::new);
System.out.println("result: " + result);
} catch (Throwable t) {
t.printStackTrace();
}
optionalValue = wordList.stream().filter(s -> s.contains("red")).findFirst();
optionalValue.ifPresent(s -> System.out.println(s + " contains red"));
var results = new HashSet<String>();
optionalValue.ifPresent(results::add);
Optional<Boolean> added = optionalValue.map(results::add);
System.out.println(added);
System.out.println(inverse(4.0).flatMap(OptionalTest::squareRoot));
System.out.println(inverse(-1.0).flatMap(OptionalTest::squareRoot));
System.out.println(inverse(0.0).flatMap(OptionalTest::squareRoot));
Optional<Double> result2 = Optional.of(-4.0).flatMap(OptionalTest::inverse).flatMap(OptionalTest::squareRoot);
System.out.println(result2);
}
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.8收集结果
当处理完流之后,通常会想要查看其结果。
此时可以调用iterator
方法,它会产生用来访问元素的旧式风格的迭代器。
或者,可以调用forEach
方法,将某个函数应用于每个元素:
stream.forEach(System.out::println);
在并行流上,
forEach
方法会以任意顺序遍历各个元素。如果想要按照流中的顺序来处理它们,可以调用forEachOrdered
方法。当然,这个方法会丧失并行处理的部分甚至全部优势。
但是,更常见的情况是,想要将结果收集到数据结构中。
此时,可以调用toArray
方法,获得由流的元素构成的数组。因为无法在运行时创建泛型数组,所以表达式stream.toArray
会返回一个Object[]
数组。如果想要让数组具有正确的类型,可以将其传递到数组构造器中:
String[] result = stream.toArray(String[]::new);
// stream.toArray() has type Object[]
针对将流中的元素收集到另一个目标中,有一个便捷方法
collect
可用,它会接受一个Collector
接口的实例。收集器是一种收集众多元素并产生单一结果的对象,Collectors
类提供了大量用于生成常见收集器的工厂方法。要想将流的元素收集到一个列表中,应该使用Collectors.toList
方法产生的收集器:
List<String> result = stream.collect(Collectors.toList());
类似地,可以将流的元素收集到一个集中:
Set<String> result = stream.collect(Collectors.toSet());
如果想要控制获得的集的种类,那么可以使用:
TreeSet<String> result = stream.collect(Collectors.toCollection(TreeSet::new));
假设想要通过连接操作来收集流中的所有字符串,可以调用:
String result = stream.collect(Collectors.joining());
如果想要在元素之间增加分隔符,可以将分隔符传递给
joining
方法:
String result = stream.collect(Collectors.joining(", "));
如果流中包含除字符串以外的其他对象,那么需要先将其转换为字符串:
String result = stream.map(Object::toString).collect(Collectors.joining(", "));
如果想要将流的结果约简为总和、数量、平均值、最大值和最小值,可以使用
summarizing(Int|Long|Double)
方法中的某一个。这些方法会接受一个将流对象映射为数值的函数,产生类型为(Int|Long|Double)SummaryStatistics
的结果,同时计算总和、数量、平均值、最大值和最小值。
IntSummaryStatistics summary = stream.collect(Collectors.summarizingInt(String::length));
double averageWordLength = summary.getAverage();
double maxWordLength = summary.getMax();
public class CollectingResults {
public static Stream<String> noVowels() throws IOException {
var contents = Files.readString(Paths.get("/Users/kirito/program/java/study/java_study/core_java_volume_two_11/src/main/resources/alice30.txt"));
List<String> wordList = List.of(contents.split("\\PL+"));
Stream<String> words = wordList.stream();
return words.map(s -> s.replaceAll("[aeiouAEIOU]", ""));
}
public static <T> void show(String label, Set<T> set) {
System.out.print(label + ": " + set.getClass().getName());
System.out.println("[" + set.stream().limit(10).map(Object::toString).collect(Collectors.joining(", ")) + "]");
}
public static void main(String[] args) throws IOException {
Iterator<Integer> iter = Stream.iterate(0, n -> n + 1).limit(10).iterator();
while (iter.hasNext()) {
System.out.println(iter.next());
}
Object[] numbers = Stream.iterate(0, n -> n + 1).limit(10).toArray();
System.out.println("Object array: " + numbers); // Note it's an Object[] array
try {
var number = (Integer) numbers[0]; // OK
System.out.println("number: " + number);
System.out.println("The following statement throws an exception: ");
var numbers2 = (Integer[]) numbers; // Throws exception
} catch (ClassCastException ex) {
System.out.println(ex);
}
Integer[] numbers3 = Stream.iterate(0, n -> n + 1).limit(10).toArray(Integer[]::new);
System.out.println("Integer array: " + numbers3); // Note it's an Integer[] array
Set<String> noVowelSet = noVowels().collect(Collectors.toSet());
show("noVowelSet", noVowelSet);
String result = noVowels().limit(10).collect(Collectors.joining());
System.out.println("Joining: " + result);
result = noVowels().limit(10).collect(Collectors.joining(", "));
System.out.println("Joining with commas: " + result);
IntSummaryStatistics summary = noVowels().collect(Collectors.summarizingInt(String::length));
double averageWordLength = summary.getAverage();
double maxWordLength = summary.getMax();
System.out.println("Average word length: " + averageWordLength);
System.out.println("Max word length: " + maxWordLength);
System.out.println("forEach: ");
noVowels().limit(10).forEach(System.out::println);
}
}
1.9收集到映射表中
假设有一个
Stream<Person>
,并且想要将其元素收集到一个映射表中,这样后续就可以通过它们的id来查找人员了。Collectors.toMap
方法有两个函数引元,它们用来产生映射表的键和值:
Map<Integer, String> idToName = people.collect(Collectors.toMap(Person::getId, Person::getName));
通常情况下,值应该是实际的元素,因此第二个函数可以使用
Function.identity()
。
Map<Integer, Person> idToPerson = people.collect(Collectors.toMap(Person::getId, Function.identity()));
如果有多个元素具有相同的键,就会存在冲突,收集器将会抛出一个
IllegalStateException
异常。可以通过提供第三个函数引元来覆盖这种行为,该函数会针对给定的已有值和新值来解决冲突并确定键对应的值。这个函数应该返回已有值、新值或者它们的组合。
// 构建一个映射表,用来存储所有可用的locale中的语言,不关心同一种语言是否可能会出现两次,
// 因此只记录第一项
Stream<Locale> locales = Stream.of(Locale.getAvailableLocales());
Map<String, String> languageNames = locales.collect(Collectors.toMap(
Locale::getDisplayLanguage,
loc -> loc.getDisplayLanguage(loc),
(existingValue, newValue) -> existingValue
));
// 假设现在想要了解给定国家的所有语言,这样就需要一个Map<String, Set<String>>,
// 首先为每种语言都存储一个单例集,无论何时,只要找到了给定国家的新语言,
// 就会对已有集和新集进行并操作
Map<String, Set<String>> countryLanguageSets = locales.collect(Collectors.toMap(
Locale::getDisplayCountry,
l -> Collections.singleton(l.getDisplayLanguage()),
(a, b) -> { // Union of a and b
var union = new HashSet<String>(a);
union.addAll(b);
return union;
}
));
如果想要得到
TreeMap
,那么可以将构造器作为第四个引元来提供。必须提供一种合并函数。
Map<Integer, Person> idToPerson = people.collect(Collectors.toMap(
Person::getId,
Function.identity(),
(existingValue, newValue) -> {
throw new IllegalStateException();
},
TreeMap::new
));
public class CollectingInfoMaps {
public static class Person {
private int id;
private String name;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
@Override
public String toString() {
return getClass().getName() + "[id=" + id + ",name=" + name + "]";
}
}
public static Stream<Person> people() {
return Stream.of(new Person(1001, "Peter"), new Person(1002, "Paul"), new Person(1003, "Mary"));
}
public static void main(String[] args) {
Map<Integer, String> idToName = people().collect(
Collectors.toMap(Person::getId, Person::getName)
);
System.out.println("idToName: " + idToName);
Map<Integer, Person> idToPerson = people().collect(
Collectors.toMap(Person::getId, Function.identity())
);
System.out.println("idToPerson: " + idToPerson.getClass().getName() + idToPerson);
idToPerson = people().collect(
Collectors.toMap(Person::getId, Function.identity(),
(existingValue, newValue) -> {
throw new IllegalStateException();
},
TreeMap::new
)
);
System.out.println("idToPerson: " + idToPerson.getClass().getName() + idToPerson);
Stream<Locale> locales = Stream.of(Locale.getAvailableLocales());
Map<String, String> languageNames = locales.collect(
Collectors.toMap(Locale::getDisplayLanguage, l -> l.getDisplayLanguage(l), (existingValue, newValue) -> existingValue)
);
System.out.println("languageNames: " + languageNames);
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;
})
);
System.out.println("countryLanguageSets: " + countryLanguageSets);
}
}
1.10群组和分区
将具有相同特性的值群聚成组是非常常见的,并且
groupingBy
方法直接就支持它。例如,通过国家聚成组Locale
:
Map<String, List<Locale>> countryToLocales = locales.collect(Collectors.groupingBy(Locale::getCountry));
List<Locale> swissLocales = countryToLocales.get("CH");
当分类函数是断言函数(即返回
boolean
值的函数)时,流的元素可以分为两个列表:该函数返回true
的元素和其他的元素。在这种情况下,使用partitioningBy
比使用groupingBy
更高效。例如,将所有locale
分成使用英语和使用所有其他语言的两类:
Map<Boolean, List<Locale>> englishAndOtherLocales = locales.collect(
Collectors.partitioningBy(
l -> l.getLanguage().equals("en")
)
);
List<Locale> englishLocales = englishAndOtherLocales.get(true);
如果调用
groupingByConcurrent
方法,就会在使用并行流时获得一个被并行组装的并行映射表。这与toConcurrentMap
方法完全类似。
1.11下游收集器
groupingBy
方法会产生一个映射表,它的每个值都是一个列表。如果想要以某种方式来处理这些列表,就需要提供一个下游收集器。例如,如果想要获得集而不是列表,那么可以使用Collectors.toSet
收集器:
// 默认静态导入java.util.stream.Collector.*
Map<String, Set<Locale>> countryToLocaleSet = locales.collect(
groupingby(Locale::getCountry, toSet());
);
Java提供了多种可以将收集到的元素约简为数字的收集器:
counting
会产生收集到的元素的个数:
Map<String, Long> countryToLocaleCounts = locales.collect(
groupingBy(Locale::getCountry, counting())
);
summing(Int|Long|Double)
会接受一个函数作为引元,将该函数应用到下游元素中,并产生它们的和:
Map<String, Integer> stateToCityPopulation = cities.collect(
groupingBy(City::getState, summingInt(City::getPopulation))
);
maxBy
和minBy
会接受一个比较器,并分别产生下游元素中的最大值和最小值:
Map<String, Optional<City>> stateToLargestCity = cities.collect(
groupingBy(
City::getState,
maxBy(Comparator.comparing(City::getPopulation))
)
);
collectingAndThen
收集器在收集器后面添加了一个最终处理步骤。例如,如果想要知道有多少不同的结果,那么久可以将它们收集到一个集中,然后计算其尺寸:
Map<Character, Integer> stringCountsByStartingLetter = strings.collect(
groupingBy(
s -> s.charAt(0),
collectingAndThen(toSet(), Set::size)
)
);
mapping
收集器的做法正好相反,它会将一个函数应用于收集到的每个元素,并将结果传递给下游收集器。
// 按照首字符对字符串进行分组,在每个组内部,会计算字符串的长度,然后将这些长度收集到一个集中
Map<Character, Set<Integer>> stringLengthsByStartingLetter = strings.collect(groupingBy(
s -> s.charAt(0),
mapping(String::length, toSet())
));
// 另一种把某国所有的语言收集到一个集中的方式
Map<String, Set<String>> countryToLanguages = locales.collect(groupingBy(
Locale::getDisplayCountry,
mapping(Local::getDisplayLanguage, toSet())
));
还有一个
flatMapping
方法,可以与返回流的函数一起使用。
如果群组和映射函数的返回值为
int
、long
或double
,那么可以将元素收集到汇总统计对象中。例如:
// 可以从每个组的汇总统计对象中获取这些函数的总和、数量、平均值、最小值和最大值
Map<String, IntSummaryStatistics> stateToCityPopulationSummary = cities.collect(groupingBy(
City::getState,
summarizingInt(City::getPopulation)
));
filtering
收集器会将一个过滤器应用到每个组上。例如:
// States without large cities have empty sets
Map<String, Set<City>> largeCitiesByState = cities.collect(groupingBy(
City::getState,
filtering(
c -> c.getPopulation() > 500000,
toSet()
)
));
将收集器组合起来是一种很强大的方式,但是它也可能会导致产生非常复杂的表达式。
最佳用法是与groupingBy
和partitioningBy
一起处理下游的映射表中的值。否则,应该直接在流上应用诸如map
、reduce
、count
、max
或min
这样的方法。
public class DownstreamCollectors {
public static class City {
private String name;
private String state;
private int population;
public City(String name, String state, int population) {
this.name = name;
this.state = state;
this.population = population;
}
public String getName() {
return name;
}
public String getState() {
return state;
}
public int getPopulation() {
return population;
}
}
public static Stream<City> readCities(String filename) throws IOException {
return Files.lines(Paths.get(filename)).map(l -> l.split(", ")).map(a -> new City(a[0], a[1], Integer.parseInt(a[2])));
}
public static void main(String[] args) throws IOException{
Stream<Locale> locales = Stream.of(Locale.getAvailableLocales());
Map<String, Set<Locale>> countryToLocaleSet = locales.collect(groupingBy(Locale::getCountry, toSet()));
System.out.println("countryToLocaleSet: " + countryToLocaleSet);
locales = Stream.of(Locale.getAvailableLocales());
Map<String, Long> countryToLocaleCounts = locales.collect(groupingBy(Locale::getCountry, counting()));
System.out.println("countryToLocaleCounts: " + countryToLocaleCounts);
String path = "/Users/kirito/program/java/study/java_study/core_java_volume_two_11/src/main/resources/cities.txt";
Stream<City> cities = readCities(path);
Map<String, Integer> stateToCityPopulation = cities.collect(groupingBy(City::getState, summingInt(City::getPopulation)));
System.out.println("stateToCityPopulation: " + stateToCityPopulation);
cities = readCities(path);
Map<String, Optional<String>> stateToLongestCityName = cities.collect(groupingBy(City::getState, mapping(City::getName, maxBy(Comparator.comparing(String::length)))));
System.out.println("stateToLongestCityName: " + stateToLongestCityName);
locales = Stream.of(Locale.getAvailableLocales());
Map<String, Set<String>> countryToLanguages = locales.collect(groupingBy(Locale::getDisplayCountry, mapping(Locale::getDisplayName, toSet())));
System.out.println("countryToLanguages: " + countryToLanguages);
cities = readCities(path);
Map<String, IntSummaryStatistics> stateToCityPopulationSummary = cities.collect(groupingBy(City::getState, summarizingInt(City::getPopulation)));
System.out.println(stateToCityPopulationSummary.get("NY"));
cities = readCities(path);
Map<String, String> stateToCityNames = cities.collect(groupingBy(City::getState, reducing("", City::getName, (s, t) -> s.length() == 0 ? t : s + ", " + t)));
cities = readCities(path);
stateToCityNames = cities.collect(groupingBy(City::getState, mapping(City::getName, joining(", "))));
System.out.println("stateToCityNames: " + stateToCityNames);
}
}
1.12约简操作
reduce
方法是一种用于从流中计算某个值的通用机制,其最简单的形式将接受一个二元函数,并从前两个元素开始持续应用它。如果该函数是求和函数,那么就很容易解释这种机制:
List<Integer> values = ...;
// 可以写成reduce(Integer::sum)
Optional<Integer> sum = values.stream().reduce((x, y) -> x + y);
更一般地,可以使用任何约简操作将部分结果x与下一个值y组合起来以产生新的部分结果。
另一种看待约简的方式。给定约简操作op,该约简会产生v0 op v1 op v2 op …,其中将函数调用op(vi, vi+1)写作vi op vi+1。有很多种在实践中很有用的可结合操作,例如求和、乘积、字符串连接、最大值和最小值、求集的并与交等。
如果要用并行流来约简,那么这项约简操作必须是可结合的,即组合元素时使用的顺序不会产生任何影响。在数学标记法中,(x op y) op z必须等于x op (y op z)。减法是一个不可结合操作的例子。例如,(6 - 3) - 2 ≠ \neq = 6 - (3 - 2)。
通常,会有一个幺元值e使得e op x = x,可以使用这个元素作为计算的起点。例如,0是加法的幺元值。由此,可以使用第二种形式的reduce
:
// 如果流为空,则会返回幺元值,就再也不需要处理Optional类了
List<Integer> values = ...;
Integer sum = values.stream().reduce(0, (x, y) -> x + y);
现在,假设有一个对象流,并且想要对某些属性求和,例如字符串流中所有字符串的长度,那么就不能使用简单形式的
reduce
,而是需要(T, T) -> T
这样的函数,即引元和结果的类型相同的函数。但是假如在这种情况下,有两种类型:流的元素具有String
类型,而累积结果是整数。有一种形式的reduce
可以处理这种情况。
首先,需要提供一个累积器函数(total, word) -> total + word.length()
。这个函数会被反复调用,产生累积的总和。但是,当计算被并行化时,会有多个这种类型的计算,需要将它们的结果合并。因此,需要提供第二个函数来执行此处理:
int result = words.reduce(
0,
(total, word) -> total + word.length(),
(total1, total2) -> total1 + total2
);
在实践中,可能并不会频繁地用到
reduce
方法。通常,映射为数字流并使用其方法来计算总和、最大值和最小值会更容易。在这个特定的示例中,可以调用words.mapToInt(String::length).sum()
,因为它不涉及装箱操作,所以更简单也更高效。
有时reduce
会显得不够通用。例如,假设想要收集BitSet
中的结果。如果收集操作是并行的,那么就不能直接将元素放到单个BitSet
中,因为BitSet
对象不是线程安全的。因此,不能使用reduce
,因为每个部分都需要以其自己的空集开始,并且reduce
只能提供一个幺元值。此时,应该使用collect
,它会接受单个引元:
- 一个提供者,它会创建目标对象的新实例,例如散列集的构造器。
- 一个累积器,它会将一个元素添加到该目标上,例如
add
方法。- 一个组合器,它会将两个对象合并成一个,例如
addAll
。
BitSet result = stream.collect(BitSet::new, BitSet::set, BitSet::or);
1.13基本类型流
流库中具有专门的类型
IntStream
(short
、char
、byte
和boolean
)、LongStream
和DoubleStream
(float
)。
为了创建IntStream
,需要调用IntStream.of
和Arrays.stream
方法:
IntStream stream = IntStream.of(1, 2, 3, 5);
stream = Arrays.stream(values, from, to); // values is an int[] array
与对象流一样,还可以使用静态的
generate
和iterate
方法。此外,IntStream
和LongStream
有静态方法range
和rangeClosed
,可以生成步长为1的整数范围:
IntStream zeroToNinetyNine = IntStream.range(0, 100); // Upper bound is excluded
IntStream zeroToHundred = IntStream.rangeClosed(0, 100); // Upper bound is included
CharSequence
接口拥有codePoints
和chars
方法,可以生成由字符的unicode码或由utf-16编码机制的码元构成的IntStream
。
String sentence = "\uD835\uDD46 is the set of octonions.";
IntStream codes = sentence.codePoints();
当有一个对象流时,可以用
mapToInt
、mapToLong
或mapToDouble
将其转换为基本类型流。例如,如果有一个字符串流,并想将其长度处理为整数,那么就可以在IntStream
中实现此目的:
Stream<String> words = ...;
IntStream lengths = words.mapToInt(String::length);
为了将基本类型流转换为对象流,需要使用
boxed
方法:
Stream<Integer> integers = IntStream.range(0, 100).boxed();
通常,基本类型流上的方法与对象流上的方法类似。主要差异:
toArray
方法会返回基本类型数组。- 产生可选结果的方法会返回一个
OptionalInt
、OptionalLong
或OptionalDouble
。这些类与Optional
类似,但是具有getAsInt
、getAsLong
和getAsDouble
方法,而不是get
方法。- 具有分别返回总和、平均值、最大值和最小值的
sum
、average
、max
和min
方法。对象流没有定义这些方法。summaryStatistics
方法会产生一个类型为IntSummaryStatistics
、LongSummaryStatistics
或DoubleSummaryStatistics
的对象,它们可以同时报告流的总和、数量、平均值、最大值和最小值。
Random
类具有ints
、longs
和doubles
方法,它们会返回由随机数构成的基本类型流。如果需要的是并行流中的随机数,那么需要使用SplittableRandom
类。
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 {
IntStream is1 = IntStream.generate(() -> (int) (Math.random() * 100));
show("is1", is1);
IntStream is2 = IntStream.range(5, 10);
show("is2", is2);
IntStream is3 = IntStream.rangeClosed(5, 10);
show("is3", is3);
Path path = Paths.get("/Users/kirito/program/java/study/java_study/core_java_volume_two_11/src/main/resources/alice30.txt");
var contents = new String(Files.readString(path));
Stream<String> words = Stream.of(contents.split("\\PL+"));
IntStream is4 = words.mapToInt(String::length);
show("is4", is4);
var sentence = "\uD835\uDD46 is the set of octonions.";
System.out.println(sentence);
IntStream codes = sentence.codePoints();
System.out.println(codes.mapToObj(c -> String.format("%X", c)).collect(Collectors.joining()));
Stream<Integer> integers = IntStream.range(0, 100).boxed();
IntStream is5 = integers.mapToInt(Integer::intValue);
show("is5", is5);
}
}
1.14并行流
流使并行处理块操作变得很容易。这个过程几乎是自动的,但是需要遵守一些规则。
首先,必须有一个并行流。可以用Collection.parallelStream()
方法从任何集合中获取一个并行流:
Stream<String> parallelWords = words.parallelStream();
而且,
parallel
方法可以将任意的顺序流转换为并行流。
Stream<String> parallelWords = Stream.of(wordArray).parallel();
只要在终结方法执行时流处于并行模式,所有的中间流操作就将被并行化。
当流操作并行运行时,其目标是让其返回结果与顺序执行时返回的结果相同。重要的是,这些操作是无状态的,并且可以以任意顺序执行。
例如,下面的示例是一项无法完成的任务:
// 对字符串流中的所有短单词计数
var shortWords = new int[12];
words.parallelStream().forEach(
s -> {
if (s.length() < 12) {
shortWords[s.length()]++; // ERROR--race condition
}
}
);
System.out.println(Arrays.toString(shortWords));
传递给
forEach
的函数会在多个并发线程中运行,每个都会更新共享的数组。这是一种典型的竞争情况。
需要确保传递给并行流操作的任何函数都可以安全地并行执行,达到这个目的的最佳方式是远离易变状态。
Map<Integer, Long> shortWordCounts = words.parallelStream()
.filter(s -> s.length() < 12)
.collect(groupingBy(
String::length,
counting()
));
默认情况下,从有序集合(数组和列表)、范围、生成器和迭代器产生的流,或者通过调用
Stream.sorted
产生的流,都是有序的。它们的结果是按照原来元素的顺序累积的,因此是完全可预知的。如果运行相同的操作两次,将会得到完全相同的结果。
排序并不排斥高效的并行处理。例如,当计算stream.map(fun)
时,流可以被划分为n部分,它们会被并行地处理。然后,结果将会按照顺序重新组装起来。
当放弃排序需求时,有些操作可以被更有效地并行化。通过在流上调用Stream.unordered
方法,就可以明确表示对排序不感兴趣。Stream.distinct
就是从这种方式中获益的一种操作。在有序的流中,distinct
会保留所有相同元素中的第一个,这对并行化是一种阻碍,因为处理每个部分的线程在其之前的所有部分都被处理完之前,并不知道应该丢弃哪些元素。如果可以接受保留唯一元素中任意一个的做法,那么所有部分就可以并行地处理(使用共享的集合来跟踪重复元素)。
还可以通过放弃排序要求来提高limit
方法的速度。如果只想从流中取出任意n个元素而并不在意到底要获取哪些,那么可以调用:
Stream<String> sample = words.parallelStream().unordered().limit(n);
合并映射表的代价很高昂。正是这个原因,
Collectors.groupingByConcurrent
方法使用了共享的并发映射表。为了从并行化中获益,映射表中值的顺序不会与流中的顺序相同。
Map<Integer, List<String>> result = words.parallelStream().collect(
// Values aren't collected in stream order
Collectors.groupingByConcurrent(String::length)
);
当然,如果使用独立于排序的下游收集器,那么就不必在意了,例如:
Map<Integer, Long> wordCounts = words.parallelStream().collect(
groupingByConcurrent(String::length, counting())
);
不要指望通过将所有的流都转换为并行流就能够加速操作,要牢记:
- 并行化会导致大量的开销,只有面对非常大的数据集才划算。
- 只有在底层的数据源可以被有效地分隔为多个部分时,将流并行化才有意义。
- 并行流使用的线程池可能会因为诸如文件I/O或网络访问这样的操作被阻塞而饿死。
只有面对海量的内存数据和运算密集处理,并行流才会工作最佳。
public class ParallelStreams {
public static void main(String[] args) throws IOException {
var contents = new String(Files.readString(Paths.get("/Users/kirito/program/java/study/java_study/core_java_volume_two_11/src/main/resources/alice30.txt")));
List<String> wordList = List.of(contents.split("\\PL+"));
// Very bad code ahead
var shortWords = new int[10];
wordList.parallelStream().forEach(s -> {
if (s.length() < 10) {
shortWords[s.length()]++;
}
});
System.out.println(Arrays.toString(shortWords));
// Try again--the result will likely be different (and also wrong)
Arrays.fill(shortWords, 0);
wordList.parallelStream().forEach(s -> {
if (s.length() < 10) {
shortWords[s.length()]++;
}
});
System.out.println(Arrays.toString(shortWords));
// Remedy: Group and count
Map<Integer, Long> shortWordCounts = wordList.parallelStream().filter(s -> s.length() < 10).collect(groupingBy(String::length, counting()));
System.out.println(shortWordCounts);
// Downstream order not deterministic
Map<Integer, List<String>> result = wordList.parallelStream().collect(groupingByConcurrent(String::length));
System.out.println(result.get(14));
result = wordList.parallelStream().collect(groupingByConcurrent(String::length));
System.out.println(result.get(14));
Map<Integer, Long> wordCounts = wordList.parallelStream().collect(groupingByConcurrent(String::length, counting()));
System.out.println(wordCounts);
}
}