专业的编程技术博客社区

网站首页 > 博客文章 正文

java stream流系列-collector应用 (四)

baijin 2024-12-26 12:26:44 博客文章 5 ℃ 0 评论

Collector是一种特殊类型的终端操作。Collector触发流的计算。它将来自连接到源的 Stream 的数据收集到指定的 Collection 对象中。

让我们看下面的字符串流。

Stream<String> nameStream = Stream.of("NAME1", "NAME2", "NAME3",
                                      "NAME4", "NAME5", "NAME6",
                                      "NAME7", "NAME8", "NAME9",
                                      "", "", "               ");

我们希望将所有非空字符串添加到 List 集合中。我们可以通过先修剪字符串,然后过滤修剪后的空字符串(如果有)来实现此目的,如下所示。

List<String> names = new ArrayList<>();
nameStream
        .map(s -> s.trim())
        .filter(s -> !s.isEmpty())
        .forEach(s -> names.add(s));

或者我们可以使用下面的方法参考来更具声明性。

nameStream
        .map(String::trim)
        .filter(Predicate.not(String::isEmpty))
        .forEach(names::add);

我们已经在本系列文章中多次看到过使用 forEach 和 println 的此类示例。在这里,我们不是打印,而是将名称添加到 List 对象中。但这种方法有一个重大缺点。

请记住,无论我们在 lambda 或方法引用中使用什么数据,都应该始终是有效的最终数据。如果您观察到,我们正在将元素添加到外部列表中。这意味着我们在 forEach 中使用的 lambda 正在改变 lambda 外部的集合的状态。如果我们以并发方式使用它,这会产生不一致的结果,因为 ArrayList 不是并发集合。这意味着我们不能将此逻辑与并行流一起使用。

这就是Collector介入并帮助我们的地方。

专业提示:永远不要使用外部集合从流的终端操作中添加元素。

Collectors”和“Collect”类

Stream中的collect()方法是一个重载方法。还引入了一个名为 Collectors 的帮助器类,它包含一堆静态工厂方法,这些方法返回预定义的 Collector 对象以将元素收集到相应的集合中。

1. Collectors.toList()

我们可以使用 Collectors.toList() 和collect()方法来代替常规的ArrayList,如下所示。

List<String> names = nameStream
        .map(String::trim)
        .filter(Predicate.not(String::isEmpty))
        .collect(Collectors.toList());

静态工厂方法 Collectors.toList() 返回一个 Collector 对象,该对象将元素收集到 List 对象中。现在,collect() 方法是一个终端操作,它只需获取此 Collector 对象并神奇地返回一个不可变的 List 对象。

2. Collectors.toSet()

我们甚至可以将名称收集到一个集合中。我们只需要使用 Collectors.toSet() 如下。

Set<String> nameSet = nameStream
        .map(String::trim)
        .filter(Predicate.not(String::isEmpty))
        .collect(Collectors.toSet());

toList() 默认情况下使用 ArrayList,而 toSet() 底层使用 HashSet。但是如果想给出我们自己的 List 或 Set 集合怎么办?这就是 Collectors.toCollection() 派上用场的地方

3. Collectors.toMap()

我们还可以将 Stream 的元素收集到 Map 中。假设我们想要将上述 name Stream 的字符串收集到一个map中,该map字符串为键,字符串的长度为值。那么我们可以按照下面的方法来做。

Map<String, Integer> map = nameStream.
        map(String::trim)
        .filter(Predicate.not(String::isEmpty))
        .collect(Collectors.toMap(s -> s, String::length));

toMap() 方法有两个参数。两者都是 Function 类型。第一个参数用于键,第二个参数用于map中的值。看一下第一个参数 s -> s。这个 lambda 没有做任何事情,输入一个字符串并返回相同的字符串。

出于这种目的,Function 类中存在一个名为 Identity() 的方法,它可以执行相同的操作。建议使用此方法而不是使用 lambda。这样写

Map<String, Integer> map = nameStream.
        map(String::trim)
        .filter(Predicate.not(String::isEmpty))
        .collect(Collectors.toMap(Function.identity(),
                                  String::length));

4. Collectors.toCollection()

我们甚至可以使用 Collectors.toCollection() 将所有元素收集到我们自己的集合中,如下所示。

收集到 LinkedList 而不是 ArrayList

List<String> names = nameStream
        .map(s -> s.trim())
        .filter(s -> !s.isEmpty())
        .collect(Collectors.toCollection(LinkedList::new));

收集到 TreeSet 而不是 HashSet

Set<String> nameSet = nameStream
        .map(String::trim)
        .filter(Predicate.not(String::isEmpty))
        .collect(Collectors.toCollection(TreeSet::new));

5. Collectors.joining()

当我们想要使用如下分隔符连接字符串格式的所有元素时, join() 会派上用场。

String s = Stream.of("one", "two", "three").
        map(String::trim)
        .filter(Predicate.not(String::isEmpty))
        .collect(Collectors.joining(", "));
System.out.println(s);

流中的所有字符串都通过逗号作为分隔符连接。

6. Collectors.partitioningBy()

paritioningBy() 方法有助于根据特定条件拆分流的元素。

我们想要的是一个 Employee 员工对象列表,以这样的方式进行分区:第一个分区包含工资大于 1000 的员工。第二个分区包含工资小于或等于 1000 的员工。可以使用 Collectors.partitioningBy() 来实现,如下所示

List<Employee> employees = List.of(
        new Employee(1, "EMP1", Employee.Sex.FEMALE, 900.0),
        new Employee(2, "EMP2", Employee.Sex.MALE, 1290.0),
        new Employee(3, "EMP3", Employee.Sex.FEMALE, 1200.0),
        new Employee(4, "EMP4", Employee.Sex.MALE, 900.0),
        new Employee(5, "EMP5", Employee.Sex.FEMALE, 1290.0)
);

以下是我们如何使用 Collectors.paritioningBy()。

Map<Boolean, List<Employee>> greater1000Partition =
   employees.stream().collect(
       Collectors.partitioningBy(emp -> emp.getSalary() > 1000));

PartitioningBy() 方法采用Predicate(不懂可以看我函数式编程系列文章)来对元素进行分区。

现在,返回类型:Map<Boolean, List<Employee>>。更有趣的是。返回值表示映射中的键是布尔类型。该map:

键为 true 且值为与给定Predicate匹配的 Employee 对象列表的一项。

键为 false 且值 Employee 对象列表与给定Predicate不匹配的另一个条目。

使用下面的代码打印map的值

greater1000Partition.forEach((k, v) -> {
            String value = v.stream()
                    .map(Employee::getName)
                    .collect(Collectors.joining(", "));
            System.out.println(k + " === " + value);
        }
);

上面的代码打印了布尔值键和与Predicate匹配的每个员工的姓名,然后使用 Collectors.joining() 通过逗号连接所有姓名。这是输出。

false === EMP1, EMP4
true === EMP2, EMP3, EMP5

7. Collectors.groupingBy()

假设我们想要对 Employee 对象进行分组或分类,以便相同的薪资员工属于一组,那么我们可以使用 groupingBy() 方法。

groupingBy() 收集器与partitioningBy() 类似。唯一的不同是partitioningBy() 接受一个Predidate,而groupingBy() 接受一个Function。该函数充当分类器,根据该分类器进行分组。

Map<Double, List<Employee>> empGroupBySalary =
        employees.stream()                 
           .collect(Collectors.groupingBy(Employee::getSalary));

现在可以看到返回类型的变化。键的类型为 Double,代表工资。谁拿同样的薪水就属于这个组。

empGroupBySalary.forEach((k, v) -> {
            String value = v.stream()
                    .map(Employee::getName)
                    .collect(Collectors.joining(", "));
            System.out.println(k + " === " + value);
        }
);

输出

1200.0 === EMP3
1290.0 === EMP2, EMP5
900.0 === EMP1, EMP4

8. Collectors.mapping()

在上面的示例中,我们得到的是 Map<Double, List<Employee>>。但是,如果我们想要一个 Map<Double, List<String>> 只表示员工姓名而不是员工对象本身,该怎么办?

正是出于这些目的,Collector.groupingBy() 有另一种变种。

Collector.groupingBy() 是一个重载方法,并且有另一个带有两个参数的变体:一个函数和一个收集器。第二个收集器称为下游收集器。

这里我们使用 Collectors.mapping() 作为下游收集器,它将 List<Employee> 对象转换为 List<String>。 Collectors.mapping() 采用两个参数,如下面的代码所示。

Map<Double, List<String>> empNamesGroupBySalary =
        employees.stream().collect(
                Collectors.groupingBy(
                        Employee::getSalary,
                        Collectors.mapping(Employee::getName, 
                          Collectors.toList() 
                        )
                )
        );

第一个参数是表示转换的函数。

第二个参数是收集器,转换后的对象被收集到其中。这里我们使用了列表收集器。

在这里我们可以做一件更有趣的事情。如果我们想要一个包含所有由逗号分隔的名称的字符串,而不是包含所有名称,就像我们在上面的输出中打印的那样,该怎么办?

这很简单。我们使用 join() 收集器代替 toList() 收集器。这样做

Map<Double, String> empNamesGroupBySalary1 =
        employees.stream().collect(
                Collectors.groupingBy(
                        Employee::getSalary,
                        Collectors.mapping(
                                Employee::getName,
                                Collectors.joining(", ")
                        )
                )
        );

如果我们打印上面的map,

empNamesGroupBySalary1.forEach((k, v) ->
        System.out.println(k + " === " + v));

输出

1200.0 === EMP3
1290.0 === EMP2,EMP5
900.0 === EMP1,EMP4

9. Collectors.counting()

如果你正确理解了上面的例子,这非常简单。我们想要的是统计属于每个组的员工数量。我们只需要使用 Collectors.counting() 而不是 Collector.mapping()。

Collectors.couting() 方法不带任何参数。它只是计算元素。

Map<Double, Long> empGroupBySalaryCount =
        employees.stream().collect(
                Collectors.groupingBy(
                        Employee::getSalary,
                        Collectors.counting()));

输出

1200.0 === 1
1290.0 === 2
900.0 === 2

现在知道Collector优雅?想象一下,如果没有Collector,为所有这些场景编写代码是多么痛苦。 groupingBy 收集器是面试中经常被问到的最重要的事情。

10. Collectors.collectingAndThen()

我们所见过的所有上述双参数收集器,例如 groupingBy() 和 mapping() 首先将函数作为第一个参数,将收集器作为第二个参数。他们首先应用函数表示的转换,然后将其收集到指定的收集器中。

但如果我们想要相反的情况呢?我们希望首先收集元素,然后应用转换。 CollectionAndThen() 方法完全具有相同的目的。

假设在上面的 count() 示例中我们得到了一个 Map<Double,Long>。但我们希望计数是一个整数值。默认情况下,counting() 方法返回一个 long 值。在这种情况下,我们可以在应用收集器后使用collectingAndThen()方法将Long转换为Integer。我们是这样做的。

Map<Double, Integer> empGroupBySalaryCount =
        employees.stream().collect(
                Collectors.groupingBy(
                        Employee::getSalary,
                        Collectors.collectingAndThen(
                                Collectors.counting(),
                                Long::intValue
                        )
                )
        );

这就是 Stream Collector的全部内容。还有其他Collector。但这 10 个是最常用的,涵盖了行业中使用的 90%。

#java#

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表