java Stream(让你变的牛逼的操作)

本文介绍了Java Stream的概念,包括流的定义、操作和使用方法。通过实例展示了如何使用filter、map、limit、distinct、skip等方法进行数据处理,以及如何通过reduce进行归约操作。此外,还讨论了流的无状态和有状态操作,以及map-reduce模式在流处理中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

流定义:

从支持数据处理操作的源生成的元素序列。

  • 元素序列:就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。因为集合是数据结构,所以它的主要目的是以特定的时间/空间复杂度存储和访问元素。但流的目的在于表达计算。集合讲的是数据,流将的是计算。
  • 源:流会使用一个提供数据的源,如集合、数组或输入/输出资源。请注意,从有序集合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。
  • 数据处理操作:流的数据处理功能支持类似数据库的操作。以及函数式编程语言中常用的操作,如filter、map、reduce、find、match、sort等。流操作可以顺序执行,也可以并行执行。

流操作有两个重要的特点

  • 流水线:很多操作本身会返回一个流,这样多个操作就可以连接起来,形成一个大的 流水线
  • 内部迭代:与使用迭代器显示迭代不一样,流的迭代是在背后进行的。
List<String> threeHighCaloricDishNames = 
 menu.stream() 
 .filter(d -> d.getCalories() > 300).map(Dish::getName)
 .limit(3) 
 .collect(toList()); 
System.out.println(threeHighCaloricDishNames);

在本例中,我们先对menu(菜单)调用stream方法,由菜单得到一个流。数据源是菜肴列表(菜单),它给流提供了一个元素序列。接下来,对流应用一些列数据操作:filter、map、limit和collect。除了collect之外,所有的操作都会返回另一个流,这样它们就可以接成一个流水线,于是就可以看作是对源的查询。最后,collect操作开始处理流水线,并返回结果。在调用collect之前,没有任何结果产生,实际上根本从menu里选择元素。可以理解成这样,链中的方法都在排队等待,直到调用collect。

  1. filter--接受Lambda,从流中排除某些元素,在本例中,通过传递lambda d -> d.getCalories() > 300,选择出热量超过300卡路里的菜肴。
  2. map--接受一个Lambda,将元素转换为其他形式或提取信息。在本例中,通过传递方法引用Dish::getName提取每一道菜名。
  3. limit--截断流,使其他元素不超过给定数量。
  4. collect--将流转换为其他形式。

流操作

java.util.stream.Stream中的Stream接口定义了许多操作。它们可以分为两大类。再看前面例子

List<String> names = menu.stream() 
 .filter(d -> d.getCalories() > 300)
 .map(Dish::getName) 
 .limit(3) 
 .collect(toList());

我们可以看到两类操作:

  • filter、map和limit可以连成一条流水线
  • collect触发流水线执行关闭它。

可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作。

使用流

1.1用谓词筛选

Stream接口支持filter方法。该操作会接受一个谓词(一个返回boolean的函数)作为参数,并返回所有符合谓词元素的流,筛选出所有素菜,创建一张素食菜单。

1.2筛选出各异的元素

流还支持一个叫做distinct的方法,它会返回一个元素各异的流。如下筛选出列表中所有的偶数。

List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4); 
numbers.stream() 
 .filter(i -> i % 2 == 0) 
 .distinct() 
 .forEach(System.out::println);

1.3截断流

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

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

1.4跳过元素

流还支持skip(n)方法,返回一个扔掉前n个元素的流。如果流中元素不足n,则返回一个空流。

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

映射

一个非常常见的数据处理套路就是从某些对象中选择信息。比如SQL里,从列表中选择一列。StreamApi也通过map和flatMap方法提供了类似的方法。

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

流支持map方法,他会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素。例如,下面的代码把方法引用Dish::getName传给map方法,来提取流中菜肴的名称:

List<String> dishNames = menu.stream() 
 .map(Dish::getName) 
 .collect(toList());

2.2流的扁平化

对于一张单词表,如何返回一张列表,列出里面各不相同的字符呢?例如,给定单词 列表["Hello","World"],你想要返回列表["H","e","l", "o","W","r","d"]。

可能我们最先想到的是distinct来过滤重复的字符。

words.stream() 
 .map(word -> word.split("")) 
 .distinct() 
 .collect(toList());

这方法的问题在于,传递给map方法的Lambda为每个单词返回了一个String[]。因此map返回的流实际上是Stream<String[]>类型。而我们真正想要的是Stream<String>来表示一个字符流。

如果我们尝试使用flatMap就可以解决问题了。

List<String> uniqueCharacters = 
 words.stream() 
 .map(w -> w.split("")) 
 .flatMap(Arrays::stream) 
 .distinct() 
 .collect(Collectors.toList());

使用flatMap方法的效果是,各个数组不是分别映射成一个流,而是映射成流的内容。

2.3查找和匹配

常见的数据处理套路是看看数据集中的某些元素是否匹配一个给定的属性。Stream API同allMatch、anyMatch、noneMatch、findFirst、findAny方法提供这样的工具。

3.1anyMatch方法的意思是:流中是否有一个元素匹配给定的谓词。比如,可以用它来看菜单中是否有素食。

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

anyMatch方法返回一个boolean,因此是一个终端操作

3.2检查谓词是否匹配所有元素

allMatch方法的原理和anyMatch类似,但它确定的流中的所有元素。

比如,你可以用它来看看菜品是否有利健康

boolean isHealthy = menu.stream() 
 .allMatch(d -> d.getCalories() < 1000);

3.3检查谓词是否与全部元素都不匹配

noneMatch与allMatch相对,确定是否全部元素都不匹配

boolean isHealthy = menu.stream() 
 .noneMatch(d -> d.getCalories() >= 1000);

其实这三个就类似于&& ,||,!

3.4查找元素

findAny方法返回当前流中的任意元素。它可以和其他流操作结合使用。比如,你可能想找到一道素食菜肴。

Optional<Dish> dish = 
 menu.stream() 
 .filter(Dish::isVegetarian) 
 .findAny();

Optional后续说。

查找第一个元素: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
何时使用findFirst和findAny
你可能会想,为什么会同时有findFirst和findAny呢?答案是并行。找到第一个元素
在并行上限制更多。如果你不关心返回的元素是哪个,请使用findAny,因为它在使用并行流
时限制较少。

3.5归约

归约就是将流中的元素组合起来进行更复杂的计算。使用reduce操作来表达更复杂的查询。比如,计算菜单中的总卡路里或菜单中的最高的卡路里。

研究reduce方法之前,先来看看如何使用for-each循环来对数字列表中元素求和。

int sum = 0; 
for (int x : numbers) { 
 sum += x; 
}

numbers中的每个元素都用加法运算反复迭代得到结果,通过反复使用加法,把数字列表归约成一个数字。

要是将所有数字相乘,而不用去复制粘贴这段代码,那不是很好。而reduce正适合这样的重复性操作,它对这种重复应用的模式做了抽象。我可以这样。

int sum = numbers.stream().reduce(0, (a, b) -> a + b);

首先,0作为lambda(a)的第一个参数,从流中获得4作为第二个参数(b)。0+4得到4。它成了新的累计值。然后再调用累计值和流中下一个元素5调用Lambda,产生新的累计值9。

这段代码使用方法引用会更简单。

int sum = numbers.stream().reduce(0, Integer::sum);

map-reduce模式

map和reduce的连接通常称为map-reduce模式。因谷歌谷歌用它进行网络搜索出名,因为它容易并行化。

比如,怎样用mapreduce方法数一数流中有多少个菜呢?

int count = menu.stream() .map(d -> 1) .reduce(0, (a, b) -> a + b);

把流中的每个元素都映射为1,然后求和。

 

流操作:无状态和有状态

诸如map或filter等操作会从输入流中获取每一个元素,并在输出流中得到0或1个结果。即它们没有内部状态(没有数据留存)。

但是像reduce,sum,max等操作需要内部状态(需要用到先前的数据)来累计结果。像这想操作不管流有多少元素,内部状态都是有界的。但是像sort或者distinct等操作一开始都和filter和map差不多。都是接收一个流再生成一个流,但关键的区别是从流中排序和删除重复项时都需要知道先前的历史。例如,排序要求所有元素都放入缓冲区后才能给输出流加入一个项目,这样的操作对缓存要求是无上限的,即无界。我们叫这些操作叫有状态操作。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值