第5章-使用流

gong_yz大约 36 分钟JAVAJAVA8

使用内部迭代的话,Stream API可以决定并行运行你的代码。这要是用外部迭代的话就办不到了,因为你只能用单一线程挨个迭代。

在本章中,你将会看到Stream API支持的许多操作。这些操作能让你快速完成复杂的数据查询,如筛选、切片、映射、查找、匹配和归约。接下来,我们会看看一些特殊的流:数值流、来自文件和数组等多种来源的流,最后是无限流。

筛选和切片

在本节中,我们来看看如何选择流中的元素:用谓词筛选,筛选出各不相同的元素,忽略流中的头几个元素,或将流截短至指定长度。

用谓词筛选(filter)

Streams接口支持filter方法。该操作会接受一个**谓词(一个返回boolean的函数)**作为参数,并返回一个包括所有符合谓词的元素的流。例如,你可以像图5-1所示的这样,筛选出所有素菜,创建一张素食菜单:

List<Dish> vegetarianMenu = menu.stream() 
 							  .filter(Dish::isVegetarian)  //方法引用检查菜肴是否适合素食者
 							  .collect(toList());
image-20220401145501751
image-20220401145501751

筛选各异的元素(distinct)

流还支持一个叫作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);
image-20220401145637898
image-20220401145637898

截短流(limit(n))

流支持**limit(n)**方法,该方法会返回一个不超过给定长度的流。所需的长度作为参数传递给limit。如果流是有序的,则最多会返回前n个元素。比如,你可以建立一个List,选出热量超过300卡路里的头三道菜:

List<Dish> dishes = menu.stream() 
 					  .filter(d -> d.getCalories() > 300) 
 					  .limit(3) 
 					  .collect(toList());

图5-3展示了filter和limit的组合。你可以看到,该方法只选出了符合谓词的头三个元素,然后就立即返回了结果。

请注意limit也可以用在无序流上,比如源是一个Set。这种情况下,limit的结果不会以任何顺序排列。

image-20220401150434975
image-20220401150434975

跳过元素(skip(n))

流还支持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());
image-20220401150845888
image-20220401150845888

在我们讨论映射操作之前,在测验5.1上试试本节学过的内容吧。

测验5.1:筛选

你将如何利用流来筛选前两个荤菜呢?

答案:你可以把filter和limit复合在一起来解决这个问题,并用collect(toList())将流转换成一个列表。

List<Dish> dishes = 
 menu.stream() 
 	.filter(d -> d.getType() == Dish.Type.MEAT) 
 	.limit(2) 
 	.collect(toList());

映射(map && flatMap)

一个非常常见的数据处理套路就是从某些对象中选择信息。比如在SQL里,你可以从表中选择一列。Stream API也通过mapflatMap方法提供了类似的工具。

对流中每一个元素应用函数

流支持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());

流的扁平化

你已经看到如何使用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说明了这个问题。

image-20220401154847854
image-20220401154847854

幸好可以用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的效果比较一下。

    image-20220401155219442
    image-20220401155219442

一言以蔽之,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)]。


查找和匹配

另一个常见的数据处理套路是看看数据集中的某些元素是否匹配一个给定的属性。Stream API通过allMatchanyMatchnoneMatchfindFirstfindAny方法提供了这样的工具。

检查谓词是否至少匹配一个元素anyMatch

anyMatch方法可以回答“流中是否有一个元素能匹配给定的谓词”。比如,你可以用它来看看菜单里面是否有素食可选择:

if(menu.stream().anyMatch(Dish::isVegetarian)){ 
 	System.out.println("The menu is (somewhat) vegetarian friendly!!"); 
}

检查谓词是否匹配所有元素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也是一个短路操作:它只需要创建一个给定大小的流,而用不着处理流中所有的元素。在碰到无限大小的流的时候,这种操作就有用了:它们可以把无限流变成有限流。我们会在5.7节中介绍无限流的例子。

查找元素findAny

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里面几种可以迫使你显式地检查值是否存在或处理值不存在的情形的方法也不错。

  • 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() 
 	.ifPresent(d -> System.out.println(d.getName());

查找第一个元素findFirst

有些流有一个出现顺序(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

何时使用findFirstfindAny

你可能会想,为什么会同时有findFirst和findAny呢?

答案是并行。找到第一个元素在并行上限制更多。如果你不关心返回的元素是哪个,请使用findAny,因为它在使用并行流时限制较少。


归约

在本节中,你将看到如何把一个流中的元素组合起来,使用reduce操作来表达更复杂的查询,比如“计算菜单中的总卡路里”或“菜单中卡路里最高的菜是哪一个”。此类查询需要将流中所有元素反复结合起来,得到一个值,比如一个Integer。这样的查询可以被归类为归约操作(将流归约成一个值)。用函数式编程语言的术语来说,这称为折叠(fold),因为你可以将这个操作看成把一张长长的纸(你的流)反复折叠成一个小方块,而这就是折叠操作的结果。

元素求和

你可以像下面这样对流中所有的元素求和:

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。
image-20220402172343877
image-20220402172343877

你可以使用方法引用让这段代码更简洁。在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接受两个参数:

  • 一个初始值
  • 一个Lambda来把两个流元素结合起来并产生一个新值

Lambda是一步步用加法运算符应用到流中每个元素上的,如图5-7所示。因此,你需要一个给定两个元素能够返回最大值的Lambda。reduce操作会考虑新值和流中下一个元素,并产生一个新的最大值,直到整个流消耗完!你可以像下面这样使用reduce来计算流中的最大值,如图5-8所示。

Optional<Integer> max = numbers.stream().reduce(Integer::max);
image-20220402172622066
image-20220402172622066

要计算最小值,你需要把Integer.min传给reduce来替换Integer.max:

Optional<Integer> min = numbers.stream().reduce(Integer::min);

你当然也可以写成Lambda (x, y) -> x < y ? x : y而不是Integer::min,不过后者比较易读。

测验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();

流操作:无状态和有状态

你已经看到了很多的流操作。乍一看流操作简直是灵丹妙药,而且只要在从集合生成流的时候把Stream换成parallelStream就可以实现并行。

当然,对于许多应用来说确实是这样,就像前面的那些例子。你可以把一张菜单变成流,用filter选出某一类的菜肴,然后对得到的流做map来对卡路里求和,最后reduce得到菜单的总热量。这个流计算甚至可以并行进行。但这些操作的特性并不相同。它们需要操作的内部状态还是有些问题的。

诸如map或filter等操作会从输入流中获取每一个元素,并在输出流中得到0或1个结果。这些操作一般都是无状态的:它们没有内部状态(假设用户提供的Lambda或方法引用没有内部可变状态)。

但诸如reduce、sum、max等操作需要内部状态来累积结果。在上面的情况下,内部状态很小。在我们的例子里就是一个int或double。不管流中有多少元素要处理,内部状态都是有界的。

相反,诸如sort或distinct等操作一开始都和filter和map差不多——都是接受一个流,再生成一个流(中间操作),但有一个关键的区别。从流中排序和删除重复项时都需要知道先前的历史。例如,排序要求所有元素都放入缓冲区后才能给输出流加入一个项目,这一操作的存储要求是无界的。要是流比较大或是无限的,就可能会有问题(把质数流倒序会做什么呢?它应当返回最大的质数,但数学告诉我们它不存在)。我们把这些操作叫作有状态操作。

你现在已经看到了很多流操作,可以用来表达复杂的数据处理查询。表5-1总结了迄今讲过的操作。

image-20220402173728167
image-20220402173728167

付诸实践

我们来看一个不同的领域:执行交易的交易员。你的经理让你为八个查询找到答案。

  1. 找出2011年发生的所有交易,并按交易额排序(从低到高)。
  2. 交易员都在哪些不同的城市工作过?
  3. 查找所有来自于剑桥的交易员,并按姓名排序。
  4. 返回所有交易员的姓名字符串,按字母顺序排序。
  5. 有没有交易员是在米兰工作的?
  6. 打印生活在剑桥的交易员的所有交易额。
  7. 所有交易中,最高的交易额是多少?
  8. 找到交易额最小的交易。

领域:交易员和交易

以下是你要处理的领域,一个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 String name;
	private 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 void setCity(String newCity){
		this.city = newCity;
	}

	public String toString(){
		return "Trader:"+this.name + " in " + this.city;
	}
}


public class Transaction{

	private Trader trader;
	private int year;
	private 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 +"}";
	}
}

解答

找出2011年的所有交易并按交易额排序(从低到高)

  List<Transaction> tr2011 =
                transactions.stream()
                        .filter(transaction -> transaction.getYear() == 2011)
                        .sorted(comparing(Transaction::getValue))
                        .collect(toList());

交易员都在哪些不同的城市工作过

        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())
                        .distinct()
                        .sorted()
                        .reduce("", (n1, n2) -> n1 + n2);  //逐个拼接每个名字,得到一个将所有名字连接起来的String

请注意,此解决方案效率不高(所有字符串都被反复连接,每次迭代的时候都要建立一个新的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引入了三个原始类型特化流接口来解决这个问题:IntStreamDoubleStreamLongStream,分别将流中的元素特化为intlongdouble,从而避免了暗含的装箱成本。

每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的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还支持其他的方便方法,如maxminaverage等。

转换回对象流

例如,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尤其有用。

默认值OptionalInt

求和的那个例子很容易,因为它有一个默认值:0。但是,如果你要计算IntStream中的最大元素,就得换个法子了,因为0是错误的结果。如何区分没有元素的流和最大值真的是0的流呢?前面我们介绍了Optional类,这是一个可以表示值存在或不存在的容器。Optional可以用Integer、String等参考类型来参数化。对于三种原始流特化,也分别有一个Optional原始类型特化版本:OptionalIntOptionalDoubleOptionalLong

例如,要找到IntStream中的最大元素,可以调用max方法,它会返回一个OptionalInt:

OptionalInt maxCalories = menu.stream() 
 							.mapToInt(Dish::getCalories) 
 							.max();

现在,如果没有最大值的话,你就可以显式处理OptionalInt去定义一个默认值了:

int max = maxCalories.orElse(1);  //如果没有最大值的话,显式提供一个默认最大值

数值范围

和数字打交道时,有一个常用的东西就是数值范围。比如,假设你想要生成1和100之间的所有数字。Java 8引入了两个可以用于IntStream和LongStream的静态方法,帮助生成这种范围:rangerangeClosed。这两个方法都是第一个参数接受起始值第二个参数接受结束值。但range是不包含结束值的,而rangeClosed则包含结束值。让我们来看一个例子:

IntStream evenNumbers = IntStream.rangeClosed(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是不包含结束值的。


数值流应用:勾股数

勾股数是什么

什么是勾股数(毕达哥拉斯三元数)呢?

  • 某些三元数(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所示。

    image-20220422111807972
    image-20220422111807972

lambda构建勾股数

第一版写法

        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)})
                        );

解释

  • 使用具有三个元素的int数组,比如new int[]{3, 4, 5},来表示勾股数(3, 4, 5)。现在你就可以用数组索引访问每个元素
  • .filter(b -> Math.sqrt(a*a + b*b) % 1 == 0):筛选成立的组合,假设周围的代码给a提供了一个值,并且stream提供了b可能出现的值,filter将只选出那 些可以与a组成勾股数的b。
  • flatMap:创建一个从1到100的数值范围来生成a的值。对每个给定的a值,创建一个三元数流。要是把a的值映射到三元数流的话,就会得到一个由流构成的流。flatMap方法在做映射的同时,还会把所有生成的三元数流扁平化成一个流。这样你就得到了一个三元数流。
  • 生成b值:IntStream.mapToObj,这个方法会返回一个对象值流
  • IntStream.rangeClosed(1, 100).boxed():以在给定区间内生成一个数值流,**.boxed()**会将从rangeClosed返回的IntStream生成一个Stream<Integer>
  • IntStream的mapToObj方法:返回一个对象值流

现在你可以运行解决方案,并且可以利用我们前面看到的limit命令,明确限定从生成的流中要返回多少组勾股数了:

pythagoreanTriples.limit(5).forEach(t -> System.out.println(t[0] + ", " + t[1] + ", " + t[2]));

你还能做得更好吗?

  • 目前的解决办法并不是最优的,因为你要求两次平方根。让代码更为紧凑的一种可能的方法是,先生成所有的三元数(aa, bb, aa+bb),然后再筛选符合条件的:

            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方法从集合生成流了。此外,我们还介绍了如何根据数值范围创建数值流。但创建流的方法还有许多!本节将介绍如何从值序列、数组、文件来创建流,甚至由生成函数来创建无限流!

由值创建流

你可以使用静态方法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.iterateStream.generate。这两个操作可以创建所谓的无限流

  • 不像从固定集合创建的流那样有固定大小的流。由iterate和generate产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!
  • 一般来说,应该使用limit(n)来对这种流加以限制,以避免打印无穷多个值。

1、迭代

我们先来看一个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日,依此类推。

测验5.4:斐波纳契元组序列

什么是斐波纳契元组序列?

  • 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) …

你的任务是用iterate方法生成斐波纳契元组序列中的前20个元素?

  • 第一个问题是,iterate方法要接受一个UnaryOperator<T>作为参数,而你需要一个像(0,1)这样的元组流。你还是可以(这次又是比较草率地)使用一个数组的两个元素来代表元组。例如,new int[]{0,1}就代表了斐波纳契序列(0, 1)中的第一个元素。这就是iterate方法的初始值:

    Stream.iterate(new int[]{0, 1}, ???) 
     	  .limit(20) 
     	  .forEach(t -> System.out.println("(" + t[0] + "," + t[1] +")"));
    
  • 在这个测验中,你需要搞清楚???代表的代码是什么。请记住,iterate会按顺序应用给定的Lambda。

  • 答案:

    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…

2、生成

与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用它来计算下一项。此外,在下一次调用它的时候,还要更新IntSupplier的状态。下面的代码就是如何创建一个在调用时返回下一个斐波纳契项的IntSupplier:

IntSupplier fib = new IntSupplier(){ 
 	private int previous = 0; 
 	private int current = 1; 
 	public int getAsInt(){ 
 		int oldPrevious = this.previous; 
 		int nextValue = this.previous + this.current; 
 		this.previous = this.current; 
 		this.current = nextValue; 
 		return oldPrevious; 
 	} 
}; 
IntStream.generate(fib).limit(10).forEach(System.out::println);
  • 前面的代码创建了一个IntSupplier的实例。此对象有可变的状态:它在两个实例变量中记录了前一个斐波纳契项和当前的斐波纳契项。getAsInt在调用时会改变对象的状态,由此在每次调用时产生新的值。相比之下,使用iterate的方法则是纯粹不变的:它没有修改现有状态,但在每次迭代时会创建新的元组。你应该始终采用不变的方法,以便并行处理流,并保持结果正确。
  • 注意,因为你处理的是一个无限流,所以必须使用limit操作来显式限制它的大小;否则,终端操作(这里是forEach)将永远计算下去。同样,你不能对无限流做排序或归约,因为所有元素都需要处理,而这永远也完不成!

小结

流让你可以简洁地表达复杂的数据处理查询。此外,流可以透明地并行化。以下是你应从本章中学到的关键概念。

  • Streams API可以表达复杂的数据处理查询。常用的流操作总结在表5-1中。
  • 你可以使用filter、distinct、skip和limit对流做筛选和切片。
  • 你可以使用map和flatMap提取或转换流中的元素。
  • 你可以使用findFirst和 findAny方法查找流中的元素。你可以用allMatch、noneMatch和anyMatch方法让流匹配给定的谓词。
  • 这些方法都利用了短路:找到结果就立即停止计算;没有必要处理整个流。
    • 你可以利用reduce方法将流中所有的元素迭代合并成一个结果,例如求和或查找最大元素。
  • filter和map等操作是无状态的,它们并不存储任何状态。reduce等操作要存储状态才能计算出一个值。sorted和distinct等操作也要存储状态,因为它们需要把流中的所有元素缓存起来才能返回一个新的流。这种操作称为有状态操作。  流有三种基本的原始类型特化:IntStream、DoubleStream和LongStream。它们的操作也有相应的特化。
  • 流不仅可以从集合创建,也可从值、数组、文件以及iterate与generate等特定方法创建。
  • 无限流是没有固定大小的流。