杰瑞科技汇

Collectors in Java,如何高效使用?

Of course! "Collectors" in Java almost always refers to the java.util.stream.Collectors class. It's an incredibly powerful and essential utility for working with streams, introduced in Java 8.

Let's break it down into a comprehensive guide.


What are Collectors?

At its core, a Collector is a utility that transforms elements from a stream into a different form, like a List, Set, Map, or even a custom object.

Think of a stream as a pipeline of data. A Collector is the tool you place at the end of that pipeline to "collect" the processed elements into a useful container.

The primary way to use a Collector is with the Stream.collect() method, which has a signature like this:

<R, A> R collect(Collector<? super T, A, R> collector);

This looks complex, but the key takeaway is: you pass a Collector to collect(), and it returns the final collection (R).


The Collectors Factory Class

java.util.stream.Collectors is a final utility class that provides dozens of static factory methods to create common, ready-to-use Collector instances. You almost never implement the Collector interface yourself; you just use the factory methods from this class.

Here are the most important categories of collectors, with examples.

Collecting into Simple Collections (Lists, Sets, Maps)

These are the most basic and frequently used collectors.

toList()

Collects the stream elements into a new List.

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
List<String> names = Stream.of("Alice", "Bob", "Charlie")
                          .collect(Collectors.toList());
// names will be ["Alice", "Bob", "Charlie"]

toSet()

Collects the stream elements into a new Set. This automatically removes any duplicate elements.

import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Set<Integer> uniqueNumbers = Stream.of(1, 2, 2, 3, 4, 4, 5)
                                  .collect(Collectors.toSet());
// uniqueNumbers will be [1, 2, 3, 4, 5] (order is not guaranteed)

toMap()

Collects stream elements into a Map. You must provide two functions:

  1. A mapper function to create the key for each element.
  2. A mapper function to create the value for each element.
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
class Person {
    String name;
    int age;
    // constructor, getters...
}
Stream<Person> peopleStream = Stream.of(
    new Person("Alice", 30),
    new Person("Bob", 25)
);
Map<String, Integer> nameToAgeMap = peopleStream
    .collect(Collectors.toMap(
        Person::getName, // Key mapper: person -> person.getName()
        Person::getAge   // Value mapper: person -> person.getAge()
    ));
// nameToAgeMap will be {"Alice": 30, "Bob": 25}

Handling Duplicates: What if two people have the same name? toMap() will throw an IllegalStateException. To handle this, you can provide a third argument: a merge function.

// If two people have the same name, keep the first one encountered.
Map<String, Integer> nameToAgeMapKeepFirst = peopleStream
    .collect(Collectors.toMap(
        Person::getName,
        Person::getAge,
        (existingVal, newVal) -> existingVal // Merge function
    ));
// If two people have the same name, add their ages.
Map<String, Integer> nameToAgeMapSumAges = peopleStream
    .collect(Collectors.toMap(
        Person::getName,
        Person::getAge,
        Integer::sum // Merge function
    ));

Aggregation Collectors (Sum, Average, Count, Max, Min)

These are powerful reduction operations that summarize a stream into a single value.

counting()

Counts the number of elements in the stream.

long count = Stream.of("a", "b", "c")
                   .collect(Collectors.counting());
// count is 3

summingInt(), summingLong(), summingDouble()

Calculates the sum of elements.

import java.util.stream.Stream;
int sum = Stream.of(1, 2, 3, 4, 5)
                .collect(Collectors.summingInt(Integer::intValue));
// sum is 15

averagingInt(), averagingLong(), averagingDouble()

Calculates the average of elements.

double average = Stream.of(1, 2, 3, 4, 5)
                      .collect(Collectors.averagingInt(Integer::intValue));
// average is 3.0

maxBy() and minBy()

Finds the maximum or minimum element in the stream, based on a Comparator.

import java.util.Comparator;
import java.util.Optional;
Optional<Integer> max = Stream.of(1, 2, 3, 4, 5)
                             .collect(Collectors.maxBy(Comparator.naturalOrder()));
// max is Optional[5]
Optional<Integer> min = Stream.of(1, 2, 3, 4, 5)
                             .collect(Collectors.minBy(Comparator.naturalOrder()));
// min is Optional[1]

Grouping and Partitioning

This is where Collectors truly shines, allowing for complex data summarization.

groupingBy()

This is the most powerful collector. It groups elements based on a classification function.

  1. Simple Grouping: Group a list of people by their city.
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
class Person {
    String name;
    String city;
    // constructor, getters...
}
List<Person> people = List.of(
    new Person("Alice", "New York"),
    new Person("Bob", "London"),
    new Person("Charlie", "New York")
);
// Group by city
Map<String, List<Person>> peopleByCity = people.stream()
    .collect(Collectors.groupingBy(Person::getCity));
/*
Result:
{
  "New York": [Alice, Charlie],
  "London": [Bob]
}
*/
  1. Grouping with a Downstream Collector: You can perform a secondary aggregation on each group. For example, count the number of people in each city.
// Group by city and count the people in each group
Map<String, Long> peopleCountByCity = people.stream()
    .collect(Collectors.groupingBy(
        Person::getCity,        // Classifier
        Collectors.counting()   // Downstream collector
    ));
/*
Result:
{
  "New York": 2,
  "London": 1
}
*/
  1. Multi-level Grouping: You can group by multiple criteria. For example, group by city, and then by age range.
// Group by city, then by age group
Map<String, Map<String, List<Person>>> complexGrouping = people.stream()
    .collect(Collectors.groupingBy(
        Person::getCity,
        Collectors.groupingBy(p -> p.getAge() > 30 ? "Senior" : "Junior")
    ));

partitioningBy()

A special case of groupingBy() that splits a stream into two groups based on a predicate (a condition that returns true or false). It always returns a Map<Boolean, ...>.

// Partition people into adults (age >= 18) and minors
Map<Boolean, List<Person>> adultsAndMinors = people.stream()
    .collect(Collectors.partitioningBy(p -> p.getAge() >= 18));
/*
Result:
{
  true: [Alice, Bob, Charlie],  // Assuming they are all adults
  false: []
}
*/

You can also use a downstream collector with partitioningBy.

// Partition adults and minors, then count them
Map<Boolean, Long> adultAndMinorCounts = people.stream()
    .collect(Collectors.partitioningBy(
        p -> p.getAge() >= 18,
        Collectors.counting()
    ));

Joining Collectors

Useful for working with String streams.

joining()

Concatenates the elements of a stream into a single String.

import java.util.stream.Stream;
String allNames = Stream.of("Alice", "Bob", "Charlie")
                       .collect(Collectors.joining());
// allNames is "AliceBobCharlie"
String namesWithComma = Stream.of("Alice", "Bob", "Charlie")
                             .collect(Collectors.joining(", "));
// namesWithComma is "Alice, Bob, Charlie"
String formattedNames = Stream.of("Alice", "Bob", "Charlie")
                             .collect(Collectors.joining(", ", "[", "]"));
// formattedNames is "[Alice, Bob, Charlie]"

Collecting to Custom Objects (Collectors.toCollection())

Sometimes you need a specific type of collection, like a LinkedList or an ImmutableList. You can achieve this by providing a Supplier that creates the desired collection.

import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
// Collect into a LinkedList instead of an ArrayList
LinkedList<String> nameList = Stream.of("Alice", "Bob", "Charlie")
    .collect(Collectors.toCollection(LinkedList::new));
// For an immutable list (Java 10+)
List<String> immutableList = Stream.of("Alice", "Bob", "Charlie")
    .collect(Collectors.toUnmodifiableList());

Summary Table

Goal Collector Method Example
To List Collectors.toList() .collect(Collectors.toList())
To Set (removes duplicates) Collectors.toSet() .collect(Collectors.toSet())
To Map Collectors.toMap() .collect(Collectors.toMap(Person::getName, Person::getAge))
To count elements Collectors.counting() .collect(Collectors.counting())
To sum elements Collectors.summingInt() .collect(Collectors.summingInt(Person::getAge))
To find average Collectors.averagingInt() .collect(Collectors.averagingInt(Person::getAge))
To find max/min Collectors.maxBy() / Collectors.minBy() .collect(Collectors.maxBy(Comparator.comparing(...)))
To group elements Collectors.groupingBy() .collect(Collectors.groupingBy(Person::getCity))
To partition by a condition Collectors.partitioningBy() .collect(Collectors.partitioningBy(p -> p.getAge() > 30))
To join strings Collectors.joining() .collect(Collectors.joining(", "))
To a custom collection type Collectors.toCollection() .collect(Collectors.toCollection(LinkedList::new))

Best Practices

  1. Prefer Collectors over manual loops: It leads to more concise, readable, and often more performant code.
  2. Use method references: They make the code cleaner. Person::getName is better than p -> p.getName().
  3. Be aware of null: Most collectors will throw a NullPointerException if the stream contains null values and your mapper functions don't handle it. Use Objects.requireNonNullElse or filter out nulls first if necessary.
  4. For immutable collections: Use toUnmodifiableList(), toUnmodifiableSet(), or toUnmodifiableMap() when you want to ensure the resulting collection cannot be modified after creation. This is a great practice for thread safety and defensive programming.
分享:
扫描分享到社交APP
上一篇
下一篇