JAVA8 - 使用流
从迭代到流得操作
在处理集合时,我们通常会迭代遍历它得元素,并在每个元素上执行某项操作。例如我们想要对某本书中得所有长单词进行计数。首先,将所有单词放到一个列表中:
String contents = Files.readString(Path.of("alice.txt"));
List<String> result= List.of(contents.split("\\PL+"));
现在,可以迭代它了:
int count = 0;
for(String s : result){
if(s.length() > 12){
count ++;
}
}
使用流时,相同得操作看起来像下面这样:
long count = result.stream()
.filter(word -> word.length() > 12)
.count();
现在我们不必扫描整个代码去查找过滤和计数操作,方法名就可以直接告诉我们意欲何为。而且,循环要非常详细地指定操作得顺序,而流却能够以其想要得任何方式来调度这些操作,只要结果正确即可。
仅将stram 修改为parallelStream 就可以让流库以并行方式来执行过滤和计数
long count = result.parallelStream()
.filter(word -> word.length() > 12)
.count();
流遵循了"做什么而非怎么做"的原则。在流的示例中,我们描述了需要做什么:获取长单词,并对它们计数。我们没有指定该操作应该以什么顺序或者在哪个线程中执行。相比之下,循环要确切地指定计算应该如何工作,因此也就丧失了进行优化的机会。
流表面行看起来和集合很类似,都可以让我们转换和获取数据。但是,它们之间存在显著差异:
- 流并不存储元素,这些元素可能存储在底层的集合中,或者是按需生成的。
- 流的操作不会修改其数据源。例如,filter方法不会从流中移除元素,而是会生成一个新的流,其中不包含被过滤掉的元素
- 流的操作尽可能时惰性的。这意味着直至需要其结果时,操作才会执行。例如,如果我们只想查找前5个长单词而不是所有长单词,那么filter 方法就会匹配到第5个单词后停止过滤。因此,我们甚至可以操作无限流。
查找和匹配
Dish 类:
package com.demo3;
public class Dish {
private final String name;
private final boolean vegetarian; //素食注意
private final int calories;
private final Type type;
public Dish(String name, boolean vegetarian, int calories, Type type) {
this.name = name;
this.vegetarian = vegetarian;
this.calories = calories;
this.type = type;
}
public String getName() {
return name;
}
public boolean isVegetarian() {
return vegetarian;
}
public int getCalories() {
return calories;
}
public Type getType() {
return type;
}
@Override
public String toString() {
return name;
}
public enum Type {MEAT, FISH, OTHER}
}
anyMatch、allMatch、noneMatch 这三个操作都用到了我们所谓的短路,这就是大家熟悉的JAVA中 && 和 || 运算符短路在流中的版本
package com.demo3;
import java.util.Arrays;
import java.util.List;
public class Test1 {
public static List<Dish> menu = Arrays.asList(
new Dish("pork",false,800,Dish.Type.MEAT),
new Dish("beef",false,700,Dish.Type.MEAT),
new Dish("chicken",false,400,Dish.Type.MEAT),
new Dish("french fries",true,530,Dish.Type.OTHER),
new Dish("rice",true,350,Dish.Type.OTHER),
new Dish("reason fruit",true,120,Dish.Type.OTHER)
);
public static void main(String[] args) {
test1();
}
public static void test1(){
//检查谓词是否至少匹配一个元素
if (menu.stream().anyMatch(Dish::isVegetarian)){
System.out.println("The menu is (somewhat) vegetarian friendly!!");
}
//检查谓词是否匹配所有元素
boolean b = menu.stream().anyMatch(d -> d.getCalories() < 1000);
System.out.println(b); //ture
boolean b1 = menu.stream().noneMatch(d -> d.getCalories() >= 1000);
System.out.println(b1); //true
}
}
查找元素
findAny 方法将返回当前流中的任意元素。它可以与其他流操作结合使用。比如,你可能想找一道素食菜肴。你可以结合使用filter 和 findAny 方法来实现这个查询:
Optional<Dish> dish = menu.steam()
.filter(Dish::isVegetarian)
.findAny();
流水线将在后台进行优化使其只需走一遍,并在利用短路找到结果时立即结束。
Optional 简介
Optional
用返回众所周知容易出问题的null 了。
Optional 里面几种可以迫使你显示地检查值是否存在或处理不存在的情形的方法也不错
获取Optional 值:
- T get() 会在值存在问题时返回值,否则抛出一个NoSuchElement 异常
- T orElse(T other) 会在值存在时返回值,否则返回一个默认值
- T orElseGet(Suppler<? extend T> other) 产生Optional 的值,或者Optional 为空时,产生调用other的结果
<X extends Throwable> T orElseThrow(Suppler<? extends X> expectionSupplier)
产生Optional 的值,或者Optional 为空时,抛出调用expectionSupplier的结果
判断Optional值是否存在:
- isPresent() 将在Optional 包含值的时候返回true, 否则返回false
消费Optional值:
-
ifPresent(v -> process v) 只有在其存在的情况下才消费该值
例如如果在该值存在的情况下才想要将其加入到某个集合中,那么就可以调用
optionalValue.ifPresent(v -> list.add(v)) -
可选值存在时执行一种动作,在可选值不存在的时候执行另一种动作,可以使用ifPresentOrElse
optionalValue.ifPresentOrElse(v -z System.out.println("Found" + v),
() -> logger.warning("No match"))
例如,在前面的代码中你需要显示地检查Optional 对象中是否存在一道菜可以访问其名称:
menu.stream().filter(Dish::isVegetarian).findAny().ifPresent(d -> System.out.println(d.getName()));
查找第一个元素
有些流有一个出现顺序来指定流中项目出现的逻辑顺序(比如List 或 排序好的数据列生成的流)。 对于这种流,你可能想要找到第一个元素。为此有一个findFirst 方法,它的工作方法
类似于 findAny 。例如,给定一个数字列表,下面的代码能找出第一个平方能被3整除的数:
@Test
public void test2(){
List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> first = someNumbers.stream()
.map(x -> x * x)
.filter(x -> x % 3 == 0)
.findFirst();
System.out.println(first.orElse(-1));
}
何时使用findFirst 和 findAny
你可能会想,为什么同时会有 findFirst 和 findAny ? 答案是并行。找到第一个元素在并行上限制更多。如果你不关系返回的元素是哪个
请使用findAny,因为它在使用并行流时限制较少。
归约
本节你将看到如何把一个流中的元素组合起来,使用reduce 操作来表达更复杂的查询,比如"计算菜单中的总卡路里" 或 "菜单中卡路里最高的菜是哪一个"。此种查询需要将流中所有元素反复结合起来,得到一个值,比如一个Integer。这样的查询可以被归类为归约操作(将流归约成一个值)。用函数式编程语言来说,这称为折叠(fold)
元素求和
List<Integer> numbers = Arrays.asList(1, 2, 3);
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
reduce 接受两个参数:
- 一个初始值,这里是0
- 一个BianryOperator
来将两个元素结合起来产生一个新值,这里我们用的是 lambda (a,b) -> a + b
reduce操作:首先0作为Lambda(a) 的第一个参数,从流中获取作为第二个参数(b)。0 + 1 得到 1,它成了新的累积值。然后再用累积值和流中下一个元素2 调用Lambda,产生新的累积值3,依此类推,得到最终结果6
在JAVA8 中,Integer 类现在有了一个静态的sum方法来对两数求和,这恰好是我们想要的
List<Integer> numbers = Arrays.asList(1, 2, 3);
int sum = numbers.stream().reduce(0, Integer::sum);
无初始值
reduce 还有一个重载的变体,它不接受初始值,但是会返回一个Optinal 对象:
Optional<Integer> sum = numbers.stream().reduce(Integer::sum);
为什么会返回一个空Optional
付诸实践
领域:交易员和交易
Trader 类:
package com.demo3;
public class Trader {
private final String name;
private final String city;
public Trader(String name, String city) {
this.name = name;
this.city = city;
}
public String getName() {
return name;
}
public String getCity() {
return city;
}
@Override
public String toString() {
return "Trader{" +
"name='" + name + '\'' +
", city='" + city + '\'' +
'}';
}
}
交易类:
package com.demo3;
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 trader;
}
public int getYear() {
return year;
}
public int getValue() {
return value;
}
@Override
public String toString() {
return "Transaction{" +
"trader=" + trader +
", year=" + year +
", value=" + value +
'}';
}
}
习题:
package com.demo3;
import org.junit.Before;
import org.junit.Test;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class Test2 {
private List<Transaction> transactions;
@Before
public void init(){
Trader raoul = new Trader("Raoul", "Cambridge");
Trader mario = new Trader("Mario", "Milan");
Trader alan = new Trader("Alan", "Cambridge");
Trader brian = new Trader("Brian", "Cambridge");
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)
);
}
@Test
public void test(){
//找出2011年发生的所有交易,并按交易额排序
List<Transaction> collect = transactions.stream()
.filter(t -> t.getYear() == 2011)
.sorted(Comparator.comparing(Transaction::getValue))
.collect(Collectors.toList());
//交易员都在哪些不同的城市工作过?
List<String> citys = transactions.stream()
.map(t -> t.getTrader().getCity())
.distinct()
.collect(Collectors.toList());
//查找所有来自于剑桥的交易员,并按姓名排序
List<Trader> traders = transactions.stream()
.map(t -> t.getTrader())
.filter(t -> "Cambridge".equals(t.getCity()))
.sorted(Comparator.comparing(Trader::getName))
.collect(Collectors.toList());
//返回所有交易员的姓名字符串,按字母顺序排序
//方式一:效率不高
String reduce = transactions.stream()
.map(t -> t.getTrader().getName())
.distinct()
.sorted()
.reduce("", (a, b) -> a + b);
//实现方式二:推荐
String collect1 = transactions.stream()
.map(t -> t.getTrader().getName())
.distinct()
.sorted()
.collect(Collectors.joining());
//有没有交易员在米兰工作的
transactions.stream()
.anyMatch(t -> "Milan".equals(t.getTrader().getCity()));
//打印生活在剑桥的交易员的所有交易额
transactions.stream()
.filter(t -> "Cambridge".equals(t.getTrader().getCity()))
.map(t -> t.getValue())
.forEach(System.out::println);
//所有交易中,最高的交易额是多少
Optional<Integer> hightestValue = transactions.stream()
.map(Transaction::getValue)
.reduce(Integer::max);
//找到交易额最小的交易
//实现方式一:
Optional<Transaction> smallestTransaction = transactions.stream()
.reduce((t1, t2) -> t1.getValue() < t2.getValue() ? t1 : t2);
//实现方式二: 推荐
Optional<Transaction> min = transactions.stream()
.min(Comparator.comparing(Transaction::getValue));
}
}
数值流
在前面看到了可以使用reduce 方法计算流中元素的总和。例如:
int calories = menu.stream()
.map(Dish::getCalories)
.reduce(0,Integer::sum)
这段代码的问题是,它暗含的装箱成本。每个Ingeter 都必须拆箱成一个原始类型,再进行求和。要是可以直接下像下面这样调用sum方法,岂不是更好?
int calories = menu.stream()
.map(Dish::getCalories)
.sum()
原始类型流特化
JAVA8 引入了三个原始类型特化流接口来解决这个问题:IntStream、DoubleStream 和 LongStream,分别将流中的元素特化为int、double、long,从而避免了暗含的装箱成本。每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的sum,找到最大元素的max。此外还有在必要时再把它们转回对象流的方法。要记住的是,这些特化的原因并不在于流的复杂性,而是装箱造成的复杂性 -- 即类似 int 和 Integer 之间的效率差异
1.映射到数值流
将流转化为特定流的常用方法是mapToInt、mapToDouble、mapToLong
//映射到数值流
menu.stream()
.mapToInt(Dish::getCalories) //返回一个IntStream
.sum();
这里,mapToInt 会从每道菜中提取热量(用一个Integer表示),并返回一个IntStream(而不是一个Stream
如果流是空的,sum默认返回0. IntStream 还支持其他的方便方法。如max、min、average等
2.转回对象流
同样,一旦有了数值流,可能会想把它转回非特化流。例如:IntStream 上的操作只能产生原始整数:IntStream 的map 操作接受的Lambda 必须接受int 并返回int(一个IntUnaryOperator)。
但是你可能想要生成另一类值,比如Dish。为此需要访问Stream接口定义的那些更广义的操作。要把一个原始流转换成一般流(每个int都会装箱成一个Integer),可以使用boxed方法:
//转回对象流
IntStream intStream = menu.stream().mapToInt(Dish::getCalories).map()
Stream<Integer> boxed = intStream.boxed();
3.默认值OptionalInt
如何区分没有元素的流和最大元素真的是0的流哪?Optional 可以用Integer、String 等参考类型来参数化。对于三种原始流特化,也分别有一个Optional 原始类型特化版本:OptionalInt、OptionalDouble 和 OptionalLong。
例如,要找intStream 中最大元素,可以调用max方法,它会返回一个OptionalInt:
OptionalInt maxCalories = menu.stream()
.mapToInt(Dish::getCalories)
.max();
现在如果没有最大值的话,你就可以显示处理OptionalInt 去定义一个默认值了
int max = maxCalories.orElse(1)
4.数值范围
JAVA8 引入了两个可以用于IntStrame 和 DoubleStream 的静态方法,帮助生成范围:range和 rangeClosed,这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但 range是不包含结束值的,而rangeClosed 则包含结束值
案例:生成一个三元组,即勾股数
//生成勾股数
Stream<double[]> stream = IntStream.rangeClosed(1, 20).boxed()
.flatMap(a -> IntStream.rangeClosed(a, 20)
.mapToObj(b -> new double[]{a, b, Math.sqrt(a * a + b * b)})
.filter(t -> t[2] % 1 == 0));
stream.forEach(a -> {
System.out.println(Arrays.toString(a));
});
//输出:
[3.0, 4.0, 5.0]
[5.0, 12.0, 13.0]
[6.0, 8.0, 10.0]
[8.0, 15.0, 17.0]
[9.0, 12.0, 15.0]
[12.0, 16.0, 20.0]
[15.0, 20.0, 25.0]
由函数生成 无限流
Stream API 提供了两个静态方法来从函数生成流:Stream.iterate 和 Stream.generate。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。一般来说,应该使用limit(n) 来对这种流加以限制,以避免打印无穷多个值
1.迭代
Stream.iterate(0, n -> n + 2)
.limit(10)
.forEach(System.out::println);
iterate 方法接受一个初始值,即0,还有一个依次应用在每个产生的新值上的Lambda(UnaryOperator
案例:斐波纳契元组序列
@Test
public void test3(){
//生成斐波纳契数列 二元组的第一个元素就是斐波纳契数列:0 1 1 2 3
Stream.iterate(new int[]{0,1}, t -> new int[]{t[1], t[0]+ t[1]}) //(0,1),(1,1),(1,2),(2,3),(3,5)
.limit(10)
.forEach(t->{
System.out.print(t[0] + ","); //0,1,1,2,3,5,8,13,21,34,
});
peek 方法会生产另一个流,它的元素与原来流中得元素相同,但是在每次获取一个元素时,都会调用一个函数。这对于调试来说很方便:
@Test
public void test4() {
List<Double> doubles = Stream.iterate(1.0, p -> p * 2)
.peek(e -> System.out.println(e))
.limit(10).toList();
System.out.println(doubles);
}
输出:
1.0
2.0
4.0
8.0
16.0
32.0
64.0
128.0
256.0
512.0
[1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0, 512.0]
当访问一个元素时,就会打印出来一条消息。通过这种方式,可以验证iterate返回得无限流是被惰性处理得
2.生成
与iterate 类型,generate 方法也可以生成一个无限流,它接受一个Supplier
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
以上代码将生成一个流,其中有5个0到1之间的随机双精度数。例如:
0.12376822049025438
0.9012854052507832
0.13011714513659323
0.41297063949587853
0.47395467040347194
收集器
想象一下,你有一个由Transation 构成的List,并且向按照名义货币进行分组。在没有Lambda的Java 里,哪怕像这种简单的用例实现起来都很啰嗦,就像下面这样。
Map<Currency, List<Transaction>> transactionsByCurrencies = new HashMap();
for(Transaction transaction : transactions ){
Currency currency = transaction.getCurrency();
List<Transaction> transactionsForCurrency = transactionsByCurrencies .get(currency)
if(transactionsForCurrency == null){
transactionsForCurrency = new ArrayList<>();
transactionsByCurrencies.put(currency, transactionsForCurrency )
}
transactionsForCurrency.add(transaction);
}
用Stream collect 方法的一个更通用的Collector 参数,你就可以用一句话实现完全相同效果:
Map<Currency, List<Transaction>> transactionsByCurrencies = transactions.stream.collect(Collectors.groupBy(Transaction::getCurrency))
预定义收集器
也就是那些可以从Collectors类提供的工厂方法创建的收集器,它们主要提供了三大功能:
- 将流元素归约和汇总为一个值
- 元素分组
- 元素分区
举一个简单的例子,数一数菜单里面有多少菜:
//方式一:
Long collect = menu.stream().collect(Collectors.counting());
//方式二:
long count = menu.stream().count();
1.查找流中的最大值和最小值
//查找热量最大的菜
Optional<Dish> dishOptional = menu.stream()
.collect(Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)));
dishOptional.ifPresent(System.out::println);
//查找热量最小的菜
Optional<Dish> minDishOptional = menu.stream()
.collect(Collectors.minBy(Comparator.comparingInt(Dish::getCalories)));
minDishOptional.ifPresent(System.out::println);
2.汇总
Collections 类专门为汇总提供了一个工厂方法:Collections.sumingInt。它可以接受一个把对象映射为求和所需int的函数,并返回一个收集器;该收集器在传递给普通的collect方法后即执行我们需要的汇总操作。举个例子来说,你可以这样求出菜单列表的总热量:
Integer collect = menu.stream()
.collect(Collectors.summingInt(Dish::getCalories));
但汇总不仅仅是求和,还有Collectors.averagingInt,连同对应的averagingLong 和 averagingDouble 可以计算数值的平均数。
很多时候,你可能想要两个或者更多这样的结果,而且你希望只需一次操作就可以完成。在这种情况下,你可以使summarizingInt 工厂方法返回的收集器,通过一次summarizingInt操作就可以输出菜单中元素的个数,并得到菜肴热量总和、平均值、最大值和最小值
IntSummaryStatistics summary = menu.stream()
.collect(Collectors.summarizingInt(Dish::getCalories));
System.out.println(summary); //IntSummaryStatistics{count=6, sum=2900, min=120, average=483.333333, max=800}
这个收集器会把所有这些信息收集到一个叫做 IntSummaryStatistics 的类里。它提供了方便的取值getter 来访问结果.
3.连接字符串
//连接字符串
String collect1 = menu.stream().map(m -> m.getName())
.collect(Collectors.joining(", "));
System.out.println(collect1); //pork, beef, chicken, french fries, rice, reason fruit
4.广义的归约汇总
事实上,之前讨论的所有收集器,都是一个可以用 reducing 工厂方法定义的归约过程的特殊情况而已。Collectors.reducing 工厂方法是所有这些特殊情况的一般化。
int totalCalories = menu.stream.collect(reducing(0,Dish::getCalories,(i, j) -> i + j))
它需要三个参数:
- 第一个参数是归约操作的起始值,也是流中没有元素时的返回值,所以很显然对于数值而言0是一个合适的值
- 第二个参数是一个函数, 将菜肴转换成一个表示其所含热量的int
- 第三个参数是一个BinaryOperator,将两个项目累积成一个同类型的值。这里它就是对两个int求和
同样,可以使用下面这样单参数形式的reducing来找到热量最高的菜:
//找到热量最高的菜
Optional<Dish> collect2 = menu.stream().collect(Collectors.reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2))
可以把单参数reducing 工厂方法创建的收集器看作三参数方法的特殊情况,它把流中的第一个项目作为起点,把恒等函数(即一个函数仅仅是返回其输入参数)作为一个转换函数。这也意味着,要是把单参数reducing收集器传递给空流的collect方法,收集器就没有起点,因此它返回一个Optional
4.1收集框架的灵活性:以不同的方法执行同样的操作
//方式一:
Integer totalCalories = menu.stream().collect(Collectors.reducing(0, Dish::getCalories, Integer::sum));
//方式二:
Integer totalCalories2 = menu.stream().map(Dish::getCalories).reduce(Integer::sum).get();
//方式三:推荐,因为它最简明,也很可能最易读。同时它也是性能最好的一个,因为IntStream 可以让我们避免自动拆箱操作
int sum = menu.stream().mapToInt(Dish::getCalories).sum();
分组
一个常见的数据库操作是根据一个或多个属性对集合中的项目进行分组
假设你要把菜单中共的菜按照类型进行分类,有肉的放一组,有鱼的放一组,其他的都放另一组
//根据Dish 的 type 属性进行分组
Map<Dish.Type, List<Dish>> collect = menu.stream().collect(Collectors.groupingBy(Dish::getType));
System.out.println(collect); //{FISH=[prawns, salmon], MEAT=[pork, beef, chicken], OTHER=[french fries, rice, reason fruit, pizza]}
这里,给groupingBy 方法传递了一个Function,它提取了流中每一道Dish的Dish::Type。我们把这个Function 叫做分类函数,因为它用来把流中的元素分成不同的组。
但是分类函数不一定像方法引用那样可用,因为你想用以分类的条件可能比简单的属性访问器要负责。例如,你可能想把热量不到400卡路里的菜划分为"低热量"(diet),热量400到700卡路里的菜划为"普通"(normal),高于700卡路里的划为"高热量"(fat)。此时需要把这个逻辑写成Lambda 表达式:
Map<CaloricLevel, List<Dish>> collect = menu.stream().collect(Collectors.groupingBy(m -> {
if (m.getCalories() <= 400) return CaloricLevel.DIET;
else if (m.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
}));
System.out.println(collect);
//{NORMAL=[beef, french fries, salmon], DIET=[chicken, rice, reason fruit, pizza, prawns], FAT=[pork]}
1.多级分组
要实现多级分组,可以使用一个由双参数版本的Collectors.groupingBy 工厂方法创建的收集器,它除了普通的分类函数之外,还可以接受collector类型的第二个参数。那么渝澳进行二级分组的话,我们可以把一个内层groupingBy传递给外层 groupingBy, 并定义一个为流中项目分类的二级标准:
//多级分组
Map<Dish.Type, Map<CaloricLevel, List<Dish>>> collect = menu.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.groupingBy(
d -> {
if (d.getCalories() <= 400) return CaloricLevel.DIET;
else if (d.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
}
)));
System.out.println(collect);
//{MEAT={DIET=[chicken], FAT=[pork], NORMAL=[beef]}, FISH={DIET=[prawns], NORMAL=[salmon]}, OTHER={DIET=[rice, reason fruit, pizza], NORMAL=[french fries]}}
2.按子组收集数据
上面看到可以把第二个groupingBy 收集器传递给外层收集器来实现多级分组。但进一步说,传递给第一个groupingBy 的第二个收集器可以是任何类型,而不一定是另一个groupingBy。
例如,要数一数菜单中每类菜有多少个,可以传递 counting 收集器作为 groupingBy 收集器的第二个参数:
Map<Dish.Type, Long> collect = menu.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.counting()));
System.out.println(collect); //{FISH=2, MEAT=3, OTHER=4}
需要注意的是,普通的单参数groupingBy(f)(f是分类函数) 实际上是groupingBy(f,toList()) 的简便写法
再举一个例子:查找菜单中每个类型热量最高的菜,按类型分组
Map<Dish.Type, Optional<Dish>> collect = menu.stream().
collect(Collectors.groupingBy(Dish::getType, Collectors.maxBy(Comparator.comparingInt(Dish::getCalories))));
System.out.println(collect);
//{MEAT=Optional[pork], OTHER=Optional[french fries], FISH=Optional[salmon]}
3.把收集器的结果转换为另一种类型
因为分组操作的Map结果中的每个值上包装的Optional 没什么用,所以你可能想要把它们去掉。要做到这一点,或者更一般的说,把收集器返回的结果转换为另一种类型,可以使用Collectors.collectingAnaThen 工厂发方法返回的收集器,如下所示:
//把收集器的结果转换为另外一种类型
Map<Dish.Type, Dish> collect = menu.stream().
collect(groupingBy(Dish::getType,collectingAndThen(maxBy(Comparator.comparingInt(Dish::getCalories)), Optional::get)));
System.out.println(collect); //{MEAT=pork, FISH=salmon, OTHER=french fries}
4. 与groupingBy 联合使用的其他收集器的例子
有时候 和 gruopingBy 联合使用的另一个收集器是 mapping 方法生成的。这个方法接收两个参数:一个函数对流中的元素做变换,另一个则将变换的结果对象收集起来。其目的是在累加之前对每个输入元素应用一个映射函数,这样就可以让接受特定类型元素的收集器使用不同类型的对象
Map<Dish.Type, Set<CaloricLevel>> collect = menu.stream()
.collect(groupingBy(Dish::getType, mapping(dish -> {
if (dish.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
}, toSet())));
System.out.println(collect); //{MEAT=[DIET, FAT, NORMAL], OTHER=[DIET, NORMAL], FISH=[DIET, NORMAL]}
本文来自博客园,作者:chuangzhou,转载请注明原文链接:https://www.cnblogs.com/czzz/p/17975818
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
2023-01-19 Pytest - 测试用例管理及运行管理
2022-01-19 Python - 多线程
2022-01-19 Python 网络编程简单实现