Collectors in a Nutshell

  • Below is an example of using a Collector to make a Map whose keys are currencies and whose values are lists of transactions with corresponding currencies
Map<Currency, List<Transaction>> transactionsByCurrencies = 
	transactions.stream().collect(groupingBy(Transaction::getCurrency));
  • Like the groupingBy method, Collectors provide many pre-defined advanced reduction methods.
    • Methods that reduce and summarize the elements from the stream into a single value
    • Grouping elements from the stream
    • Partitioning elements from the stream

 

Reducing and Summarizing

Finding Maximum and Minimum Values

  • Collectors.maxBy and Collectors.minBy methods take a Comparator as argument to compare the elements in the stream
    • This is one of the advantages of using stream and Collectors. Collectors.maxBy(Comparator) is easy to understand - we are getting the maximum value from the stream by the comparator we put in.
Comparator<Person> ageComparator = Comparator.comparingInt(Person::getAge);

Optional<Person> oldestPerson = people.stream()
	.collect(maxBy(ageComparator));

Summarization

  • Collectors.summingInt and Collectors.averagingInt accept a function that maps an object into int, and return a Collector which will perform the requested operation when passed into collect method.
int totalCals = menu.stream()
	.collect(summingInt(Dish::getCalories));

 

summingInt collector (Modern Java in Action)

  • We can also use Collectors.summarizingInt which will return IntSummaryStatistics containing all the statistics about the given integers.
IntSummaryStatistics personAgeStatistics = people.stream()
	.collect(summarizingInt(Person::getAge));

Joining Strings

  • Collectors.joining will return a collector which concatenates strings into a single string. 
  • If the elements in the stream are not strings, the default toString method will be invoked.
  • We can put a string as an argument to separate the strings when concatenating
String shortMenu = menu.stream().map(Dish::getName).collect(joining());
String anotherShortMenu = menu.stream().collect(joining());	// invoke toString

String separatedShortMenu = menu.stream()
	.map(Dish::getName).collect(joining(", "));

Generalized Summarization with Reduction

  • Collectors.reducing method is a generalized version of reducing.
    • First argument: initial value of the reduction process
    • Second argument: Method to transform element into target data type
    • Third argument: BinaryOperator that aggregates 2 items into a single value of the same type
  • One argument version is a special type where the first argument (initial value) is the first item of the string and the second argument is an identity function.
// Three arguments version
int totalAge = people.stream().collect(reducing(
	0, Person::getAge, (a1, a2) -> a1 + a2));

// One argument version
Optional<Person> oldestPerson = people.stream()
	.collect(reducing(
    	(p1, p2) -> p1.getAge() > p2.getAge() ? p1: p2));

 

Grouping

  • We can easily group elements of a stream into a set or a list based on one or more properties.
  • We pass a classification function to groupingBy method
Map<Dish.Type, List<Dish> dishesByType
	= menu.stream().collect(groupingBy(Dish::getType));

Classification of an item in the stream (Modern Java In Action)

 

  • We can use a lambda expression instead of a method reference to classify elements via a more complicated function.

Manipulating Grouped Elements

  • When we apply a filtering predicate before grouping like below, keys that do not have elements will not appear in the resulting map.
Map<Dish.Type, List<Dish>> caloricDishesByType = menu.stream()
	.filter(dish -> dish.getCalories() > 500)
    .collect(groupingBy(Dish::getType));
  • We can move the filtering predicate inside the collect method as a second predicate - in this case, keys that do not have any element will still appear in the resulting map.
Map<Dish.Type, List<Dish>> caloricDishesByType = menu.stream()
	.collect(groupingBy(Dish::getType, 
    	filtering(dish -> dish.getCalories() > 500, toList()));
  • Just like the filtering method above, we can use the mapping method as the second argument too.
Map<Dish.Type, List<String>> dishNamesByType = menu.stream()
	.collect(groupingBy(Dish::getType,
    	mapping(Dish::getName, toList()));

 

Multilevel Grouping

  • We can pass another groupingBy method as the second argument of a groupingBy method for multi-level grouping.
Map<Dish.Type, Map<Cuisine, List<Dish>>> dishesByCuisine = menu.stream()
	.collect(groupingBy(Dish::getType,
    		groupingBy(Dish::getCuisine)));

 

Collecting Data in Subgroups

  • More generally, we can pass any type of collector as the second argument of a groupingBy method.
  • By using the counting method, we can count the number of items in each group after grouping.
Map<Dish.Type, Long> typesCount = menu.stream()
	.collect(groupingBy(Dish::getType, counting()));
  • Many times we will have Optional in the resulting map depending on which filtering or mapping method we use.
  • To remove this Optional, or more generally to adapt the result returned by a collector into a different type, we can use Collectors.collectingAndThen method. 
Map<Dish.Type, Dish> mostCaloricDishByType = menu.stream()
	.collect(groupingBy(Dish::getType,
    	collectingAndThen(
        	maxBy(comparingInt(Dish::getCalories)),
            Optional::get
        )
    );
  • Collectors.collectingAndThen has 2 arguments - the first is the collector and the second is a transformation function.

Nested collectors (Modern Java in Action)

  • We have the outermost groupingBy collector denoted as a blue dashed box.
  • The groupingBy collector wraps the three collectingAndThen collectors, so that the result of those can be collected again with the groupingBy collector.
  • collectingAndThen collector wraps the maxBy collector, and the result of the maxBy collector is transformed by Optional::get method.

 

Partitioning

  • Partitioning is a special case of grouping where a predicate is used as a classification function
  • Since predicates return a Boolean, the resulting grouping Map will have at most 2 keys, which are Boolean.
Map<Boolean, List<Dish>> partitionedMenu = menu.stream()
	.collect(partitioningBy(Dish::isVegetarian));
    // isVegeterian is a partitioning function (predicate)

Advantages of Partitioning

  • It is easier and more intuitive to use partitioning when you want to separate a stream into two lists.
List<Dish> vegetarianDishes = menu.stream()
	.collect(partitioningBy(Dish::isVegeterian))
   	.get(true);
    // since the resulting grouping of collect is a map with true and false being keys
  • We can also apply multi-level mapping by using an overloaded version of partitioningBy method.
Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType = menu.stream()
	.collect(partitioningBy(Dish::isVegetarian, groupingBy(Dish::getType)));
    
    // result will be something like
    // {true = {OTHER=[Salad, Fruit]}, false = {FISH=[salmon], MEAT=[pork]}}

 

 

Main Static Factory methods of the Collectors Class

(Modern Java In Action)

'Java > Modern Java In Action' 카테고리의 다른 글

Working with Streams  (0) 2023.11.05
Introducing Streams  (1) 2023.10.30

+ Recent posts