Java8中流Stream API可以通过内部迭代形式来对集合数据遍历。不需要开发人员去显式地编写迭代代码(外部迭代)。这种处理数据的方式很有用,Stream API可以对代码进行多种优化,如并行运行代码。此外Stream API提供了许多复杂操作,它们能快速完成复杂的数据查询,如筛选
、切片
、映射
、查找
、匹配
和归约
。
本节通过用谓词筛选,筛选出各不相同的元素,跳过流中的前几个元素,或将流截短至指定长度等几个方面来介绍如何选择流中的元素。
——————【用谓词筛选】:filter(Predicate
Streams接口支持filter
方法,该操作接收一个predicate
(一个返回boolean的函数)作为参数,并返回一个包括所有符合predicate
的元素的流。例如从菜单中筛选出素菜并创建一张素食菜单。
List<Dish> vegetarianMenu = menu.stream()
.filter(Dish::isVegetarian)//方法引用检查是否素菜
.collect(toList());
——————【筛选各异的元素】:distinct()
流还支持一个叫作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)
方法,该方法会返回一个不超过给定长度的流。所需的长度作为参数传递给limit 。如果流是有序的,则最多会返回前n个元素。值得注意limit也可以用在无序流上,比如源是一个Set 。这种情况下,limit的结果不会以任何顺序排列。例如下面代码筛选出热量超过300卡路里的前三道菜:
List<Dish> dishes = menu.stream()
.filter(d -> d.getCalories() > 300)
.limit(3)
.collect(toList())
——————【跳过元素】: skip(n)
流还支持 skip(n)
方法,返回一个扔掉或者跳过了前n个元素的流。如果流中元素不足n个,则返回一个空流。值的注意的是,limit(n)
和 skip(n)
是互补的。下面的代码将跳过超过300卡路里的前两道菜,并返回其余元素构成的流。
List<Dish> dishes = menu.stream()
.filter(d -> d.getCalories() > 300)
.skip(2)
.collect(toList());
一个常见的数据处理套路就是从某些对象中选择信息。比如在SQL里,从表中选择特定列。Stream API中通过map
和flatMap
方法提供了类似的功能。
——————【对流中每一个元素应用函数】:map()
流支持map
方法,它接收一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素(映射与转换类似,其中细微差别在于它是创建一个新版本,而不是去修改)。如下面例子将方法引用Dish::getName
传递给map从菜单提取菜名,它返回的是Stream
,因此可以进行流链式处理,将方法引用String::length
传递另外一个map提取每道菜的名称长度,最后收集到List中。
List<Integer> dishNameLengths = menu.stream()
.map(Dish::getName) //提取菜名
.map(String::length)//提取菜名长度
.collect(toList());
——————【流的扁平化】:flatMap()
前面看到通过map方法可以将以菜名单词映射为其长度,如果给定一张单词列表如[“Hello”,“World”],要求返回 [“H”,“e”,“l”, “o”,“W”,“r”,“d”] 。这又如何做呢?
【错误版本一】:开始我们可能想到用map将每个单词映射成一张字符表,然后调用distinct
来过滤重复字符,代码如下:
words.stream()
.map(word -> word.split(""))
.distinct()
.collect(toList());
这个方法问题在于,传递给map
方法的Lambda实际上为每个单词返回一个String[]
。因此map返回的是Stream
类型。但需求是用Stream
,下图说明了这个问题。
【错误版本二】:现在需要一个字符流,而不是数组流,因此考虑使用Arrays.stream()
的方法,它接收一个数组并产生一个流。例如:
String[] arrayOfWords = {
"Goodbye", "World"};
Stream<String> streamOfwords = Arrays.stream(arrayOfWords);
因此使用map
加Arrays.stream()
尝试解决上面问题代码如下:
words.stream()
.map(word -> word.split(""))//将每个单词转换为由其字母构成的数组
.map(Arrays::stream) //让每个数组变成一个单独的流
.distinct()
.collect(toList());
实际上这种解决方案仍然行不通,因为这段代码最终结果是一个List
,现在思路是将每个单词转换成一个字母数组,然后再将每个数组变成独立的流。
【正确版本】:使用flatMap
来解决这个问题,
List<String> uniqueCharacters = words.stream()
.map(w -> w.split("")) //将每个单词转换为由其字母构成的数组
.flatMap(Arrays::stream) //将各个生成流扁平化为单个流
.distinct()
.collect(Collectors.toList());
使用flatMap方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。当使用flatMap(Arrays::stream)
时生成的独立流都合并为一个流,即偏平化为一个流。下图展示了这个过程。
另一个常见的数据处理方式是查看数据集中某些元素是否与给定的属性匹配。Stream API通过流的allMatch
、anyMatch
、noneMatch
、findFirst
和findAny
方法提供了这样的工具。
——————【流中任何元素是否匹配给定predicate】:anyMatch(Predicate super T> predicate)
anyMatch
方法表示的是流中是否有一个元素能匹配给定的谓词,方法返回一个boolean,因此它是一个终端操作。例如菜单中是否存在素食:
if(menu.stream().anyMatch(Dish::isVegetarian)){
System.out.println("The menu is (somewhat) vegetarian friendly!!");
}
——————【流中所有元素是否匹配给定predicate】:allMatch(Predicate super T> predicate)
allMatch
方法原理跟anyMatch
方法相似,但它查看的是流中的元素是否都能匹配给定的Predicate,例如菜单中的所有菜的热量都低于1000卡路里:
boolean isHealthy = menu.stream()
.allMatch(d -> d.getCalories() < 1000);
——————【流中是否没有元素匹配给定predicate】:noneMatch(Predicate super T> predicate)
和allMatch
相对的是noneMatch
。它可以确保流中没有任何元素与给定的Predicate匹配。例如上面例子中菜单中的所有菜的热量都低于1000卡路里可以重写为这样:
boolean isHealthy = menu.stream()
.noneMatch(d -> d.getCalories() >= 1000);
allMatch
、allMatch
和noneMatch
这三个操作都用到了短路求值,这也是Java中&&
和||
短路逻辑在流中的版本。
短路求值
有些操作不需要处理整个流就能得到结果,比如and连接的布尔类型的表达式,只要有一个表达式为false,那么整个表达式就返回false。所以用不着计算整个表达式,这就是短路。流中某些操作如allMatch、anyMatch、noneMatch、findFirst和findAny,都是不需要处理整个流就能得到结果,只要找到一个元素,就可以有结果了。同样, limit也是一个短路操作:它只需要创建一个给定大小的流,而用不着处理流中所有的元素。在碰到无限大小的流的时候,这种操作就有用了:它们可以把无限流变成有限流。
——————【返回流中任意元素】:findAny()
findAny
方法将返回当前流中的任意元素。它可以与其他流操作结合使用。例如从菜单中素食菜,可以结合filter
和findAny
方法来实现查询:
Optional<Dish> dish = menu.stream()
.filter(Dish::isVegetarian)
.findAny();
此处对Optional
做一简单介绍,Optional
类( java.util.Optional
)是一个容器类,代表一个值存在或不存在。Java8中引入Optional
解决了当不存在元素时,出现null的问题。Optional
提供了显示地检查值是否存在或者处理值不存在时的处理方法:
isPresent()
将在 Optional 包含值的时候返回 true , 否则返回 false 。ifPresent(Consumer block)
会在值存在的时候执行给定的代码块。其中Consumer
函数式接口,它接收一个T类型参数,并返回void的Lambda表达式。T get()
会在值存在时返回值,否则抛出一个NoSuchElement
异常。T orElse(T other)
会在值存在时返回值,否则返回一个默认值。 例如前面的代码中需要显式地检查Optional
对象中是否存在一道菜并访问其名称:
menu.stream()
.filter(Dish::isVegetarian)
.findAny()//返回一个Optional
.ifPresent(d -> System.out.println(d.getName());//如果包含一个值就打印它,否则什么都不做
——————【查找第一个元素】:findFirst()
有些流是有一个出现顺序来指定流中元素出现的逻辑顺序(比如List或者排好序的数据列生成的流)。可以通过findFirst
方法从这类流中找到第一个元素。例如从给定数字列表中找到第一个平方能被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
操作提供了更复杂的查询,将流中的元素进行组合。例如计算菜单中的总卡路里,此类查询需要将流中元素反复结合起来,得到一个值。这样的查询可以归类为归约操作(将流归约为一个值)。
传统的Java求和,一般通过for-each
循环对数字列表中的元素求和:
int sum = 0;
for (int x : numbers) {
sum += x;
}
这段代码有两个重要参数:一个是总和变量初始值,本例是0。另外一个将列表中所有元素结合的操作,这里是+
。reduce
对这种操作进行了抽象,可以像下面这样对流中所有元素求和:
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
reduce
接收两个参数:
BinaryOperator
来将两个元素结合起来产生一个新值,这里用的是lambda: (a, b) -> a + b
。如果需要将所有元素相乘,只需要传递另外一个Lambda: (a, b) -> a * b
。 下图展示了reduce
操作如何作用于一个流:Lambda反复结合每个元素,直到流被归约成一个值。
首先将0作为Lambda表达值第一个参数a的值,从流中获取4作为第二个参数b的值。0+4得到4,它成了新的累积值。然后在用累积值和流中下一个元素5调用Lambda,产生累积值为9。接下来,再用累积值和下一个元素3调用Lambda,得到12。最后,用12和流中最后一个元素9调用Lambda,得到最终结果21 。
这段代码使用方法引用更简洁,Java8中Integer类有一个静态sum方法来对两个数求和。
int sum = numbers.stream().reduce(0, Integer::sum);
reduce还有一个重载变体它不接受初始值,但是会返回一个 Optional 对象。使用Optional是考虑到流中没有任何元素的情况下, reduce 操作无法返回其和,因为它没有初始值。
Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));
使用reduce
也可以计算流中的最大或者最小的元素,需要给reduce
传一个给定两个元素能够返回最大值的Lambda。reduce
操作会比较新值和流中下一个元素的,并产生一个最大值,直到整个流消耗完。
Optional<Integer> max = numbers.stream().reduce(Integer::max);
同样也可以将Integer::min
传给reduce来替换Integer::max
来计算最小值。或者也可以写成Lambda表达式(x,y)->x
Integer::min
可读性更强。
相比与传统逐步迭代求和,使用reduce
的好处在于,这里的迭代被内部迭代抽象调了,这让内部实现得以选择并行执行 reduce
操作。另外需要注意的是,诸如map
或者filter
等操作会从输入流中获取每一个元素,并在输出流中的到0或者1个结果,这些操作一般都是无状态的,它们没有内部状态。
但诸如reduce
、sum
、max
等操作需要内部状态来累积结果。这种情况下内部状态很小。上面例子里就是一个int或double 。不管流中有多少元素要处理,内部状态都是有界的。
相反,诸如sort
或distinct
等操作和filter
和map
一样,都是接受一个流,再生成一个流(中间操作),但有一个关键的区别。从流中排序和删除重复项时都需要知道先前的历史。例如,排序要求所有元素都放入缓冲区后才能给输出流加入一个项目,这一操作的存储要求是无界的。要是流比较大或是无限的,就可能会有问题。这些操作叫作有状态操作。
下面是一个交易员和执行交易的一个例子,可以使用上面提到的流复杂操作来解决响应问题。
//Transaction定义
public class Transaction {
private Trader trader;
private int year;
private int value;
private String currency;
public Transaction(Trader trader, int year, int value) {
this.trader = trader;
this.year = year;
this.value = value;
}
//省略setter、getter方法
}
//Trader定义
public class Trader{
private final String name;
private final String city;
public Trader(String n, String c){
this.name = n;
this.city = c;
}
//省略getter/setter方法
}
//数据
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));
//1、找出2011年发生的所有交易,并按交易额排序(从低到高)。
List<Transaction> tr2011 = transactions.stream()
.filter(t -> t.getYear()==2011) //筛选2011年交易
.sorted(comparing(Transaction::getValue)) //按交易额排序
.collect(toList());
///2、交易员都在哪些不同的城市工作过?
List<String> cities = transactions.stream()
.map(t -> t.getTrader().getCity()) //提取交易员所在城市
.distinct(); //城市去重
//3、查找所有来自于剑桥的交易员,并按姓名排序。
List<Trader> traders = transactions.stream()
.map(Transaction::getTrader) //提取所有交易员
.filter(t -> "Cambridge".equals(t.getTrader().getCity())) //仅所在城市为剑桥的交易员
.distinct() //去重
.sorted(comparing(Trader::getName)); //按姓名排序
//4、返回所有交易员的姓名字符串,按字母顺序排序。
String traderStr = transactions.stream()
.map(t -> t.getTrader().getName()) //提取交易员姓名
.distinct() //去重
.sorted() //排序
.collect(joining(",")); //拼接所有名字
//5、有没有交易员是在米兰工作的?
boolean milanBased = transactions.stream()
.anyMatch(t -> "Milan".equals(t.getTrader().getCity()));
//6、打印生活在剑桥的交易员的所有交易额。
transactions.stream()
.filter(t ->"Cambridge".equals(t.getTrader().getCity())) //选择在剑桥的交易员
.map(Transaction::getValue) //提取交易额
.forEach(System.out::println); //打印值
//7、所有交易中,最高的交易额是多少?
Optional<Integer> highestValue = transactions.stream()
.map(Transaction::getValue) //提取交易额
.max(Integer::max); //取最大值
//8、找到交易额最小的交易。
Optional<Transaction> smallestTransaction = transactions.stream()
.min(comparing(Transaction::getValue));
1、可以用filter
、distinct
、skip
和limit
对流做筛选和切片。
2、可以用map
和flatMap
提取或转换流中的元素。
3、可以用findFirst
和findAny
方法查找流中的元素。用allMatch
、noneMatch
和anyMatch
方法让流匹配给定的Predicate。这些方法都利用了短路:找到结果就立即停止计算;没有必要处理整个流。
4、可以利用reduce
方法将流中所有的元素迭代合并成一个结果,例如求和或查找最大元素。
5、filter
和map
等操作是无状态的,它们并不存储任何状态。reduce
等操作要存储状态才能计算出一个值。sorted
和distinct
等操作也要存储状态,因为它们需要把流中的所有元素缓存起来才能返回一个新的流。这种操作称为有状态操作。