Stream API
Stream API 是依照map/filter/reduce办法解决内存中数据的最佳工具。
本系列教程由Record讲起,而后联合Optional,探讨collector的设计。
应用Record对不可变数据进行建模
Java 语言为您提供了几种创立不可变类的办法。可能最间接的是创立一个蕴含final字段的final类。上面是此类的示例。
public final class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; }}
编写这些元素后,须要为字段增加拜访器。您还将增加一个 toString
)() 办法,可能还有一个 equals
)() 以及一个 hashCode()
) 办法。手写所有这些十分乏味且容易出错,侥幸的是,您的 IDE 能够为您生成这些办法。
如果须要通过网络或文件系统将此类的实例从一个应用程序传送到另一个应用程序,则还能够思考使此类可序列化。如果这样做,还要增加一些无关如何序列化的信息。JDK 为您提供了几种管制序列化的办法。
最初,您的Point
类可能有一百多行,次要是IDE 生成的代码,只是为了对须要写入文件的两个整数不可变集进行建模。
Record曾经增加到 JDK 以扭转这所有。只需一行代码即可为您提供所有这些。您须要做的就是申明record的状态;其余部分由编译器为您生成。
呼叫Record声援
Record可帮忙您使此代码更简略。从 Java SE 14 开始,您能够编写以下代码。
public record Point(int x, int y) {}
这一行代码为您创立以下元素。
- 它是一个不可变的类,有两个字段:
x
和y
- 它有一个规范的构造函数,用于初始化这两个字段。
toString
)()、equals
)() 和hashCode()
) 办法是由编译器为您创立的,其默认行为与 IDE 将生成的内容绝对应。如果须要,能够通过增加本人的实现来批改此行为。- 它能够实现
Serializable
接口,以便您能够通过网络或通过文件系统发送到其余应用程序。序列化和反序列化record的形式遵循本教程开端介绍的一些非凡规定。
record使创立不可变的数据集变得更加简略,无需任何 IDE 的帮忙。升高了谬误的危险,因为每次批改record的组件时,编译器都会自动更新 equals(
)) 和 hashCode()
) 办法。
record的类
record也是类,是用关键字record
而不是class
申明的类。让咱们申明以下record。
public record Point(int x, int y) {}
编译器在创立record时为您创立的类是final的。
此类继承了 java.lang.Record
类。因而,您的record不能继承其余任何类。
一条record能够实现任意数量的接口。
申明record的组成部分
紧跟record名称的块是(int x, int y)
。它申明了record组件。对于record的每个组件,编译器都会创立一个同名的公有final字段。您能够在record中申明任意数量的组件。
除了字段,编译器还为每个组件生成一个拜访器。此拜访器跟组件的名称雷同,并返回其值。对于此record,生成的两个办法如下。
public int x() { return this.x;}public int y() { return this.y;}
如果此实现实用于您的应用程序,则无需增加任何内容。不过,也能够定义本人的拜访器。
编译器为您生成的最初一个元素是 Object
类中 toString()、
)equals
)() 和 hashCode()
) 办法的重写。如果须要,您能够定义本人对这些办法的笼罩。
无奈增加到record的内容
有三件事不能增加到record中:
- 额定申明的实例字段。不能增加任何与组件不对应的实例字段。
- 实例字段的初始化。
- 实例的初始化块。
您能够应用动态字段,动态初始化块。
应用规范构造函数结构record
编译器还会为您创立一个构造函数,称为规范构造函数 canonical constructor。此构造函数以record的组件作为参数,并将其值复制到字段中。
在某些状况下,您须要笼罩此默认行为。让咱们钻研两种状况:
- 您须要验证组件的状态
- 您须要制作可变组件的正本。
应用紧凑构造函数
能够应用两种不同的语法来从新定义record的规范构造函数。能够应用紧凑构造函数或规范构造函数自身。
假如您有以下record。
public record Range(int start, int end) {}
对于该名称的record,应该预期 end
大于start
.您能够通过在record中编写紧凑构造函数来增加验证规定。
public record Range(int start, int end) { public Range {//不须要参数块 if (end <= start) { throw new IllegalArgumentException("End cannot be lesser than start"); } }}
紧凑构造函数不须要申明其参数块。
请留神,如果抉择此语法,则无奈间接调配record的字段,例如this.start = start
- 这是通过编译器增加代码为您实现的。 然而,您能够为参数调配新值,这会导致雷同的后果,因为编译器生成的代码随后会将这些新值调配给字段。
public Range { // set negative start and end to 0 // by reassigning the compact constructor's // implicit parameters if (start < 0) start = 0;//无奈给this.start赋值 if (end < 0) end = 0;}
应用规范构造函数
如果您更喜爱非紧凑模式(例如,因为您不想重新分配参数),则能够本人定义规范构造函数,如以下示例所示。
public record Range(int start, int end) {//跟紧凑结构不能共存 public Range(int start, int end) { if (end <= start) { throw new IllegalArgumentException("End cannot be lesser than start"); } if (start < 0) { this.start = 0; } else { this.start = start; } if (end > 100) { this.end = 10; } else { this.end = end; } }}
这种状况下,您编写的构造函数须要为record的字段手动赋值。
如果record的组件是可变的,则应思考在规范构造函数和拜访器中制作它们的正本。
自定义构造函数
还能够向record增加自定义构造函数,只有此构造函数内调用record的规范构造函数即可。语法与经典语法雷同。对于任何类,调用this()
必须是构造函数的第一个语句。
让咱们查看以下State
record。它由三个组件定义:
- 此州的名称
- 该州首府的名称
- 城市名称列表,可能为空。
咱们须要存储城市列表的正本,确保它不会从此record的内部批改。 这能够通过应用紧凑模式,将参数重新分配给正本。
领有一个不必城市作参数的构造函数在您的应用程序中很有用。这能够是另一个构造函数,它只接管州名和首都名。第二个构造函数必须调用规范构造函数。
而后,您能够将城市作为 vararg 传递。为此,您能够创立第三个构造函数。
public record State(String name, String capitalCity, List<String> cities) { public State { // List.copyOf returns an unmodifiable copy, // so the list assigned to `cities` can't change anymore cities = List.copyOf(cities); } public State(String name, String capitalCity) { this(name, capitalCity, List.of()); } public State(String name, String capitalCity, String... cities) { this(name, capitalCity, List.of(cities));//也是不可变的 }}
请留神,List.copyOf()
) 办法的参数不承受空值。
获取record的状态
您不须要向record增加任何拜访器,因为编译器会为您执行此操作。一条record的每个组件都有一个拜访器办法,该办法具备此组件的名称。
然而,某些状况下,您须要定义本人的拜访器。 例如,假如上一节中的State
record在结构期间没有创立列表的不可批改的正本 - 那么它应该在拜访器中执行此操作,以确保调用方无奈扭转其外部状态。 您能够在record中增加以下代码以返回此正本。
public List<String> cities() { return List.copyOf(cities);}
序列化record
如果您的record类实现了可序列化,则能够序列化和反序列化
record。不过也有限度。
- 可用于替换默认序列化过程的任何零碎都不适用于record。创立 writeObject() 和 readObject() 办法不起作用,也不能实现
Externalizable
。 - record可用作代理对象来序列化其余对象。
readResolve()
办法能够返回record。也能够在record中增加writeReplace()。
- 反序列化record始终调用规范构造函数。因而,在此构造函数中增加的所有验证规定都将在反序列化record时强制执行。
这使得record在应用程序中作为数据传输对象十分适合。
在理论场景中应用record
record是一个多功能的概念,您能够在许多上下文中应用。
第一种办法是在应用程序的对象模型中携带数据。用record充当不可变的数据载体,也是它们的设计目标。
因为能够申明本地record,因而还能够应用它们来进步代码的可读性。
让咱们思考以下场景。您有两个建模为record的实体:City
,State
public record City(String name, State state) {}
public record State(String name) {}
假如您有一个城市列表,您须要计算领有最多城市数量的州。能够应用 Stream API 首先应用每个州领有的城市数构建各州的柱状图。此柱状图由Map
建模。
List<City> cities = List.of();Map<State, Long> numberOfCitiesPerState = cities.stream() .collect(Collectors.groupingBy( City::state, Collectors.counting() ));
获取此柱状图的最大值是以下代码。
Map.Entry<State, Long> stateWithTheMostCities = numberOfCitiesPerState.entrySet().stream() .max(Map.Entry.comparingByValue())//最多城市 .orElseThrow();
最初一段代码是技术性的;它不具备任何业务意义;因为应用Map.Entry
实例对柱状图的每个元素进行建模。
应用本地record能够大大改善这种状况。上面的代码创立一个新的record类,该类蕴含一个州和该州的城市数。它有一个构造函数,该构造函数将 Map.Entry
的实例作为参数,将键值对流映射到record流。
因为须要按城市数比拟这些集,因而能够增加工厂办法来提供此比拟器。代码将变为以下内容。
record NumberOfCitiesPerState(State state, long numberOfCities) { public NumberOfCitiesPerState(Map.Entry<State, Long> entry) { this(entry.getKey(), entry.getValue());//mapping过程 } public static Comparator<NumberOfCitiesPerState> comparingByNumberOfCities() { return Comparator.comparing(NumberOfCitiesPerState::numberOfCities); }}NumberOfCitiesPerState stateWithTheMostCities = numberOfCitiesPerState.entrySet().stream() .map(NumberOfCitiesPerState::new) .max(NumberOfCitiesPerState.comparingByNumberOfCities())//record替换Entry .orElseThrow();
您的代码当初以有意义的形式提取最大值。您的代码更具可读性,更易于了解,不易出错,从久远来看更易于保护。
应用collector作为末端操作
让咱们回到Stream API。
应用collector收集流元素
您曾经应用了一个十分有用的模式collect(Collectors.toList())
来收集由 List
中的流解决的元素。此 collect()
办法是在 Stream
接口中定义的末端办法,它将 Collector
类型的对象作为参数。此Collector
接口定义了本人的 API,可用于创立任何类型的内存中构造来存储流解决的数据。能够在Collection
或Map
的任何实例中进行收集,它可用来创立字符串,并且您能够创立本人的Collector
实例以将本人的构造增加到列表中。
将应用的大多数collector都能够应用 Collectors
工厂类的工厂办法之一创立。这是您在编写 Collectors.toList() 或 Collectors.toSet()
) 时所做的。应用这些办法创立的一些collector能够组合应用,从而产生更多的collector。本教程涵盖了所有这些要点。
如果在此工厂类中找不到所需的内容,则能够决定通过实现 Collector
接口来创立本人的collector。本教程还介绍了如何实现此接口。
Collector API 在 Stream 接口和专用数字流IntStream
、LongStream
和 DoubleStream
中的解决形式不同:。Stream
接口有两个 collect()
办法重载,而数字流只有一个。短少的正是将collector对象作为参数的那个。因而,不能将collector对象与专用的数字流一起应用。
在汇合中收集
Collectors
工厂类提供了三种办法,用于在Collection
接口的实例中收集流的元素。
toList()
) 将它们收集在List
对象中。toSet()
) 将它们收集在Set
对象中。- 如果须要任何其余
Collection
实现,能够应用toCollection(supplier),
)其中supplier
参数将用于创立所需的Collection
对象。如果您须要在LinkedList
实例中收集您的数据,您应该应用此办法。
代码不应依赖于这些办法以后返回的 List
或 Set
的确切实现,因为它不是规范的一部分。
您还能够应用 unmodifiableList()
) 和 toUnmodifiableSet()
) 两种办法获取 List
和 Set
的不可变实现。
以下示例显示了此模式的理论利用。首先,让咱们在一个一般List
实例中收集。
List<Integer> numbers =IntStream.range(0, 10) .boxed()//须要装箱 .collect(Collectors.toList());System.out.println("numbers = " + numbers);
此代码应用 boxed()
) 中继办法从 IntStream.range() 创立的 IntStream
) 创立一个 Stream
,办法是对该流的所有元素进行装箱。运行此代码将打印以下内容。
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
第二个示例创立一个只有偶数且没有反复项的 HashSet
。
Set<Integer> evenNumbers =IntStream.range(0, 10) .map(number -> number / 2) .boxed() .collect(Collectors.toSet());System.out.println("evenNumbers = " + evenNumbers);
运行此代码将产生以下后果。
evenNumbers = [0, 1, 2, 3, 4]
最初一个示例应用 Supplier
对象来创立用于收集流元素的 LinkedList
实例。
LinkedList<Integer> linkedList =IntStream.range(0, 10) .boxed() .collect(Collectors.toCollection(LinkedList::new));System.out.println("linked listS = " + linkedList);
运行此代码将产生以下后果。
linked list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
应用collector计数
Collectors
工厂类为您提供了几种办法来创立collector,这些collector执行的操作与一般末端办法为您提供的操作雷同。Collectors.counting
)() 工厂办法就是这种状况,它与在流上调用 count()
) 雷同。
这是值得注意的,您可能想晓得为什么应用两种不同的模式实现了两次这样的性能。将在下一节无关在map中收集时答复此问题,您将在其中组合collector以创立更多collector。
目前,编写以下两行代码会导致雷同的后果。
Collection<String> strings = List.of("one", "two", "three");long count = strings.stream().count();long countWithACollector = strings.stream().collect(Collectors.counting());System.out.println("count = " + count);System.out.println("countWithACollector = " + countWithACollector);
运行此代码将产生以下后果。
count = 3countWithACollector = 3
收集在字符串中
Collectors
工厂类提供的另一个十分有用的collector是 joining()
) 。此collector仅实用于字符串流,并将该流的元素连贯为单个字符串。它有几个重载。
- 第一个将分隔符作为参数。
- 第二个将分隔符、前缀和后缀作为参数。
让咱们看看这个collector的实际效果。
String joined = IntStream.range(0, 10) .boxed() .map(Object::toString) .collect(Collectors.joining());System.out.println("joined = " + joined);
运行此代码将生成以下后果。
joined = 0123456789
能够应用以下代码向此字符串增加分隔符。
String joined = IntStream.range(0, 10) .boxed() .map(Object::toString) .collect(Collectors.joining(", "));System.out.println("joined = " + joined);
后果如下。
joined = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
让咱们看看最初一个重载,它接管分隔符、前缀和后缀。
String joined = IntStream.range(0, 10) .boxed() .map(Object::toString) .collect(Collectors.joining(", ", "{"), "}");System.out.println("joined = " + joined);
后果如下。
joined = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
请留神,此collector能够正确处理流为空或仅解决单个元素的极其状况。
当您须要生成此类字符串时,此collector十分不便。即便您后面的数据不在汇合中或只有几个元素,您也可能想应用它。如果是这种状况,应用 String.join()
) 工厂类或 StringJoiner
对象都将失常工作,无需领取创立流的开销。
应用Predicate对元素进行分区
Collector API 提供了三种模式,用于从流的元素创立map。咱们介绍的第一个应用布尔键创立map。它是应用 partitionningBy()
) 工厂办法创立的。
流的所有元素都将绑定到布尔值true
或false
。map将绑定到每个值的所有元素存储在列表中。因而,如果将此collector利用于Stream
,它将生成具备以下类型的map:Map<Boolean,List<T>>
。
测试的Predicate应作为参数提供给collector。
上面的示例演示此collector的操作。
Collection<String> strings = List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve");Map<Boolean, List<String>> map = strings.stream() .collect(Collectors.partitioningBy(s -> s.length() > 4));map.forEach((key, value) -> System.out.println(key + " :: " + value));
运行此代码将生成以下后果。
false :: [one, two, four, five, six, nine, ten]true :: [three, seven, eight, eleven, twelve]
此工厂办法具备重载,它将另一个collector作为参数。此collector称为上游collector。咱们将在本教程的下一段中介绍,届时咱们将介绍 groupingBy()
) 。
在map中收集并进行分组
咱们提供的第二个collector十分重要,因为它容许您创立柱状图。
对map中的流元素进行分组
可用于创立柱状图的collector是应用 Collectors.groupingBy()
) 办法创立的。此办法具备多个重载。
collector将创立map。通过对其利用 Function
实例,为流的每个元素计算一个键。此函数作为 groupingBy()
) 办法的参数提供。它在Collector API 中称为分类器 classifier。
除了不应该返回 null 之外,此函数没有任何限度。
此函数可能会为流的多个元素返回雷同的键。groupingBy()
) 反对这一点,并将所有这些元素收集在一个列表中。
因而,如果您正在解决 Stream
并应用 Function<
T, K> 作为分类器,则 groupingBy()
) 会创立一个 Map<K,List<T>>
。
让咱们查看以下示例。
Collection<String> strings = List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve");Map<Integer, List<String>> map = strings.stream() .collect(Collectors.groupingBy(String::length));//返回<Integer, List<String>>map.forEach((key, value) -> System.out.println(key + " :: " + value));
此示例中应用的分类器是一个函数,用于从该流返回每个字符串的长度。因而,map按字符串长度将字符串分组到列表中。它具备Map<Interger,List<String>>
的类型。
运行此代码将打印以下内容。
3 :: [one, two, six, ten]4 :: [four, five, nine]5 :: [three, seven, eight]6 :: [eleven, twelve]
对分组后的值进行解决
计算数量
groupingBy()
) 办法还承受另一个参数,即另一个collector。此collector在Collector API 中称为上游collector,但它没有什么特地的。使它成为上游collector的起因只是,它作为参数传递给前一个collector的创立。
此上游collector用于收集由 groupingBy()
) 创立的map的值。
在后面的示例中,groupingBy()
) 创立了一个map,其值是字符串列表。如果为 groupingBy()
) 办法提供上游collector,API 将一一流式传输这些列表,并应用上游collector收集这些流。
假如您将 Collectors.counting()
) 作为上游collector传递。将计算的内容如下。
[one, two, six, ten] .stream().collect(Collectors.counting()) -> 4L[four, five, nine] .stream().collect(Collectors.counting()) -> 3L[three, seven, eight] .stream().collect(Collectors.counting()) -> 3L[eleven, twelve] .stream().collect(Collectors.counting()) -> 2L
此代码不是 Java 代码,因而您无奈执行它。它只是在那里解释如何应用这个上游collector。
上面将创立的map取决于您提供的上游collector。键不会批改,但值可能会。在 Collectors.counting()
) 的状况下,值将转换为 Long
。而后,map的类型将变为 Map<Integer,Long>
。
后面的示例变为以下内容。
Collection<String> strings = List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve");Map<Integer, Long> map = strings.stream() .collect( Collectors.groupingBy( String::length, Collectors.counting()));//List<String>转为Stream向下传递,变成Longmap.forEach((key, value) -> System.out.println(key + " :: " + value));
运行此代码将打印以下后果。它给出了每个长度的字符串数,这是字符串长度的柱状图。
3 :: 44 :: 35 :: 36 :: 2
连贯列表的值
您还能够将 Collectors.joining()
) collector作为上游collector传递,因为此map的值是字符串列表。请记住,此collector只能用于字符串流。这将创立 Map<Integer,String>
的实例。您能够将上一个示例更改为以下内容。
Collection<String> strings = List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve");Map<Integer, String> map = strings.stream() .collect( Collectors.groupingBy( String::length, Collectors.joining(", ")));//变成Stringmap.forEach((key, value) -> System.out.println(key + " :: " + value));
运行此代码将生成以下后果。
3 :: one, two, six, ten4 :: four, five, nine5 :: three, seven, eight6 :: eleven, twelve
管制map的实例
此 groupingBy()
) 办法的最初一个重载将supplier
的实例作为参数,以便您管制须要此collector创立的 Map
实例。
您的代码不应依赖于 groupingBy()
) 返回的确切map类型,因为它不是规范的一部分。
应用ToMap在map中收集
Collector API 为您提供了创立map的第二种模式:Collectors.toMap()
) 模式。此模式实用于两个函数,这两个函数都利用于流的元素。
- 第一个称为密钥mapper,用于创立密钥。
- 第二个称为值mapper,用于创立值。
此collector的应用场景与 Collectors.groupingBy()
) 不同。特地是,它不解决流的多个元素生成雷同密钥的状况。这种状况下,默认状况下会引发IllegalStateException
。
这个collector能十分不便的创立缓存。假如User
类有一个类型为 Long
的primaryKey
属性。您能够应用以下代码创立User
对象的缓存。
List<User> users = ...;Map<Long, User> userCache = users.stream() .collect(User::getPrimaryKey, Function.idendity());//key必须不同
应用 Function.identity()
) 工厂办法只是通知collector不要转换流的元素。
如果您心愿流的多个元素生成雷同的键,则能够将进一步的参数传递给 toMap()
) 办法。此参数的类型为 BinaryOperator
。当检测到抵触元素时,实现将它利用于抵触元素。而后,您的binary operator将生成一个后果,该后果将代替先前的值放入map中。
上面演示如何应用具备抵触值的此collector。此处的值用分隔符连贯在一起。
Collection<String> strings = List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve");Map<Integer, String> map = strings.stream() .collect( Collectors.toMap( element -> element.length(), element -> element, (element1, element2) -> element1 + ", " + element2));//雷同key,解决抵触,返回新值map.forEach((key, value) -> System.out.println(key + " :: " + value));
在此示例中,传递给 toMap()
) 办法的三个参数如下:
element -> element.length()
是键mapper。element -> element
是值mapper。(element1, element2) -> element1 + ", " + element2)
是合并函数,雷同键的两个元素会调用。
运行此代码将生成以下后果。
3 :: one, two, six, ten4 :: four, five, nine5 :: three, seven, eight6 :: eleven, twelve
另外也能够将supplier作为参数传递给 toMap()
) 办法,以管制此collector将应用的 Map
接口实例。
toMap()
) collector有一个孪生办法 toConcurrentMap(),
)它将在并发map中收集数据。实现不保障map的确切类型。
从柱状图中提取最大值
groupingBy()
) 是剖析计算柱状图的最佳模式。让咱们钻研一个残缺的示例,其中您构建柱状图,而后尝试依据要求找到其中的最大值。
提取惟一的最大值
您要剖析的柱状图如下。它看起来像咱们在后面的示例中应用的那个。
Collection<String> strings = List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve");Map<Integer, Long> histogram = strings.stream() .collect( Collectors.groupingBy( String::length, Collectors.counting()));histogram.forEach((key, value) -> System.out.println(key + " :: " + value));
打印此柱状图将失去以下后果。
3 :: 4 //冀望是4 =>34 :: 35 :: 36 :: 2
从此柱状图中提取最大值应失去后果:3 :: 4
。Stream API 具备提取最大值所需的所有工具。可怜的是,Map
接口上没有stream()
办法。要在map上创立流,您首先须要获取能够从map获取的汇合之一。
entrySet()
) 办法的映射集。keySet()
) 办法的键集。- 或者应用
values()
) 办法收集值。
这里你须要键和最大值,所以正确的抉择是流式传输 entrySet()
) 返回的汇合。
您须要的代码如下。
Map.Entry<Integer, Long> maxValue = histogram.entrySet().stream() .max(Map.Entry.comparingByValue()) .orElseThrow();System.out.println("maxValue = " + maxValue);
您能够留神到,此代码应用 Stream
接口中的 max()
) 办法,该办法将comparator作为参数。实际上,Map.Entry
接口确实有几个工厂办法来创立这样的comparator。咱们在此示例中应用的这个,创立了一个能够比拟 Map.Entry
实例的comparator,应用这些键值对的值。仅当值实现Comparable
接口时,此比拟才无效。
这种代码模式十分一般,只有具备可比拟的值,就能够在任何map上应用。咱们能够使其特地一点,更具可读性,这要归功于Java SE 16中记录Record的引入。
让咱们创立一个record来模仿此map的键值对。创立record只须要一行。因为该语言容许local records,因而您能够到任何办法中。
record NumberOfLength(int length, long number) { static NumberOfLength fromEntry(Map.Entry<Integer, Long> entry) { return new NumberOfLength(entry.getKey(), entry.getValue());//mapping过程 } static Comparator<NumberOfLength> comparingByLength() { return Comparator.comparing(NumberOfLength::length); }}
应用此record,以前的模式将变为以下内容。
NumberOfLength maxNumberOfLength = histogram.entrySet().stream() .map(NumberOfLength::fromEntry) .max(NumberOfLength.comparingByLength())//Record替换Entry,前面要援用字段 .orElseThrow();System.out.println("maxNumberOfLength = " + maxNumberOfLength);
运行此示例将打印出以下内容。
maxNumberOfLength = NumberOfLength[length=3, number=4]
您能够看到此record看起来像 Map.Entry
接口。它有一个mapping键值对的工厂办法和一个用于创立comparator的工厂办法。柱状图的剖析变得更加可读和易于了解。
提取多个最大值
后面的示例是一个很好的示例,因为列表中只有一个最大值。可怜的是,现实生活中的状况通常不是那么好,您可能有几个与最大值匹配的键值对。
让咱们从上一个示例的汇合中删除一个元素。
Collection<String> strings = List.of("two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve");Map<Integer, Long> histogram = strings.stream() .collect( Collectors.groupingBy( String::length, Collectors.counting()));histogram.forEach((key, value) -> System.out.println(key + " :: " + value));
打印此柱状图将失去以下后果。
3 :: 34 :: 35 :: 3//冀望是3 =>[3,4,5]6 :: 2
当初咱们有三个键值对的最大值。如果应用后面的代码模式提取它,则将抉择并返回这三个中的一个,暗藏其余两个。
解决此问题的解决方案是创立另一个map,其中键是字符串数量,值是与之匹配的长度。换句话说:您须要反转此map。对于 groupingBy()
) 来说,这是一个很好的场景。此示例将在本局部的前面介绍,因为咱们还须要一个元素来编写此代码。
应用中继collector
到目前为止,咱们介绍的collector只是计数、连贯和收集到列表或map中。它们都属于末端操作。Collector API 也提供了执行中继操作的其余collector:mapping、filtering和flatmapping。您可能想晓得这样的意义是什么。事实上,这些非凡的collector并不能独自创立。它们的工厂办法都须要上游collector作为第二个参数。
也就是说,您这样创立的整体collector是中继操作和末端操作的组合。
应用collector来mapping
咱们能够查看的第一个中继操作是mapping操作。mapping collector是应用 Collectors.mapping()
) 工厂办法创立的。它将惯例mapping函数作为第一个参数,将必须的上游collector作为第二个参数。
在上面的示例中,咱们将mapping与列表中mapping后的元素的汇合相结合。
Collection<String> strings = List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve");List<String> result = strings.stream() .collect( Collectors.mapping(String::toUpperCase, Collectors.toList()));//集成了mappingSystem.out.println("result = " + result);
Collectors.mappping()
) 工厂办法创立一个惯例collector。您能够将此collector作为上游collector传递给任何承受collector的办法,例如,包含 groupingBy()
) 或 toMap()。
)您可能还记得在“提取多个最大值”一节中,咱们留下了一个对于反转map的悬而未决的问题。让咱们应用这个mapping collector来解决问题。
在此示例中,您创立了一个柱状图。当初,您须要应用 groupingBy()
) 反转此柱状图以查找所有最大值。
以下代码创立此类map。
Map<Integer, Long> histogram = ...; var map = histogram.entrySet().stream() .map(NumberOfLength::fromEntry) .collect( Collectors.groupingBy(NumberOfLength::number));//<Long, List<NumberOfLength>>
让咱们查看此代码并确定所构建map的确切类型。
此map的键是每个长度在原始流中存在的次数。它是NumberOfLength
record的number
局部,Long。
类型。
这些值是此流的元素,收集到列表中。因而,是NumberOfLength
的对象列表。这张map的确切类型是Map<Long,NumberOfLength>
。
当然,这不是您所要的。您须要的只是字符串的长度,而不是record。从record中提取组件是一个mapping过程。您须要将这些NumberOfLength
实例mapping为其length
组件。当初咱们介绍了mapping collector,能够解决这一点。您须要做的就是将正确的上游collector增加到 groupingBy()
) 调用中。
代码将变为以下内容。
Map<Integer, Long> histogram = ...; var map = histogram.entrySet().stream() .map(NumberOfLength::fromEntry) .collect( Collectors.groupingBy( NumberOfLength::number, Collectors.mapping(NumberOfLength::length, Collectors.toList())));//<Long, List<Integer>>
构建的map的值当初是应用NumberOfLength::length
对NumberOfLength
做mapping后生成的对象列表。此map的类型为Map<Long,List<Integer>>,
这正是您所须要的。
要获取所有最大值,您能够像之前那样,应用 key 获取最大值而不是值。
柱状图中的残缺代码,包含最大值提取,如下所示。
Map<Long, List<Integer>> map = histogram.entrySet().stream() .map(NumberOfLength::fromEntry) .collect( Collectors.groupingBy( NumberOfLength::number,//变成了number=>length列表 Collectors.mapping(NumberOfLength::length, Collectors.toList())));Map.Entry<Long, List<Integer>> result = map.entrySet().stream() .max(Map.Entry.comparingByKey())//再求key的max .orElseThrow();System.out.println("result = " + result);
运行此代码将生成以下内容。
result = 3=[3, 4, 5]//最多的length列表
这意味着有三种长度的字符串在此流中呈现三次:3、4 和 5。
此示例显示嵌套在另外两个collector中的collector,在应用此 API 时,这种状况常常产生。乍一看可能看起来很吓人,但它只是应用上游collector组合成了collector。
您能够看到为什么领有这些中继collector很乏味。通过应用collector提供的中继操作,您能够为简直任何类型的解决创立上游collector,从而对map的值进行后续解决。
应用collector进行filtering和flatmapping
filtering collector遵循与mapping collector雷同的模式。它是应用 Collectors.filtering()
) 工厂办法创立的,该办法接管惯例Predicate来filter数据,同时要有必须的上游collector。
由 Collectors.flatMapping()
) 工厂办法创立的flatmapping collector也是如此,它接管flatmapping函数(返回流的函数)和必须的上游collector。
应用末端collector
Collector API 还提供了几个末端操作,对应于Stream API 上可用的末端操作。
maxBy()
和)minBy()。
)这两个办法都将comparator作为参数,如果解决的流自身为空,则返回一个Optional对象。summingInt
)()、summingLong()
) 和summingDouble()。
)这三种办法将mapping函数作为参数,别离将流的元素mapping为int
,long
,double
,而后对它们求和。averageagingInt()
)、averageagingLong()
)和averageagingDouble()
).这三种办法也将mapping函数作为参数,在计算平均值之前别离将流的元素map为int
,long
,double
。这些collector的工作形式与IntStream
、LongStream
和DoubleStream
中定义的相应average()
) 办法不同。它们都返回一个Double
实例,对于空流返回 0。而数字流的average()
) 办法返回一个Optional对象,该对象对于空流为空。
创立本人的collector
理解collector的工作原理
如前所述,Collectors
工厂类仅解决对象流,因为将collector对象作为参数的 collect()
办法仅存在于 Stream
中。如果您须要收集数字流,那么您须要理解collector的组成元素是什么。
简略说,collector建设在四个根本组件之上。前两个用于收集流的元素。第三个仅用于并行流。某些类型的collector须要第四个,这些collector须要对构建的容器作后续解决。
第一个组件用于创立收集流元素的容器。此容器易于辨认。例如,在上一部分介绍的状况下,咱们应用了 ArrayList
类、HashSet
类和 HashMap
类。能够应用supplier
实例对创立此类容器进行建模。第一个组件称为supplier。
第二个组件旨在将流中的单个元素增加到容器。Stream API 的实现将反复调用此操作,将流的所有元素一一增加到容器中。
在Collector API中,此组件由BiConsumer
的实例建模。这个biconsumer有两个参数。
- 第一个是容器自身,流的先前元素填充了局部。
- 第二个是应增加的流元素。
此biconsumer在Collector API 的上下文中称为accumulator。
这两个组件应该足以让collector工作,但 Stream API 带来了一个束缚,使collector失常工作须要另外两个组件。
你可能还记得,Stream API 反对并行化。本教程稍后将更具体地介绍这一点。您须要晓得的是,并行化将流的元素拆分为子流,每个元素都由 CPU 的内核解决。Collector API 能够在这样的上下文中工作:每个子流将只收集在本人的容器实例中。
解决完这些子流后,您将领有多个容器,每个容器都蕴含它所解决的子流中的元素。这些容器是雷同的,因为它们是与同一supplier一起创立的。当初,您须要一种办法将它们合并为一个。为了可能做到这一点,Collector API 须要第三个组件,即combiner,它将这些容器合并在一起。combiner由 BinaryOperator
的实例建模,该实例接管两个局部填充的容器并返回一个。
Stream API 的 collect()
也有个重载,这个 BinaryOperator
变成了 BiConsumer
,咱们次要应用这个。
第四个组件称为finisher,本局部稍后将介绍。
在汇合中收集原始类型
应用前三个组件,您能够尝试专用数字流中的 collect()
) 办法。IntStream.collect()
) 办法有三个参数:
Supplier
的实例,称为supplier;ObjIntConsumer
的实例,称为accumulator;BiConsumer
的实例,称为combiner。
让咱们编写代码以在List<Integer>
中收集IntStream
。
Supplier<List<Integer>> supplier = ArrayList::new;//容器ObjIntConsumer<List<Integer>> accumulator = Collection::add;//元素如何进入容器BiConsumer<List<Integer>, List<Integer>> combiner = Collection::addAll;//多个片段如何合并List<Integer> collect = IntStream.range(0, 10) .collect(supplier, accumulator, combiner );System.out.println("collect = " + collect);
运行此代码将生成以下后果。
collect = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
将这些数据收集为Set只须要更改supplier
的实现并相应地调整类型。
在 StringBuffer 中收集原始类型
让咱们钻研一下如何本人实现 Collectors.joining()
) ,以将原始类型流的元素连贯在单个字符串中。String
类是不可变的,因而无奈在其中累积元素。您能够应用可变的 StringBuffer
类。
在 StringBuffer
中收集元素遵循与前一个雷同的模式。
Supplier<StringBuffer> supplier = StringBuffer::new;//ObjIntConsumer<StringBuffer> accumulator = StringBuffer::append;BiConsumer<StringBuffer, StringBuffer> combiner = StringBuffer::append;StringBuffer collect = IntStream.range(0, 10) .collect(supplier, accumulator, combiner);System.out.println("collect = " + collect);
运行此代码将生成以下后果。
collect = 0123456789
应用finisher对collector进行后续解决
你在上一段中编写的代码简直实现了你须要的:它在 StringBuffer
实例中连贯字符串,你能够通过调用它的 toString()
) 办法来创立一个惯例的 String
对象。然而 Collectors.joining()
) collector间接生成一个字符串
,而无需你调用 toString()。
)那么它是怎么做到的呢?
Collector API 准确地定义了第四个组件来解决这种状况,称为finisher。finisher是一个Function
,它获取累积元素的容器并将其转换为其余内容。在 Collectors.joining()
) 的状况下,这个函数只是上面的。
Function<StringBuffer, String> finisher = stringBuffer -> stringBuffer.toString();
对很多collector来说,finisher只是恒等函数。比方:toList()
)、toSet
)()、groupingBy()
) 和 toMap()。
)
其余状况下,collector外部应用的可变容器成为中继容器,在返回到应用程序之前,该容器将mapping为其余对象(可能是另一个容器)。这就是Collector API 解决不可变列表、set或map创立的形式。finisher用于将中继容器密封到不可变容器中,返回到应用程序。
finisher还有其余用处,能够进步代码的可读性。Collectors
工厂类有一个工厂办法,咱们还没有介绍:collectingAndThen()
) 办法。此办法将collector作为第一个参数,将finisher作为第二个参数。它会将第一个collector收集的后果,应用您提供的finisher对其进行mapping。
您可能还记得以下示例,咱们曾经在后面的局部中屡次查看过该示例。它是对于提取柱状图的最大值。
Collection<String> strings = List.of("two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve");Map<Integer, Long> histogram = strings.stream() .collect( Collectors.groupingBy( String::length, Collectors.counting()));Map.Entry<Integer, Long> maxValue = histogram.entrySet().stream() .max(Map.Entry.comparingByValue()) .orElseThrow();System.out.println("maxValue = " + maxValue);
第一步,您构建了 Map<Integer,Long>
类型的柱状图,在第二步中,您提取了此柱状图的最大值,按值比拟键值对。
第二步实际上是将map转换为非凡的键/值对。您能够应用以下函数对其进行建模。
Function<Map<Integer, Long>, Map.Entry<Integer, Long>> finisher = map -> map.entrySet().stream() .max(Map.Entry.comparingByValue()) .orElseThrow();
此函数的类型起初可能看起来很简单。事实上,它只是从map中提取一个键值对,类型为 Map.Entry
。
当初您曾经有了这个函数,您能够应用 collectingAndThen()
) 将此最大值提取步骤集成到collector自身中。而后,模式将变为以下内容。
Collection<String> strings = List.of("two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve");Function<Map<Integer, Long>, Map.Entry<Integer, Long>> finisher = map -> map.entrySet().stream() .max(Map.Entry.comparingByValue()) .orElseThrow();//提取此finisher须要特地留神类型,生产Map,产出EntryMap.Entry<Integer, Long> maxValue = strings.stream() .collect( Collectors.collectingAndThen( Collectors.groupingBy( String::length, Collectors.counting()), finisher ));System.out.println("maxValue = " + maxValue);
您可能想晓得为什么须要编写此看起来非常复杂的代码?
当初,您曾经领有了由单个collector建模的最大值提取器,您能够将其用作另一个collector的上游collector。做到这一点,能够组合更多的collector对您的数据进行更简单的计算。
将两个collector的后果与三通collector相结合
在 Java SE 12 的 Collectors
类中增加了一个名为 teeing()
) 的办法。此办法须要两个上游collector和一个合并函数。
让咱们通过一个场景,看看您能够应用collector做什么。设想一下,您有以下Car
,Truck
两种record。
enum Color { RED, BLUE, WHITE, YELLOW}enum Engine { ELECTRIC, HYBRID, GAS}enum Drive { WD2, WD4}interface Vehicle {}record Car(Color color, Engine engine, Drive drive, int passengers) {}record Truck(Engine engine, Drive drive, int weight) {}
Car
对象有几个组成部分:色彩、引擎、驱动器以及它能够运输的肯定数量的乘客。Truck
有引擎,有驱动器,能够运输一定量的货物。两者都实现雷同的接口:Vehicle
假如您有一系列Vehicle
,您须要找到所有装备电动引擎的Car
。依据您的应用程序,您可能会应用流filter您的Car
汇合。或者,如果您晓得下一个需要,将是找到装备混合能源引擎的Car
,您可能更违心筹备一个map,以引擎为键,并以装备该引擎的Car
列表作为值。在这两种状况API 都会为你提供正确的模式来获取所需的内容。
假如您须要将所有电动Truck
增加到此汇合中。也有可能想一次解决所有Vehicle
,然而用于filter数据的Predicate变得越来越简单。它可能如下所示。
Predicate<Vehicle> predicate = vehicle -> vehicle instanceof Car car && car.engine() == Engine.ELECTRIC || vehicle instanceof Truck truck && truck.engine() == Engine.ELECTRIC;//这个是instanceof新用法,前面间接赋值变量,同时跟短路操作
您真正须要的是以下内容:
- filter
Vehicle
以取得所有电动Car
- filter
Vehicle
以取得所有电动Truck
- 合并两个后果。
这正是teeing collector能够为您做的事件。teeing collector由 Collectors.teeing()
) 工厂办法创立,该办法接管三个参数。
- 第一个上游collector,用于收集流的数据。
- 第二个上游collector,也用于收集数据。
- 一个bifunction,用于合并由两个上游collector创立的两个容器。
您的数据将一次性解决,以保障最佳性能。
咱们曾经介绍了应用collector来filter流元素的模式。合并函数只是对 Collection.addAll()
) 办法的调用。以下是代码:
List<Vehicle> electricVehicles = vehicles.stream() .collect( Collectors.teeing( Collectors.filtering( vehicle -> vehicle instanceof Car car && car.engine() == Engine.ELECTRIC, Collectors.toList()), Collectors.filtering( vehicle -> vehicle instanceof Truck truck && truck.engine() == Engine.ELECTRIC, Collectors.toList()), (cars, trucks) -> { cars.addAll(trucks); return cars; }));
实现collector接口
为什么要实现collector接口?
有三种办法能够创立本人的collector。
包含将现有collector与Collectors
工厂类联合,将collector作为上游collector传递给另一个collector,或者作为finisher一起应用 collectingAndThen()
) 。咱们下面教程中曾经介绍过。
您还能够调用 collect()
) 办法,该办法接管构建collector的三个元素。这些办法在原始类型流和对象流上都可用。他们接管了咱们在后面局部中提出的三个参数。
- 用于创立可变容器的supplier,其中累积了流的元素。
- accumulator,由biconsumer建模。
- combiner也由biconsumer建模,用于组合两个局部填充的容器,用于并行流的状况。
第三种办法是本人实现 Collector
接口,并将您的实现传递给咱们曾经介绍过的 collect()
办法。实现本人的collector能够为您提供最大的灵活性,但也更具技术性。
理解collector的参数类型
让咱们检查一下这个接口的参数。
interface Collector<T, A, R> { // content of the interface}
让咱们首先查看以下类型:T
,R
第一种类型是 ,它对应于此collector正在解决的流元素的类型。T
最初一个类型是 ,它是此collector生成的类型。R
比方在 Stream
实例上调用的 toList()
) collector,类型R
为 List
。它 toSet()
) collector将是 Set
。
groupingBy()
) 办法接管一个函数作参数,来计算返回map的键。如果用它收集 Stream
,则须要传递一个对T
实例作mapping 的函数。因而,生成的map的类型将为 Map<K,List<T>>
。也就是R
的类型。
A
类型解决起来比较复杂。您可能已尝试应用 IDE 来存储您在后面的示例中创立的collector之一。如果这样做,您可能意识到 IDE 没有为此类型提供显式值。以下示例就是这种状况。
Collection<String> strings = List.of("two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve");Collector<String, ?, List<String>> listCollector = Collectors.toList();List<String> list = strings.stream().collect(listCollector);Collector<String, ?, Set<String>> setCollector = Collectors.toSet();Set<String> set = strings.stream().collect(setCollector);Collector<String, ?, Map<Integer, Long>> groupingBy = Collectors.groupingBy(String::length, Collectors.counting());Map<Integer, Long> map = strings.stream().collect(groupingBy);
对于所有这些collector,第二个参数类型仅为 ?
。
如果须要实现Collector
接口,则必须为A
提供个显式值。A
是此collector应用的中继可变容器的理论类型。对于 toList(
)) collector,它将是 ArrayList
,对于 toSet()
) collector,它将是 HashSet
。事实上,此类型被 toList()
) 的返回类型暗藏了,这就是为什么在后面的示例中无奈将 ?
类型替换为 ArrayList
的起因。
即便外部可变容器是由实现间接返回的,也可能产生类型A
和R
不同的状况。例如, toList()
) ,您能够通过批改 ArrayList
<T> 和 List<T> 来实现 Collector><
T,A,R> 接口。
理解collector的特色
collector定义了外部特色,流实现用它来优化collector应用。
有三个。
IDENTITY_FINISH
批示此collector的finisher是恒等函数。该实现不会为具备此特色的collector调用finisher。UNORDERED
批示此collector不保留它解决流元素的程序。toSet()
) collector就是这种状况。而toList()
) 就没有。CONCURRENT
个性示意accumulator用来存储已解决元素的容器反对并发拜访。这一点对于并行流很重要。
这些特色在collectorCollector.Characteristics
枚举中定义,并由Collector
接口的 characteristics()
) 办法以set返回。
实现 toList() 和 toSet() collector
应用这些元素,您当初能够从新创立相似于 toList()
) collector的实现。
class ToList<T> implements Collector<T, List<T>, List<T>> { public Supplier<List<T>> supplier() { return ArrayList::new; } public BiConsumer<List<T>, T> accumulator() { return Collection::add; } public BinaryOperator<List<T>> combiner() { return (list1, list2) -> {list1.addAll(list2); return list1; }; } public Function<List<T>, List<T>> finisher() { return Function.identity(); } public Set<Characteristics> characteristics() { return Set.of(Characteristics.IDENTITY_FINISH);//不调用finisher }}
能够应用以下模式应用此collector。
Collection<String> strings = List.of("one", "two", "three", "four", "five") ;List<String> result = strings.stream().collect(new ToList<>());System.out.println("result = " + result);
此代码打印出以下后果。
result = [one, two, three, four, five]
实现一个相似于 toSet()
) 的collector只须要两处批改。
supplier()
) 办法将返回HashSet::new
characteristics()
) 办法会将Features.UNORDERED
增加到返回的set中。
实现 joining() collector
从新创立此collector的实现很乏味,因为它只对字符串进行操作,并且它的finisher不是恒等函数。
此collector在 StringBuffer
实例中累积它解决的字符串,而后调用 toString()
) 办法以生成final后果。
此collector的特色集为空。它的确保留了解决元素的程序(因而没有UNORDERED特色),它的finisher不是恒等函数,并且不能并发应用。
让咱们看看如何实现这个collector。
class Joining implements Collector<String, StringBuffer, String> { public Supplier<StringBuffer> supplier() { return StringBuffer::new; } public BiConsumer<StringBuffer, String> accumulator() { return StringBuffer::append; } public BinaryOperator<StringBuffer> combiner() { return StringBuffer::append; } public Function<StringBuffer, String> finisher() {//会调用 return Object::toString; } public Set<Characteristics> characteristics() { return Set.of(); }}
您能够在以下示例中看到如何应用此collector。
Collection<String> strings = List.of("one", "two", "three", "four", "five") ;String result = strings.stream().collect(new Joining());System.out.println("result = " + result);
运行此代码将生成以下后果。
result = onetwothreefourfive
要反对分隔符、前缀和后缀能够应用 StringJoiner
。
应用Optional
创立Optional对象
Optional
类是具备公有构造函数的final类。因而,创立它实例的惟一办法是调用其工厂办法之一。其中有三个。
- 您能够通过调用
Optional.empty()
) 创立一个空的Optional。 - 您能够通过调用
Optional.of()
) 将某元素作为参数。不容许将 null传递给此办法。这种状况下,您将取得一个 NullPointerException
。 - 您能够通过调用
Optional.ofNullable()
) 将某元素作为参数。能够将null传递给此办法。这种状况下,您将取得一个空的Optional。
这些是创立此类实例的惟一办法。如您所见,不能将null间接赋给Optional对象。关上非空Optional将始终返回非null。
Optional<T>
有三个等效的类,用于专用数字流:OptionalInt
、OptionalLong
和 OptionalDouble
。这些类是原始类型(即值)的包装器。ofNullable()
) 办法对这些类没有意义,因为原始值不能为null。
关上Optional对象
有几种办法能够应用Optional元素并拜访它包装的元素(如果有)。你能够间接查问你领有的实例,如果外面有货色,就关上它,或者你能够在下面应用相似流的办法: map()
), flatMap()
), filter()
),甚至是 forEach()
) 的等价物。
关上Optional以获取其内容时应审慎,因为如果Optional为空,它将引发 NoSuchElementException
。除非您确定Optional元素中存在元素,否则应首先通过测试来爱护此操作。
有两种办法可供您测试Optional对象:isPresent()
) 和 isEmpty(),
)它们在 Java SE 11 中增加。
而后,要关上您的Optional,您能够应用以下办法。
get()
):此办法已被弃用,因为 is 看起来像一个 getter,但如果Optional为空,它能够抛出NoSuchElementException
。orElseThrow()
) 是自 Java SE 10 以来的首选模式。它与get()
)办法雷同,但它的名称毫无疑问它能够抛出NoSuchElementException
。orElseThrow(Supplier exceptionSupplier):
)与后面的办法雷同。它应用您传递的supplier作为参数来创立它引发的异样。
您还能够提供一个对象,如果Optional对象为空,将返回该对象。
orElse(T returnObject):
)如果在空的Optional值上调用,则返回参数。orElseGet(Supplier supplier):
)与前一个雷同。实际上,仅在须要时调用所提供的supplier。
最初,如果此Optional为空,则能够创立另一个Optional。
or(supplier<Optional>supplier):
)如果它不为空,则返回此未修改的Optional,如果空,则调用提供的supplier。此supplier创立另一个Optional供办法返回。
解决Optional对象
Optional
类还提供模式,以便您能够将Optional对象与流解决集成。它具备间接对应Stream API 的办法,您能够应用这些办法以雷同的形式解决数据,并且将与流无缝集成。这些办法是 map()
), filter()
),和flatMap()
),前两个接管的参数与Stream API中的办法雷同,后者的函数参数须要返回Optional<T>
而不是Stream
。
这些办法按以下规定返回Optional对象。
- 如果调用的对象为空,则返回Optional。
- 如果不为空,则它们的参数、函数或Predicate将利用于此Optional的内容。将后果包装在另一个Optional中返回。
应用这些办法能够在某些流模式中生成更具可读性的代码。
假如您有一个具备id
属性的Customer
实例列表。您须要查找具备给定 ID 的客户的名称。
您能够应用以下模式执行此操作。
String findCustomerNameById(int id){ List<Customer> customers = ...; return customers.stream() .filter(customer->customer.getId() == id); .findFirst()//返回Optional .map(Customer::getName) .orElse("UNKNOWN");}
您能够看到 map()
) 办法来自 Optional
类,它与流解决很好地集成在一起。你不须要查看 findFirst()
) 办法返回的Optional对象是否为空;调用map()
)实际上能够为您执行此操作。
找出发表文章最多的两位联结作者
让咱们看另一个更简单的示例。通过此示例,向您展现Stream API、Collector API 和Optional对象的几种次要模式。
假如您有一组须要解决的文章。一篇文章有题目、发表年份和作者列表。作者有一个名字。
您的列表中有很多文章,您须要晓得哪些作者一起联结发表了最多的文章。
您的第一个想法可能是为文章构建一对作者的流。这实际上是文章和作者集的笛卡尔乘积。您并不需要此流中的所有对。您对两位作者实际上是同一对的状况不感兴趣;一对作者(A1,A2)与(A2,A1)理论雷同。若要实现此束缚,能够增加约束条件,申明一对作者时,申明作者按字母程序排序。
让咱们为这个模型写两条record。
record Article (String title, int inceptionYear, List<Author> authors) {}record Author(String name) implements Comparable<Author> { public int compareTo(Author other) { return this.name.compareTo(other.name); }}record PairOfAuthors(Author first, Author second) { public static Optional<PairOfAuthors> of(Author first, Author second) {//用Optional实现了排序后的创立 if (first.compareTo(second) > 0) { return Optional.of(new PairOfAuthors(first, second)); } else { return Optional.empty(); } }}
在PairOfAuthors
record中的创立工厂办法,能够管制哪些实例是容许的,并避免不须要的创立。若要表明此工厂办法可能无奈生成后果,能够将其包装在Optional办法中。这齐全尊重了以下准则:如果无奈生成后果,则返回一个空的 optional。
让咱们编写一个函数,为给定的文章创立一个 Stream<PairOfAuthors>
。您能够用两个嵌套流生成笛卡尔乘积。
作为第一步,您能够编写一个bifunction,从文章和作者创立此流。
BiFunction<Article, Author, Stream<PairOfAuthors>> buildPairOfAuthors = (article, firstAuthor) -> article.authors().stream().flatMap(//对每个author都遍历authors创立作者对,生成Stream<PairOfAuthors> secondAuthor -> PairOfAuthors.of(firstAuthor, secondAuthor).stream());//Optional的Stream
此bifunction从 firstAuthor
和secondAuthor
创立一个Optional对象,取自基于文章作者构建的流。您能够看到 stream(
)) 办法是在 of()
) 办法返回的Optional对象上调用的。如果Optional流为空,则返回的流为空,否则仅蕴含一对作者。此流由 flatMap()
) 办法解决。此办法关上流,空的流将隐没,并且只有无效的对将呈现在生成的流中。
您当初能够构建一个函数,该函数应用此bifunction从文章中创立作者对流。
Function<Article, Stream<PairOfAuthors>> toPairOfAuthors = article -> article.authors().stream() .flatMap(firstAuthor -> buildPairOfAuthors.apply(article, firstAuthor));
找到联结发表最多的两位作者能够通过柱状图来实现,柱状图中的键是作者对,值是他们一起写的文章数。
您能够应用 groupingBy()
) 构建柱状图。让咱们首先创立一对作者的流。
Stream<PairOfAuthors> pairsOfAuthors = articles.stream() .flatMap(toPairOfAuthors);
此流的构建形式是,如果一对作者一起写了两篇文章,则这对作者在流中呈现两次。因而,您须要做的是计算每个对在此流中呈现的次数。这能够通过 groupingBy()
) 来实现,其中分类器是恒等函数:对自身。此时,这些值是您须要计数的对列表。所以上游collector只是 counting()
) collector。
Map<PairOfAuthors, Long> numberOfAuthorsTogether = articles.stream() .flatMap(toPairOfAuthors)//所有文章的Stream<PairOfAuthors> .collect(Collectors.groupingBy( Function.identity(), Collectors.counting()//<PairOfAuthors, Long> ));
找到一起发表文章最多的作者包含提取此map的最大值。您能够为此解决创立以下函数。
Function<Map<PairOfAuthors, Long>, Map.Entry<PairOfAuthors, Long>> maxExtractor = map -> map.entrySet().stream() .max(Map.Entry.comparingByValue()) .orElseThrow();
此函数在 Stream.max()
) 办法返回的Optional对象上调用 orElseThrow()
) 办法。
这个Optional对象能够为空吗?要使其为空,map自身必须为空,这意味着原始流中没有成对的作者。只有您至多有一篇文章至多有两位作者,那么这个Optional就不为空。
找出每年发表文章最多的两位联结作者
让咱们更进一步,想晓得您是否能够依据年份进行雷同的解决。事实上,如果能应用单个collector实现,接下来就能够将其作为上游collector传递给 groupingBy(Article::inceptionYear)
) 。
对map后续提取最大值能够应用collectingAndThen()
)。此模式已在上一节“应用finisher对collector进行后续解决”中介绍过。此collector如下。
让咱们提取 groupingBy()
) collector和finisher。如果应用 IDE 键入此代码,能够获取collector的正确类型。
Collector<PairOfAuthors, ?, Map<PairOfAuthors, Long>> groupingBy = Collectors.groupingBy( Function.identity(), Collectors.counting() );Function<Map<PairOfAuthors, Long>, Map.Entry<PairOfAuthors, Long>> finisher = map -> map.entrySet().stream() .max(Map.Entry.comparingByValue()) .orElseThrow();
当初,您能够将它们合并到单个 collectingAndThen()
) 中。将 groupingBy()
) 作为为第一个参数,将finisher
作为第二个。
Collector<PairOfAuthors, ?, Map.Entry<PairOfAuthors, Long>> pairOfAuthorsEntryCollector = Collectors.collectingAndThen( Collectors.groupingBy( Function.identity(), Collectors.counting() ), map -> map.entrySet().stream() .max(Map.Entry.comparingByValue()) .orElseThrow() );
当初,您能够应用初始flatmap操作和此collector编写残缺模式。
Map.Entry<PairOfAuthors, Long> numberOfAuthorsTogether = articles.stream() .flatMap(toPairOfAuthors) .collect(pairOfAuthorsEntryCollector);
多亏了 flatMapping
)(),您能够通过合并中继 flatMap()
) 和末端collector来应用单个collector编写此代码。以下代码等效于上一个代码。
Map.Entry<PairOfAuthors, Long> numberOfAuthorsTogether = articles.stream() .collect( Collectors.flatMapping( toPairOfAuthors, pairOfAuthorsEntryCollector));
找到每年发表最多的两位联结作者,只需将这个 flatMapping
)() 作为上游collector传递给正确的 groupingBy()
) 即可。
Collector<Article, ?, Map.Entry<PairOfAuthors, Long>> flatMapping = Collectors.flatMapping( toPairOfAuthors, pairOfAuthorsEntryCollector));Map<Integer, Map.Entry<PairOfAuthors, Long>> result = articles.stream() .collect( Collectors.groupingBy( Article::inceptionYear, flatMapping ) );
你可能还记得,在这个flatMapping
)()的深处,有一个对Optional.orElseThrow()
)的调用。在这个的模式中,很容易查看此调用是否会失败,因为此时有一个空的Optional很容易猜到。
当初咱们已将此collector用作上游collector,状况就不同了。你怎么能确定,每年至多有一篇文章由至多两位作者撰写?爱护此代码免受任何 NoSuchElementException
的影响会更平安。
防止关上Optional
在第一个上下文中能够承受的模式当初更加危险。解决它包含首先不要调用orElseThrow()。
)
这种状况下,collector将变为以下项。它不是创立一对作者和一长串数字的键值对,而是将后果包装在一个Optional对象中。
Collector<PairOfAuthors, ?, Optional<Map.Entry<PairOfAuthors, Long>>> pairOfAuthorsEntryCollector = Collectors.collectingAndThen( Collectors.groupingBy( Function.identity(), Collectors.counting() ), map -> map.entrySet().stream() .max(Map.Entry.comparingByValue()) );
请留神,orElseThrow()
) 不再被调用,从而导致collector的签名中有一个Optional。
这个Optional也呈现在 flatMapping()
) collector的签名中。
Collector<Article, ?, Optional<Map.Entry<PairOfAuthors, Long>>> flatMapping = Collectors.flatMapping( toPairOfAuthors, pairOfAuthorsEntryCollector );
应用此collector会创立一个类型为 Map<Integer, Optional<Map.Entry<PairOfAuthors, Long>>>
类型的map,咱们不须要这种类型:领有一个值为Optional的map是无用的,而且可能很低廉。这是一种反模式。可怜的是,在计算此最大值之前,您无奈猜想此Optional是否为空。
构建此中继map后,您须要删除空的Optional来构建示意所需柱状图的map。咱们将应用与之前雷同的技术:在flatMap()) 中调用Optional的stream()
))办法,以便 flatMap()
) 操作静默删除空的Optional。
模式如下。
Map<Integer, Map.Entry<PairOfAuthors, Long>> histogram = articles.stream() .collect( Collectors.groupingBy( Article::inceptionYear, flatMapping ) ) // Map<Integer, Optional<Map.Entry<PairOfAuthors, Long>>> .entrySet().stream() .flatMap( entry -> entry.getValue()//如果Optional为空,会成为空流,从而平安跳过 .map(value -> Map.entry(entry.getKey(), value)) .stream()) .collect(Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue )); // Map<Integer, Map.Entry<PairOfAuthors, Long>>
请留神此模式中的flatmap函数。它承受一个entry
作参数 ,类型为 Optional<Map.Entry<PairOfAuthors, Long>>
,并在此Optional上调用 map()。
)
如果Optional为空,则此调用返回空的Optional。而后疏忽map函数。接下来调用 stream(
)) 返回一个空流,该流将从支流中删除,因为咱们处于 flatMap()
) 调用中。
如果Optional中有一个值,则应用此值调用map函数。此map函数创立一个具备雷同键和此现有值的新键值对。此键值对的类型为 Map.Entry,
并且通过此 map()
) 办法将其包装在Optional对象中。对 stream(
)) 的调用会创立一个蕴含此Optional内容的流,而后由 flatMap()
) 调用关上该流。
此模式用空的Optional将 Stream<Map.Entry<Integer, Optional<Map.Entry<PairOfAuthors, Long>>>>
mapping为 Stream<Map.Entry<Integer, Map.Entry<PairOfAuthors, Long>>>
,删除所有具备空Optional的键/值对。
应用 toMap()
) collector能够平安地从新创立map,因为您晓得在此流中不能应用两次雷同的键。
此模式应用了Optional和Stream API 的三个要点。
Optional.map()
) 办法,如果在空Optional上调用,则返回空的Optional。Optional.stream()
) 办法,该办法在 Optional的内容上关上流。如果Optional为空,则返回的流也为空。它容许您从Optional空间无缝的挪动到流空间。Stream.flatMap()
) 办法,用于关上从Optional构建的流,以静默形式删除空流。
生产Optional的内容
Optional
类还具备两个将Consumer作为参数的办法。
ifPresent(Consumer consumer)
):此办法应用此Optional的内容(如果有)调用提供的Consumer。它实际上等同于Stream.forEach(Consumer)
) 办法。ifPresentOrElse(Consumer consumer, Runnable runnable):
)如果 Optional非空,此办法与前一个办法雷同。如果空,则调用提供的Runnable
实例。
烧哥总结
(验证中,代码库继续更新)
lambda将匿名类换成了匿名办法,能代表某个操作,让代码更直观(语法糖),但良好的命名
很重要。
改写为lambda首先得是函数接口,Operator是Function的简化版。
能够序列化,从而能够作为字段、办法参数和返回类型,实现了办法援用
、链式调用、函数式编程。
lambda曾经深刻JDK外部,所以性能方面很关注,为防止装箱拆箱,提供了很多原生类型专用版,但有时候要手动装箱。
为了性能
,防止在内存中解决大量数据,同时也进步可读性,呈现了Stream API。
流解决的整个过程最好都是流,所以有flatmap、mapMulti各种中继操作,
甚至末端collector也能够有上游collector,甚至collector能够串联、三通,比方神奇的Collectors.flatMapping()
流不应该作为变量或参数。
流中不应该扭转外围变量,会捕捉外界变量,升高解决性能
,也会把并行流变成多线程并发。
每次中继操作都产生一个新流。
同样为了性能
,reduce能够并行,但要具备可联合性
、要有幺元
。如果幺元未知,会返回Optional。
三参数的reduce组合了mapping过程。
专用数字流
的sum、min、max、count、average、summaryStatistics为末端操作。
转换为流的源如果用Set,会是乱序的。
map、flatmap会删除SORTED、DISTINCTED、NONNULL。
本教程未具体阐明的:spliterator、不可变流、并发流。
Stream.collect
(Collectors.toList()) 只能用于对象流,数字流要么装箱,要么用三参数那个,或者自定义collector,五个参数。
flatmap会跳过空流,包含Optional.stream()
产生的流,所以看到Optional,不要orElseThrow()
,能够用flatmap取出。
API是看起来越来越简单,Collectors.mapping()
public static <T,U,A,R> Collector<T,?,R> mapping(Function<? super T,? extends U> mapper, Collector<? super U,A,R> downstream)
办法名后面四个,返回类型里三个,还有问号,参数里super了三个。
Map<City, Set<String>> lastNamesByCity = people.stream().collect( groupingBy(Person::getCity, mapping(Person::getLastName, toSet())));
尽管很好用。