关于java17:从头学Java17Stream-API一

43次阅读

共计 36397 个字符,预计需要花费 91 分钟才能阅读完成。

Stream API

Stream API 是依照 map/filter/reduce 办法解决内存中数据的最佳工具。
本系列中的教程蕴含从基本概念始终到 collector 设计和并行流。

在流上增加中继操作

将一个流 map 为另一个流

map 流是应用函数转换其元素。此转换可能会更改该流解决的元素的类型,但您也能够在不更改。

您能够应用 map()) 办法将一个流 map 为另一个流,该办法将此 Function 作为参数。map 流意味着该流解决的所有元素都将应用该函数进行转换。

代码模式如下:

List<String> strings = List.of("one", "two", "three", "four");
Function<String, Integer> toLength = String::length;
Stream<Integer> ints = strings.stream()
                              .map(toLength);

您能够此代码,并将其粘贴到 IDE 中以运行它。你不会看到任何货色,你可能想晓得为什么。

答案其实很简略:该流上没有定义末端操作。这段代码没有做任何事件。它不解决任何数据。

让咱们增加一个十分有用的末端操作collect(Collectors.toList()),它将解决后的元素放在一个列表中。如果您不确定此代码的真正作用,请不要放心; 咱们将在本教程的前面局部介绍这一点。代码将变为以下内容。

List<String> strings = List.of("one", "two", "three", "four");
List<Integer> lengths = strings.stream()
                               .map(String::length)
                               .collect(Collectors.toList());
System.out.println("lengths =" + lengths);

运行此代码将打印以下内容:

lengths = [3, 3, 5, 4]

您能够看到此模式创立了一个 Stream,由 map(String::length)) 返回。你也能够通过调用 mapToInt)()而不是惯例的map()) 调用来使其成为一个专门的 IntStream。这个mapToInt()) 办法将 ToIntFuction 作为参数。在上一示例中 .map(String::length) 更改为 .mapToInt(String::length) 不会创立编译器谬误。String::length 办法援用能够是两种类型:Function<String、Integer>ToIntFunction<String>

专用流没有 collect() 办法将 Collector 作参数。因而,如果用 mapToInt(),)则无奈再在列表中收集后果,至多不能应用此模式。让咱们获取无关该流的一些统计信息。这个 summaryStatistics()) 办法十分不便,并且仅在这些专门的原始类型流上可用。

List<String> strings = List.of("one", "two", "three", "four");
IntSummaryStatistics stats = strings.stream()
                                    .mapToInt(String::length)
                                    .summaryStatistics();
System.out.println("stats =" + stats);

后果如下:

stats = IntSummaryStatistics{count=4, sum=15, min=3, average=3,750000, max=5}

Stream 转到原始类型的流有三种办法:mapToInt)()、mapToLong()) 和 mapToDouble()。)

filter 流

filter 是在流解决中应用 Predicate 抛弃某些元素。此办法可用于对象流和原始类型流。

假如您须要计算长度为 3 的字符串。您能够编写以下代码来执行此操作:

List<String> strings = List.of("one", "two", "three", "four");
long count = strings.stream()
                    .map(String::length)
                    .filter(length -> length == 3)
                    .count();
System.out.println("count =" + count);

运行此代码将生成以下内容:

count = 2

请留神,您刚刚应用了 Stream API 的另一个末端操作 count(),)它只计算已解决元素的数量。此办法返回long,您能够应用它计算很多元素。比 ArrayList 的更多。

flatmap 流以解决 1:p 关系

让咱们在一个示例中查看 flatMap) 操作。假如您有两个实体:StateCity。一个 state 实例蕴含多个 city 实例,存储在一个列表中。

这是 City 类的代码。

public class City {
    
    private String name;
    private int population;

    // constructors, getters
    // toString, equals and hashCode
}

这是 State 类的代码,以及与 City 类的关系。

public class State {
    
    private String name;
    private List<City> cities;

    // constructors, getters
    // toString, equals and hashCode
}

假如您的代码正在解决状态列表,并且在某些时候您须要计算所有城市的人口。

您能够编写以下代码:

List<State> states = ...;

int totalPopulation = 0;
for (State state: states) {for (City city: state.getCities()) {totalPopulation += city.getPopulation();
    }
}

System.out.println("Total population =" + totalPopulation);

此代码的外部循环是 map-reduce 的一种模式,您能够应用以编写:

totalPopulation += state.getCities().stream().mapToInt(City::getPopulation).sum();

外层和内层有点不匹配,将流放入 states 循环中不是一个很好的代码模式。

这正是 flatmap 的作用。此运算符在对象之间关上一对多关系,并基于这些关系创立流。flatMap()) 办法将一个非凡函数作为参数,返回 Stream 对象。给定类和另一个类之间的关系由此函数定义。

在咱们的示例中,此函数很简略,因为 State 类中有一个List<City>。所以你能够按以下形式编写它。

Function<State, Stream<City>> stateToCity = state -> state.getCities().stream();

List 不是强制性的。假如您有一个蕴含 Map <String,Country>Continent 类,其中键是国家 / 地区的代码(CAN 示意加拿大,MEX 示意墨西哥,FRA 示意法国等)。假如该类有一个返回此 map 的办法getCountries()

这种状况下,能够通过这种形式编写此函数。

Function<Continent, Stream<Country>> continentToCountry = 
    continent -> continent.getCountries().values().stream();

flatMap()) 办法分两个步骤中解决流。

  • 第一步,应用此函数 map 流的所有元素。从 Stream<State> 创立一个 Stream<Stream<City>>,因为每个州都 map 为城市流。
  • 第二步包含 展平 产生的流。并不是城市流的流(每个州一个流),您最终会失去一个繁多的流,其中蕴含所有州的 所有城市

因而,应用 flatmap,之前的嵌套 for 编写的代码能够改写为:

List<State> states = ...;

int totalPopulation = 
        states.stream()
              .flatMap(state -> state.getCities().stream())// 对每个 state,都转换为 city 流,最初合并
              .mapToInt(City::getPopulation)
              .sum();

System.out.println("Total population =" + totalPopulation);

应用 flatmap 和 MapMulti 验证元素转换

flatMap) 可用于验证流元素的转换。

假如您有一个示意整数的字符串流。您须要应用 Integer.parseInt()) 将它们转换为整数。可怜的是,其中一些字符串有问题:兴许有些字符串为空,null,或者开端有额定的空白字符。所有这些都会使解析失败,并呈现 NumberFormatException。当然,您能够尝试 filter 此流,用 Predicate 删除谬误的字符串,但最平安的办法是应用 try-catch 模式。

尝试应用 filter 不是正确的办法。您要编写的 Predicate 将如下所示。

Predicate<String> isANumber = s -> {
    try {int i = Integer.parseInt(s);
        return true;
    } catch (NumberFormatException e) {return false;}
};

第一个缺点是您须要理论进行转换以查看它是否无效。而后,您将不得不在 map 函数中再次执行此操作:不要这样做!第二个缺点是,从 catch 块 return,绝不是一个好主见。

您真正须要做的是,当此字符串中有一个正确的整数时返回一个整数,如果有问题,则什么都不返回。这是 flatmap 的工作。如果能够解析整数,则能够返回蕴含后果的流。另一种状况下,您能够返回空流。

而后,能够编写以下函数。

Function<String, Stream<Integer>> flatParser = s -> {
    try {return Stream.of(Integer.parseInt(s));
    } catch (NumberFormatException e) { }
    return Stream.empty();};

List<String> strings = List.of("1", "","2","3 ","", "3");
List<Integer> ints = 
    strings.stream()
           .flatMap(flatParser)
           .collect(Collectors.toList());
System.out.println("ints =" + ints);

运行此代码将生成以下后果。所有有问题的字符串都已静默删除。

ints = [1, 2, 3]

这种 flatmap 代码的应用成果很好,但它有一个开销:为流的每个元素都会创立一个流。从 Java SE 16 开始,Stream API 中增加了一个办法:当您创立 零个或一个对象的多个流 时。此办法称为 mapMulti(),) 并将 BiConsumer 作为参数。

BiConsumer 应用两个参数:

  • 须要 map 的流元素
  • BiConsumer 须要对 map 后果调用的Consumer

应用元素调用 Consumer 会将该元素增加到生成的流中。如果 map 无奈实现,则 biconsumer 不会调用此消费者,并且不会增加任何元素。

让咱们用这个 mapMulti()) 办法重写你的模式。

List<Integer> ints =
        strings.stream()
               .<Integer>mapMulti((string, consumer) -> {// 对每一个 str
                    try {consumer.accept(Integer.parseInt(string));// 都转换为 Integer
                    } catch (NumberFormatException ignored) {// 去掉异样}
               })
               .collect(Collectors.toList());
System.out.println("ints =" + ints);

运行此代码会产生与以前雷同的后果。所有有问题的字符串都已被静默删除,但这一次,没有创立其余流。

ints = [1, 2, 3]

若要应用此办法,须要通知编译器 Consumer 的类型。这是通过这种非凡语法实现的,您能够在调用 mapMulti()) 之前定义此类型。它不是您在 Java 代码中常常看到的语法。您能够在动态和非动态上下文中应用它。

删除反复项并对流进行排序

Stream API 有两个办法,distinct()) 和 sorted(),)去重和排序。distinct())办法应用 hashCode)()和 equals()) 办法来发现反复项。sorted()) 办法有一个重载,它须要一个 comparator,它将用于比拟和排序流的元素。如果未提供,则假设流的元素具备可比性。否则,则会引发 ClassCastException

您可能还记得本教程的前一部分,流应该是不存储任何数据的空对象。此规定也有例外,这两个办法就是。

事实上,为了发现反复项,distinct()) 办法须要存储流的元素。当它解决一个元素时,它首先查看该元素是否曾经被看到。

sorted()) 办法也是如此。此办法须要存储所有元素,而后在外部缓冲区中对它们进行排序,而后再将它们发送到管道的下一步。

distinct())办法能够用于非绑定(有限)流,而 sorted()) 不能。

限度和跳过流的元素

Stream API 提供了两种抉择流元素的办法:基于索引或应用 Predicate。

第一种办法,应用 skip)()和 limit()) 办法,两者都将 long 作为参数。应用这些办法时,须要防止一个小陷阱。您须要记住,每次在流中调用中继办法时,都会创立一个新流。因而,如果您在 skip()) 之后调用 limit(),)请不要遗记从该新流开始计算。

假如您有一个蕴含所有整数的流,从 1 开始。您须要抉择 3 到 8 之间的整数。正确的代码如下。

List<Integer> ints = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9);

List<Integer> result = 
    ints.stream()
        .skip(2)// 产生了新流
        .limit(5)// 不是 limit(8)
        .collect(Collectors.toList());

System.out.println("result =" + result);

此代码打印以下内容。

result = [3, 4, 5, 6, 7]

Java SE 9 又引入了两种办法。它不是依据元素在流中的索引跳过和限度元素,而是依据 Predicate。

  • dropWhile(Predicate))如果 Predicate 为 true,始终跳过元素,直到 Predicate 为 false。此时,该流前面所有元素都将传输到下一个流。
  • takeWhile(Predicate))做相同的事件:如果 Predicate 为 true, 它始终将元素传输到下一个流,直到 Predicate 为 false,前面都跳过。 这个是短路的

请留神,这些办法的工作形式相似于门。一旦 dropWhile()) 关上了门让解决后的元素流动,它就不会敞开它。一旦 takeWhile()) 敞开了门,它就不能从新关上它,没有更多的元素将被发送到下一个操作。

串联流

Stream API 提供了多种模式,可将多个流连接成一个。最显著的办法是应用 Stream 接口中定义的工厂办法:concat()。)

此办法采纳两个流并生成一个流,其中蕴含第一个流生成的元素,而后是第二个流的元素。

您可能想晓得为什么此办法不必 vararg 来连贯任意数量的流。如果你有两个以上,JavaDoc API 文档倡议你应用另一种模式,基于 flatmap。

让咱们在一个例子上看看这是如何工作的。

List<Integer> list0 = List.of(1, 2, 3);
List<Integer> list1 = List.of(4, 5, 6);
List<Integer> list2 = List.of(7, 8, 9);

// 1st pattern: concat
List<Integer> concat = 
    Stream.concat(list0.stream(), list1.stream())
          .collect(Collectors.toList());

// 2nd pattern: flatMap
List<Integer> flatMap =
    Stream.of(list0.stream(), list1.stream(), list2.stream())// 相似 city 的外层组成的流
          .flatMap(Function.identity())// 变成了 city 流
          .collect(Collectors.toList());

System.out.println("concat  =" + concat);
System.out.println("flatMap =" + flatMap);

运行此代码将产生以下后果:

concat  = [1, 2, 3, 4, 5, 6]
flatMap = [1, 2, 3, 4, 5, 6, 7, 8, 9]

最好应用 flatMap)()形式的起因是 concat()) 在连贯期间会创立中继流。当您应用 Stream.concat()) 时,会创立一个新流来连贯您的两个流。如果须要连贯三个流,则最终将创立一个第一个流来解决第一个串联,第二个流用于第二个串联。因而,每个串联都须要一个很快就会被抛弃的流。

应用 flatmap 模式,您只需创立一个流来保留所有流并执行 flatmap。开销要低得多。

您可能想晓得为什么增加了这两种模式。看起来 concat()) 并不是很有用。事实上,由 concat 和 flatmap 模式产生的流之间存在轻微的区别。

如果连贯的两个流的源的大小已知,则生成的流的大小也是已知的。实际上,它只是两个串联流的总和。

在流上应用 flatmap 可能会创立未知数量的元素,以便在生成的流中进行解决。Stream API 会 失落 对元素 数量 的跟踪。

换句话说:concat 产生一个 SIZED 流,而 flatmap 不会。此 SIZED 属性是流可能具备的一种属性,本教程稍后将介绍。

调试流

有时,在运行时能查看流解决的元素可能很不便。Stream API 有一个办法:peek()) 办法。此办法用于调试数据处理管道。不应在生产代码中应用此办法。

相对不要应用此办法在应用程序中执行一些副作用。

此办法将 Consumer 作为参数,将每个元素上调用。让咱们实际效果。

List<String> strings = List.of("one", "two", "three", "four");
List<String> result =
        strings.stream()
                .peek(s -> System.out.println("Starting with =" + s))
                .filter(s -> s.startsWith("t"))
                .peek(s -> System.out.println("Filtered =" + s))
                .map(String::toUpperCase)
                .peek(s -> System.out.println("Mapped =" + s))
                .collect(Collectors.toList());
System.out.println("result =" + result);

如果运行此代码,您将在管制台上看到以下内容。

Starting with = one
Starting with = two
Filtered = two
Mapped = TWO
Starting with = three
Filtered = three
Mapped = THREE
Starting with = four
result = [TWO, THREE]

让咱们剖析一下这个输入。

  1. 要解决的第一个元素是one。你能够看到它被 filter 掉了。
  2. 第二个是two。此元素通过 filter,而后 map 为大写。而后将其增加到后果列表中。
  3. 第三个是three,它也通过 filter,并且在增加到后果列表之前也 map 为大写。
  4. 第四个也是最初一个是 four 被 filter 步骤回绝的

有一点你在本教程后面看到,当初很显著:流的确解决了它必须一一解决的所有元素,从流的开始到完结。这在之前曾经提到过,当初你能够看到它的实际效果。

您能够看到,此 peek(System.out::println) 模式对于一一跟踪流解决的元素十分有用,而无需调试代码。调试流很艰难,因为须要小心搁置断点的地位。大多数状况下,在流解决上搁置断点会跳转到 Stream 接口的实现。这不是你须要的。您须要将这些断点放在 lambda 表达式的代码中。

创立流

创立流

在本教程中,您曾经创立了许多流,所有这些都是通过调用 Collection 接口的 stream()) 办法创立的。此办法十分不便:以这种形式创立流只须要两行简略的代码,您能够应用此流来试验 Stream API 的简直任何性能。

如您所见,还有许多其余办法。理解这些办法后,您能够在应用程序中的许多地位利用 Stream API,并编写更具可读性和可维护性的代码。

让咱们疾速浏览您将在本教程中看到的内容,而后再深入研究它们中的每一个。

第一组模式应用 Stream 接口中的工厂办法。应用它们,您能够从以下元素创立流:

  • vararg 参数;
  • supplier;
  • unary operator,从前一个元素生成下一个元素;
  • builder。

您甚至能够创立空流,这在某些状况下可能很不便。

您曾经看到能够在汇合上创立流。如果您领有的只是一个 iterator,而不是一个成熟的汇合,那么有一个模式适宜您:您能够在 iterator 上创立流。如果你有一个数组,那么还有一个模式能够在数组的元素上创立一个流。

它并不止于此。JDK 中的许多模式也已增加到家喻户晓的对象中。而后,您能够从以下元素创立流:

  • 字符串的字符;
  • 文本文件的行;
  • 通过应用正则表达式拆分字符串来创立的元素;
  • 一个随机变量,能够创立随机数流。

您还能够应用 builder 模式创立流。

从汇合或 iterator 创立流

您曾经晓得 Collection 接口中有一个可用的 stream())。这可能是创立流的最经典办法。

在某些状况下,您可能须要在 map 内容上创立流。Map 接口中没有 stream() 办法,因而无奈间接创立此类流。然而,您能够通过三个汇合拜访 map 的内容:

  • 键的汇合,keySet())
  • 键值对的汇合,entrySet())
  • 值的汇合,values()。)

Stream API 提供了一种从简略 iterator 创立流的模式,它可能是在非标准数据源上创立流的十分不便的办法。模式如下。

Iterator<String> iterator = ...;

long estimateSize = 10L;
int characteristics = 0;
Spliterator<String> spliterator = Spliterators.spliterator(iterator, estimateSize, characteristics);

boolean parallel = false;
Stream<String> stream = StreamSupport.stream(spliterator, parallel);

此模式蕴含几个神奇元素,本教程稍后将介绍。让咱们疾速浏览它们。

estimateSize是您认为此流将生产的元素数。在某些状况下,此信息很容易取得:例如,如果要在数组或汇合上创立流。但在某些状况下是未知的。

本教程稍后将介绍 characteristics 参数。它用于优化数据的解决。

parallel参数告知 API 要创立的流是否为并行流。本教程稍后将介绍。

创立空流

让咱们从最简略的开始:创立一个空流。Stream接口中有一个工厂办法。您能够通过以下形式应用它。

Stream<String> empty = Stream.empty();
List<String> strings = empty.collect(Collectors.toList());

System.out.println("strings =" + strings);

运行此代码会在主机上显示以下内容。

strings = []

在某些状况下,创立空流可能十分不便。事实上,您在本教程的前一部分看到了一个。您看到的模式应用空流和 flatmap 从流中删除有效元素。从 Java SE 16 开始,此模式已被 mapMulti()) 模式所取代。

从 vararg 或数组创立流

两种模式十分类似。第一个在 Stream 接口中应用 of()) 工厂办法。第二个应用 Arrays 工厂类的 stream()) 工厂办法。事实上,如果你查看 Stream.of()) 办法的源代码,你会看到它调用了 Arrays.stream()。)

这是第一个理论模式。

Stream<Integer> intStream = Stream.of(1, 2, 3);
List<Integer> ints = intStream.collect(Collectors.toList());

System.out.println("ints =" + ints);

运行第一个示例将提供以下内容:

ints = [1, 2, 3]

这是第二个。

String[] stringArray = {"one", "two", "three"};
Stream<String> stringStream = Arrays.stream(stringArray);
List<String> strings = stringStream.collect(Collectors.toList());

System.out.println("strings =" + strings);

运行第二个示例将提供以下内容:

strings = [one, two, three]

从 supplier 创立流

Stream 接口上有两种工厂办法。

第一个是 generate(),)它以 supplier 为参数。每次须要新元素时,都会调用该 supplier。

您能够应用以下代码创立这样的流,但不要这样做!

Stream<String> generated = Stream.generate(() -> "+");
List<String> strings = generated.collect(Collectors.toList());

如果你运行这段代码,你会发现它永远不会进行。如果您这样做并且有足够的急躁,您可能会看到 OutOfMemoryError。如果没有,最好通过 IDE 终止应用程序。它真的产生了有限的流。

咱们还没有介绍这一点,但领有这样的流是齐全非法的!您可能想晓得它们有什么用?事实上有很多。要应用它们,您须要在某个时候剪切此流,而 Stream API 为您提供了几种办法来执行此操作。你曾经看到了一个,还有更多。

你看到的那个是调用该流上的 limit()。)让咱们重写后面的示例,并修复它。

Stream<String> generated = Stream.generate(() -> "+");
List<String> strings = 
        generated
           .limit(10L)
           .collect(Collectors.toList());

System.out.println("strings =" + strings);

运行此代码将打印以下内容。

strings = [+, +, +, +, +, +, +, +, +, +]

limit()) 办法称为 短路 办法:它能够进行流元素的生产。您可能还记得,流中的数据是一次性解决的:每个元素遍历流中定义的所有操作,从第一个到最初一个。这就是为什么这个 limit 操作能够进行生成更多元素。

从 unary operator 和种子创立流

如果您须要生成恒定的流,应用 supplier 十分有用。如果你须要一个具备不同值的有限流,那么你能够应用 iterate()) 模式。

此模式实用于种子,种子是第一个生成的元素。而后,它应用 UnaryOperator 通过转换前一个元素来生成流的下一个元素。

Stream<String> iterated = Stream.iterate("+", s -> s + "+");
iterated.limit(5L).forEach(System.out::println);

您应该看到以下后果。

+
++
+++
++++
+++++

应用此模式时,不要遗记限度流解决的元素数。

从 Java SE 9 开始,此模式具备重载,它将 Predicate 作为参数。当此 Predicate 变为 false 时,iterate()) 办法将进行生成元素。后面的代码能够通过以下形式应用此模式。

Stream<String> iterated = Stream.iterate("+", s -> s.length() <= 5, s -> s + "+");
iterated.forEach(System.out::println);

运行此代码会失去与上一个代码雷同的后果。

从一系列数字创立流

应用以前的模式创立一系列数字很容易。然而,应用专门的数字流及其 range()) 工厂办法会更容易。

range()) 办法采纳初始值和范畴的下限(不蕴含)。也能够在 rangeClosed()) 办法中蕴含下限。调用 LongStream.range(0L,10L)) 将简略地生成一个流,其中所有 long 都在 0 到 9 之间。

这个 range()) 办法也能够用来遍历数组的元素。这是您能够做到这一点的办法。

String[] letters = {"A", "B", "C", "D"};
List<String> listLetters =
    IntStream.range(0, 10)
             .mapToObj(index -> letters[index % letters.length])
             .collect(Collectors.toList());
System.out.println("listLetters =" + listLeters);

后果如下。

listLetters = [A, B, C, D, A, B, C, D, A, B

基于此模式,您能够做很多事件。请留神,因为 IntStream.range)()创立了一个 IntStream(原始类型流),因而您须要应用 mapToObj()) 办法将其 map 为对象流。

创立随机数流

Random类用于创立随机数字序列。从 Java SE 8 开始,已向此类增加了几个办法来创立不同类型的随机数流int,long,double

您能够创立提供种子参数的 Random 实例。此种子是一个long。随机数取决于该种子。对于给定的种子,您将始终取得雷同的数字序列。这在许多状况下可能很不便,包含编写测试。这种状况下,您能够依赖事后晓得的数字序列。

有三种办法能够生成这样的流,它们都在 Random 类中定义:ints())、longs()) 和doubles()。)

所有这些办法都有几个重载可用,它们承受以下参数:

  • 此流将生成的元素数;
  • 生成的随机数的下限和上限。

上面是生成 10 个介于 1 和 5 之间的随机整数的第一种代码模式。

Random random = new Random(314L);
List<Integer> randomInts = 
    random.ints(10, 1, 5)
          .boxed()
          .collect(Collectors.toList());
System.out.println("randomInts =" + randomInts);

如果您应用的种子与此示例中应用的种子雷同,则控制台中将具备以下内容。

randomInts = [4, 4, 3, 1, 1, 1, 2, 2, 4, 2]

请留神,咱们在专用数字流中应用了 boxed()) 办法,它只是将此流 map 为等效的包装器类型流。因而,通过此办法将 IntStream map 为 Stream<Integer>

这是生成随机布尔值流的第二种模式。该流的任何元素都是 true,概率为 80%。

Random random = new Random(314L);
List<Boolean> booleans =
    random.doubles(1_000, 0d, 1d)
          .mapToObj(rand -> rand <= 0.8) // you can tune the probability here
          .collect(Collectors.toList());

// Let us count the number of true in this list
long numberOfTrue =
    booleans.stream()
            .filter(b -> b)// 返回 boolean
            .count();
System.out.println("numberOfTrue =" + numberOfTrue);

如果您应用的种子与咱们在本示例中应用的种子雷同,您将看到以下后果。

numberOfTrue = 773

您能够调整此模式以生成具备所需概率的任何类型的对象。上面是另一个示例,它生成带有字母 A、B、C 和 D 的流。每个字母的概率如下:

  • A 的 50%;
  • B 的 30%;
  • C 的 10%;
  • D 的 10%。
Random random = new Random(314L);
List<String> letters =
    random.doubles(1_000, 0d, 1d)
          .mapToObj(rand ->
                    rand < 0.5 ? "A" : // 50% of A
                    rand < 0.8 ? "B" : // 30% of B
                    rand < 0.9 ? "C" : // 10% of C
                                 "D")  // 10% of D
          .collect(Collectors.toList());

Map<String, Long> map =
    letters.stream()
            .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));

map.forEach((letter, number) -> System.out.println(letter + "::" + number));

应用雷同的种子,您将取得以下后果。

A :: 470
B :: 303
C :: 117
D :: 110

此时,应用此 groupingBy()) 构建 map 可能看起来不明确。不必放心,本教程稍后将介绍。

从字符串的字符创立流

String 类在 Java SE 8 中增加了一个 chars()) 办法。此办法返回一个 IntStream,该 IntStream 为您提供此字符串的字符。

每个字符都作为一个整数给出,ASCII 代码。在某些状况下,您可能须要将此整数转换为字符串,只需保留此字符即可。

您有两种模式能够执行此操作,具体取决于您应用的 JDK 版本。

在 Java SE 10 之前,您能够应用以下代码。

String sentence = "Hello Duke";
List<String> letters =
    sentence.chars()
            .mapToObj(codePoint -> (char)codePoint)
            .map(Object::toString)
            .collect(Collectors.toList());
System.out.println("letters =" + letters);

在 Java SE 11 的 Character 类中增加了一个 toString()) 工厂办法,您能够应用它来简化此代码。

String sentence = "Hello Duke";
List<String> letters =
    sentence.chars()
            .mapToObj(Character::toString)
            .collect(Collectors.toList());
System.out.println("letters =" + letters);

两个代码都打印出以下内容。

letters = [H, e, l, l, o,  , D, u, k, e]

从文本文件的行创立流

可能在文本文件上关上流是一种十分弱小的模式。

Java I/O API 有一个从文本文件中读取一行的模式:BufferedReader.readLine()。)您能够从循环调用此办法,并逐行读取整个文本文件以对其进行解决。

应用 Stream API 解决这些行可为你提供更具可读性和更易于保护的代码。

有几种模式能够创立这样的流。

如果须要基于 buffered reader 重构现有代码,则能够应用在此对象上定义的 lines()) 办法。如果要编写新代码,则能够应用工厂办法 Files.lines()。)最初一种办法将 Path 作为参数,并具备一个重载办法,采纳 CharSet为参数,以防您正在读取的文件未以 UTF-8 编码。

您可能晓得,文件资源与任何 I/O 资源一样,当您不再须要它时,应将其敞开。

好消息是 Stream 接口实现了AutoCloseable。流自身就是一个资源,您能够在须要时敞开它。下面您看到的所有示例都运行在内存中,并不需要,但某种状况下必定是必须的。

上面是计算日志文件中正告数量的示例。

Path log = Path.of("/tmp/debug.log"); // adjust to fit your installation
try (Stream<String> lines = Files.lines(log)) {
    
    long warnings = 
        lines.filter(line -> line.contains("WARNING"))
             .count();
    System.out.println("Number of warnings =" + warnings);
    
} catch (IOException e) {// do something with the exception}

try-with-resources 模式将调用流的 close()) 办法,该办法将正确敞开已解析的文本文件。

从正则表达式创立流

这一系列模式的最初一个示例是增加到 Pattern 类的办法,用于在将正则表达式利用于字符串生成的元素上创立流。

假如您须要在给定的分隔符上拆分字符串。您有两种模式来执行此操作。

  • 你能够调用 String.split()) 办法;
  • 或者,您能够应用 Pattern.compile().split()) 模式。

这两种模式都为您提供了一个字符串数组,其中蕴含拆分的后果元素。

您看到了从此数组创立流的模式。让咱们编写此代码。

String sentence = "For there is good news yet to hear and fine things to be seen";

String[] elements = sentence.split(" ");
Stream<String> stream = Arrays.stream(elements);

Pattern类也有一个适宜你的办法。你能够调用 Pattern.compile().splitAsStream()。)上面是能够应用此办法编写的代码。

String sentence = "For there is good news yet to hear and fine things to be seen";

Pattern pattern = Pattern.compile(" ");
Stream<String> stream = pattern.splitAsStream(sentence);
List<String> words = stream.collect(Collectors.toList());

System.out.println("words =" + words);

运行此代码将生成以下后果。

words = [For, there, is, good, news, yet, to, hear, and, fine, things, to, be, seen]

您可能想晓得这两种模式中哪一种是最好的。要答复这个问题,您须要认真查看第一种模式。首先,创立一个数组来存储拆分的后果,而后在此数组上创立一个流。

在第二种模式中没有创立数组,因而开销更少。

您曾经看到某些流可能应用 短路 操作(本教程稍后将具体介绍这一点)。如果您有这样的流,拆分整个字符串并创立生成的数组可能是一个重要但无用的开销。不确定流管道是否会应用其所有元素来生成后果。

即便您的流须要应用所有元素,将所有这些元素存储在数组中依然是不必要的。

因而,在两种状况下,应用 splitAsStream()) 模式更好。它在内存和 CPU 方面更好。

应用 builder 模式创立流

应用此模式创立流的过程分为两个步骤。首先,在 builder 中增加流将应用的元素。而后,从此 builder 创立流。应用 builder 创立流后,您将无奈向其增加更多元素,也无奈再次应用它来构建另一个流。如果你这样做,你会失去一个IllegalStateException

模式如下。

Stream.Builder<String> builder = Stream.<String>builder();

builder.add("one")
       .add("two")
       .add("three")
       .add("four");

Stream<String> stream = builder.build();

List<String> list = stream.collect(Collectors.toList());
System.out.println("list =" + list);

运行此代码将打印以下内容。

list = [one, two, three, four]

在 HTTP 源上创立流

咱们在本教程中介绍的最初一个模式是对于剖析 HTTP 响应的主体。您看到您能够在文本文件的行上创立流,也能够在 HTTP 响应的注释上执行雷同的操作。此模式由增加到 JDK 11 的 HTTP Client API 提供。

这是它的工作原理。咱们将在在线提供的文本中应用它:查尔斯狄更斯的《双城记》,由古腾堡我的项目在线提供:https://www.gutenberg.org/files/98/98-0.txt

文本文件的结尾提供无关文本自身的信息。这本书的结尾是“A TALE OF TWO CITIES”。文件的开端是散发此文件的许可证。

咱们只须要本书的文本,并心愿删除此分布式文件的页眉和页脚。

// The URI of the file
URI uri = URI.create("https://www.gutenberg.org/files/98/98-0.txt");

// The code to open create an HTTP request
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(uri).build();


// The sending of the request
HttpResponse<Stream<String>> response = client.send(request, HttpResponse.BodyHandlers.ofLines());
List<String> lines;
try (Stream<String> stream = response.body()) {
    lines = stream
        .dropWhile(line -> !line.equals("A TALE OF TWO CITIES"))
        .takeWhile(line -> !line.equals("*** END OF THE PROJECT GUTENBERG EBOOK A TALE OF TWO CITIES ***"))
        .collect(Collectors.toList());
}
System.out.println("# lines =" + lines.size());

运行此代码将打印出以下内容。

# lines = 15904

流由您提供的 body handler 创立,作为 send()) 办法的参数。HTTP Client API 为您提供了多个 body handler。下面是由工厂办法 HttpResponse.BodyHandlers.ofLines()) 创立的。这种生产响应主体的形式十分节俭内存。如果认真编写流,响应的注释将永远不会存储在内存中。

咱们决定将所有文本行放在一个列表中,然而,您不肯定须要这样做。实际上,大多数状况下,将此数据存储在内存中可能是一个坏主意。

reduce 流

reduce 流

到目前为止,您在本教程中理解到,reduce 流包含以相似于 SQL 语言中的形式聚合该流的元素。在您运行的示例中,您还应用 collect(Collectors.toList()) 模式在列表中收集了您构建的流的元素。所有这些操作在 Stream API 中称为 末端操作,包含 reduce 流。

在流上调用末端操作时,须要记住两件事。

  1. 没有末端操作的流不会解决任何数据。如果您在应用程序中发现这样的流,则很可能是一个谬误。
  2. 一个流同时只能有一个中继或末端操作调用。您不能重复使用流; 如果你尝试这样做,你会失去一个IllegalStateException

应用 binary operator 来 reduce 流

在 Stream 接口中定义的 reduce()) 办法有三个重载。它们都采纳 BinaryOperator 对象作为参数。让咱们看看如何应用这个 binary operator。

让咱们举个例子。假如您有一个整数列表,您须要计算这些整数的总和。您能够应用经典的 for 循环模式编写以下代码来计算此总和。

List<Integer> ints = List.of(3, 6, 2, 1);

int sum = ints.get(0);
for (int index = 1; index < ints.size(); index++) {sum += ints.get(index);
}
System.out.println("sum =" + sum);

运行它会打印出以下后果。

sum = 12

此代码的作用如下。

  1. 将列表中的前两个元素相加。
  2. 而后取下一个元素并将其求和到您计算的局部总和。
  3. 反复该过程,直到达到列表开端。

如果仔细检查此代码,能够看到能够应用 binary operator 对 SUM 运算符进行建模,以取得雷同的后果。而后,代码将变为以下内容。

List<Integer> ints = List.of(3, 6, 2, 1);
BinaryOperator<Integer> sum = (a, b) -> a + b;

int result = ints.get(0);
for (int index = 1; index < ints.size(); index++) {result = sum.apply(result, ints.get(index));
}
System.out.println("sum =" + result);

当初您能够看到此代码仅依赖于 binary operator 自身。假如您须要计算 一个 MAX。您须要做的就是为此提供正确的 binary operator。

List<Integer> ints = List.of(3, 6, 2, 1);
BinaryOperator<Integer> max = (a, b) -> a > b ? a: b;

int result = ints.get(0);
for (int index = 1; index < ints.size(); index++) {result = max.apply(result, ints.get(index));
}
System.out.println("max =" + result);

论断是,您的确能够通过仅提供仅对两个元素进行操作的 binary operator 来计算 reduce。这就是 reduce()) 办法在 Stream API 中的工作形式。

抉择能够并行应用的 binary operator

不过,您须要理解两个注意事项。让咱们在这里介绍第一个,在下一节中介绍第二个。

第一个是能够并行计算的流。本教程稍后将更具体地介绍这一点,但当初须要探讨它,因为它对这个 binary operator 有影响。数据源分为两局部,每局部独自解决。每个过程都与您刚刚看到的过程雷同,它应用 binary operator。而后,在解决每个局部时,两个局部后果将应用雷同的 binary operator 合并。

解决数据流非常简单:只需在给定流上调用 parallel()) 即可。

让咱们来看看事件是如何工作的,为此,您能够编写以下代码。您只是在模仿如何并行执行计算。当然,这是并行流的适度简化版本,只是为了解释事件是如何工作的。

让咱们创立一个 reduce()) 办法,该办法采纳 binary operator 并应用它来 reduce 整数列表。代码如下。

int reduce(List<Integer> ints, BinaryOperator<Integer> sum) {int result = ints.get(0);
    for (int index = 1; index < ints.size(); index++) {result = sum.apply(result, ints.get(index));
    }
    return result;
}

上面是应用此办法的次要代码。

List<Integer> ints = List.of(3, 6, 2, 1);
BinaryOperator<Integer> sum = (a, b) -> a + b;

int result1 = reduce(ints.subList(0, 2), sum);
int result2 = reduce(ints.subList(2, 4), sum);

int result = sum.apply(result1, result2);
System.out.println("sum =" + result);

为了明确起见,咱们将您的数据源分为两局部,并将它们别离 reduce 为两个整数:reduce1reduce2。而后,咱们应用雷同的 binary operator 合并了这些后果。这基本上就是并行流的工作形式。

这段代码十分简化,它只是为了显示你的 binary operator 应该具备的一个十分非凡的属性。拆分流元素的形式不应影响计算结果。以下所有拆分都应提供雷同的后果:

  • 3 + (6 + 2 + 1)
  • (3 + 6) + (2 + 1)
  • (3 + 6 + 2) + 1

这表明您的 binary operator 应该具备一个称为 联合性 的已知属性。传递给 reduce()) 办法的 binary operator 应该是可联合的。

Stream API 中 reduce()) 办法重载版本的 JavaDoc API 文档指出,您作为参数提供的 binary operator 必须是可联合的。

如果不是这样,会产生什么?嗯,这正是问题所在:编译器和 Java 运行时都不会检测到它。因而,您的数据将被解决,没有显著的谬误。你可能有正确的后果,也可能没有; 这取决于外部解决数据的形式。事实上,如果你屡次运行代码,你最终可能会失去不同的后果。这是您须要留神的十分重要的一点。

如何测试 binary operator 是否可联合?在某些状况下,这可能非常简单:SUMMINMAX是家喻户晓的关联运算符。在其余一些状况下,这可能要艰难得多。查看的一种办法,能够是在随机数据上运行 binary operator,并验证是否始终取得雷同的后果。

治理具备任何幺元的 binary operator

第二个是 binary operator 应该具备的这种联合性属性的后果。

此联合性属性是由以下事实保障的:数据的拆分形式不应影响计算结果。如果将汇合 A 拆分为两个子集 B 和 C,则 reduce A 应该失去与 reduce(B 的 reduce 和 C 的 reduce)雷同的后果。

能够将后面的属性写入更通用的以下表达式:

A = B ⋃ C ⇒ Red(A)= Red(RedB),Red(C))

事实证明,这导致了另一个结果。假如事件停顿不顺利,B实际上是空的。这种状况下,C = A。后面的表达式变为以下内容:

Red(A)= Red(Red(∅),Red(A))

当且仅当空集(∅)的 reduce 是 reduce 操作的 幺元identity element 时,才是正确的。

这是数据处理中的个别属性:空集的 reduce 是 reduce 操作的幺元。

这在数据处理中的确是一个问题,尤其是在并行数据处理中,因为一些十分经典的 reduce binary operator 没有幺元,即 MINMAX。求空集的最小元素没有意义,因为 MIN 操作没有幺元。

此问题必须在 Stream API 中解决,因为您可能必须解决空流。您看到了创立空流的模式,并且很容易看出 filter()) 调用能够 filter 掉所有数据,从而返回空流。

Stream API 所做的抉择如下。幺元未知(不存在或未提供)的 reduce 将返回 Optional 类的实例。咱们将在本教程前面更具体地介绍此类。此时您须要晓得的是,此 Optional 类是一个能够为空的包装类。每次对没有已知幺元的流调用末端操作时,Stream API 都会将后果包装在该对象中。如果解决的流为空,则此 Optional 也将为空,下一步如何解决由您和您的应用程序决定。

摸索 Stream API 的 reduce 办法

正如咱们后面提到的,Stream API 有三个重载的 reduce()) 办法,咱们当初能够具体介绍这些重载。

应用幺元进行 reduce

第一个采纳幺元和 BinaryOperator 的实例。因为您提供的第一个参数已知是 binary operator 的幺元,因而实现可能会应用它来简化计算。它不须要任何元素,而是从这个幺元开始,启动过程。应用的算法具备以下模式。

List<Integer> ints = List.of(3, 6, 2, 1);
BinaryOperator<Integer> sum = (a, b) -> a + b;
int identity = 0;

int result = identity;// 人为设定初始值
for (int i: ints) {result = sum.apply(result, i);
}

System.out.println("sum =" + result);

你能够留神到,即便你须要解决的列表是空的,这种编写形式也能很好地工作。这种状况下,它将返回幺元,这是您须要的。

API 不会查看您提供的元素的确是 binary operator 的幺元这一事实。提供不是的元素将返回损坏的后果。

您能够在以下示例中看到这一点。

Stream<Integer> ints = Stream.of(0, 0, 0, 0);

int sum = ints.reduce(10, (a, b) -> a + b);// 初始值为 10
System.out.println("sum =" + sum);

您心愿此代码在管制台上打印值 0。因为 reduce()) 办法调用的第一个参数不是 binary operator 的幺元,所以后果实际上是谬误的。运行此代码将在主机上打印以下内容。

sum = 10

这是您应该应用的正确代码。

Stream<Integer> ints = Stream.of(0, 0, 0, 0);

int sum = ints.reduce(0, (a, b) -> a + b);// 初始值为 0
System.out.println("sum =" + sum);

此示例阐明在编译或运行代码时传递谬误的幺元不会触发任何谬误或异样。确保传递的对象的确是 binary operator 的幺元的确取决于您。

此属性的测试能够采纳与测试联合性雷同的形式实现。将候选幺元与尽可能多的值组合在一起。如果您找到一个因组合而扭转的值,那么您的值就不是适合的候选。反之并不成立,如果您找不到任何谬误的组合,并不一定意味着您的候选就是正确。

不应用幺元进行 reduce

reduce()) 办法的第二个重载采纳没有幺元的 BinaryOperator 实例作为参数。正如预期的那样,它返回一个 Optional 对象,包装 reduce 的后果。您能够应用 Optional 做的最简略的事件就是关上它并查看其中是否有任何货色。

让咱们举一个没有幺元的 reduce 示例。

Stream<Integer> ints = Stream.of(2, 8, 1, 5, 3);
Optional<Integer> optional = ints.reduce((i1, i2) -> i1 > i2 ? i1: i2);

if (optional.isPresent()) {System.out.println("result =" + optional.orElseThrow());
} else {System.out.println("No result could be computed");
}

运行此代码将产生以下后果。

result = 8

请留神,此代码应用 orElseThrow()) 办法关上可选代码,该办法当初是执行此操作的首选办法。此模式已在 Java SE 10 中增加,以取代最后在 Java SE 8 中引入的更传统的 get()) 办法。

这个 get()) 办法的问题在于,如果可选为空,它可能会抛出 一个 NoSuchElementException。此办法的命名 orElseThrow)()比 get()) 更直观,它提醒您,如果您尝试关上一个空的可选,您将收到异样。

应用 Optional 能够实现更多操作,您将在本教程前面理解这些操作。

在一种办法中组合 map 和 reduce

第三个略微简单一些。它组合了外部 map 和具备多个参数的 reduce。

让咱们检查一下此办法的签名。

<U> U reduce(U identity,
             BiFunction<U, ? super T, U> accumulator,
             BinaryOperator<U> combiner);

此办法与 U 一起应用,U在本地定义并由 binary operator 应用。binary operator 的工作形式与 reduce()) 方才那个重载雷同,只是它不利用于流的元素,而仅利用于它们的 map 后的版本。

这种 map 和 reduce 自身实际上组合成一个操作:累加器 accumulator。请记住,在本局部的结尾,您看到 reduce 是逐渐进行的,并且一次生产一个元素。在每一步,reduce 操作的第一个参数是到目前为止生产的所有元素的 reduce 局部。

幺元同时也是组合后的幺元。确实是这样。

假如您有一个 String 实例流,您须要对所有字符串的长度求和。

combiner 组合了两个整数:到目前为止解决的字符串长度的总和。

accumulator 从流中获取一个元素,将其 map 为一个整数(该字符串的长度),并将其增加到到目前为止计算的总和中。

以下是该算法的工作原理。

相应的代码如下。

Stream<String> strings = Stream.of("one", "two", "three", "four");

BinaryOperator<Integer> combiner = (length1, length2) -> length1 + length2;// 求得局部总和

// 累加 map 操作:局部总和 Integer,跟新元素 String 作运算,返回新总和 Integer
BiFunction<Integer, String, Integer> accumulator =
        (partialReduction, element) -> partialReduction + element.length();

int result = strings.reduce(0, accumulator, combiner);// 初始值为 0
System.out.println("sum =" + result);

运行此代码将生成以下后果。

sum = 15

在下面的示例中,map 过程理论为以下函数。

Function<String, Integer> mapper = String::length;

因而,您能够将 accumulator 重写为以下模式。这种写法分明地显示了 map 的组合过程。

Function<String, Integer> mapper = String::length;
BinaryOperator<Integer> combiner = (length1, length2) -> length1 + length2;

BiFunction<Integer, String, Integer> accumulator =
        (partialReduction, element) -> partialReduction + mapper.apply(element);

在流上增加末端操作

防止应用 reduce 办法

如果流不以末端操作完结,则不会解决任何数据。咱们曾经介绍了末端操作 reduce(),)您在其余示例中看到了几个末端操作。当初让咱们介绍其余几个。

应用 reduce()) 办法并不是 reduce 流的最简略办法。您须要确保您提供的 binary operator 是可联合的,而后您须要晓得它是否具备幺元。您须要查看许多点,以确保您的代码正确并产生您冀望的后果。如果你能够防止应用 reduce()) 办法,那么你相对应该这样做,因为它很容易出错。

侥幸的是,Stream API 为您提供了许多其余 reduce 流的办法:咱们在介绍专门的数字流时介绍的 sum()、min()和 max()) 是您能够应用的便捷办法。事实上,你只能吧 reduce()) 办法作为最初的伎俩,只有当你没有其余解决方案时。

计算元素数量

count()) 办法存在于所有流接口中,包含专用流和对象流。它用 long 返回该流解决的元素数。这个数字可能很大,实际上大于 Integer.MAX_VALUE

您可能想晓得为什么须要如此多的数字。实际上,您能够从许多源创立流,包含能够生成大量元素的源,大于 Integer.MAX_VALUE。即便不是这种状况,也很容易创立一个中继操作,将流解决的元素数量成倍增加。咱们在本教程后面介绍的 flatMap()) 办法能够做到这一点。有很多办法能够让你最终超过 Integer.MAX_VALUE。这就是 Stream API 反对它的起因。

上面是 count()) 办法的一个示例。

Collection<String> strings =
        List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten");

long count =
        strings.stream()
                .filter(s -> s.length() == 3)
                .count();
System.out.println("count =" + count);

运行此代码将生成以下后果。

count = 4

一一生产元素

Stream API 的 forEach()) 办法容许您将流的每个元素传递给 Consumer 接口的实例。此办法对于打印流解决的元素十分不便。这就是以下代码的作用。

Stream<String> strings = Stream.of("one", "two", "three", "four");
strings.filter(s -> s.length() == 3)
       .map(String::toUpperCase)
       .forEach(System.out::println);

运行此代码将打印以下内容。

ONE
TWO

这种办法非常简单,但您可能会用错。

请记住,您编写的 lambda 表达式应防止扭转其内部作用域。有时,在状态外产生渐变称为 传导副作用。方才的 Consumer 很非凡,因为没有什么特地的副作用。实际上也有,调用 System.out.println()) 会对应用程序的控制台产生副作用。

让咱们思考以下示例。

Stream<String> strings = Stream.of("one", "two", "three", "four");
List<String> result = new ArrayList<>();

strings.filter(s -> s.length() == 3)
       .map(String::toUpperCase)
       .forEach(result::add);

System.out.println("result =" + result);

运行后面的代码会打印出以下内容。

result = [ONE, TWO]

因而,您可能会想应用此代码,因为它很简略,而且“失常工作”。好吧,这段代码正在做一些谬误的事件。让咱们来看看它们。

从流中调用 result::add,将该流解决的所有元素增加到内部result 列表中。此 Consumer 正在对流自身范畴之外的变量产生副作用。

拜访此类变量会使您的 lambda 表达式成为 捕捉式 lambda 表达式。创立这样的 lambda 表达式尽管齐全非法,但会升高性能。如果性能是应用程序中的重要问题,则应防止编写捕捉式 lambda。

此外,这种形式也会阻止此流的并行。实际上,如果您尝试使此流并行,您将有多个线程并行拜访您的 result 列表。而 ArrayList 并不是并发平安的类。

有两种变通模式。上面的示例演示应用汇合对象。第二种模式应用 collector 对象,稍后将介绍。

Stream<String> strings = Stream.of("one", "two", "three", "four");

List<String> result = 
    strings.filter(s -> s.length() == 3)
           .map(String::toUpperCase)
           .collect(Collectors.toList());

这段代码同样创立 ArrayList 的实例,并将流解决的元素增加到其中。不会产生任何副作用,因而不会对性能造成影响。

并行性和并发性由 Collector API 自身解决,因而您能够平安地使此流并行。

从 Java SE 16 开始,您有第二种更简略的模式。

Stream<String> strings = Stream.of("one", "two", "three", "four");

List<String> result = 
    strings.filter(s -> s.length() == 3)
           .map(String::toUpperCase)
           .toList();

此模式生成 List 的非凡不可变实例。如果你须要一个可变列表,你应该应用上一种。另外,它还比在 ArrayList 中收集流的性能更好。这一点将在下一段介绍。

收集到汇合或数组中

Stream API 提供了多种将流元素收集到汇合中的办法。在上一节中,您初步理解了其中两种。让咱们看看其余的。

在抉择所需的模式之前,您须要问本人几个问题。

  • 是否须要构建不可变列表?
  • 你对 ArrayList 的实例感到称心吗?或者你更喜爱LinkedList
  • 您是否确切地晓得您的流将解决多少个元素?
  • 您是否须要在准确的、可能是第三方或自制的 List 中收集您的元素?

Stream API 能够解决所有这些状况。

在 ArrayList 中收集

您曾经在后面的示例中应用了此模式。它是您能够应用的最简略的办法,并返回 ArrayList 实例中的元素。

上面是这种模式的理论示例。

Stream<String> strings = Stream.of("one", "two", "three", "four");

List<String> result = 
    strings.filter(s -> s.length() == 3)
           .map(String::toUpperCase)
           .collect(Collectors.toList());

此模式创立 ArrayList 的简略实例,并在其中累积流的元素。如果有太多元素,ArrayList 的外部数组无奈存储它们,则以后数组将被复制到一个更大的数组中,并由 GC 回收。

如果你想防止这种状况,并且晓得你的流将产生的元素数量,那么你能够应用 Collectors.toCollection()),它以 supplier 作为参数来创立汇合,你将在其中收集解决的元素。以下代码应用此模式创立初始容量为 10,000 的 ArrayList 实例。

Stream<String> strings = ...;

List<String> result = 
    strings.filter(s -> s.length() == 3)
           .map(String::toUpperCase)
           .collect(Collectors.toCollection(() -> new ArrayList<>(10_000)));

在不可变 List 中收集

在某些状况下,您须要在不可变列表中累积元素。这听起来可能自圆其说,因为收集意味着将元素增加到必须可变的容器中。实际上,这就是 Collector API 的工作形式,本教程前面将具体介绍。在此累加操作完结时,Collector API 能够继续执行最初一个可选操作,在本例中,该操作包含在返回之前密封这个列表。

为此,您只需应用以下模式。

Stream<String> strings = ...;

List<String> result = 
    strings.filter(s -> s.length() == 3)
           .map(String::toUpperCase)
           .collect(Collectors.toUnmodifiableList()));

在此示例中,result是一个不可变列表。

从 Java SE 16 开始,有一种更好的办法能够在不可变列表中收集数据,这在某些状况下可能更无效。模式如下。

Stream<String> strings = ...;

List<String> result = 
    strings.filter(s -> s.length() == 3)
           .map(String::toUpperCase)
           .toList();

如何提高效率?第一种模式是建设在应用 collector 的根底上的,首先在一般 ArrayList 中收集元素,而后将其密封,使其在解决实现后不可变。您的代码看到的只是从此 ArrayList 构建的不可变列表。

如您所知,ArrayList 的实例是在具备固定大小的外部数组上构建的。此阵列可能已满。这种状况下,ArrayList 实现会检测到它并将其复制到更大的数组中。此机制对使用者是通明的,但会带来开销:复制此数组须要一些工夫。

在某些状况下,在生产所有流之前,Stream API 能够跟踪要解决的元素数。这种状况下,创立大小适合的外部数组更无效,因为它防止了将小数组到较大数组的开销。

此优化已在 Stream.toList()) 办法中实现,该办法已增加到 Java SE 16 中。如果您须要的是不可变的列表,那么您应该应用此模式。

在自制 List 中收集

如果您须要在本人的列表或 JDK 之外的第三方 List 中收集数据,则能够应用 Collectors.toCollection()) 模式。用于调整 ArrayList 初始大小的 supplier 也可用于构建 Collection 的任何实现,包含不属于 JDK 的实现。您所须要的只是一个 supplier。在以下示例中,咱们提供了一个 supplier 来创立 LinkedList 的实例。

Stream<String> strings = ...;

List<String> result = 
    strings.filter(s -> s.length() == 3)
           .map(String::toUpperCase)
           .collect(Collectors.toCollection(LinkedList::new));

在 Set 中收集

因为 Set 接口是 Collection 接口的扩大,因而能够应用 Collectors.toCollection(HashSet::new)) 在 Set 实例中收集数据。这很好,但 Collector API 依然为您提供了一个更简洁的模式:Collectors.toSet()。)

Stream<String> strings = ...;

Set<String> result = 
    strings.filter(s -> s.length() == 3)
           .map(String::toUpperCase)
           .collect(Collectors.toSet());

您可能想晓得这两种模式之间是否有任何区别。答案是必定的,存在轻微的区别,您将在本教程前面看到。

如果你须要的是一个不可变的汇合,Collector API 还有另一种模式:Collectors.toUnmodifiableSet()。)

Stream<String> strings = ...;

Set<String> result = 
    strings.filter(s -> s.length() == 3)
           .map(String::toUpperCase)
           .collect(Collectors.toUnmodifiableSet());

在数组中收集

Stream API 也有本人的一组 toArray()) 办法重载。其中有两个。

第一个是一般的 toArray()) 办法,它返回Object[] . 如果流的确切类型已知,则应用此模式时此类型将失落。

第二个参数采纳 IntFunction 类型的参数。乍一看可能很吓人,但编写此函数的实现实际上非常容易。如果须要构建一个字符串数组,则此函数的实现为 String[]::new

Stream<String> strings = ...;

String[] result = 
    strings.filter(s -> s.length() == 3)
           .map(String::toUpperCase)
           .toArray(String[]::new);

System.out.println("result =" + Arrays.toString(result));

运行此代码将生成以下后果。

result = [ONE, TWO]

提取流的最大值和最小值

Stream API 为此提供了几种办法,具体取决于您以后正在应用的流。

咱们曾经介绍了来自专用数字流的 max())和 min()) 办法:IntStreamLongStreamDoubleStream。您晓得这些操作没有幺元,因而所有都将返回 Optional。

顺便说一下,同样来自数字流的 average()) 办法也返回一个 Optional 对象,因为 average 操作也没有幺元。

Stream 接口还具备两个办法 max()) 和 min(),)它们也返回一个 Optional 对象。与对象流的区别在于,Stream的元素实际上能够是任何类型的。为了可能计算最大值或最小值,实现须要比拟这些对象。这就是您须要为这些办法提供 comparator 的起因。

这是 max()) 办法的理论利用。

Stream<String> strings = Stream.of("one", "two", "three", "four");
String longest =
     strings.max(Comparator.comparing(String::length))
            .orElseThrow();
System.out.println("longest =" + longest);

它将打印以下内容。

longest = three

请记住,尝试关上空的 Optional 对象会抛出 NoSuchElementException,这是您不心愿在应用程序中看到的内容。仅当您的流没有任何要解决的数据时,才会这样。在这个简略的示例中,你有一个流,它解决多个字符串,没有 filter 操作。此流不会为空,因而您能够平安地关上。

在流中查找元素

Stream API 为您提供了两个末端操作来查找元素:findFirst()) 和 findAny()。)这两个办法不承受任何参数,并返回流的单个元素。为了正确处理空流的状况,此元素包装在 Optional 对象中。如果流为空,则此 Optional 也为空。

理解返回哪个元素须要您理解流可能是 程序 的。程序流只是一种流,其中元素的程序很重要,并由 Stream API 保留。默认状况下,在任何程序源(例如 List 接口的实现)上创立的流自身都是程序的。

在这样的流上,称说第一个、第二个或第三个元素是有意义的。找到这样一个流 的第一个元素也是 齐全有意义的。

如果您的流无序,或者如果程序在流解决中失落了,则查找 第一个 元素是无奈定义的,并且调用 findFirst()) 实际上会返回流的 任何元素。您将在本教程前面看到无关程序流的更多详细信息。

请留神,调用 findFirst()) 会在流实现中触发一些查看,以确保在对该流进行排序时取得该流 的第一个元素。如果您的流是并行流,这可能代价很高。在许多状况下,获取的是不是 第一个 元素并无所谓,包含流仅解决单个元素的状况。在所有这些状况下,您应该应用 findAny()) 而不是 findFirst()。)

让咱们看看 findFirst()) 的实际效果。

Collection<String> strings =
        List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten");

String first =
    strings.stream()
           // .unordered()
           // .parallel()
           .filter(s -> s.length() == 3)
           .findFirst()
           .orElseThrow();

System.out.println("first =" + first);

此流是在 List 的实例上创立的,这使它成为 程序 流。请留神,在第一个版本中正文了 unordered())和 parallel()) 两行。

屡次运行此代码将始终失去雷同的后果。

first = one

unordered()) 中继办法调用使 程序 流成为 无序 流。这种状况下,它没有任何区别,因为您的流是按程序解决的。您的数据是从始终以雷同程序遍历其元素的列表中提取的。出于同样的起因,将 findFirst()) 办法调用替换为 findAny()) 办法调用也没有任何区别。

能够对此代码进行的第一个批改是勾销正文 parallel()) 办法调用。当初,您有一个 并行 解决的程序流。屡次运行此代码将始终失去雷同的后果:one。这是因为您的流是 程序的,因而无论您的流是如何解决的,第一个元素都是确定的。

要使此流无 ,您能够勾销正文 unordered())办法调用,或者将 (List.of)) 替换为 Set.of()。)在这两种状况下,应用 findFirst()) 终止流将从该并行流返回一个随机元素。并行流的解决形式使其如此。

您能够在此代码中进行的第二个批改是将 List.of()) 替换为 Set.of()。)当初不再是程序的。此外,Set.of()) 返回的实现,使得汇合元素的遍历以随机程序产生。屡次运行此代码会显示 findFirst())和 findAny)()都返回一个随机字符串,即便 unordered)()和 parallel()) 都正文掉。查找 无序 源 * 的第一个元素无奈定义,后果是随机的。

从这些示例中,您能够推断出在并行流的实现中采取了一些预防措施来跟踪哪个元素是第一个。这造成了开销,因而,只有在的确须要时才应调用 findFirst()。)

查看流的元素是否与 Predicate 匹配

在某些状况下,在流中查找元素或未能在流中找到元素可能是您真正须要的。您查找的元素不肯定与您的应用程序无关;但是否存在十分重要。

以下代码将用于查看给定元素是否存在。

boolean exists =
    strings.stream()
           .filter(s -> s.length() == 3)
           .findFirst()
           .isPresent();

实际上,此代码查看返回的 Optional 是否为空。

下面的模式工作失常,但 Stream API 提供了一种更无效的办法。实际上,构建此 Optional 对象是一种开销,如果您应用以下三种办法之一,则无需领取该开销。这三种办法将 Predicate 作为参数。

  • anyMatch(Predicate):)如果找到与给定 Predicate 匹配的一个元素,则返回true
  • allMatch(Predicate):)如果流的所有元素都与 Predicate 匹配,则返回true
  • noneMatch(Predicate):)相同

让咱们看看这些办法的理论利用。

Collection<String> strings =
    List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten");

boolean noBlank  = 
        strings.stream()
               .allMatch(Predicate.not(String::isBlank));
boolean oneGT3   = 
        strings.stream()
               .anyMatch(s -> s.length() == 3);
boolean allLT10  = 
        strings.stream()
               .noneMatch(s -> s.length() > 10);
        
System.out.println("noBlank =" + noBlank);
System.out.println("oneGT3  =" + oneGT3);
System.out.println("allLT10 =" + allLT10);

运行此代码将生成以下后果。

noBlank = true
oneGT3  = true
allLT10 = true

短路流的解决

您可能曾经留神到咱们在此处介绍的不同末端操作之间的重要差别。

其中一些须要解决流生产的所有数据。COUNT、MAX、MIN、AVERAGE 操作以及 forEach)()、toList)()或toArray()) 办法调用就是这种状况。

咱们介绍的最初一个末端操作并非如此。一旦找到元素,findFirst()) 或 findAny()) 办法就会进行解决您的数据,无论还有多少元素须要解决。anyMatch()、allMatch()和 noneMatch()也是如此:它们可能会中断流的解决并失去后果,而不用生产源所有元素。

这些办法在 Stream API 中称为 短路 办法,因为它们能够半路生成后果,而无需解决流的所有元素。

在某些状况下,这些最初的办法依然可能解决所有元素:

  • findFirst()) 和 findAny())返回了空的 Optional,只能在所有元素都已解决后。
  • anyMatch()返回了false
  • allMatch()和 noneMatch())返回 了true

查找流的特色

流的特色

Stream API 依赖于一个非凡的对象,即 Spliterator 接口的实例。此接口的名称来源于这样一个事实,即 Stream API 中 spliterator 的角色相似于 iterator 在汇合 API 中的角色。此外,因为 Stream API 反对并行处理,因而 spliterator 对象还控制流在解决并行化时,不同 CPU 之间如何拆分其元素。名称是 splititerator 的 组合。

具体介绍此 spliterator 对象超出了本教程的范畴。您须要晓得的是,此 spliterator 对象具备流 的特色。这些特色不是您常常应用到的,但理解它们是什么将帮忙您在某些状况下编写更好、更高效的管道。

流的特色如下。

特色 评论
ORDERED 程序的,解决流元素的程序很重要。
DISTINCT 去重的,该流解决的元素中没有反复呈现。
NONNULL 该流中没有空元素。
SORTED 排序的,对该流的元素曾经进行排序。
SIZED 有数量的,此流解决的元素数是已知的。
SUBSIZED 拆分此流会产生两个 SIZED 流。

有两个特色,不可变 IMMUTABLE 并发的 CONCURRENT,本教程未介绍。

每个流在创立时都设置或勾销设置了所有这些特色。

请记住,能够通过两种形式创立流。

  1. 您能够从数据源创立流,咱们介绍了几种不同的模式。
  2. 每次对现有流调用中继操作时,都会创立一个新流。

给定流的特色取决于创立它的源,或者创立它的流的特色,以及创立的操作。如果您的流是应用源创立的,则其特色取决于该源,如果您应用另一个流创立它,则它们将取决于该其余流以及您正在应用的操作类型。

让咱们更具体地介绍每个特色。

ORDERED 流

程序流是应用 程序 数据源创立的。可能想到的第一个示例是 List 接口的任何实例。还有其余的:Files.lines(path)) 和 Pattern.splitAsStream(string)) 也生成 ORDERED 流。

跟踪流元素的程序可能会导致并行流的开销。如果不须要此个性,则能够通过在现有流上调用 unordered()) 中继办法来删除它。这将返回没有此特色的新流。你为什么要这样做?在某些状况下,放弃流 ORDERED 可能会很低廉,例如,当您应用并行流时。

SORTED 流

SORTED的流是已排序的流。能够从已排序的源(如 TreeSet 实例)或通过调用 sorted()) 办法创立此流。晓得流已被排序可能会被流的某些实现拿来用,以防止再次进行排序。此优化可能不会始终不变,因为 SORTED 流可能会应用与第一次不同的 comparator 再次排序。

有一些中继操作能够革除 SORTED 特色。在上面的代码中,您能够看到 strings,filteredStream 两者都是 SORTED 流,而 lengths 不是。

Collection<String> stringCollection = List.of("one", "two", "two", "three", "four", "five");

Stream<String> strings = stringCollection.stream().sorted();
Stream<String> filteredStrings = strings.filtered(s -> s.length() < 5);
Stream<Integer> lengths = filteredStrings.map(String::length);

map 或 flatmap SORTED 流会从生成的流中删除此特色。

DISTINCT 流

DISTINCT 流是它正在解决的元素之间没有反复项的流。例如,当从 HashSet 构建流时,或者从对 distinct()) 中继办法调用的调用中构建流时,能够取得这样的特色。

DISTINCT 特色在 filter 流时保留,但在 map 或 flatmap 流时失落。

让咱们查看以下示例。

Collection<String> stringCollection = List.of("one", "two", "two", "three", "four", "five");

Stream<String> strings = stringCollection.stream().distinct();
Stream<String> filteredStrings = strings.filtered(s -> s.length() < 5);
Stream<Integer> lengths = filteredStrings.map(String::length);
  • stringCollection.stream()) 不是 DISTINCT 的,因为它是从 List 的实例构建的。
  • stringsDISTINCT 的,因为此流是通过调用 distinct()) 中继办法创立的。
  • filteredStrings依然是 DISTINCT:从流中删除元素不能创立反复项。
  • length已被 map,因而 DISTINCT 特色失落。

NONNULL 流

非空流是不蕴含 值的流。汇合框架中的一些构造不承受空值,包含 ArrayDeque 和并发构造,如 ArrayBlockingQueueConcurrentSkipListSet 和调用 ConcurrentHashMap.newKeySet()) 返回的并发 Set。应用 Files.lines(path)) 和 Pattern.splitAsStream(line)) 创立的流也是非 流。

至于后面的特色,一些中继操作能够产生具备不同特色的流。

  • filter 或排序非空流将返回非 流。
  • 在 NONNULL 流上调用 distinct()) 也会返回一个 NONNULL 流。
  • map 或 flatmap NONNULL 流将返回没有此特色的流。

SIZED 和 SUBSIZED 流

SIZED 流

当您想要应用并行流时,最初一个特色十分重要。本教程稍后将更具体地介绍并行流。

SIZED 流是晓得它将解决多少个元素的流。从 Collection 的任何实例创立的流都是这样的流,因为 Collection 接口具备 size()) 办法,因而获取此数字很容易。

另一方面,在某些状况下,您晓得流将解决无限数量的元素,但除非您解决流自身,否则您无奈晓得此数量。

对于应用 Files.lines(path)) 模式创立的流,状况就是如此。您能够获取文本文件的大小(以字节为单位),但此信息不会告诉您此文本文件有多少行。您须要剖析文件以获取此信息。

Pattern.splitAsStream(line)) 模式也是。晓得您正在剖析的字符串中的字符数并不能给出任何对于此模式将产生多少元素的提醒。

SUBSIZED 流

SUBSIZED 特色,与并行流的拆分形式无关。简略说,并行化机制将流分成两局部,并在 CPU 正在执行的不同可用内核之间调配计算。此拆分由流应用的 Spliterator 实例实现。具体实现取决于您应用的数据源。

假如您须要在 ArrayList 上关上一个流。此列表的所有数据都保留在 ArrayList 实例的外部数组中。兴许您还记得 ArrayList 对象上的外部数组是一个固定数组,因为当您从此数组中删除元素时,所有后续元素都会向左挪动一个单元格,以便不会留下任何孔。

这使得拆分 ArrayList 变得简单明了。要拆分 ArrayList 的实例,您能够将此外部数组拆分为两局部,两局部中的元素数量雷同。这使得在 ArrayList 实例上创立的流具备 SUBSIZED个性:您甚至能够设定拆分后每个局部中将保留多少个元素。

假如当初您须要在 HashSet 实例上关上一个流。HashSet 将其元素存储在数组中,但此数组的应用形式与 ArrayList 应用的数组不同。实际上,多个元素能够存储在此数组的给定单元格中。拆分这个数组没有问题,然而如果不计算一下,就无奈提前晓得每个局部中将保留多少个元素。即便你把这个数组从两头离开,也无奈保障两半的元素数量就是雷同。这就是为什么在 HashSet 实例上创立的流是 SIZED而不是 SUBSIZED

map 流可能会更改返回流的 SIZEDSUBSIZED 特色。

  • map 和排序流会保留 SIZEDSUBSIZED特色。
  • flatmap、filter 和调用 distinct()) 会擦除这些特色。

最好用有 SIZEDSUBSIZED 的流进行并行计算。

正文完
 0