1.Java8的流库

Java8的流库

通过使用流,可以说明想要完成什么任务,而不是说明如何去实现它。将操作的调度留给具体实现去解决。

1.1从迭代到流的操作
long count = words.stream().filter(w -> w.length() > 12).count();

仅将stream修改为parallelStream就可以让流库以并行方式来执行。

流遵循了"做什么而非怎么做"的原则。
虽然表面上看起来和集合很类似,都可以转换和获取数据。但是,它们之间存在着显著的差异:

  1. 流并不存储其元素。这些元素可能存储在底层的集合中,或者是按需生成的。
  2. 流的操作不会修改其数据源。例如,filter方法不会从流中移除元素,而是会生成一个新的流,其中不包含被过滤掉的元素。
  3. 流的操作是尽可能惰性执行的。这意味着直至需要其结果时,操作才会执行。

操作流时的典型流程:

  1. 创建一个流。
  2. 指定将初始流转换为其他流的中间操作。可能包含多个步骤。
  3. 应用终止操作,从而产生结果。这个操作会强制执行之前的惰性操作。从此之后,这个流就再也不能用了。
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.3filtermapflatMap方法

流的转换会产生一个新的流,它的元素派生自另一个流中的元素。
filter的引元是Predicate<T>,即从Tboolean的函数。
通常,想要按照某种方式来转换流中的值,此时,可以使用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方法会返回流中元素的数量。
其他的简单约简还有maxmin,它们分别返回最大值和最小值。需要注意的是,这些方法返回的是一个类型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"));

还有allMatchnoneMatch方法,它们分别在所有元素和没有任何元素匹配谓词的情况下返回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>

但是这样就需要使用之前警告过要慎用的isPresentget方法。下面的调用显得更优雅:

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)这个调用在objnull时,会产生一个空的流,否则会产生一个只包含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))
);
  • maxByminBy会接受一个比较器,并分别产生下游元素中的最大值和最小值:
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方法,可以与返回流的函数一起使用。

如果群组和映射函数的返回值为intlongdouble,那么可以将元素收集到汇总统计对象中。例如:

// 可以从每个组的汇总统计对象中获取这些函数的总和、数量、平均值、最小值和最大值
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()
    )
));

将收集器组合起来是一种很强大的方式,但是它也可能会导致产生非常复杂的表达式。
最佳用法是与groupingBypartitioningBy一起处理下游的映射表中的值。否则,应该直接在流上应用诸如mapreducecountmaxmin这样的方法。

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,它会接受单个引元:

  1. 一个提供者,它会创建目标对象的新实例,例如散列集的构造器。
  2. 一个累积器,它会将一个元素添加到该目标上,例如add方法。
  3. 一个组合器,它会将两个对象合并成一个,例如addAll
BitSet result = stream.collect(BitSet::new, BitSet::set, BitSet::or);
1.13基本类型流

流库中具有专门的类型IntStream(shortcharbyteboolean)、LongStreamDoubleStream(float)。
为了创建IntStream,需要调用IntStream.ofArrays.stream方法:

IntStream stream = IntStream.of(1, 2, 3, 5);
stream = Arrays.stream(values, from, to);   // values is an int[] array

与对象流一样,还可以使用静态的generateiterate方法。此外,IntStreamLongStream有静态方法rangerangeClosed,可以生成步长为1的整数范围:

IntStream zeroToNinetyNine = IntStream.range(0, 100);   // Upper bound is excluded
IntStream zeroToHundred = IntStream.rangeClosed(0, 100);    // Upper bound is included

CharSequence接口拥有codePointschars方法,可以生成由字符的unicode码或由utf-16编码机制的码元构成的IntStream

String sentence = "\uD835\uDD46 is the set of octonions.";
IntStream codes = sentence.codePoints();

当有一个对象流时,可以用mapToIntmapToLongmapToDouble将其转换为基本类型流。例如,如果有一个字符串流,并想将其长度处理为整数,那么就可以在IntStream中实现此目的:

Stream<String> words = ...;
IntStream lengths = words.mapToInt(String::length);

为了将基本类型流转换为对象流,需要使用boxed方法:

Stream<Integer> integers = IntStream.range(0, 100).boxed();

通常,基本类型流上的方法与对象流上的方法类似。主要差异:

  • toArray方法会返回基本类型数组。
  • 产生可选结果的方法会返回一个OptionalIntOptionalLongOptionalDouble。这些类与Optional类似,但是具有getAsIntgetAsLonggetAsDouble方法,而不是get方法。
  • 具有分别返回总和、平均值、最大值和最小值的sumaveragemaxmin方法。对象流没有定义这些方法。
  • summaryStatistics方法会产生一个类型为IntSummaryStatisticsLongSummaryStatisticsDoubleSummaryStatistics的对象,它们可以同时报告流的总和、数量、平均值、最大值和最小值。

Random类具有intslongsdoubles方法,它们会返回由随机数构成的基本类型流。如果需要的是并行流中的随机数,那么需要使用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);
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值