《Java8实战》第6章 用流收集数据

news2024/11/25 1:41:01

collect 是一个归约操作,就像 reduce 一样可以接受各种做法作为参数,将流中的元素累积成一个汇总结果。具体的做法是通过定义新的Collector 接口来定义的,因此区分 Collection、Collector 和 collect 是很重要的。
用 collect 和收集器能够做什么。

  • 对一个交易列表按货币分组,获得该货币的所有交易额总和(返回一个 Map<Currency, Integer>)。
  • 将交易列表分成两组:贵的和不贵的(返回一个 Map<Boolean, List>)。
  • 创建多级分组,比如按城市对交易分组,然后进一步按照贵或不贵分组(返回一个Map<String, Map<Boolean, List>>)。

用指令式风格对交易按照货币分组:

Map<Currency, List<Transaction>> transactionsByCurrencies = new HashMap<>(); 
for (Transaction transaction : transactions) { 
   Currency currency = transaction.getCurrency(); 
   List<Transaction> transactionsForCurrency = transactionsByCurrencies.get(currency); 
   // 如果分组 Map 中没有这种货币的条目,就创建一个
	if (transactionsForCurrency == null) { 
     transactionsForCurrency = new ArrayList<>(); 
     transactionsByCurrencies.put(currency, transactionsForCurrency); 
   } 
	// 将当前遍历的 Transaction 加入同一货币的 Transaction 的 List
   transactionsForCurrency.add(transaction); 
} 

Stream 中 collect 方法的一个更通用的 Collector 参数
Map<Currency, List> transactionsByCurrencies = transactions.stream().collect(groupingBy(Transaction::getCurrency));
一行代码搞定

6.1 收集器简介

函数式编程相对于指令式编程的一个主要优势:你只需指出希望的结果——“做什么”,而不用操心执行的步骤——“如何做”。

6.1.1 收集器用作高级归约

优秀的函数式 API 设计的另一个好处:更易复合和重用。
image.png

6.1.2 预定义收集器

三大功能:

  • 将流元素归约和汇总为一个值;
  • 元素分组;
  • 元素分区。

6.2 归约和汇总

利用 counting 工厂方法返回的收集器,数一数菜单里有多少种菜:
long howManyDishes = menu.stream().collect(Collectors.counting());
还可以写得更为直接:
long howManyDishes = menu.stream().count();

6.2.1 查找流中的最大值和最小值

想要找出菜单中热量最高的菜。可以用这两个收集器,Collectors.maxBy 和Collectors.minBy,来计算流中的最大值或最小值。
还可以创建一个 Comparator 来根据所含热量对菜肴进行比较。
Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);

6.2.2 汇总

Collectors 类专门为汇总提供了一个工厂方法:Collectors.summingInt。它可接受一个把对象映射为求和所需 int 的函数,并返回一个收集器;
求出菜单列表的总热量:
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
image.png

6.2.3 连接字符串

joining 工厂方法返回的收集器会把对流中每一个对象应用 toString 方法得到的所有字符串连接成一个字符串。
String shortMenu = menu.stream().map(Dish::getName).collect(joining());
joining 在内部使用了 StringBuilder 来把生成的字符串逐个追加起来。
如果 Dish 类有一个 toString 方法来返回菜肴的名称,那你无需用提取每一道菜名称的函数来对原流做映射就能够得到相同的结果:
String shortMenu = menu.stream().collect(joining());
joining 工厂方法有一个重载版本可以接受元素之间的分界符
String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));

6.2.4 广义的归约汇总

  1. 收集框架的灵活性:以不同的方法执行同样的操作

int totalCalories = menu.stream().collect(reducing(0, Dish::getCalories, Integer::sum));
image.png

测验 6.1:用 reducing 连接字符串
以下哪一种 reducing 收集器的用法能够合法地替代 joining 收集器(如 6.2.3节用法)?
String shortMenu = menu.stream().map(Dish::getName).collect(joining());
(1) String shortMenu = menu.stream().map(Dish::getName) .collect( reducing ( (s1, s2) -> s1 + s2 ) ).get();
(2) String shortMenu = menu.stream().collect( reducing( (d1, d2) -> d1.getName() + d2.getName() ) ).get();
(3) String shortMenu = menu.stream().collect( reducing( “”,Dish::getName, (s1, s2) -> s1 + s2 ) );
答案:语句(1)和语句(3)是有效的,语句(2)无法编译。
(1) 这会将每道菜转换为菜名,就像原先使用 joining 收集器的语句一样。然后用一个String 作为累加器归约得到的字符串流,并将菜名逐个连接在它后面。
(2) 这无法编译,因为 reducing 接受的参数是一个 BinaryOperator,也就是一个BiFunction<T,T,T>。这就意味着它需要的函数必须能接受两个参数,然后返回一个相同类型的值,但这里用的 Lambda 表达式接受的参数是两个菜,返回的却是一个字符串。
(3) 这会把一个空字符串作为累加器来进行归约,在遍历菜肴流时,它会把每道菜转换成菜名,并追加到累加器上。请注意,前面讲过,reducing 要返回一个 Optional 并不需要三个参数,因为如果是空流的话,它的返回值更有意义——也就是作为累加器初始值的空字符串。
请注意,虽然语句(1)和语句(3)都能够合法地替代 joining 收集器,但是它们在这里是用来展示为何可以(至少在概念上)把 reducing 看作本章中讨论的所有其他收集器的概括。然而就实际应用而言,不管是从可读性还是性能方面考虑,我们始终建议使用 joining 收集器。

6.3 分组

一个常见的数据库操作是根据一个或多个属性对集合中的项目进行分组。
根据类型分组
Map<Dish.Type, List<Dish>> dishesByType = menu.stream().collect(groupingBy(Dish::getType));
你给 groupingBy 方法传递了一个 Function(以方法引用的形式),它提取了流中每一道 Dish 的 Dish.Type。我们把这个 Function 叫作分类函数,因为它用来把流中的元素分成不同的组。
image.png

public enum CaloricLevel { DIET, NORMAL, FAT } 
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream().collect( 
 groupingBy(dish -> { 
   if (dish.getCalories() <= 400) return CaloricLevel.DIET; 
   else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL; 
   else return CaloricLevel.FAT; 
 } )); 

6.3.1 操作分组的元素

执行完分组操作后,你往往还需要对每个分组中的元素执行操作。
假设你希望只按照菜肴的热量进行过滤操作,譬如找出那些热量大于 500 卡路里的菜肴。你可能会说,这种情况只要在分组之前执行过滤谓词就好了

Map<Dish.Type, List<Dish>> caloricDishesByType = 
 menu.stream().filter(dish -> dish.getCalories() > 500) 
 .collect(groupingBy(Dish::getType)); 

应该有三种类型的:OTHER、 MEAT、FISH
但是因为fish类型的没有符合条件,就没有显示,因为他是先过滤,再分组

{OTHER=[french fries, pizza], MEAT=[pork, beef]} 

为了解决这个问题,Collectors 类重载了工厂方法 groupingBy,除了常见的分类函数,它的第二变量也接受一个 Collector 类型的参数。

Map<Dish.Type, List<Dish>> caloricDishesByType = 
 menu.stream().collect(groupingBy(Dish::getType, 
 	filtering(dish -> dish.getCalories() > 500, toList()))); 

{OTHER=[french fries, pizza], MEAT=[pork, beef], FISH=[]}
这种方式就保存了fish的列表,即使他是空的

操作分组元素的另一种常见做法是使用一个映射函数对它们进行转换。
Collectors 类通过 mapping 方法提供了另一个 Collector 函数,它接受一个映射函数和另一个 Collector 函数作为参数。作为参数的 Collector 会收集对每个元素执行该映射函数的运行结果。

Map<Dish.Type, List<String>> dishNamesByType = 
 menu.stream().collect(groupingBy(Dish::getType, mapping(Dish::getName, toList()))); 

6.3.2 多级分组

要实现多级分组,可以使用一个由双参数版本的 Collectors.groupingBy 工厂方法创建的收集器,它除了普通的分类函数之外,还可以接受 collector 类型的第二个参数。那么要进行二级分组的话,可以把一个内层 groupingBy 传递给外层 groupingBy,并定义一个为流中项目分类的二级标准

Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel = 
menu.stream().collect( 
 groupingBy(Dish::getType, // 一级分类函数
 groupingBy(dish -> { // 二级分类函数
   if (dish.getCalories() <= 400) return CaloricLevel.DIET; 
   else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL; 
   else return CaloricLevel.FAT; 
 } ) 
 ) 
);

image.png

6.3.3 按子组收集数据

数一下每个分类的数量,可以传递 counting 收集器作为groupingBy 收集器的第二个参数
Map<Dish.Type, Long> typesCount = menu.stream().collect(groupingBy(Dish::getType, counting()));
求每个分组中热量最高的

Map<Dish.Type, Optional<Dish>> mostCaloricByType = 
 menu.stream().collect(groupingBy(Dish::getType, 
 maxBy(comparingInt(Dish::getCalories)))); 
  1. 把收集器的结果转换为另一种类型

查找每个子组中热量最高的 Dish

Map<Dish.Type, Dish> mostCaloricByType = 
 menu.stream() 
 .collect(groupingBy(Dish::getType, // 分类函数
 collectingAndThen( 
 maxBy(comparingInt(Dish::getCalories)), // 包装后的收集器
 Optional::get))); // 转换函数

主要是转换函数 Optional::get把Optional的值给提取出来
image.png

  1. 与 groupingBy 联合使用的其他收集器的例子

对热量求和

Map<Dish.Type, Integer> totalCaloriesByType = 
 menu.stream().collect(groupingBy(Dish::getType, 
	summingInt(Dish::getCalories))); 

然而常常和 groupingBy 联合使用的另一个收集器是 mapping 方法生成的。这个方法接受两个参数:一个函数对流中的元素做变换,另一个则将变换的结果对象收集起来。

6.4 分区

分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函数。

Map<Boolean, List<Dish>> partitionedMenu = menu.stream().collect(
	partitioningBy(Dish::isVegetarian));

得出:
{false=[pork, beef, chicken, prawns, salmon], 
 true=[french fries, rice, season fruit, pizza]}

返回true的value值
List<Dish> vegetarianDishes = partitionedMenu.get(true); 

也可以使用谓词区分
List<Dish> vegetarianDishes = menu.stream().filter(Dish::isVegetarian).collect(toList());

6.4.1 分区的优势

分区的好处在于保留了分区函数返回 true 或 false 的两套流元素列表。
你可以使用两个筛选操作来访问 partitionedMenu 这个 Map 中false 键的值:一个利用谓词,一个利用该谓词的非。

Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType = 
menu.stream().collect( 
 partitioningBy(Dish::isVegetarian, // 分区函数
 		groupingBy(Dish::getType))); // 第二个收集器

找到素食和非素食中热量最高的菜:

Map<Boolean, Dish> mostCaloricPartitionedByVegetarian = 
menu.stream().collect( 
 partitioningBy(Dish::isVegetarian, 
   collectingAndThen(maxBy(comparingInt(Dish::getCalories)), 
   		Optional::get)));

测验 6.2:使用 partitioningBy
我们已经看到,和 groupingBy 收集器类似,partitioningBy 收集器也可以结合其他收集器使用。尤其是它可以与第二个 partitioningBy 收集器一起使用来实现多级分区。以下多级分区的结果会是什么呢?
(1) menu.stream().collect(partitioningBy(Dish::isVegetarian, partitioningBy(d -> d.getCalories() > 500)));
(2) menu.stream().collect(partitioningBy(Dish::isVegetarian, partitioningBy(Dish:: getType)));
(3) menu.stream().collect(partitioningBy(Dish::isVegetarian, counting()));
答案:
(1) 这是一个有效的多级分区,产生以下二级 Map:
{false={false=[chicken, prawns, salmon], true=[pork, beef]},
true={false=[rice, season fruit], true=[french fries, pizza]}}
(2) 这无法编译,因为 partitioningBy 需要一个谓词,也就是返回一个布尔值的函数。方法引用 Dish::getType 不能用作谓词。
(3) 它会计算每个分区中项目的数目,得到以下 Map:{false=5, true=4}

6.4.2 将数字按质数和非质数分区

public boolean isPrime(int candidate) { 
 return IntStream.range(2, candidate) // 产生一个自然数范围,从2开始,直至但不包括待测数
 	.noneMatch(i -> candidate % i == 0); // 如果待测数字不能被流中任何数字整除则返回 true
} 

image.png

6.5 收集器接口

Collector 接口包含了一系列方法,为实现具体的归约操作(即收集器)提供了范本。

Collector 接口
public interface Collector<T, A, R> { 
 Supplier<A> supplier(); 
 BiConsumer<A, T> accumulator(); 
 Function<A, R> finisher(); 
 BinaryOperator<A> combiner(); 
 Set<Characteristics> characteristics(); 
}

T 是流中要收集的项目的泛型。 
A 是累加器的类型,累加器是在收集过程中用于累积部分结果的对象。 
R 是收集操作得到的对象(通常但并不一定是集合)的类型。 

6.5.1 理解 Collector 接口声明的方法

上面的前四个方法都会返回一个会被 collect 方法调用的函数,第五个方法 characteristics 则提供了一系列特征,也就是一个提示列表,告诉 collect 方法在执行归约操作的时候可以应用哪些优化(比如并行化)。

  1. 建立新的结果容器:supplier 方法

supplier 方法必须返回一个结果为空的 Supplier,也就是一个无参数函数,在调用时它会创建一个空的累加器实例,供数据收集过程使用。
比如我们的 ToListCollector,在对空流执行操作的时候,这个空的累加器也代表了收集过程的结果。在我们的 ToListCollector 中,supplier 返回一个空的 List,如下所示:

public Supplier<List<T>> supplier() { 
 return () -> new ArrayList<T>(); 
}
也可以这样子写
public Supplier<List<T>> supplier() { 
 return ArrayList::new; 
}
  1. 将元素添加到结果容器:accumulator 方法

accumulator 方法会返回执行归约操作的函数。
这个函数执行时会有两个参数:保存归约结果的累加器(已收集了流中的前 n–1 个项目),还有第 n 个元素本身。

public BiConsumer<List<T>, T> accumulator() { 
 return (list, item) -> list.add(item); 
}public BiConsumer<List<T>, T> accumulator() { 
 return List::add; 
} 
  1. 对结果容器应用最终转换:finisher 方法

finisher 方法必须返回在累积过程的最后要调用的一个函数,以便将累加器对象转换为整个集合操作的最终结果。
累加器对象恰好符合预期的最终结果,因此无须进行转换。所以 finisher 方法只需返回 identity 函数:

public Function<List<T>, List<T>> finisher() { 
 return Function.identity(); 
} 

image.png

  1. 合并两个结果容器:combiner 方法
public BinaryOperator<List<T>> combiner() { 
 return (list1, list2) -> { 
   list1.addAll(list2); 
   return list1; } 
} 

image.png

  1. characteristics 方法

characteristics 会返回一个不可变的 Characteristics 集合,它定义了收集器的行为——尤其是关于流是否可以并行归约,以及可以使用哪些优化的提示。
Characteristics 是一个包含三个项目的枚举。

  • UNORDERED——归约结果不受流中项目的遍历和累积顺序的影响。
  • CONCURRENT——accumulator 函数可以从多个线程同时调用,且该收集器可以并行归约流。如果收集器没有标为 UNORDERED,那它仅在用于无序数据源时才可以并行归约。
  • IDENTITY_FINISH——这表明完成器方法返回的函数是一个恒等函数,可以跳过。这种情况下,累加器对象将会直接用作归约过程的最终结果。这也意味着,将累加器 A 不加检查地转换为结果 R 是安全的。

6.5.2 全部融合到一起

import java.util.*; 
import java.util.function.*; 
import java.util.stream.Collector; 
import static java.util.stream.Collector.Characteristics.*; 
public class ToListCollector<T> implements Collector<T, List<T>, List<T>> { 
 @Override 
 public Supplier<List<T>> supplier() { 
   return ArrayList::new; // 创建集合操作的起始点
 } 
 @Override 
 public BiConsumer<List<T>, T> accumulator() { 
   return List::add; // 累积遍历过的项目,原位修改累加器
 } 
 @Override 
 public Function<List<T>, List<T>> finisher() { 
   return Function.identity(); // 恒等函数
 } 
 @Override 
 public BinaryOperator<List<T>> combiner() { 
 return (list1, list2) -> { 
   list1.addAll(list2); // 修改第一个累加器,将其与第二个累加器的内容合并
   return list1; // 返回修改后的第一个累加器
 }; 
 } 
 @Override 
 public Set<Characteristics> characteristics() { 
   return Collections.unmodifiableSet(EnumSet.of( 
   IDENTITY_FINISH, CONCURRENT)); // 为收集器添加 IDENTITY_FINISH和 CONCURRENT 标志
 } 
} 

这个实现与 Collectors.toList 方法并不完全相同,但区别仅仅是一些小的优化。构造之间的其他差异在于,oList 是一个工厂,而 ToListCollector 必须用 new 来实例化。

进行自定义收集而不去实现 Collector
对于 IDENTITY_FINISH 的收集操作,还有一种方法可以得到同样的结果而无须从头实现新的 Collector 接口。
Stream 有一个重载的 collect 方法可以接受另外三个函数——supplier、accumulator 和 combiner.

List<Dish> dishes = menuStream.collect( 
 ArrayList::new, // 供应源
 List::add, // 累加器
 List::addAll); // 组合器

6.6 开发你自己的收集器以获得更好的性能

将前 n 个自然数按质数和非质数分区

public Map<Boolean, List<Integer>> partitionPrimes(int n) { 
 return IntStream.rangeClosed(2, n).boxed() 
 .collect(partitioningBy(candidate -> isPrime(candidate)); 
}

通过限制除数不超过被测试数的平方根,我们对最初的 isPrime 方法做了一些改进:

public boolean isPrime(int candidate) { 
 int candidateRoot = (int) Math.sqrt((double) candidate); 
 return IntStream.rangeClosed(2, candidateRoot) 
 	.noneMatch(i -> candidate % i == 0); 
} 

如果想更好的性能,就需要自己开发

6.6.1 仅用质数做除数

一个可能的优化是仅看被测试数是不是能够被质数整除,所以就得开发一个收集器
假设你有这个列表,那就可以把它传给 isPrime 方法,将方法重写如下:

public static boolean isPrime(List<Integer> primes, int candidate) { 
 return primes.stream().noneMatch(i -> candidate % i == 0); 
} 

在下一个质数大于被测数平方根时立即停止测试

public static boolean isPrime(List<Integer> primes, int candidate){ 
 int candidateRoot = (int) Math.sqrt((double) candidate); 
 return primes.stream() 
       .takeWhile(i -> i <= candidateRoot) 
       .noneMatch(i -> candidate % i == 0); 
} 
  1. 第 1 步:定义 Collector 类的签名
public interface Collector<T, A, R> 

public class PrimeNumbersCollector 
 implements Collector<Integer, // 流中元素的类型
 Map<Boolean, List<Integer>>, // 累加器类型 
 Map<Boolean, List<Integer>>> // collect 操作的结果类型
  1. 第 2 步:实现归约过程

接下来,你需要实现 Collector 接口中声明的五个方法。supplier 方法会返回一个在调用时创建累加器的函数:

public Supplier<Map<Boolean, List<Integer>>> supplier() { 
 return () -> new HashMap<Boolean, List<Integer>>() {{ 
   put(true, new ArrayList<Integer>()); 
   put(false, new ArrayList<Integer>()); 
 }}; 
}

这里不但创建了用作累加器的 Map,还为 true 和 false 两个键初始化了对应的空列表

public BiConsumer<Map<Boolean, List<Integer>>, Integer> accumulator() { 
 return (Map<Boolean, List<Integer>> acc, Integer candidate) -> { 
 acc.get( isPrime(acc.get(true), candidate) ) // 根据 isPrime 的结果,获取质数或非质数列表
 .add(candidate); // 将被测数添加到相应的列表中
 }; 
} 

调用了 isPrime 方法,将待测试是否为质数的数以及迄今找到的质数列表(也就是累积 Map 中 true 键对应的值)传递给它。

  1. 第 3 步:让收集器并行工作(如果可能)
public BinaryOperator<Map<Boolean, List<Integer>>> combiner() { 
 return (Map<Boolean, List<Integer>> map1, 
   Map<Boolean, List<Integer>> map2) -> { 
     map1.get(true).addAll(map2.get(true)); 
     map1.get(false).addAll(map2.get(false)); 
     return map1; 
   }; 
} 
  1. 第 4 步:finisher 方法和收集器的 characteristics 方法

accumulator 正好就是收集器的结果,用不着进一步转换,那么 finisher 方法就返回 identity 函数:

public Function<Map<Boolean, List<Integer>>, 
 Map<Boolean, List<Integer>>> finisher() { 
   return Function.identity(); 
} 

就characteristics 方法而言,我们已经说过,它既不是CONCURRENT 也不是UNORDERED,却是 IDENTITY_FINISH 的:

public Set<Characteristics> characteristics() { 
 return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH)); 
}

实现

PrimeNumbersCollector
public class PrimeNumbersCollector 
 implements Collector<Integer, 
 Map<Boolean, List<Integer>>, 
 Map<Boolean, List<Integer>>> { 
 @Override 
 public Supplier<Map<Boolean, List<Integer>>> supplier() { 
 return () -> new HashMap<Boolean, List<Integer>>() {{ // 从一个有两个空List的Map开始收集过程
	put(true, new ArrayList<Integer>()); 
 put(false, new ArrayList<Integer>()); 
 }}; 
 } 
 @Override 
 public BiConsumer<Map<Boolean, List<Integer>>, Integer> accumulator() { 
 return (Map<Boolean, List<Integer>> acc, Integer candidate) -> { 
 acc.get( isPrime( acc.get(true), // 将已经找到的质数列表传递给isPrime 方法
 candidate) ) 
 .add(candidate); // 根据 isPrime 方法的返回值,从 Map 中取质数或非质数列表,把当前的被测数加进去
 }; 
 } 
 @Override 
 public BinaryOperator<Map<Boolean, List<Integer>>> combiner() { 
 return (Map<Boolean, List<Integer>> map1, 
 Map<Boolean, List<Integer>> map2) -> { // 将第二个Map 合并到第一个
 map1.get(true).addAll(map2.get(true)); 
 map1.get(false).addAll(map2.get(false)); 
 return map1; 
 }; 
 } 
 @Override 
 public Function<Map<Boolean, List<Integer>>, 
 Map<Boolean, List<Integer>>> finisher() { 
 return Function.identity(); // 收集过程最后无须转换,因此用identity函数收尾
 } 
 @Override 
 public Set<Characteristics> characteristics() { 
// 这个收集器是 IDENTITY_FINISH,但既不是 UNORDERED 也不是 CONCURRENT,因为质数是按顺序发现的
   return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH));
 } 
}

public Map<Boolean, List<Integer>> 
 partitionPrimesWithCustomCollector(int n) { 
 return IntStream.rangeClosed(2, n).boxed() 
 .collect(new PrimeNumbersCollector()); 
} 

6.6.2 比较收集器的性能

partitioningBy 工厂方法创建的收集器和你刚刚开发的自定义收集器在功能上是一样的,但是有没有实现用自定义收集器超越 partitioningBy 收集器性能的目标呢?现在让我们写个测试框架来跑一下吧:

public class CollectorHarness { 
	 public static void main(String[] args) {
  	long fastest = Long.MAX_VALUE; 
     for (int i = 0; i < 10; i++) { 
     long start = System.nanoTime(); 
// 将前一百万个自然数按质数和非质数分区
     partitionPrimes(1_000_000); 
// 取运行时间的毫秒值
     long duration = (System.nanoTime() - start) / 1_000_000; 
// 检查这个执行是否是最快的一个
     if (duration < fastest) fastest = duration; 
     } 
     System.out.println( 
     "Fastest execution done in " + fastest + " msecs"); 
     } 
    } 

6.7 小结

collect 是一个终端操作,它接受的参数是将流中元素累积到汇总结果的各种方式(称为收集器)。

  • 预定义收集器包括将流元素归约和汇总到一个值,例如计算最小值、最大值或平均值。这些收集器总结在表 6-1 中。
  • 预定义收集器可以用 groupingBy 对流中元素进行分组,或用 partitioningBy 进行分区。
  • 收集器可以高效地复合起来,进行多级分组、分区和归约。
  • 你可以实现 Collector 接口中定义的方法来开发自己的收集器。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/425177.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

PADS-微处理器、USB转UART芯片、MINI-USB PCB封装设计

目录 1 微处理器PCB封装设计 2 USB转UART芯片PCB封装设计 3 MINI-USB PCB封装设计 4 添加验证 详细步骤不再文字说明&#xff0c;直接截取关键步骤截图 1 微处理器PCB封装设计 查看芯片手册 器件高度 器件高度1.6 管脚中心点间距0.5&#xff0c;管脚焊盘长1.2、宽0.3&…

[ 云计算 | Azure ] Chapter 04 | 核心体系结构之数据中心、区域与区域对、可用区和地理区域

本章节主要内容进行讲解&#xff1a;Azure云计算的核心体系结构组件中的&#xff1a;Azure物理基础设施&#xff08;Physical infrastructure&#xff09;&#xff0c;区域&#xff08;Regions&#xff09;和区域对&#xff08;Region Pairs&#xff09;、地理数据中心&#xf…

一文解决ethtool 原理介绍和解决网卡丢包排查思路

前言 之前记录过处理因为 LVS 网卡流量负载过高导致软中断发生丢包的问题&#xff0c;RPS 和 RFS 网卡多队列性能调优实践[1]&#xff0c;对一般人来说压力不大的情况下其实碰见的概率并不高。这次想分享的话题是比较常见服务器网卡丢包现象排查思路&#xff0c;如果你是想了解…

UE 简单插件制作

本文主要是提供几个写UE插件的实例&#xff0c;借此来了解在UE里使用C创建自定义插件的做法&#xff1a; 写一个使场景变暗的简单插件写一个自定义窗口&#xff0c;展示项目里所有的动画资产 写一个使场景变暗的简单插件 参考&#xff1a;Unreal Engine 5 - Writing Plugins …

统信UOS专业版系统安装教程 - 手动分区安装UOS系统

全文导读&#xff1a;本文主要介绍了安装UOS系统过程中使用手动分区安装方法&#xff0c;一般没有特殊要求建议使用全盘安装UOS系统。 准备环境 制作好统信UOS专业版启动U盘 一台CPU频率≥2GHz、内存≥4GB、硬盘≥64GB的电脑 安装步骤 一、制作UOS 系统启动盘 制作UOS 系…

自动驾驶TPM技术杂谈 ———— CCRT验收标准(评分标准)

文章目录介绍评价方法指标体系算分方法一级指标二级指标三级指标四级指标五级指标行车辅助能力得分说明跟车能力得分说明前车静止识别与响应得分说明前车低速识别与响应得分说明前车减速识别与响应得分说明前车切入识别与响应得分说明前车切出识别与响应得分说明跟随前车起停得…

数据结构小知识------时间与空间复杂度

本章思维导图&#xff1a; 一&#xff0c;时间复杂度 1.1时间复杂度的概念 &#x1f310;&#xff1a;什么是时间复杂度呢&#xff1f;时间复杂度其实就是一个程序运行时它的指令运行的次数。 在这里&#xff0c;程序默认每条指令的运行时间是一样的。所以时间复杂度就可以理解…

【云原生进阶之容器】第六章容器网络6.4.2--Flannel的安装与部署

1 flannel的安装与部署 见链接一篇文章带你了解Flannel - Flannel - 操作系统 - 深度开源 1.1 部署环境规划 1.2 安装部署 #tar -xf flannel-v0.13.0.tar.gz #mv /apps/svr/flannel-v0.13.0 #ln –svfn /apps/svr/flannel-v0.13.0 /apps/svr/flannel 1.2.1 调整Flannel配置…

设计模式(超详细)

设计模式 原则 什么是SOLID原则&#xff1f; S单一职责SRP Single-Responsibility Principle 一个类,最好只做一件事,只有一个引起它的变化。单一职责原则可以看做是低耦合,高内聚在面向对象原则的引申,将职责定义为引起变化的原因,以提高内聚性减少引起变化的原因。 比如…

项目资源管理流程:五步专业指南

项目资源管理是描述大多数项目经理的一项关键职能的方式——收集完成工作所需的团队成员、设备和其他材料&#xff08;也称为资源&#xff09;。 以下是项目资源管理的步骤清单&#xff1a; 步骤1&#xff1a;资源规划 为了确定完成项目的资源需求&#xff0c;你首先需要了…

SpringCloud-Gateway实现网关

网关作为流量的入口&#xff0c;常用的功能包括路由转发、权限校验、限流等Spring Cloud 是Spring官方推出的第二代网关框架&#xff0c;由WebFluxNettyReactor实现的响应式的API网关&#xff0c;它不能在传统的servlet容器工作&#xff0c;也不能构建war包。基于Filter的方式提…

个人开发者如何选择阿里云服务器配置CPU内存带宽?

阿里云服务器个人用怎么选择&#xff1f;云服务器吧建议选择ECS共享型s6&#xff0c;不限制CPU性能&#xff0c;选择1核2G或2核4G都可以&#xff0c;云服务器s6处理器采用2.5 GHz主频的Intel Xeon Platinum 8269CY&#xff08;Cascade Lake&#xff09;&#xff0c;睿频3.2 GHz…

【论文阅读--WSOL】Spatial-Aware Token for Weakly Supervised Object Localization

文章目录方法实验Limitation论文&#xff1a;https://arxiv.org/abs/2303.10438代码&#xff1a;https://github.com/wpy1999/SAT/blob/main/Model/SAT.py方法 这篇文章的方法应该属于FAM这一类。 额外添加的一个spatial token&#xff0c;从第10-12层开始&#xff0c;利用其得…

Vue3技术1之Vue3简介、创建Vue3工程、分析工程结构、安装开发者工具与初识setup

Vue3技术1Vue3简介发展提升创建Vue3工程使用vue-cli创建使用vite创建分析工程结构&#xff08;由vue-cli创建的&#xff09;main.jsvue.config.jsApp.vue安装开发者工具初识setupsetup的两种返回值返回一个对象App.vue返回一个函数App.vueVue2与Vue3混合使用App.vue总结Vue3简介…

【致敬未来的攻城狮计划】— 连续打卡第一天:提前对CPK_RA2E1是瑞萨RA系列开发板的初体验,了解一下(文字上的初理解)

系列文章目录 系列文章目录 前言 一、瑞萨MCU&#xff08;CPK_RA2E1是瑞萨RA系列开发板&#xff09;是什么&#xff1f; 首先引入是什么&#xff1f; 他的优势在哪&#xff1f; 瑞萨CPK_RA2E1 对标stm32 相似之处和不同之处&#xff1f; 瑞萨CPK_RA2E1如何开发&#xff…

集成定时器事件

一&#xff0c;定时器事件 1、概述 libevent提供了高性能定时器的功能&#xff0c;方便执行延迟回调逻辑。在添加事件监听的时候&#xff0c;可以不指定fd和监听的事件&#xff0c;指定超时的时间&#xff0c;实现定时器功能。定时器的实现主要依赖下面的数据结构&#xff0c;…

java 多线程基础 万字详解(通俗易懂)

目录 一、前言 二、定义 1.进程 : 2.线程 : 3.单线程与多线程 : 4.并发与并行 : 三、线程的创建 1.创建线程的两种基本方式 : 1 继承Thread类&#xff0c;并重写run方法 1.5 多线程的执行机制(重要) 2 实现Runnable接口&#xff0c;并重写run方法 2. 两种创建线程方式…

【C++】继承---下(子类默认成员函数、虚继承对象模型的详解等)

前言&#xff1a; 上篇文章我们一起初步了解了继承的概念和使用&#xff0c;本章我们回家新一步深入探讨继承更深层次的内容。 前文回顾——>继承---上 目录 &#xff08;一&#xff09;派生类的默认成员函数 &#xff08;1&#xff09;6个默认成员函数 &#xff08;…

Pytorch全连接神经网络实现手写数字识别

问题Mnist手写数字识别数据集作为一个常见数据集&#xff0c;包含10个类别&#xff0c;在此次深度学习的过程中&#xff0c;我们通过pytorch提供的库函数&#xff0c;运用全连接神经网络实现手写数字的识别方法设置参数input_size 784hidden_size 500output_size 10num_epoc…

JavaScript对象类型之function

目录 一、Function 定义函数 调用函数 默认参数 匿名函数 箭头函数 二、函数是对象 三、函数作用域 四、闭包 五、let、var与作用域 一、Function 定义函数 function 函数名(参数) {// 函数体return 结果; } 例如&#xff1a; function add(a, b) {return a b; …