本章内容
筛选、切片和匹配
查找、匹配和归约
使用数值范围等数值流
从多个源创建流
无限流
在上一章中你已看到了,流让你从外部迭代转向内部迭代。 这样,你就用不着写下面这样的代码来显式地管理数据集合的迭代(外部迭代)了:
List<Dish> vegetarianDishes = new ArrayList<>;for(Dish d: menu){ if(d.isVegetarian){ vegetarianDishes.add(d); }}
你可以使用支持filter
和collect
操作的Stream API(内部迭代)管理对集合数据的迭代。你只需要将筛选行为作为参数传递给filter
方法就行了。
import static java.util.stream.Collectors.toList;List<Dish> vegetarianDishes = menu.stream .filter(Dish::isVegetarian) .collect(toList);
这种处理数据的方式很有用,因为你让Stream API管理如何处理数据。这样Stream API就可以在背后进行多种优化。此外,使用内部迭代的话,Stream API可以决定并行运行你的代码。这要是用外部迭代的话就办不到了,因为你只能用单一线程挨个迭代。
在本章中,你将会看到Stream API支持的许多操作。这些操作能让你快速完成复杂的数据查询,如筛选、切片、映射、查找、匹配和归约。接下来,我们会看看一些特殊的流:数值流、来自文件和数组等多种来源的流,最后是无限流。
5.1 筛选和切片
在本节中,我们来看看如何选择流中的元素:用谓词筛选,筛选出各不相同的元素,忽略流中的头几个元素,或将流截短至指定长度。
5.1.1 用谓词筛选
Streams
接口支持filter
方法(你现在应该很熟悉了)。该操作会接受一个谓词(一个返回boolean
的函数)作为参数,并返回一个包括所有符合谓词的元素的流。例如,你可以像图5-1所示的这样,筛选出所有素菜,创建一张素食菜单:
List<Dish> vegetarianMenu = menu.stream .filter(Dish::isVegetarian) ←─方法引用检查菜肴是否适合素食者 .collect(toList);
图 5-1 用谓词筛选一个流
5.1.2 筛选各异的元素
流还支持一个叫作distinct
的方法,它会返回一个元素各异(根据流所生成元素的hashCode
和equals
方法实现)的流。例如,以下代码会筛选出列表中所有的偶数,并确保没有重复。图5-2直观地显示了这个过程。
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);numbers.stream .filter(i -> i % 2 == 0) .distinct .forEach(System.out::println);
图 5-2 筛选流中各异的元素
5.1.3 截短流
流支持limit(n)
方法,该方法会返回一个不超过给定长度的流。所需的长度作为参数传递给limit
。如果流是有序的,则最多会返回前n
个元素。比如,你可以建立一个List
,选出热量超过300卡路里的头三道菜:
List<Dish> dishes = menu.stream .filter(d -> d.getCalories > 300) .limit(3) .collect(toList);
图5-3展示了filter
和limit
的组合。你可以看到,该方法只选出了符合谓词的头三个元素,然后就立即返回了结果。
图 5-3 截短流
请注意limit
也可以用在无序流上,比如源是一个Set
。这种情况下,limit
的结果不会以任何顺序排列。
5.1.4 跳过元素
流还支持skip(n)
方法,返回一个扔掉了前n
个元素的流。如果流中元素不足n
个,则返回一个空流。请注意,limit(n)
和skip(n)
是互补的!例如,下面的代码将跳过超过300卡路里的头两道菜,并返回剩下的。图5-4展示了这个查询。
List<Dish> dishes = menu.stream .filter(d -> d.getCalories > 300) .skip(2) .collect(toList);
图 5-4 在流中跳过元素
在我们讨论映射操作之前,在测验5.1上试试本节学过的内容吧。
测验5.1:筛选
你将如何利用流来筛选前两个荤菜呢?
答案:你可以把
filter
和limit
复合在一起来解决这个问题,并用collect(toList)
将流转换成一个列表。List<Dish> dishes = menu.stream .filter(d -> d.getType == Dish.Type.MEAT) .limit(2) .collect(toList);
5.2 映射
一个非常常见的数据处理套路就是从某些对象中选择信息。比如在SQL里,你可以从表中选择一列。Stream API也通过map
和flatMap
方法提供了类似的工具。
5.2.1 对流中每一个元素应用函数
流支持map
方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的细微差别在于它是“创建一个新版本”而不是去“修改”)。例如,下面的代码把方法引用Dish::getName
传给了map
方法,来提取流中菜肴的名称:
List<String> dishNames = menu.stream .map(Dish::getName) .collect(toList);
因为getName
方法返回一个String
,所以map
方法输出的流的类型就是Stream<String>
。
让我们看一个稍微不同的例子来巩固一下对map
的理解。给定一个单词列表,你想要返回另一个列表,显示每个单词中有几个字母。怎么做呢?你需要对列表中的每个元素应用一个函数。这听起来正好该用map
方法去做!应用的函数应该接受一个单词,并返回其长度。你可以像下面这样,给map
传递一个方法引用String::length
来解决这个问题:
List<String> words = Arrays.asList("Java 8", "Lambdas", "In", "Action");List<Integer> wordLengths = words.stream .map(String::length) .collect(toList);
现在让我们回到提取菜名的例子。如果你要找出每道菜的名称有多长,怎么做?你可以像下面这样,再链接上一个map
:
List<Integer> dishNameLengths = menu.stream .map(Dish::getName) .map(String::length) .collect(toList);
5.2.2 流的扁平化
你已经看到如何使用map
方法返回列表中每个单词的长度了。让我们拓展一下:对于一张单词表,如何返回一张列表,列出里面各不相同的字符呢?例如,给定单词列表["Hello","World"]
,你想要返回列表["H","e","l", "o","W","r","d"]
。
你可能会认为这很容易,你可以把每个单词映射成一张字符表,然后调用distinct
来过滤重复的字符。第一个版本可能是这样的:
words.stream .map(word -> word.split("")) .distinct .collect(toList);
这个方法的问题在于,传递给map
方法的Lambda为每个单词返回了一个String
(String
列表)。因此,map
返回的流实际上是Stream<String>
类型的。你真正想要的是用Stream<String>
来表示一个字符流。图5-5说明了这个问题。
图 5-5 不正确地使用map
找出单词列表中各不相同的字符
幸好可以用flatMap
来解决这个问题!让我们一步步看看怎么解决它。
1. 尝试使用map
和Arrays.stream
首先,你需要一个字符流,而不是数组流。有一个叫作Arrays.stream
的方法可以接受一个数组并产生一个流,例如:
String arrayOfWords = {"Goodbye", "World"};Stream<String> streamOfwords = Arrays.stream(arrayOfWords);
把它用在前面的那个流水线里,看看会发生什么:
words.stream .map(word -> word.split("")) ←─将每个单词转换为由其字母构成的数组 .map(Arrays::stream) ←─让每个数组变成一个单独的流 .distinct .collect(toList);
当前的解决方案仍然搞不定!这是因为,你现在得到的是一个流的列表(更准确地说是Stream<String>
)!的确,你先是把每个单词转换成一个字母数组,然后把每个数组变成了一个独立的流。
2. 使用flatMap
你可以像下面这样使用flatMap
来解决这个问题:
List<String> uniqueCharacters = words.stream .map(w -> w.split("")) ←─将每个单词转换为由其字母构成的数组 .flatMap(Arrays::stream) ←─将各个生成流扁平化为单个流 .distinct .collect(Collectors.toList);
使用flatMap
方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。所有使用map(Arrays::stream)
时生成的单个流都被合并起来,即扁平化为一个流。图5-6说明了使用flatMap
方法的效果。把它和图5-5中map
的效果比较一下。
图 5-6 使用flatMap
找出单词列表中各不相同的字符
一言以蔽之,flatmap
方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。
在第10章,我们会讨论更高级的Java 8模式,比如使用新的Optional
类进行null
检查时会再来看看flatMap
。为巩固你对于map
和flatMap
的理解,试试测验5.2吧。
测验5.2:映射
(1) 给定一个数字列表,如何返回一个由每个数的平方构成的列表呢?例如,给定[1, 2, 3, 4, 5],应该返回[1, 4, 9, 16, 25]。
答案:你可以利用
map
方法的Lambda,接受一个数字,并返回该数字平方的Lambda来解决这个问题。List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);List<Integer> squares = numbers.stream .map(n -> n * n) .collect(toList);
(2) 给定两个数字列表,如何返回所有的数对呢?例如,给定列表[1, 2, 3]和列表[3, 4],应该返回[(1, 3), (1, 4), (2, 3), (2, 4), (3, 3), (3, 4)]。为简单起见,你可以用有两个元素的数组来代表数对。
答案:你可以使用两个
map
来迭代这两个列表,并生成数对。但这样会返回一个Stream<Stream<Integer>>
。你需要让生成的流扁平化,以得到一个Stream<Integer>
。这正是flatMap
所做的:List<Integer> numbers1 = Arrays.asList(1, 2, 3);List<Integer> numbers2 = Arrays.asList(3, 4);List<int> pairs = numbers1.stream .flatMap(i -> numbers2.stream .map(j -> new int{i, j}) ) .collect(toList);
(3) 如何扩展前一个例子,只返回总和能被3整除的数对呢?例如(2, 4)和(3, 3)是可以的。
答案:你在前面看到了,
filter
可以配合谓词使用来筛选流中的元素。因为在flatMap
操作后,你有了一个代表数对的int
流,所以你只需要一个谓词来检查总和是否能被3整除就可以了:List<Integer> numbers1 = Arrays.asList(1, 2, 3);List<Integer> numbers2 = Arrays.asList(3, 4);List<int> pairs = numbers1.stream .flatMap(i -> numbers2.stream .filter(j -> (i + j) % 3 == 0) .map(j -> new int{i, j}) ) .collect(toList);
其结果是[(2, 4), (3, 3)]。
5.3 查找和匹配
另一个常见的数据处理套路是看看数据集中的某些元素是否匹配一个给定的属性。Stream API通过allMatch
、anyMatch
、noneMatch
、findFirst
和findAny
方法提供了这样的工具。
5.3.1 检查谓词是否至少匹配一个元素
anyMatch
方法可以回答“流中是否有一个元素能匹配给定的谓词”。比如,你可以用它来看看菜单里面是否有素食可选择:
if(menu.stream.anyMatch(Dish::isVegetarian)){ System.out.println("The menu is (somewhat) vegetarian friendly!!");}
anyMatch
方法返回一个boolean
,因此是一个终端操作。
5.3.2 检查谓词是否匹配所有元素
allMatch
方法的工作原理和anyMatch
类似,但它会看看流中的元素是否都能匹配给定的谓词。比如,你可以用它来看看菜品是否有利健康(即所有菜的热量都低于1000卡路里):
boolean isHealthy = menu.stream .allMatch(d -> d.getCalories < 1000);
noneMatch
和allMatch
相对的是noneMatch
。它可以确保流中没有任何元素与给定的谓词匹配。比如,你可以用noneMatch
重写前面的例子:
boolean isHealthy = menu.stream .noneMatch(d -> d.getCalories >= 1000);
anyMatch
、allMatch
和noneMatch
这三个操作都用到了我们所谓的短路,这就是大家熟悉的Java中&&
和||
运算符短路在流中的版本。
短路求值
有些操作不需要处理整个流就能得到结果。例如,假设你需要对一个用
and
连起来的大布尔表达式求值。不管表达式有多长,你只需找到一个表达式为false
,就可以推断整个表达式将返回false
,所以用不着计算整个表达式。这就是短路。对于流而言,某些操作(例如
allMatch
、anyMatch
、noneMatch
、findFirst
和findAny
)不用处理整个流就能得到结果。只要找到一个元素,就可以有结果了。同样,limit
也是一个短路操作:它只需要创建一个给定大小的流,而用不着处理流中所有的元素。在碰到无限大小的流的时候,这种操作就有用了:它们可以把无限流变成有限流。我们会在5.7节中介绍无限流的例子。
5.3.3 查找元素
findAny
方法将返回当前流中的任意元素。它可以与其他流操作结合使用。比如,你可能想找到一道素食菜肴。你可以结合使用filter
和findAny
方法来实现这个查询:
Optional<Dish> dish = menu.stream .filter(Dish::isVegetarian) .findAny;
流水线将在后台进行优化使其只需走一遍,并在利用短路找到结果时立即结束。不过慢着,代码里面的Optional
是个什么玩意儿?
Optional
简介
Optional<T>
类(java.util.Optional
)是一个容器类,代表一个值存在或不存在。在上面的代码中,findAny
可能什么元素都没找到。Java 8的库设计人员引入了Optional<T>
,这样就不用返回众所周知容易出问题的null
了。我们在这里不会详细讨论Optional
,因为第10章会详细解释你的代码如何利用Optional
,避免和null
检查相关的bug。不过现在,了解一下Optional
里面几种可以迫使你显式地检查值是否存在或处理值不存在的情形的方法也不错。
isPresent
将在Optional
包含值的时候返回true,
否则返回false
。ifPresent(Consumer<T> block)
会在值存在的时候执行给定的代码块。我们在第3章介绍了Consumer
函数式接口;它让你传递一个接收T
类型参数,并返回void
的Lambda表达式。T get
会在值存在时返回值,否则抛出一个NoSuchElement
异常。T orElse(T other)
会在值存在时返回值,否则返回一个默认值。
例如,在前面的代码中你需要显式地检查Optional
对象中是否存在一道菜可以访问其名称:
menu.stream .filter(Dish::isVegetarian) .findAny ←─返回一个Optional<Dish> .ifPresent(d -> System.out.println(d.getName); ←─如果包含一个值就打印它,否则什么都不做
5.3.4 查找第一个元素
有些流有一个出现顺序(encounter order)来指定流中项目出现的逻辑顺序(比如由List
或排序好的数据列生成的流)。对于这种流,你可能想要找到第一个元素。为此有一个findFirst
方法,它的工作方式类似于findany
。例如,给定一个数字列表,下面的代码能找出第一个平方能被3整除的数:
List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);Optional<Integer> firstSquareDivisibleByThree = someNumbers.stream .map(x -> x * x) .filter(x -> x % 3 == 0) .findFirst; // 9
何时使用
findFirst
和findAny
你可能会想,为什么会同时有
findFirst
和findAny
呢?答案是并行。找到第一个元素在并行上限制更多。如果你不关心返回的元素是哪个,请使用findAny
,因为它在使用并行流时限制较少。
5.4 归约
到目前为止,你见到过的终端操作都是返回一个boolean
(allMatch
之类的)、void
(forEach
)或Optional
对象(findAny
等)。你也见过了使用collect
来将流中的所有元素组合成一个List
。
在本节中,你将看到如何把一个流中的元素组合起来,使用reduce
操作来表达更复杂的查询,比如“计算菜单中的总卡路里”或“菜单中卡路里最高的菜是哪一个”。此类查询需要将流中所有元素反复结合起来,得到一个值,比如一个Integer
。这样的查询可以被归类为归约操作(将流归约成一个值)。用函数式编程语言的术语来说,这称为折叠(fold),因为你可以将这个操作看成把一张长长的纸(你的流)反复折叠成一个小方块,而这就是折叠操作的结果。
5.4.1 元素求和
在我们研究如何使用reduce
方法之前,先来看看如何使用for-each
循环来对数字列表中的元素求和:
int sum = 0;for (int x : numbers) { sum += x;}
numbers
中的每个元素都用加法运算符反复迭代来得到结果。通过反复使用加法,你把一个数字列表归约成了一个数字。这段代码中有两个参数:
总和变量的初始值,在这里是
0
;将列表中所有元素结合在一起的操作,在这里是
+
。
要是还能把所有的数字相乘,而不必去复制粘贴这段代码,岂不是很好?这正是reduce
操作的用武之地,它对这种重复应用的模式做了抽象。你可以像下面这样对流中所有的元素求和:
int sum = numbers.stream.reduce(0, (a, b) -> a + b);
reduce
接受两个参数:
一个初始值,这里是0;
一个
BinaryOperator<T>
来将两个元素结合起来产生一个新值,这里我们用的是lambda (a, b) -> a + b
。
你也很容易把所有的元素相乘,只需要将另一个Lambda:(a, b) -> a * b
传递给reduce
操作就可以了:
int product = numbers.stream.reduce(1, (a, b) -> a * b);
图5-7展示了reduce
操作是如何作用于一个流的:Lambda反复结合每个元素,直到流被归约成一个值。
让我们深入研究一下reduce
操作是如何对一个数字流求和的。首先,0
作为Lambda(a
)的第一个参数,从流中获得4
作为第二个参数(b
)。0 + 4
得到4
,它成了新的累积值。然后再用累积值和流中下一个元素5
调用Lambda,产生新的累积值9
。接下来,再用累积值和下一个元素3
调用Lambda,得到12
。最后,用12
和流中最后一个元素9
调用Lambda,得到最终结果21
。
图 5-7 使用reduce
来对流中的数字求和
你可以使用方法引用让这段代码更简洁。在Java 8中,Integer
类现在有了一个静态的sum
方法来对两个数求和,这恰好是我们想要的,用不着反复用Lambda写同一段代码了:
int sum = numbers.stream.reduce(0, Integer::sum);
无初始值
reduce
还有一个重载的变体,它不接受初始值,但是会返回一个Optional
对象:
Optional<Integer> sum = numbers.stream.reduce((a, b) -> (a + b));
为什么它返回一个Optional<Integer>
呢?考虑流中没有任何元素的情况。reduce
操作无法返回其和,因为它没有初始值。这就是为什么结果被包裹在一个Optional
对象里,以表明和可能不存在。现在看看用reduce
还能做什么。
5.4.2 最大值和最小值
原来,只要用归约就可以计算最大值和最小值了!让我们来看看如何利用刚刚学到的reduce
来计算流中最大或最小的元素。正如你前面看到的,reduce
接受两个参数:
一个初始值
一个Lambda来把两个流元素结合起来并产生一个新值
Lambda是一步步用加法运算符应用到流中每个元素上的,如图5-7所示。因此,你需要一个给定两个元素能够返回最大值的Lambda。reduce
操作会考虑新值和流中下一个元素,并产生一个新的最大值,直到整个流消耗完!你可以像下面这样使用reduce
来计算流中的最大值,如图5-8所示。
Optional<Integer> max = numbers.stream.reduce(Integer::max);
图 5-8 一个归约操作——计算最大值
要计算最小值,你需要把Integer.min
传给reduce
来替换Integer.max
:
Optional<Integer> min = numbers.stream.reduce(Integer::min);
你当然也可以写成Lambda (x, y) -> x < y ? x : y
而不是Integer::min
,不过后者比较易读。
为了检验你对于reduce
操作的理解程度,试试测验5.3吧!
测验5.3:归约
怎样用
map
和reduce
方法数一数流中有多少个菜呢?答案:要解决这个问题,你可以把流中每个元素都映射成数字
1
,然后用reduce
求和。这相当于按顺序数流中的元素个数。int count = menu.stream .map(d -> 1) .reduce(0, (a, b) -> a + b);
map
和reduce
的连接通常称为map-reduce
模式,因Google用它来进行网络搜索而出名,因为它很容易并行化。请注意,在第4章中我们也看到了内置count
方法可用来计算流中元素的个数:long count = menu.stream.count;
归约方法的优势与并行化
相比于前面写的逐步迭代求和,使用
reduce
的好处在于,这里的迭代被内部迭代抽象掉了,这让内部实现得以选择并行执行reduce
操作。而迭代式求和例子要更新共享变量sum
,这不是那么容易并行化的。如果你加入了同步,很可能会发现线程竞争抵消了并行本应带来的性能提升!这种计算的并行化需要另一种办法:将输入分块,分块求和,最后再合并起来。但这样的话代码看起来就完全不一样了。你在第7章会看到使用分支/合并框架来做是什么样子。但现在重要的是要认识到,可变的累加器模式对于并行化来说是死路一条。你需要一种新的模式,这正是reduce
所提供的。你还将在第7章看到,使用流来对所有的元素并行求和时,你的代码几乎不用修改:stream
换成了parallelStream
。int sum = numbers.parallelStream.reduce(0, Integer::sum);
但要并行执行这段代码也要付一定代价,我们稍后会向你解释:传递给
reduce
的Lambda不能更改状态(如实例变量),而且操作必须满足结合律才可以按任意顺序执行。
到目前为止,你看到了产生一个Integer
的归约例子:对流求和、流中的最大值,或是流中元素的个数。你将会在5.6节看到,诸如sum
和max
等内置的方法可以让常见归约模式的代码再简洁一点儿。我们会在下一章中讨论一种复杂的使用 collect
方法的归约。例如,如果你想要按类型对菜肴分组,也可以把流归约成一个Map
而不是Integer
。
流操作:无状态和有状态
你已经看到了很多的流操作。乍一看流操作简直是灵丹妙药,而且只要在从集合生成流的时候把
Stream
换成parallelStream
就可以实现并行。当然,对于许多应用来说确实是这样,就像前面的那些例子。你可以把一张菜单变成流,用
filter
选出某一类的菜肴,然后对得到的流做map
来对卡路里求和,最后reduce
得到菜单的总热量。这个流计算甚至可以并行进行。但这些操作的特性并不相同。它们需要操作的内部状态还是有些问题的。诸如
map
或filter
等操作会从输入流中获取每一个元素,并在输出流中得到0或1个结果。这些操作一般都是无状态的:它们没有内部状态(假设用户提供的Lambda或方法引用没有内部可变状态)。但诸如
reduce
、sum
、max
等操作需要内部状态来累积结果。在上面的情况下,内部状态很小。在我们的例子里就是一个int
或double
。不管流中有多少元素要处理,内部状态都是有界的。相反,诸如
sort
或distinct
等操作一开始都和filter
和map
差不多——都是接受一个流,再生成一个流(中间操作),但有一个关键的区别。从流中排序和删除重复项时都需要知道先前的历史。例如,排序要求所有元素都放入缓冲区后才能给输出流加入一个项目,这一操作的存储要求是无界的。要是流比较大或是无限的,就可能会有问题(把质数流倒序会做什么呢?它应当返回最大的质数,但数学告诉我们它不存在)。我们把这些操作叫作有状态操作。
你现在已经看到了很多流操作,可以用来表达复杂的数据处理查询。表5-1总结了迄今讲过的操作。你可以在下一节中通过一个练习来实践一下。
表5-1 中间操作和终端操作
操作
类型
返回类型
使用的类型/函数式接口
函数描述符
filter
中间
Stream<T>
Predicate<T>
T -> boolean
distinct
中间(有状态-无界)
Stream<T>
skip
中间(有状态-有界)
Stream<T>
long
limit
中间(有状态-有界)
Stream<T>
long
map
中间
Stream<R>
Function<T, R>
T -> R
flatMap
中间
Stream<R>
Function<T, Stream<R>>
T -> Stream<R>
sorted
中间(有状态-无界)
Stream<T>
Comparator<T>
(T, T) -> int
anyMatch
终端
boolean
Predicate<T>
T -> boolean
noneMatch
终端
boolean
Predicate<T>
T -> boolean
allMatch
终端
boolean
Predicate<T>
T -> boolean
findAny
终端
Optional<T>
findFirst
终端
Optional<T>
forEach
终端
void
Consumer<T>
T -> void
collect
终端
R
Collector<T, A, R>
reduce
终端(有状态-有界)
Optional<T>
BinaryOperator<T>
(T, T) -> T
count
终端
long
5.5 付诸实践
在本节中,你会将迄今学到的关于流的知识付诸实践。我们来看一个不同的领域:执行交易的交易员。你的经理让你为八个查询找到答案。你能做到吗?我们在5.5.2节给出了答案,但你应该自己先尝试一下作为练习。
(1) 找出2011年发生的所有交易,并按交易额排序(从低到高)。
(2) 交易员都在哪些不同的城市工作过?
(3) 查找所有来自于剑桥的交易员,并按姓名排序。
(4) 返回所有交易员的姓名字符串,按字母顺序排序。
(5) 有没有交易员是在米兰工作的?
(6) 打印生活在剑桥的交易员的所有交易额。
(7) 所有交易中,最高的交易额是多少?
(8) 找到交易额最小的交易。
5.5.1 领域:交易员和交易
以下是你要处理的领域,一个Traders
和Transactions
的列表:
Trader raoul = new Trader("Raoul", "Cambridge");Trader mario = new Trader("Mario","Milan");Trader alan = new Trader("Alan","Cambridge");Trader brian = new Trader("Brian","Cambridge");List<Transaction> transactions = Arrays.asList( new Transaction(brian, 2011, 300), new Transaction(raoul, 2012, 1000), new Transaction(raoul, 2011, 400), new Transaction(mario, 2012, 710), new Transaction(mario, 2012, 700), new Transaction(alan, 2012, 950));
Trader
和Transaction
类的定义如下:
public class Trader{ private final String name; private final String city; public Trader(String n, String c){ this.name = n; this.city = c; } public String getName{ return this.name; } public String getCity{ return this.city; } public String toString{ return "Trader:"+this.name + " in " + this.city; }}public class Transaction{ private final Trader trader; private final int year; private final int value; public Transaction(Trader trader, int year, int value){ this.trader = trader; this.year = year; this.value = value; } public Trader getTrader{ return this.trader; } public int getYear{ return this.year; } public int getValue{ return this.value; } public String toString{ return "{" + this.trader + ", " + "year: "+this.year+", " + "value:" + this.value +"}"; }}
5.5.2 解答
解答在下面的代码清单中。你可以看看你对迄今所学知识的理解程度如何。干得不错!
代码清单5-1 找出2011年的所有交易并按交易额排序(从低到高)
List<Transaction> tr2011 = transactions.stream .filter(transaction -> transaction.getYear == 2011) ←─给filter传递一个谓词来选择2011年的交易 .sorted(comparing(Transaction::getValue)) ←─按照交易额进行排序 .collect(toList); ←─将生成的Stream中的所有元素收集到一个List中
代码清单5-2 交易员都在哪些不同的城市工作过
List<String> cities = transactions.stream .map(transaction -> transaction.getTrader.getCity) ←─提取与交易相关的每位交易员的所在城市 .distinct ←─只选择互不相同的城市 .collect(toList);
这里还有一个新招:你可以去掉distinct
,改用toSet
,这样就会把流转换为集合。你在第6章中会了解到更多相关内容。
Set<String> cities = transactions.stream .map(transaction -> transaction.getTrader.getCity) .collect(toSet);
代码清单5-3 查找所有来自于剑桥的交易员,并按姓名排序
List<Trader> traders = transactions.stream .map(Transaction::getTrader) ←─从交易中提取所有交易员 .filter(trader -> trader.getCity.equals("Cambridge")) ←─仅选择位于剑桥的交易员 .distinct ←─确保没有任何重复 .sorted(comparing(Trader::getName)) ←─对生成的交易员流按照姓名进行排序 .collect(toList);
代码清单5-4 返回所有交易员的姓名字符串,按字母顺序排序
String traderStr = transactions.stream .map(transaction -> transaction.getTrader.getName) ←─提取所有交易员姓名,生成一个Strings构成的Stream .distinct ←─只选择不相同的姓名 .sorted ←─对姓名按字母顺序排序 .reduce("", (n1, n2) -> n1 + n2); ←─逐个拼接每个名字,得到一个将所有名字连接起来的String
请注意,此解决方案效率不高(所有字符串都被反复连接,每次迭代的时候都要建立一个新的String
对象)。下一章中,你将看到一个更为高效的解决方案,它像下面这样使用joining
(其内部会用到StringBuilder
):
String traderStr = transactions.stream .map(transaction -> transaction.getTrader.getName) .distinct .sorted .collect(joining);
代码清单5-5 有没有交易员是在米兰工作的
boolean milanBased = transactions.stream .anyMatch(transaction -> transaction.getTrader .getCity .equals("Milan")); ←─把一个谓词传递给anyMatch,检查是否有交易员在米兰工作
代码清单5-6 打印生活在剑桥的交易员的所有交易额
transactions.stream .filter(t -> "Cambridge".equals(t.getTrader.getCity)) ←─选择住在剑桥的交易员所进行的交易 .map(Transaction::getValue) ←─提取这些交易的交易额 .forEach(System.out::println); ←─打印每个值
代码清单5-7 所有交易中,最高的交易额是多少
Optional<Integer> highestValue = transactions.stream .map(Transaction::getValue) ←─提取每项交易的交易额 .reduce(Integer::max); ←─计算生成的流中的最大值
代码清单5-8 找到交易额最小的交易
Optional<Transaction> smallestTransaction = transactions.stream .reduce((t1, t2) -> t1.getValue < t2.getValue ? t1 : t2); ←─通过反复比较每个交易的交易额,找出最小的交易
你还可以做得更好。流支持min
和max
方法,它们可以接受一个Comparator
作为参数,指定计算最小或最大值时要比较哪个键值:
Optional<Transaction> smallestTransaction = transactions.stream .min(comparing(Transaction::getValue));
5.6 数值流
我们在前面看到了可以使用reduce
方法计算流中元素的总和。例如,你可以像下面这样计算菜单的热量:
int calories = menu.stream .map(Dish::getCalories) .reduce(0, Integer::sum);
这段代码的问题是,它有一个暗含的装箱成本。每个Integer
都必须拆箱成一个原始类型,再进行求和。要是可以直接像下面这样调用sum
方法,岂不是更好?
int calories = menu.stream .map(Dish::getCalories) .sum;
但这是不可能的。问题在于map
方法会生成一个Stream<T>
。虽然流中的元素是Integer
类型,但Streams
接口没有定义sum
方法。为什么没有呢?比方说,你只有一个像menu
那样的Stream<Dish>
,把各种菜加起来是没有任何意义的。但不要担心,Stream API还提供了原始类型流特化,专门支持处理数值流的方法。
5.6.1 原始类型流特化
Java 8引入了三个原始类型特化流接口来解决这个问题:IntStream
、DoubleStream
和LongStream
,分别将流中的元素特化为int
、long
和double
,从而避免了暗含的装箱成本。每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的sum
,找到最大元素的max
。此外还有在必要时再把它们转换回对象流的方法。要记住的是,这些特化的原因并不在于流的复杂性,而是装箱造成的复杂性——即类似int
和Integer
之间的效率差异。
1. 映射到数值流
将流转换为特化版本的常用方法是mapToInt
、mapToDouble
和mapToLong
。这些方法和前面说的map
方法的工作方式一样,只是它们返回的是一个特化流,而不是Stream<T>
。例如,你可以像下面这样用mapToInt
对menu
中的卡路里求和:
int calories = menu.stream ←─返回一个Stream<Dish> .mapToInt(Dish::getCalories) ←─返回一个IntStream .sum;
这里,mapToInt
会从每道菜中提取热量(用一个Integer
表示),并返回一个IntStream
(而不是一个Stream<Integer>
)。然后你就可以调用IntStream
接口中定义的sum
方法,对卡路里求和了!请注意,如果流是空的,sum
默认返回0
。IntStream
还支持其他的方便方法,如max
、min
、average
等。
2. 转换回对象流
同样,一旦有了数值流,你可能会想把它转换回非特化流。例如,IntStream
上的操作只能产生原始整数:IntStream
的map
操作接受的Lambda必须接受int
并返回int
(一个IntUnaryOperator
)。但是你可能想要生成另一类值,比如Dish
。为此,你需要访问Stream
接口中定义的那些更广义的操作。要把原始流转换成一般流(每个int
都会装箱成一个Integer
),可以使用boxed
方法,如下所示:
IntStream intStream = menu.stream.mapToInt(Dish::getCalories); ←─将Stream 转换为数值流Stream<Integer> stream = intStream.boxed; ←─将数值流转换为Stream
你在下一节中会看到,在需要将数值范围装箱成为一个一般流时,boxed
尤其有用。
3. 默认值OptionalInt
求和的那个例子很容易,因为它有一个默认值:0
。但是,如果你要计算IntStream
中的最大元素,就得换个法子了,因为0
是错误的结果。如何区分没有元素的流和最大值真的是0
的流呢?前面我们介绍了Optional
类,这是一个可以表示值存在或不存在的容器。Optional
可以用Integer
、String
等参考类型来参数化。对于三种原始流特化,也分别有一个Optional
原始类型特化版本:OptionalInt
、OptionalDouble
和OptionalLong
。
例如,要找到IntStream
中的最大元素,可以调用max
方法,它会返回一个OptionalInt
:
OptionalInt maxCalories = menu.stream .mapToInt(Dish::getCalories) .max;
现在,如果没有最大值的话,你就可以显式处理OptionalInt
去定义一个默认值了:
int max = maxCalories.orElse(1); ←─如果没有最大值的话,显式提供一个默认最大值
5.6.2 数值范围
和数字打交道时,有一个常用的东西就是数值范围。比如,假设你想要生成1和100之间的所有数字。Java 8引入了两个可以用于IntStream
和LongStream
的静态方法,帮助生成这种范围:range
和rangeClosed
。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但range
是不包含结束值的,而rangeClosed
则包含结束值。让我们来看一个例子:
IntStream evenNumbers = IntStream.rangeClosed(1, 100) ←─表示范围[1, 100] .filter(n -> n % 2 == 0); ←─一个从1到100的偶数流System.out.println(evenNumbers.count); ←─从1 到100 有50个偶数
这里我们用了rangeClosed
方法来生成1到100之间的所有数字。它会产生一个流,然后你可以链接filter
方法,只选出偶数。到目前为止还没有进行任何计算。最后,你对生成的流调用count
。因为count
是一个终端操作,所以它会处理流,并返回结果50
,这正是1到100(包括两端)中所有偶数的个数。请注意,比较一下,如果改用IntStream.range(1, 100)
,则结果将会是49
个偶数,因为range
是不包含结束值的。
5.6.3 数值流应用:勾股数
现在我们来看一个难一点儿的例子,让你巩固一下有关数值流以及到目前为止学过的所有流操作的知识。如果你接受这个挑战,任务就是创建一个勾股数流。
1. 勾股数
那么什么是勾股数(毕达哥拉斯三元数)呢?我们得回到从前。在一堂激动人心的数学课上,你了解到,古希腊数学家毕达哥拉斯发现了某些三元数(a, b, c)
满足公式a * a + b * b = c * c
,其中a
、b
、c
都是整数。例如,(3, 4, 5)就是一组有效的勾股数,因为3 * 3 + 4 * 4 = 5 * 5或9 + 16 = 25。这样的三元数有无限组。例如,(5, 12, 13)、(6, 8, 10)和(7, 24, 25)都是有效的勾股数。勾股数很有用,因为它们描述的正好是直角三角形的三条边长,如图5-9所示。
图 5-9 勾股定理(毕达哥拉斯定理)
2. 表示三元数
那么,怎么入手呢?第一步是定义一个三元数。虽然更恰当的做法是定义一个新的类来表示三元数,但这里你可以使用具有三个元素的int
数组,比如new int{3, 4, 5}
,来表示勾股数(3, 4, 5)。现在你就可以用数组索引访问每个元素了。
3. 筛选成立的组合
假定有人为你提供了三元数中的前两个数字:a
和b
。怎么知道它是否能形成一组勾股数呢?你需要测试a * a + b * b
的平方根是不是整数,也就是说它没有小数部分——在Java里可以使用expr % 1.0
表示。如果它不是整数,那就是说c
不是整数。你可以用filter
操作表达这个要求(你稍后会了解到如何将其连接起来成为有效代码):
filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)
假设周围的代码给a
提供了一个值,并且stream
提供了b
可能出现的值,filter
将只选出那些可以与a
组成勾股数的b
。你可能在想Math.sqrt(a * a + b * b) % 1 == 0
这一行是怎么回事。简单来说,这是一种测试Math.sqrt(a * a + b * b)
返回的结果是不是整数的方法。如果平方根的结果带了小数,如9.1,这个条件就不成立(9.0是可以的)。
4. 生成三元组
在筛选之后,你知道a
和b
能够组成一个正确的组合。现在需要创建一个三元组。你可以使用map
操作,像下面这样把每个元素转换成一个勾股数组:
stream.filter(b -> Math.sqrt(a*a + b*b) % 1 == 0) .map(b -> new int{a, b, (int) Math.sqrt(a * a + b * b)});
5. 生成b
值
胜利在望!现在你需要生成b
的值。前面已经看到,Stream.rangeClosed
让你可以在给定区间内生成一个数值流。你可以用它来给b
提供数值,这里是1到100:
IntStream.rangeClosed(1, 100) .filter(b -> Math.sqrt(a*a + b*b) % 1 == 0) .boxed .map(b -> new int{a, b, (int) Math.sqrt(a * a + b * b)});
请注意,你在filter
之后调用boxed
,从rangeClosed
返回的IntStream
生成一个Stream<Integer>
。这是因为你的map
会为流中的每个元素返回一个int
数组。而IntStream
中的map
方法只能为流中的每个元素返回另一个int
,这可不是你想要的!你可以用IntStream
的mapToObj
方法改写它,这个方法会返回一个对象值流:
IntStream.rangeClosed(1, 100) .filter(b -> Math.sqrt(a*a + b*b) % 1 == 0) .mapToObj(b -> new int{a, b, (int) Math.sqrt(a * a + b * b)});
6. 生成值
这里有一个关键的假设:给出了a
的值。 现在,只要已知a
的值,你就有了一个可以生成勾股数的流。如何解决这个问题呢?就像b
一样,你需要为a
生成数值!最终的解决方案如下所示:
Stream<int> pythagoreanTriples = IntStream.rangeClosed(1, 100).boxed .flatMap(a -> IntStream.rangeClosed(a, 100) .filter(b -> Math.sqrt(a*a + b*b) % 1 == 0) .mapToObj(b -> new int{a, b, (int)Math.sqrt(a * a + b * b)}) );
好的,flatMap
又是怎么回事呢?首先,创建一个从1到100的数值范围来生成a
的值。对每个给定的a
值,创建一个三元数流。要是把a
的值映射到三元数流的话,就会得到一个由流构成的流。flatMap
方法在做映射的同时,还会把所有生成的三元数流扁平化成一个流。这样你就得到了一个三元数流。还要注意,我们把b
的范围改成了a
到100。没有必要再从1开始了,否则就会造成重复的三元数,例如(3,4,5)和(4,3,5)。
7. 运行代码
现在你可以运行解决方案,并且可以利用我们前面看到的limit
命令,明确限定从生成的流中要返回多少组勾股数了:
pythagoreanTriples.limit(5) .forEach(t -> System.out.println(t[0] + ", " + t[1] + ", " + t[2]));
这会打印:
3, 4, 55, 12, 136, 8, 107, 24, 258, 15, 17
8. 你还能做得更好吗?
目前的解决办法并不是最优的,因为你要求两次平方根。让代码更为紧凑的一种可能的方法是,先生成所有的三元数(a*a, b*b, a*a+b*b)
,然后再筛选符合条件的:
Stream<double> pythagoreanTriples2 = IntStream.rangeClosed(1, 100).boxed .flatMap(a -> IntStream.rangeClosed(a, 100) .mapToObj( b -> new double{a, b, Math.sqrt(a*a + b*b)}) ←─产生三元数 .filter(t -> t[2] % 1 == 0)); ←─元组中的第三个元素必须是整数
5.7 构建流
希望到现在,我们已经让你相信,流对于表达数据处理查询是非常强大而有用的。到目前为止,你已经能够使用stream
方法从集合生成流了。此外,我们还介绍了如何根据数值范围创建数值流。但创建流的方法还有许多!本节将介绍如何从值序列、数组、文件来创建流,甚至由生成函数来创建无限流!
5.7.1 由值创建流
你可以使用静态方法Stream.of
,通过显式值创建一个流。它可以接受任意数量的参数。例如,以下代码直接使用Stream.of
创建了一个字符串流。然后,你可以将字符串转换为大写,再一个个打印出来:
Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");stream.map(String::toUpperCase).forEach(System.out::println);
你可以使用empty
得到一个空流,如下所示:
Stream<String> emptyStream = Stream.empty;
5.7.2 由数组创建流
你可以使用静态方法Arrays.stream
从数组创建一个流。它接受一个数组作为参数。例如,你可以将一个原始类型int
的数组转换成一个IntStream
,如下所示:
int numbers = {2, 3, 5, 7, 11, 13};int sum = Arrays.stream(numbers).sum; ←─总和是41
5.7.3 由文件生成流
Java中用于处理文件等I/O操作的NIO API(非阻塞 I/O)已更新,以便利用Stream API。java.nio.file.Files
中的很多静态方法都会返回一个流。例如,一个很有用的方法是Files.lines
,它会返回一个由指定文件中的各行构成的字符串流。使用你迄今所学的内容,你可以用这个方法看看一个文件中有多少各不相同的词:
long uniqueWords = 0;try(Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset)){ ←─流会自动关闭uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" "))) ←─生成单词流 .distinct ←─删除重复项 .count; ←─数一数有多少各不相同的单词}catch(IOException e){ ←─如果打开文件时出现异常则加以处理}
你可以使用Files.lines
得到一个流,其中的每个元素都是给定文件中的一行。然后,你可以对line
调用split
方法将行拆分成单词。应该注意的是,你该如何使用flatMap
产生一个扁平的单词流,而不是给每一行生成一个单词流。最后,把distinct
和count
方法链接起来,数数流中有多少各不相同的单词。
5.7.4 由函数生成流:创建无限流
Stream API提供了两个静态方法来从函数生成流:Stream.iterate
和Stream.generate
。这两个操作可以创建所谓的