首页 >> 大全

Java8实战-函数式数据处理-steam流之筛选、切片、映射、查找、匹配和归约

2023-11-12 大全 31 作者:考证青年

在(一)中,流从外部迭代转向内部迭代。 这样,就用不着写下面这样的代码来显式地管理数据集合的迭代(外部迭代)了:

List<Dish> vegetarianDishes = new ArrayList<>();
for(Dish d: menu) {if(d.isVegetarian()) {vegetarianDishes.add(d);}
}

可以使用支持和操作的(内部迭代)管理对集合数据的迭代。只需要将筛选行为作为参数传递给方法就行了。

import static java.util.stream.Collectors.toList;
List<Dish> vegetarianDishes = menu.stream().filter(Dish::isVegetarian).collect(toList());

这种处理数据的方式很有用,因为让 API管理如何处理数据。这样 API就可以在背后进行多种优化。

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

这些操作能快速完成复杂的数据查询,如筛选、切片、映射、查找、匹配和归约。

一.筛选和切片 steam. 过滤

接口支持方法。该操作会接受一个谓词(一个返回的函数)作为参数,并返回一个包括所有符合谓词的元素的流。

例如,你可以像所示的这样,筛选出所有素菜,创建一张素食菜单:

2. steam. 去重

流还支持一个叫作的方法,它会返回一个元素各异(根据流所生成元素的和方法实现)的流。

例如,以下代码会筛选出列表中所有的偶数,并确保没有重复。

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

筛选去重过程:

steam.limit 截短

流支持limit(n)方法,该方法会返回一个不超过给定长度的流。所需的长度作为参数传递给limit。如果流是有序的,则最多会返回前n个元素。

比如:

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

和limit的组合。该方法只选出了符合谓词的头三个元素,然后就立即返回了结果。

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

steam.skip 跳过

流还支持skip(n)方法,返回一个跳过前n个元素的流。如果流中元素不足n个,则返回一个空流。limit(n)和skip(n)是互补的。

例如:

        List<Dish> menu = new ArrayList<>();//菜单menu.add(new Dish(500,"麻婆豆腐"));menu.add(new Dish(400,"爆炒猪肝"));menu.add(new Dish(400,"油焖茄子"));menu.add(new Dish(400,"蒜蓉龙虾"));List<String> names =menu.stream().filter(f -> f.getCalories() > 300).map(Dish::getName).skip(2).collect(toList());System.out.println(names);

结果:

[油焖茄子, 蒜蓉龙虾]

二.映射 map

流支持map方法,它会接受一个函数作为参数。

这个函数会被应用到每个元素上,并将其映射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的细微差别在于它是“创建一个新版本”而不是去“修改”)。

       List<Dish> menu = new ArrayList<>();//菜单menu.add(new Dish(500,"麻婆豆腐"));menu.add(new Dish(400,"爆炒猪肝"));menu.add(new Dish(400,"油焖茄子"));menu.add(new Dish(400,"蒜蓉龙虾"));List<String> dishNames = menu.stream().map(Dish::getName).collect(toList());

因为方法返回一个,所以map方法输出的流的类型就是。

例如:

给定一个单词列表,返回另一个列表,显示每个单词中有几个字母?

List<String> words = Arrays.asList("Java 8", "Lambdas", "In", "Action");
List<Integer> wordLengths = words.stream().map(String::length).collect(toList());

案例:

给定一个数字列表,如何返回一个由每个数的平方构成的列表呢?例如,给定[1, 2, 3, 4,5],应该返回[1, 4, 9, 16, 25]。

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squares = numbers.stream().map(n -> n * n).collect(toList()); 

流的扁平化

举例: 对于一张单词表,如何返回一张列表,列出里面各不相同的字符?

例如,给定单词列表[“Hello”,“World”],你想要返回列表[“H”,“e”,“l”, “o”,“W”,“r”,“d”]。

错误代码:

        List<String> words = new ArrayList<>();words.add("Hello");words.add("World");List<String[]> list = words.stream().map(word -> word.split("")).distinct().collect(toList());//输出内容list.forEach(l -> System.out.print(Arrays.toString(l) +" "));

结果:

[H, e, l, l, o] [W, o, r, l, d]

解析:

问题在于,传递给map方法的为每个单词返回了一个[](列表)。因此,map返回的流实际上是类型的。你真正想要的是用来表示一个字符流。

使用map和.()后:

映射与函数__图像映射和切片的不同之处

使用map和.()后得到的是一个流的列表,更准确地说是,问题依旧存在.

使用后:

        List<String> words = new ArrayList<>();words.add("Hello");words.add("World");List<String> list = words.stream().map(word -> word.split("")).flatMap(Arrays::stream).distinct().collect(toList());list.forEach(f -> System.out.print(f+" "));

结果:

H e l o W r d

使用方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。所有使用map(::)时生成的单个流都被合并起来,即扁平化为一个流。

案例:

给定两个数字列表,如何返回所有的数对呢?例如,给定列表[1, 2, 3]和列表[3, 4],应该返回[(1, 3), (1, 4), (2, 3), (2, 4), (3, 3), (3, 4)]

        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整除的数对呢?例如(2, 4)和(3, 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());

三.查找和匹配 检查谓词是否至少匹配一个元素

方法表示流中是否有一个元素能匹配给定的谓词。

比如,用它来看看菜单里面是否有素食可选择:

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

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

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

方法的工作原理和类似,但它会看看流中的元素是否都能匹配给定的谓词。

比如,你可以用它来看看菜品是否有利健康(即所有菜的热量都低于1000卡路里):

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

和相对的是。它可以确保流中没有任何元素与给定的谓词匹配。

用重写前面的例子:

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

、和这三个操作都用到了我们所谓的短路,这就是大家熟悉的Java中&&和||运算符短路在流中的版本。

短路求值:

有些操作不需要处理整个流就能得到结果。

例如,假设你需要对一个用and连起来的大布尔表达式求值。不管表达式有多长,你只需找到一个表达式为false,就可以推断整个表达式将返回false,所以用不着计算整个表达式。这就是短路。

对于流而言,某些操作(例如、、、和)不用处理整个流就能得到结果。只要找到一个元素,就可以有结果了。同样,limit也是一个短路操作:它只需要创建一个给定大小的流,而用不着处理流中所有的元素。在碰到无限大小的流的时候,这种操作就有用了:它们可以把无限流变成有限流。

查找元素

方法将返回当前流中的任意元素。它可以与其他流操作结合使用。

比如,你可能想找到一道素食菜肴。你可以结合使用和方法来实现这个查询:

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

类(java.util.)是一个容器类,代表一个值存在或不存在。在上面的代码中,可能什么元素都没找到。Java 8的库设计人员引入了,这样就不用返回众所周知容易出问题的null了。

查找第一个元素

有些流有一个出现顺序( order)来指定流中项目出现的逻辑顺序(比如由List或排序好的数据列生成的流)。对于这种流,你可能想要找到第一个元素。

例如,给定一个数字列表,下面的代码能找出第一个平方能被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();

何时使用和?

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

四.归约 求和

以前元素求和代码可能如下:

int sum = 0;
for (int x : numbers) {sum += x;
} numbers中的每个元素都用加法运算符反复迭代来得到结果。
通过反复使用加法,你把一个数字列表归约成了一个数字。这段代码中有两个参数:
总和变量的初始值,在这里是0;
将列表中所有元素结合在一起的操作,在这里是+

Java8引入一个方法,还能把所有的数字相加,而不必去复制粘贴这段代码,它对这种重复应用的模式做了抽象:

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 multiply = numbers.stream().reduce(1, (a, b) -> a * b); 

操作是如何作用于一个流的:反复结合每个元素,直到流被归约成一个值。

在Java 8中,类现在有了一个静态的sum方法来对两个数求和:

int sum = numbers.stream().reduce(0, Integer::sum);reduce还有一个重载的变体,它不接受初始值,但是会返回一个Optional对象:
Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b)); 考虑流中没有任何元素的情况。reduce操作无法返回其和,因为它没有初始值。
所以结果被包裹在一个Optional对象里,以表明和可能不存在。

最大值和最小值

利用来计算流中最大或最小的元素,只要用归约就可以计算最大值和最小值.

接受两个参数:

1.一个初始值

2.一个来把两个流元素结合起来并产生一个新值

是一步步用加法运算符应用到流中每个元素上的,。因此,你需要一个给定两个元素能够返回最大值的。操作会考虑新值和流中下一个元素,并产生一个新的最大值,直到整个流消耗完。

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

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

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

归约方法的优势与并行化

相比于前面写的逐步迭代求和,使用的好处在于,这里的迭代被内部迭代抽象掉了,这让内部实现得以选择并行执行操作。而迭代式求和例子要更新共享变量sum,这不是那么容易并行化的。如果你加入了同步,很可能会发现线程竞争抵消了并行本应带来的性能提升!这种计算的并行化需要另一种办法:将输入分块,分块求和,最后再合并起来。但这样的话代码看起来就完全不一样了。

现在重要的是要认识到,可变的累加器模式对于并行化来说是死路一条。你需要一种新的模式,这正是所提供的。还可以使用流来对所有的元素并行求和时,你的代码几乎不用修改:()换成了()。

int sum = .().(0, ::sum);

但要并行执行这段代码也要付一定代价,(后续解释)。

总结:

流操作:无状态和有状态

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

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

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

但诸如、sum、max等操作需要内部状态来累积结果。在上面的情况下,内部状态很小。不管流中有多少元素要处理,内部状态都是有界的。

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

流的中间操作和终端操作列表

五.巩固练习

/*** @Description 商人*/
@Data
@AllArgsConstructor
public class Trader {private String name;private String city;
}

/*** @Description 交易*/
@Data
@AllArgsConstructor
public class Transaction {private Trader trader;private int year;private int value;
}

//数据初始化
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年发生的所有交易,并按交易额排序(从低到高)。

(2) 交易员都在哪些不同的城市工作过?

(3) 查找所有来自于剑桥的交易员,并按姓名排序。

(4) 返回所有交易员的姓名字符串,按字母顺序排序。

(5) 有没有交易员是在米兰工作的?

(6) 打印生活在剑桥的交易员的所有交易额。

(7) 所有交易中,最高的交易额是多少?

(8) 找到交易额最小的交易。

答案:

找出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对象)。
//以后看到一个更为高效的解决方案

有没有交易员是在米兰工作的

boolean milanBased = transactions.stream().anyMatch(transaction -> transaction.getTrader().getCity().equals("Milan")); 

打印生活在剑桥的交易员的所有交易额

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().min(comparing(Transaction::getValue)); 

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了