目录
Stream API
简介:
Stream API ( java.util.stream) 把真正的函数式编程风格引入到 Java 中。Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。 使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。简言之,Stream API 提供了一种高效且易于使用的处理数据的方式。
Stream 和 Collection 集合的区别:Collection 是一种静态的内存数据结构,而 Stream 是有关计算的。前者是主要面向内存,存储在内存中,后者主要是面向 CPU,通过 CPU 实现计算。
注意:
①Stream 自己不会存储元素。
②Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream。
③Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。
Stream 的操作三个步骤:
-
创建 Stream:一个数据源(如:集合、数组),获取一个流
-
中间操作:一个中间操作链,对数据源的数据进行处理
-
终止操作:一旦执行终止操作,就执行中间操作链,并产生结果。之后,不会再被使用
创建 Stream
测试用的实体类
public class Employee {
private int id;
private String name;
private int age;
private double salary;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
public Employee() {
System.out.println("Employee().....");
}
public Employee(int id) {
this.id = id;
System.out.println("Employee(int id).....");
}
public Employee(int id, String name) {
this.id = id;
this.name = name;
}
public Employee(int id, String name, int age, double salary) {
this.id = id;
this.name = name;
this.age = age;
this.salary = salary;
}
@Override
public String toString() {
return "Employee{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + ", salary=" + salary + '}';
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Employee employee = (Employee) o;
if (id != employee.id)
return false;
if (age != employee.age)
return false;
if (Double.compare(employee.salary, salary) != 0)
return false;
return name != null ? name.equals(employee.name) : employee.name == null;
}
@Override
public int hashCode() {
int result;
long temp;
result = id;
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + age;
temp = Double.doubleToLongBits(salary);
result = 31 * result + (int) (temp ^ (temp >>> 32));
return result;
}
}
public class EmployeeData {
public static List<Employee> getEmployees(){
List<Employee> list = new ArrayList<>();
list.add(new Employee(1001, "name1", 34, 6000.38));
list.add(new Employee(1002, "name2", 12, 9876.12));
list.add(new Employee(1003, "name3", 33, 3000.82));
list.add(new Employee(1004, "name4", 26, 7657.37));
list.add(new Employee(1005, "name5", 65, 5555.32));
list.add(new Employee(1006, "name6", 42, 9500.43));
list.add(new Employee(1007, "name7", 26, 4333.32));
list.add(new Employee(1008, "name8", 35, 2500.32));
return list;
}
}
通过集合
Java8 中的 Collection 接口被扩展,提供了获取流的方法:default Stream stream()
@Test
public void test1(){
List<Employee> employees = EmployeeData.getEmployees();
// default Stream<E> stream() : 返回一个顺序流
Stream<Employee> stream = employees.stream();
// default Stream<E> parallelStream() : 返回一个并行流
Stream<Employee> parallelStream = employees.parallelStream();
}
通过数组
Java8 中的 Arrays 的静态方法 stream() 可以获取数组流:static Stream stream(T[] array)
@Test
public void test2(){
int[] arr = new int[]{1,2,3,4,5,6};
// 调用Arrays类的static <T> Stream<T> stream(T[] array): 返回一个流
IntStream stream = Arrays.stream(arr);
}
通过 Stream 的 of()
@Test
public void test3(){
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);
}
创建无限流
@Test
public void test4(){
// 迭代
// public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
// 遍历前10个偶数
Stream.iterate(0, t -> t + 2).limit(10).forEach(System.out::println);
// 生成
// public static<T> Stream<T> generate(Supplier<T> s)
Stream.generate(Math::random).limit(10).forEach(System.out::println);
}
中间操作
筛选与切片
测试代码:
public class StreamAPITest1 {
@Test
public void test1() {
// 1. filter(Predicate p)——接收 Lambda , 从流中排除某些元素。
List<Employee> list = EmployeeData.getEmployees();
Stream<Employee> stream = list.stream();
// 查询员工表中薪资大于7000的员工信息
stream.filter(e -> e.getSalary() > 7000).forEach(System.out::println); // forEach 终止 消费者
System.out.println();
// 2. limit(n)——截断流,使其元素不超过给定数量。
// 这里要加上 list.stream(),重新生成以下流,因为上面那个流已经关闭了
list.stream().limit(3).forEach(System.out::println);
System.out.println();
// 3. skip(n) —— 跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补
list.stream().skip(3).forEach(System.out::println);
System.out.println();
// 4. distinct()——筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素
// 增加一些重复数据
list.add(new Employee(1010,"name1",40,8000));
list.add(new Employee(1010,"name1",41,8000));
list.stream().distinct().forEach(System.out::println);
}
}
映射
测试代码:
@Test
public void test2() {
// map(Function f)——接收一个函数作为参数,将元素转换成其他形式或提取信息,该函数会被应用到每个元素上,并将其映射成一个新的元素。
List<String> list = Arrays.asList("a", "b", "c");
list.stream().map(str -> str.toUpperCase()).forEach(System.out::print);
System.out.println();
// 获取员工姓名长度大于3的员工的姓名
List<Employee> employees = EmployeeData.getEmployees();
// Employee::getName 相当于 e -> e.getName() 上面方法引用的第三种情况
Stream<String> namesStream = employees.stream().map(Employee::getName);
namesStream.filter(name -> name.length() > 5).forEach(System.out::println);
System.out.println();
// flatMap(Function f)——接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。
Stream<Character> characterStream = list.stream().flatMap(StreamAPITest1::fromStringToStream);
characterStream.forEach(System.out::println);
}
// 将字符串中的多个字符构成的集合转换为对应的 Stream 的实例
public static Stream<Character> fromStringToStream(String str){
ArrayList<Character> list = new ArrayList<>();
for(Character c : str.toCharArray()){
list.add(c);
}
return list.stream();
}
排序
测试代码:
@Test
public void test3() {
// sorted()——自然排序
List<Integer> list = Arrays.asList(12, 43, 65, 34, 87, 0, -98, 7);
list.stream().sorted().forEach(System.out::println);
System.out.println();
// sorted(Comparator com)——定制排序
List<Employee> employees = EmployeeData.getEmployees();
employees.stream().sorted((e1, e2) -> {
return Integer.compare(e1.getAge(), e2.getAge());
}).forEach(System.out::println);
}
终止操作
终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如:List、Integer,甚至是 void 。流进行了终止操作后,不能再次使用。
匹配与查找
测试代码:
@Test
public void test1() {
List<Employee> employees = EmployeeData.getEmployees();
// allMatch(Predicate p)——检查是否匹配所有元素。
// 练习:是否所有的员工的年龄都大于18
boolean allMatch = employees.stream().allMatch(e -> e.getAge() > 18);
System.out.println(allMatch);
// anyMatch(Predicate p)——检查是否至少匹配一个元素。
// 练习:是否存在员工的工资大于 10000
boolean anyMatch = employees.stream().anyMatch(e -> e.getSalary() > 10000);
System.out.println(anyMatch);
// noneMatch(Predicate p)——检查是否没有匹配的元素。
// 练习:是否存在员工姓“雷”
boolean noneMatch = employees.stream().noneMatch(e -> e.getName().startsWith("雷"));
System.out.println(noneMatch);
// findFirst——返回第一个元素
Optional<Employee> employee = employees.stream().findFirst();
System.out.println(employee);
// findAny——返回当前流中的任意元素
Optional<Employee> employee1 = employees.parallelStream().findAny();
System.out.println(employee1);
}
@Test
public void test2(){
List<Employee> employees = EmployeeData.getEmployees();
// count——返回流中元素的总个数
long count = employees.stream().filter(e -> e.getSalary() > 5000).count();
System.out.println(count);
// max(Comparator c)——返回流中最大值
// 练习:返回最高的工资:
Stream<Double> salaryStream = employees.stream().map(e -> e.getSalary());
Optional<Double> maxSalary = salaryStream.max(Double::compare);
System.out.println(maxSalary);
// min(Comparator c)——返回流中最小值
// 练习:返回最低工资的员工
Optional<Employee> employee = employees.stream().min((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()));
System.out.println(employee);
System.out.println();
// forEach(Consumer c)——内部迭代
employees.stream().forEach(System.out::println);
//使用集合的遍历操作
employees.forEach(System.out::println);
}
归约
@Test
public void test3(){
// reduce(T identity, BinaryOperator)——可以将流中元素反复结合起来,得到一个值。返回 T
// 练习1:计算1-10的自然数的和
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
Integer sum = list.stream().reduce(0, Integer::sum);
System.out.println(sum);
// reduce(BinaryOperator) ——可以将流中元素反复结合起来,得到一个值。返回 Optional<T>
// 练习2:计算公司所有员工工资的总和
List<Employee> employees = EmployeeData.getEmployees();
Stream<Double> salaryStream = employees.stream().map(Employee::getSalary);
// Optional<Double> sumMoney = salaryStream.reduce(Double::sum);
Optional<Double> sumMoney = salaryStream.reduce((d1,d2) -> d1 + d2);
System.out.println(sumMoney.get());
}
收集
通过定义新的Collector
接口来定义的,因此区分Collection
、Collector
和collect
是很重要的。
下面是一些查询的例子,看看你用collect
和收集器能够做什么。
- 对一个交易列表按货币分组,获得该货币的所有交易额总和(返回一个Map<Currency, Integer>)。
- 将交易列表分成两组:贵的和不贵的(返回一个Map<Boolean, List>)。
- 创建多级分组,比如按城市对交易分组,然后进一步按照贵或不贵分组(返回一个Map<Boolean, List>)
测试代码:
@Test
public void test4() {
// collect(Collector c)——将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法
// 查找工资大于6000的员工,结果返回为一个List或Set
List<Employee> employees = EmployeeData.getEmployees();
List<Employee> employeeList = employees.stream().filter(e -> e.getSalary() > 6000).collect(Collectors.toList());
employeeList.forEach(System.out::println);
System.out.println();
Set<Employee> employeeSet = employees.stream().filter(e -> e.getSalary() > 6000).collect(Collectors.toSet());
employeeSet.forEach(System.out::println);
}
多级分组
要实现多级分组,我们可以使用一个由双参数版本的Collectors.groupingBy
工厂方法创建的收集器,它除了普通的分类函数之外,还可以接受collector
类型的第二个参数。那么要进行二级分组的话,我们可以把一个内层groupingBy
传递给外层groupingBy
,并定义一个为流中项目分类的二级标准
public enum CaloricLevel { DIET, NORMAL, FAT }
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;
} )
)
);
这个二级分组的结果就是像下面这样的两级Map:
{MEAT={DIET=[chicken], NORMAL=[beef], FAT=[pork]},
FISH={DIET=[prawns], NORMAL=[salmon]},
OTHER={DIET=[rice, seasonal fruit], NORMAL=[french fries, pizza]}}
我们看到可以把第二个groupingBy
收集器传递给外层收集器来实现多级分组。但进一步说,传递给第一个groupingBy
的第二个收集器可以是任何类型,而不一定是另一个groupingBy
。例如,要数一数菜单中每类菜有多少个,可以传递counting
收集器作为groupingBy
收集器的第二个参数:
Map<Dish.Type, Long> typesCount = menu.stream().collect(groupingBy(Dish::getType, counting()));
{MEAT=3, FISH=2, OTHER=4}
再举一个例子,你可以把前面用于查找菜单中热量最高的菜肴的收集器改一改,按照菜的类型分类:
Map<Dish.Type, Optional<Dish>> mostCaloricByType =
menu.stream()
.collect(groupingBy(Dish::getType,
maxBy(comparingInt(Dish::getCalories))));
这个分组的结果显然是一个map,以Dish的类型作为键,以包装了该类型中热量最高的Dish的Optional<Dish>
作为值:
{FISH=Optional[salmon], OTHER=Optional[pizza], MEAT=Optional[pork]}
因为分组操作的Map
结果中的每个值上包装的Optional
没什么用,所以你可能想要把它们去掉。要做到这一点,或者更一般地来说,把收集器返回的结果转换为另一种类型,你可以使用Collectors.collectingAndThen
工厂方法返回的收集器,如下所示。
查找每个子组中热量最高的Dish
Map<Dish.Type, Dish> mostCaloricByType =
menu.stream()
.collect(groupingBy(Dish::getType, ←─分类函数
collectingAndThen(
maxBy(comparingInt(Dish::getCalories)), ←─包装后的收集器
Optional::get))); ←─转换函数
这个工厂方法接受两个参数——要转换的收集器以及转换函数,并返回另一个收集器。这个收集器相当于旧收集器的一个包装,collect操作的最后一步就是将返回值用转换函数做一个映射。在这里,被包起来的收集器就是用maxBy建立的那个,而转换函数Optional::get则把返回的Optional中的值提取出来。前面已经说过,这个操作放在这里是安全的,因为reducing收集器永远都不会返回Optional.empty()。其结果是下面的Map:
{FISH=salmon, OTHER=pizza, MEAT=pork}
把好几个收集器嵌套起来很常见,它们之间到底发生了什么可能不那么明显。下图可以直观地展示它们是怎么工作的。从最外层开始逐层向里,注意以下几点。
- 收集器用虚线表示,因此groupingBy是最外层,根据菜肴的类型把菜单流分组,得到三个子流。
- groupingBy收集器包裹着collectingAndThen收集器,因此分组操作得到的每个子流都用这第二个收集器做进一步归约。
- collectingAndThen收集器又包裹着第三个收集器maxBy。随后由归约收集器进行子流的归约操作,然后包含它的collectingAndThen收集器会对其结果应用Optional:get转换函数。
- 对三个子流分别执行这一过程并转换而得到的三个值,也就是各个类型中热量最高的Dish,将成为groupingBy收集器返回的Map中与各个分类键(Dish的类型)相关联的值。
与groupingBy联合使用的其他收集器的例子
一般来说,通过groupingBy
工厂方法的第二个参数传递的收集器将会对分到同一组中的所有流元素执行进一步归约操作。例如,你还重用求出所有菜肴热量总和的收集器,不过这次是对每一组Dish求和:
Map<Dish.Type, Integer> totalCaloriesByType =
menu.stream().collect(groupingBy(Dish::getType,
summingInt(Dish::getCalories)));
然而常常和groupingBy
联合使用的另一个收集器是mapping
方法生成的。这个方法接受两个参数
:一个函数对流中的元素做变换,另一个则将变换的结果对象收集起来。其目的是在累加之前对每个输入元素应用一个映射函数,这样就可以让接受特定类型元素的收集器适应不同类型的对象。我们来看一个使用这个收集器的实际例子。比方说你想要知道,对于每种类型的Dish,菜单中都有哪些CaloricLevel。我们可以把groupingBy和mapping收集器结合起来,如下所示:
Map<Dish.Type, Set<CaloricLevel>> caloricLevelsByType =
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() )));
{OTHER=[DIET, NORMAL], MEAT=[DIET, NORMAL, FAT], FISH=[DIET, NORMAL]}
请注意在上一个示例中,对于返回的Set是什么类型并没有任何保证。但通过使用toCollection
,你就可以有更多的控制。例如,你可以给它传递一个构造函数引用来要求HashSet:
Map<Dish.Type, Set<CaloricLevel>> caloricLevelsByType =
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; },
toCollection(HashSet::new) )));
利用Collectors.groupingBy与Function结合进行多字段分组
嵌套的调用groupingBy产生的结果是多层结构,如果分组的字段过多将不便操作,所以还有另一种方式实现,就是利用Collectors.groupingBy与Function结合进行多字段分组。
查看Collectors.groupingByAPI会发现,其中一种用法是第一个参数为Function,定义一个函数Function,该函数将元素对象映射到一个键的集合里。代码示例如下:
public class TestGroupingBy {
public static void main(String[] args) {
List<Person> personList = Arrays.asList(
new Person().setId(1).setAge(18).setType("student").setName("user - 1").setGender("male"),
new Person().setId(2).setAge(20).setType("student").setName("user - 2").setGender("male"),
new Person().setId(3).setAge(18).setType("student").setName("user - 3").setGender("male"),
new Person().setId(4).setAge(18).setType("student").setName("user - 4").setGender("male"),
new Person().setId(5).setAge(35).setType("teacher").setName("user - 5").setGender("male"),
new Person().setId(6).setAge(35).setType("teacher").setName("user - 6").setGender("male"),
new Person().setId(7).setAge(20).setType("student").setName("user - 7").setGender("male"),
new Person().setId(8).setAge(20).setType("student").setName("user - 8").setGender("female"),
new Person().setId(9).setAge(20).setType("student").setName("user - 9").setGender("female"),
new Person().setId(10).setAge(20).setType("student").setName("user - 10").setGender("female")
);
// 定义一个函数Function,该函数将元素对象映射到一个键的集合里
Function<Person, List<Object>> compositeKey = person ->
Arrays.asList(person.getGender(), person.getAge(), person.getType());
// 分组
Map<List<Object>, List<Person>> groupingMap =
personList.stream().collect(Collectors.groupingBy(compositeKey, Collectors.toList()));
}
}
通过在Debug模式下运行代码,可以看到groupingMap的数据结构如下:
groupingMap数据仅仅只有一层,但是其键值Key却是一个List,里面包含了分组字段的值,如上图中的male、35、teacher是集合中属性gender/age/type分别是male、35、teacher的元素集合。数据按Json格式贴出如下:
{
"[male, 35, teacher]": [
{
"id": 5,
"age": 35,
"type": "teacher",
"name": "user - 5",
"gender": "male"
},
{
"id": 6,
"age": 35,
"type": "teacher",
"name": "user - 6",
"gender": "male"
}
],
"[female, 20, student]": [
{
"id": 8,
"age": 20,
"type": "student",
"name": "user - 8",
"gender": "female"
},
{
"id": 9,
"age": 20,
"type": "student",
"name": "user - 9",
"gender": "female"
},
{
"id": 10,
"age": 20,
"type": "student",
"name": "user - 10",
"gender": "female"
}
],
"[male, 20, student]": [
{
"id": 2,
"age": 20,
"type": "student",
"name": "user - 2",
"gender": "male"
},
{
"id": 7,
"age": 20,
"type": "student",
"name": "user - 7",
"gender": "male"
}
],
"[male, 18, student]": [
{
"id": 1,
"age": 18,
"type": "student",
"name": "user - 1",
"gender": "male"
},
{
"id": 3,
"age": 18,
"type": "student",
"name": "user - 3",
"gender": "male"
},
{
"id": 4,
"age": 18,
"type": "student",
"name": "user - 4",
"gender": "male"
}
]
}
可以看到,如果分组字段只有一个,我们可以用比较简单的利用Stream.collect()
和Collectors.groupingBy
进行处理,但对于多个字段的分组操作,建议还是用Collectors.groupingBy
和Function
进行处理。
又看到一种方式,还没研究
Map<String, List<LyglTwoSrDzd>> dzdMxGroupMap = dzdList.stream().collect(Collectors.groupingBy(LyglTwoSrDzd::getGroupStr));
public String getGroupStr(){
return this.fysj + "," + this.qsd + "," + this.fz + "," + this.dz + "," + this.mdd + "," + this.pm + "," + this.xx + "," + this.djTlzyf + "," + this.djTlyf + "," + this.djYhs + "," + this.djTlqtfy + "," + this.djTlzyfTitle + "," + this.djTlyfTitle + "," + this.djYhsTitle + "," + this.djTlqtfyTitle;
}
分区
分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函数
。分区函数返回一个布尔值,这意味着得到的分组Map的键类型是Boolean,于是它最多可以分为两组——true是一组,false是一组。例如,如果你是素食者或是请了一位素食的朋友来共进晚餐,可能会想要把菜单按照素食和非素食分开:
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或false的两套流元素列表。要得到非素食Dish的List,你可以使用两个筛选操作来访问partitionedMenu这个Map中false键的值:一个利用谓词,一个利用该谓词的非。而且就像你在分组中看到的,partitioningBy工厂方法有一个重载版本,可以像下面这样传递第二个收集器:
Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType =
menu.stream().collect(
partitioningBy(Dish::isVegetarian, ←─分区函数
groupingBy(Dish::getType))); ←─第二个收集器
这将产生一个二级Map:
{false={FISH=[prawns, salmon], MEAT=[pork, beef, chicken]},
true={OTHER=[french fries, rice, season fruit, pizza]}}
这里,对于分区产生的素食和非素食子流,分别按类型对菜肴分组,得到了一个二级Map,和前面的的二级分组得到的结果类似。再举一个例子,你可以重用前面的代码来找到素食和非素食中热量最高的菜:
Map<Boolean, Dish> mostCaloricPartitionedByVegetarian =
menu.stream().collect(
partitioningBy(Dish::isVegetarian,
collectingAndThen(
maxBy(comparingInt(Dish::getCalories)),
Optional::get)));
你可以把分区看作分组一种特殊情况。groupingBy和partitioningBy收集器之间的相似之处并不止于此;总结:Collectors类的静态工厂方法,所有这些收集器都是对Collector接口的实现
收集器接口
Collector
接口包含了一系列方法,为实现具体的归约操作(即收集器)提供了范本。我们已经看过了Collector
接口中实现的许多收集器,例如toList
或groupingBy
。这也意味着,你可以为Collector
接口提供自己的实现,从而自由地创建自定义归约操作。
首先让我们在下面的列表中看看Collector
接口的定义,它列出了接口的签名以及声明的五个方法。
public interface Collector<T, A, R> {
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
BinaryOperator<A> combiner();
Function<A, R> finisher();
Set<Characteristics> characteristics();
}
本列表适用以下定义。
T
是流中要收集的项目的泛型。A
是累加器的类型,累加器是在收集过程中用于累积部分结果的对象。R
是收集操作得到的对象(通常但并不一定是集合)的类型。
例如,你可以实现一个ToListCollector<T>
类,将Stream<T>
中的所有元素收集到一个List<T>
里,它的签名如下:
public class ToListCollector<T> implements Collector<T, List<T>, List<T>>
现在我们可以一个个来分析Collector
接口声明的五个方法了。通过分析,你会注意到,前四个方法都会返回一个会被collect
方法调用的函数,而第五个方法characteristics
则提供了一系列特征,也就是一个提示列表,告诉collect
方法在执行归约操作的时候可以应用哪些优化(比如并行化)。
参考书籍:《Java8实战》
参考博客:https://blog.csdn.net/m0_58016522/article/details/131082125