Streams API
可以表达复杂的数据处理查询。
流让你从外部迭代转向内部迭代。这样,你就用不着写下面这样的代码来显式地管理数据集合的迭代(外部迭代)了:
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
可以决定并行运行你的代码。这要是用外部迭代的话就办不到了,因为你只能用单一线程挨个迭代。
筛选和切片
你可以使用filter
、distinct
、skip
和limit
对流做筛选和切片。
现在来看看如何选择流中的元素:用谓词筛选,筛选出各不相同的元素,忽略流中的头几个元素,或将流截短至指定长度。
用谓词筛选
Streams
接口支持filter
方法。该操作会接受一个谓词(一个返回boolean
的函数)作为参数,并返回一个包括所有符合谓词的元素的流。例如,你可以像下图所示的这样,筛选出所有素菜,创建一张素食菜单:
List<Dish> vegetarianMenu=menu.stream()
.filter(Dish:: isVegetarian) //方法引用检查菜肴是否适合素食者
.collect(toList());
筛选各异的元素
流还支持一个叫作distinct
的方法,它会返回一个元素各异(根据流所生成元素的hashCode
和equals
方法实现)的流。例如,以下代码会筛选出列表中所有的偶数,并确保没有重复。
List<Integer> numbers=Arrays.asList(1,2,1,3,3,2,4);
numbers.stream().filter(i -> i % 2 == 0).distinct().forEach(System.out:: println);
下图直观地显示了这个过程。
截短流
流支持limit(n)
方法,该方法会返回一个不超过给定长度的流。所需的长度作为参数传递给limit
。如果流是有序的,则最多会返回前n
个元素。比如,你可以建立一个List
,选出热量超过300
卡路里的头三道菜:
List<Dish> dishes=menu.stream()
.filter(d -> d.getCalories() > 300)
.limit(3)
.collect(toList());
下图展示了filter
和limit
的组合。可以看到,该方法只选出了符合谓词的头三个元素,然后就立即返回了结果。
注意limit
也可以用在无序流上,比如源是一个Set
。这种情况下,limit
的结果不会以任何顺序排列。
跳过元素
流还支持skip(n)
方法,返回一个扔掉了前n
个元素的流,如果流中元素不足n
个,则返回一个空流。注意,limit(n
)和skip(n)
是互补的!例如,下面的代码将跳过超过300
卡路里的头两道菜,并返回剩下的。
List<Dish> dishes=menu.stream()
.filter(d -> d.getCalories() > 300)
.skip(2)
.collect(toList());
下图展示了这个查询。
映射
你可以使用map
和flatMap
提取或转换流中的元素。
一个非常常见的数据处理套路就是从某些对象中选择信息。比如在SQL
里,你可以从表中选择一列。Stream API
也通过map
和flatMap
方法提供了类似的工具。
对流中的每一个元素应用函数
流支持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("Java8","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());
流的扁平化
你已经看到如何使用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>
来表示一个字符流。下图说明了这个问题。
幸好可以用flatMap
来解决这个问题!让我们一步步看看怎么解决它。
- 尝试使用
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>
)!的确,你先是把每个单词转换成一个字母数组,然后把每个数组变成了一个独立的流。
- 使用
flatMap
你可以像下面这样使用flatMap
来解决这个问题:
List<String> uniqueCharacters
=words.stream()
.map(w -> w.split("")) //将每个单词转换为由其字母构成的数组
.flatMap(Arrays:: stream) //将各个生成流扁平化为单个流
.distinct()
.collect(Collectors.toList());
使用flatMap
方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。所有使用map(Arrays:: stream)
时生成的单个流都被合并起来,即扁平化为一个流。下图说明了使用flatMap
方法的效果。可以把它和上图中map
的效果比较一下。
一言以蔽之,flatmap
方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。
查找和匹配
你可以使用findFirst
和findAny
方法查找流中的元素。你可以用allMatch
、noneMatch
和anyMatch
方法让流匹配给定的谓词。这些方法都利用了短路:找到结果就立即停止计算;没有必要处理整个流。
另一个常见的数据处理套路是看看数据集中的某些元素是否匹配一个给定的属性。Stream API
通过allMatch
、anyMatch
、noneMatch
、findFirst
和findAny
方法提供了这样的工具。
检查谓词是否至少匹配一个元素(anyMatch)
anyMatch
方法可以回答“流中是否有一个元素能匹配给定的谓词”。比如,你可以用它来看看菜单里面是否有素食可选择:
if(menu.stream().anyMatch(Dish:: isVegetarian)){
System.out.println("The menu is (somewhat) vegetarian friendly!!");
}
anyMatch
方法返回一个boolean
,因此是一个终端操作。
检查谓词是否匹配所有元素(allMatch)
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
也是一个短路操作:它只需要创建一个给定大小的流,而用不着处理流中所有的元素。在碰到无限大小的流的时候,这种操作就有用了:它们可以把无限流变成有限流。
查找元素
findAny
方法将返回当前流中的任意元素。它可以与其他流操作结合使用。比如,你可能想找到一道素食菜肴。你可以结合使用filter
和findAny
方法来实现这个查询:
Optional<Dish> dish=menu.stream()
.filter(Dish:: isVegetarian)
.findAny();
流水线将在后台进行优化使其只需走一遍,并在利用短路找到结果时立即结束。
Optional<T>
类(java.util.Optional
)是一个容器类,代表一个值存在或不存在。在上面的代码中,findAny
可能什么元素都没找到。Java 8
的库设计人员引入了Optional<T>
,这样就不用返回众所周知容易出问题的null
了。下面是Optional
里面几种可以迫使你显式地检查值是否存在或处理值不存在的情形的方法:
-
isPresent()
将在Optional
包含值的时候返回true
,否则返回false
。 -
ifPresent(Consumer<T> block)
会在值存在的时候执行给定的代码块。之前介绍的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())); //如果包含一个值就打印它,否则什么都不做
查找第一个元素
有些流有一个出现顺序(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
呢?答案是并行。找到第一个元素在并行上限制更多。如果你不关心返回的元素是哪个,请使用findAny
,因为它在使用并行流时限制较少。
归约
到目前为止,见到的终端操作都是返回一个boolean
(allMatch
之类的)、void
(forEach
)或Optional
对象(findAny
等)。也见过了使用collect
来将流中的所有元素组合成一个List
。
接下来将看到如何把一个流中的元素组合起来,使用reduce
操作来表达更复杂的查询,比如“计算菜单中的总卡路里”或“菜单中卡路里最高的菜是哪一个”。此类查询需要将流中所有元素反复结合起来,得到一个值,比如一个Integer
。这样的查询可以被归类为归约操作(将流归约成一个值)。用函数式编程语言的术语来说,这称为折叠(fold
),因为你可以将这个操作看成把一张长长的纸(你的流)反复折叠成一个小方块,而这就是折叠操作的结果。
元素求和
你可以利用reduce
方法将流中所有的元素迭代合并成一个结果,例如求和或查找最大元素。
在我们研究如何使用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);
下图展示了reduce
操作是如何作用于一个流的:Lambda
反复结合每个元素,直到流被归约成一个值。
reduce
操作对一个数字流求和的过程如下。首先,0
作为Lambda(a)
的第一个参数,从流中获得4作为第二个参数(b)
。0+4
得到4
,它成了新的累积值。然后再用累积值和流中下一个元素5
调用Lambda
,产生新的累积值9
。接下来,再用累积值和下一个元素3
调用Lambda
,得到12
。最后,用12
和流中最后一个元素9
调用Lambda
,得到最终结果21
。
你可以使用方法引用让这段代码更简洁。在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
对象里,以表明和可能不存在。
怎样用map
和reduce
方法数一数流中有多少个菜呢?要解决这个问题,你可以把流中每个元素都映射成数字1
,然后用reduce
求和。这相当于按顺序数流中的元素个数。
int count=menu.stream()
.map(d -> 1)
.reduce(0,(a,b) -> a+b);
map
和reduce
的连接通常称为map-reduce
模式,因Google
用它来进行网络搜索而出名,因为它很容易并行化。
最大值和最小值
只要用归约就可以计算最大值和最小值了。让我们来看看如何利用reduce
来计算流中最大或最小的元素。reduce
接受两个参数:
- 一个初始值
- 一个
Lambda
来把两个流元素结合起来并产生一个新值
Lambda
是一步步用加法运算符应用到流中每个元素上的,如上图所示。因此,你需要一个给定两个元素能够返回最大值的Lambda
。reduce
操作会考虑新值和流中下一个元素,并产生一个新的最大值,直到整个流消耗完!你可以像下面这样使用reduce
来计算流中的最大值,如下图所示。
Optional<Integer> max=numbers.stream().reduce(Integer:: max);
要计算最小值,你需要把Integer.min
传给reduce
来替换Integer.max
:
Optional<Integer> min=numbers.stream().reduce(Integer:: min);
你当然也可以写成Lambda(x,y) -> x<y ? x : y
而不是Integer:: min
,不过后者比较易读。
相比于前面写的逐步迭代求和,使用reduce
的好处在于,这里的迭代被内部迭代抽象掉了,这让内部实现得以选择并行执行reduce
操作。
而迭代式求和例子要更新共享变量sum
,这不是那么容易并行化的。如果你加入了同步,很可能会发现线程竞争抵消了并行本应带来的性能提升!这种计算的并行化需要另一种办法:将输入分块,分块求和,最后再合并起来。但这样的话代码看起来就完全不一样了。
可变的累加器模式对于并行化来说是死路一条。你需要一种新的模式,这正是reduce
所提供的。
使用流来对所有的元素并行求和时,你的代码几乎不用修改:stream()
换成了parallelStream()
:
int sum=numbers.parallelStream().reduce(0,Integer:: sum);
但要并行执行这段代码也要付一定代价,传递给reduce
的Lambda
不能更改状态(如实例变量),而且操作必须满足结合律才可以按任意顺序执行。
乍一看流操作简直是灵丹妙药,而且只要在从集合生成流的时候把Stream
换成parallelStream
就可以实现并行。
当然,对于许多应用来说确实是这样。你可以把一张菜单变成流,用filter
选出某一类的菜肴,然后对得到的流做map
来对卡路里求和,最后reduce
得到菜单的总热量。这个流计算甚至可以并行进行。但这些操作的特性并不相同。它们需要操作的内部状态还是有些问题的。
诸如map
或filter
等操作会从输入流中获取每一个元素,并在输出流中得到0
或1
个结果。这些操作一般都是无状态的:它们没有内部状态(假设用户提供的Lambda
或方法引用没有内部可变状态)。
但诸如reduce
、sum
、max
等操作需要内部状态来累积结果。在上面的情况下,内部状态很小。在我们的例子里就是一个int
或double
。不管流中有多少元素要处理,内部状态都是有界的。
相反,诸如sort
或distinct
等操作一开始都和filter
和map
差不多——都是接受一个流,再生成一个流(中间操作),但有一个关键的区别。从流中排序和删除重复项时都需要知道先前的历史。例如,排序要求所有元素都放入缓冲区后才能给输出流加入一个项目,这一操作的存储要求是无界的。要是流比较大或是无限的,就可能会有问题(把质数流倒序会做什么呢?它应当返回最大的质数,但数学告诉我们它不存在)。我们把这些操作叫作有状态操作。
filter
和map
等操作是无状态的,它们并不存储任何状态。reduce
等操作要存储状态才能计算出一个值。sorted
和distinct
等操作也要存储状态,因为它们需要把流中的所有元素缓存起来才能返回一个新的流。这种操作称为有状态操作。
现在已经看到了很多流操作,可以用来表达复杂的数据处理查询。下表总结了迄今讲过的操作。
操作 | 类型 | 返回类型 | 使用的类型/函数式接口 | 函数描述符 |
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> | BinaryOptional< T> | (T, T) -> T |
count | 终端 | long |
付诸实践
我们来看一个不同的领域:执行交易的交易员。你的经理让你为八个查询找到答案。运用迄今为止学到的关于流的知识。
- 找出
2017
年发生的所有交易,并按交易额排序(从低到高)。 - 交易员都在哪些不同的城市工作过?
- 查找所有来自于剑桥的交易员,并按姓名排序。
- 返回所有交易员的姓名字符串,按字母顺序排序。
- 有没有交易员是在米兰工作的?
- 打印生活在剑桥的交易员的所有交易额。
- 所有交易中,最高的交易额是多少?
- 找到交易额最小的交易。
以下是要处理的领域,一个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, 2017, 300),
new Transaction( raoul, 2018, 1000),
new Transaction( raoul, 2017, 400),
new Transaction( mario, 2018, 710),
new Transaction( mario, 2018, 700),
new Transaction( alan, 2018, 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 +"}";
}
}
- 找出
2017
年发生的所有交易,并按交易额排序(从低到高):
List<Transaction> tr2017
=transactions.stream()
.filter(transaction -> transaction.getYear()==2017) //给filter传递一个谓词来选择2017年的交易
.sorted(comparing(Transaction:: getValue)) //按照交易额进行排序
.collect(toList()); //将生成的Stream中的所有元素收集到一个List中
- 交易员都在哪些不同的城市工作过?
List<String> cities
=transactions.stream()
.map(transaction -> transaction.getTrader().getCity()) //提取与交易相关的每位交易员的所在城市
.distinct() //只选择互不相同的城市
.collect(toList());
这里还有一个新招:你可以去掉distinct()
,改用toSet()
,这样就会把流转换为集合。
Set<String> cities
=transactions.stream()
.map(transaction -> transaction.getTrader().getCity())
.collect(toSet());
- 查找所有来自于剑桥的交易员,并按姓名排序。
List<Trader> traders
=transactions.stream()
.map(Transaction:: getTrader) //从交易中提取所有交易员
.filter(trader -> trader.getCity().equals("Cambridge")) //仅选择位于剑桥的交易员
.distinct() //确保没有任何重复
.sorted(comparing(Trader:: getName)) //对生成的交易员流按照姓名进行排序
.collect(toList());
- 返回所有交易员的姓名字符串,按字母顺序排序。
String traderStr
=transactions.stream()
.map(transaction -> transaction.getTrader().getName()) //提取所有交易员姓名,生成一个Strings构成的Stream
.distinct() //只选择不相同的姓名
.sorted() //对姓名按字母顺序排序
.reduce("",(n1,n2)->n1+n2); //逐个拼接每个名字,得到一个将所有名字连接起来的String
还有一种效率更高的解决方案,它像下面这样使用joining
(其内部会用到StringBuilder
):
String traderStr
=transactions.stream()
.map(transaction -> transaction.getTrader().getName())
.distinct()
.sorted()
.collect(joining());
- 有没有交易员是在米兰工作的?
boolean milanBased
=transactions.stream()
.anyMatch(transaction -> transaction.getTrader()
.getCity()
.equals("Milan")); //把一个谓词传递给anyMatch,检查是否有交易员在米兰工作
- 打印生活在剑桥的交易员的所有交易额。
transactions.stream()
.filter(t -> "Cambridge".equals(t.getTrader().getCity())) //选择住在剑桥的交易员所进行的交易
.map(Transaction:: getValue) //提取这些交易的交易额
.forEach(System.out:: println); //打印每个值
- 所有交易中,最高的交易额是多少?
Optional<Integer> highestValue
=transactions.stream()
.map(Transaction:: getValue) //提取每项交易的交易额
.reduce(Integer:: max); //计算生成的流中的最大值
- 找到交易额最小的交易。
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));
数值流
在前面看到了可以使用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
还提供了原始类型流特化,专门支持处理数值流的方法。
原始类型流特化
Java 8
引入了三个原始类型特化流接口:IntStream
、DoubleStream
和LongStream
,分别将流中的元素特化为int
、long
和double
,从而避免了暗含的装箱成本。每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的sum
,找到最大元素的max
。此外还有在必要时再把它们转换回对象流的方法。要记住的是,这些特化的原因并不在于流的复杂性,而是装箱造成的复杂性——即类似int
和Integer
之间的效率差异。
映射到数值流
将流转换为特化版本的常用方法是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
等。
转换回对象流
同样,一旦有了数值流,你可能会想把它转换回非特化流。例如,IntStream
上的操作只能产生原始整数:IntStream
的map
操作接受的Lambda
必须接受int
并返回int
(一个IntUnaryOperator
)。但是你可能想要生成另一类值,比如Dish
。为此,你需要访问Stream
接口中定义的那些更广义的操作。要把原始流转换成一般流(每个int
都会装箱成一个Integer
),可以使用boxed
方法,如下所示:
Int StreamintStream=menu.stream().mapToInt(Dish:: getCalories); //将Stream转换为数值流
Stream<Integer> stream=intStream.boxed(); //将数值流转换为Stream
默认值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); //如果没有最大值的话,显式提供一个默认最大值
数值范围
和数字打交道时,有一个常用的东西就是数值范围。比如,假设你想要生成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
是不包含结束值的。
数值流应用:勾股数
现在我们来看一个难一点儿的例子,创建一个勾股数流。
- 表示三元数
第一步是定义一个三元数。虽然更恰当的做法是定义一个新的类来表示三元数,但这里你可以使用具有三个元素的int
数组,比如new int[]{3,4,5}
,来表示勾股数(3,4,5)
。现在你就可以用数组索引访问每个元素了。 - 筛选成立的组合
假定有人为你提供了三元数中的前两个数字: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
是可以的)。
- 生成三元组
在筛选之后,你知道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)});
- 生成
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)});
- 生成值
这里有一个关键的假设:给出了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)
。
- 运行代码
现在可以运行解决方案,并且可以利用前面看到的limit
命令,明确限定从生成的流中要返回多少组勾股数了:
pythagoreanTriples.limit(5)
.forEach(t ->
System.out.println(t[0] + ", " + t[1] + ", " + t[2]));
这会打印:
3,4,5
5,12,13
6,8,10
7,24,25
8,15,17
- 进一步优化
目前的解决办法并不是最优的,因为你要求两次平方根。让代码更为紧凑的一种可能的方法是,先生成所有的三元数(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)); //元组中的第三个元素必须是整数
构建流
流对于表达数据处理查询是非常强大而有用的。目前已经介绍了如何根据数值范围创建数值流,但创建流的方法还有许多,接下来将介绍如何从值序列、数组、文件来创建流,甚至由生成函数来创建无限流。无限流是没有固定大小的流。
由值创建流
可以使用静态方法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();
由数组创建流
你可以使用静态方法Arrays.stream
从数组创建一个流。它接受一个数组作为参数。例如,你可以将一个原始类型int
的数组转换成一个IntStream
,如下所示:
int[] numbers={2,3,5,7,11,13};
int sum=Arrays.stream(numbers).sum(); //总和是41
由文件生成流
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
方法链接起来,数数流中有多少各不相同的单词。
由函数生成流:创建无限流
Stream API
提供了两个静态方法来从函数生成流:Stream.iterate
和Stream.generate
。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterate
和generate
产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!一般来说,应该使用limit(n)
来对这种流加以限制,以避免打印无穷多个值。
迭代
先来看一个iterate
的简单例子:
Stream.iterate(0,n -> n+2).limit(10).forEach(System.out:: println);
iterate
方法接受一个初始值(在这里是0
),还有一个依次应用在每个产生的新值上的Lambda
(UnaryOperator<t>
类型)。这里,我们使用Lambda
n -> n+2
,返回的是前一个元素加上2
。因此,iterate
方法生成了一个所有正偶数的流:流的第一个元素是初始值0
。然后加上2
来生成新的值2
,再加上2
来得到新的值4
,以此类推。这种iterate
操作基本上是顺序的,因为结果取决于前一次应用。请注意,此操作将生成一个无限流——这个流没有结尾,因为值是按需计算的,可以永远计算下去。我们说这个流是无界的。这是流和集合之间的一个关键区别。我们使用limit
方法来显式限制流的大小。这里只选择了前10
个偶数。然后可以调用forEach
终端操作来消费流,并分别打印每个元素。
一般来说,在需要依次生成一系列值的时候应该使用iterate
,比如一系列日期:1
月31
日,2
月1
日,依此类推。
下面来看一个难一点儿的应用iterate
的例子,用iterate
方法生成斐波那契元组序列中的前20
个元素。
斐波纳契数列是著名的经典编程练习。下面这个数列就是斐波纳契数列的一部分:0,1,1,2,3,5,8,13,21,34,55…
数列中开始的两个数字是0
和1
,后续的每个数字都是前两个数字之和。斐波纳契元组序列与此类似,是数列中数字和其后续数字组成的元组构成的序列:(0,1)
,(1,1)
,(1,2)
,(2,3)
,(3,5)
,(5,8)
,(8,13)
,(13,21)
…
Stream.iterate(new int[]{0,1},
t -> new int[]{t[1],t[0]+t[1]})
.limit(20)
.forEach(t -> System.out.println("(" + t[0]+", " + t[1] + ")"));
iterate
需要一个Lambda
来确定后续的元素。对于元组(3,5)
,其后续元素是(5,3+5)
=(5,8)
。下一个是(8,5+8)
。即给定一个元组,其后续的元素是(t[1],t[0]+t[1])
。这可以用这个Lambda
来计算:t -> new int[]{t[1],t[0]+t[1]}
。运行这段代码,你就得到了序列(0,1),(1,1),(1,2),(2,3),(3,5),(5,8),(8,13),(13,21)…
注意,如果你只想打印正常的斐波纳契数列,可以使用map
提取每个元组中的第一个元素:
Stream.iterate(new int[]{0,1},
t -> new int[]{t[1],t[0]+t[1]})
.limit(10)
.map(t -> t[0]).forEach(System.out:: println); //这段代码将生成斐波纳契数列:0,1,1,2,3,5,8,13,21,34…
生成
与iterate
方法类似,generate
方法也可让你按需生成一个无限流。但generate
不是依次对每个新生成的值应用函数的。它接受一个Supplier<T>
类型的Lambda
提供新的值。先来看一个简单的用法:
Stream.generate(Math:: random)
.limit(5)
.forEach(System.out:: println);
这段代码将生成一个流,其中有五个0
到1
之间的随机双精度数。例如,运行一次得到了下面的结果:
0.9410810294106129
0.6586270755634592
0.9592859117266873
0.13743396659487006
0.3942776037651241
Math.Random
静态方法被用作新值生成器。同样,你可以用limit
方法显式限制流的大小,否则流将会无限长。
我们使用的供应源(指向Math.random
的方法引用)是无状态的:它不会在任何地方记录任何值,以备以后计算使用。但供应源不一定是无状态的。你可以创建存储状态的供应源,它可以修改状态,并在为流生成下一个值时使用。但很重要的一点是,在并行代码中使用有状态的供应源是不安全的。
我们在这个例子中会使用IntStream
说明避免装箱操作的代码。IntStream
的generate
方法会接受一个IntSupplier
,而不是Supplier<t>
。例如,可以这样来生成一个全是1
的无限流:
IntStream ones=IntStream.generate(() -> 1);
Lambda
允许你创建函数式接口的实例,只要直接内联提供方法的实现就可以。也可以像下面这样,通过实现IntSupplier
接口中定义的getAsInt
方法显式传递一个对象:
IntStream twos=IntStream.generate(new IntSupplier(){
public int getAsInt(){
return 2;
}
});
generate
方法将使用给定的供应源,并反复调用getAsInt
方法,而这个方法总是返回2
。但这里使用的匿名类和Lambda
的区别在于,匿名类可以通过字段定义状态,而状态又可以用getAsInt
方法来修改。这是一个副作用的例子。你迄今见过的所有Lambda
都是没有副作用的;它们没有改变任何状态。
IntSupplier
对象有可变的状态,getAsInt
在调用时会改变对象的状态,由此在每次调用时产生新的值。使用iterate
的方法则是纯粹不变的:它没有修改现有状态,但在每次迭代时会创建新的元组。你应该始终采用不变的方法,以便并行处理流,并保持结果正确。注意,因为你处理的是一个无限流,所以必须使用limit
操作来显式限制它的大小;否则,终端操作(这里是forEach
)将永远计算下去。同样,你不能对无限流做排序或归约,因为所有元素都需要处理,而这永远也完不成!