Wednesday, January 12, 2022

Java Tutorial: Collectors Class

Chapters

Collectors Class

Note: It's recommended to be knowledgeable about Stream Class before reading this article.

Collectors class that implement various useful reduction operations, such as accumulating elements into collections, summarizing elements according to various criteria, etc. Mostly all of the methods in this class return a Collector interface type. One of the forms of collect() method from Stream class accepts Collector type as argument.

My explanation here is simplified. Read the documentation for more information.

averagingInt() and its variants

Method Form: averagingInt(ToIntFunction<? super T> mapper)
averagingInt() Returns a Collector that produces the arithmetic mean of an integer-valued function applied to the input elements. If no elements are present, the result is 0.

This method has two other variants: averagingDouble() and averagingLong(). This methods return arithmetic mean of a double-valued and long-valued functions respectively.

This example demonstrate averagingInt().
import java.util.stream.Collectors;
import java.util.ArrayList;

public class SampleClass{

  public static void main(String[] args){
    ArrayList<Integer> ar = 
    new ArrayList<>();
    
    ar.add(2);
    ar.add(4);
    ar.add(6);
    ar.add(8);
    ar.add(10);
    
    Double avg =
    ar.stream()
      .collect(Collectors
        .averagingInt((n) -> n*n));
    
    //(2^2 + 4^2 + 6^2 + 8^2 + 10^2) / 5
    //= 44
    System.out.println("Result: "+avg);
    
  }
}

Result
Result: 44.0

collectingAndThen() Method

Method form:
public static <T, A, R, RR> Collector<T,A,RR> collectingAndThen(Collector<T,A,R> downstream, Function<R,RR> finisher)
Adapts a Collector to perform an additional finishing transformation.

This example demonstrates collectingAndThen().
import java.util.stream.Collectors;
import java.util.ArrayList;

public class SampleClass{

  public static void main(String[] args){
    ArrayList<Integer> ar = 
    new ArrayList<>();
    
    ar.add(2);
    ar.add(4);
    ar.add(6);
    ar.add(8);
    ar.add(10);
    
    Double avg =
    ar.stream()
      .collect(
       Collectors
       .collectingAndThen(Collectors
        .averagingInt((n) -> n*n),
       (nn) -> nn*nn);
    
    //(2^2 + 4^2 + 6^2 + 8^2 + 10^2) / 5
    //= 44
    //44^2 = 1936
    System.out.println("Result: "+avg);
    
  }
}

Result
Result: 1936.0
In the example above, averagingInt() is applied first. Then, the finisher is applied to the result of the downstream.

counting() Method

Method Form: public static <T> Collector<T,?,Long> counting()
Returns a Collector accepting elements of type T that counts the number of input elements. If no elements are present, the result is 0.

This example demonstrates counting().
import java.util.stream.Collectors;
import java.util.ArrayList;

public class SampleClass{

  public static void main(String[] args){
    ArrayList<Integer> ar = 
    new ArrayList<>();
    
    ar.add(2);
    ar.add(4);
    ar.add(6);
    ar.add(8);
    ar.add(10);
    
    Long count =
    ar.stream()
      .collect(Collectors.counting());
      
    System.out.println("Result: " + count);
  }
}

Result: 5

filtering() Method

Method Form: public static <T, A, R> Collector<T,?,R> filtering(Predicate<? super T> predicate, Collector<? super T,A,R> downstream)
Adapts a Collector to one accepting elements of the same type T by applying the predicate to each input element and only accumulating if the predicate returns true. More information can be read in the documentation.

This example demonstrates filtering().
import java.util.stream.Collectors;
import java.util.ArrayList;

public class SampleClass{

  public static void main(String[] args){
    ArrayList<Integer> ar = 
    new ArrayList<>();
    
    ar.add(2);
    ar.add(4);
    ar.add(6);
    ar.add(8);
    ar.add(10);
    
    Double avg =
    ar.stream()
      .collect(
       Collectors.filtering(
        (t) -> t <= 6,
        Collectors.averagingInt(
         (n) -> n*n)
       )
      );
    
    //2^2 + 4^2 + 6^2 / 3 
    //= 18.666666666666668
    System.out.println("Result: " + avg);
  }
}

flatMapping() Method

Method Form: public static <T, U, A, R> Collector<T,?,R> flatMapping(Function<? super T,? extends Stream<? extends U>> mapper, Collector<? super U,A,R> downstream)
Adapts a Collector accepting elements of type U to one accepting elements of type T by applying a flat mapping function to each input element before accumulation. The flat mapping function maps an input element to a stream covering zero or more output elements that are then accumulated downstream.

Each mapped stream is closed after its contents have been placed downstream. If a mapped stream is null an empty stream is used, instead. More information can be read in the documentation.

This example demonstrates flatMapping().
import java.util.stream.Collectors;
import java.util.Arrays;
import java.util.List;

public class SampleClass{

  public static void main(String[] args){
  
    Integer[][] ints = {{2,4,6},{3,7,9}};
    
    List<Integer> flatInts = 
    Arrays.stream(ints)
          .collect(
           Collectors.flatMapping(
           (e) -> Arrays.stream(e),
           Collectors.toList())
          );
    
    for(Object o : flatInts)
      System.out.print(o + " ");
    
  }
}

Result
2 4 6 3 7 9
groupingBy() Method

Returns a Collector implementing a "group by" operation on input elements of type T, grouping elements according to a classification function. This method has three forms and I'm gonna demonstrate them one-by-one.

This example demonstrate this form:
public static <T, K> Collector<T,?,Map<K,List<T>>> groupingBy(Function<? super T,? extends K> classifier)
import java.util.stream.Collectors;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;

public class SampleClass{

  public static void main(String[] args){
    ArrayList<Integer> ar = 
    new ArrayList<>();
    
    ar.add(2);
    ar.add(4);
    ar.add(7);
    ar.add(8);
    ar.add(11);
    
    Map<String,List<Integer>> map =
    ar.stream()
      .collect(Collectors.groupingBy(
        (t) -> {
          if(t % 2 == 0)
            return "even";
          else
            return "odd";
        }));
    
    System.out.println(map);
  }
}

Result
{even=[2, 4, 8], odd=[7, 11]}
Next, This example demonstrate this form:
public static <T, K, D, A, M extends Map<K, D>> Collector<T,?,M> groupingBy(Function<? super T,? extends K> classifier, Supplier<M> mapFactory, Collector<? super T,A,D> downstream)
import java.util.stream.Collectors;
import java.util.LinkedHashMap;
import java.util.Set;
import java.util.ArrayList;

public class SampleClass{

  public static void main(String[] args){
    ArrayList<Integer> ar = 
    new ArrayList<>();
    
    ar.add(2);
    ar.add(4);
    ar.add(7);
    ar.add(8);
    ar.add(11);
  
    LinkedHashMap<String,Set<Integer>> map = 
    ar.stream()
      .collect(Collectors.groupingBy(
        (t) -> {
          if(t % 2 == 0)
            return "even";
          else
            return "odd";
        }, 
        LinkedHashMap::new,
        Collectors.toSet()));
    
    System.out.println(map);
    
  }
}

Result
{even=[2, 4, 8], odd=[7, 11]}
Next, This example demonstrate this form:
public static <T, K, A, D> Collector<T,?,Map<K,D>> groupingBy(Function<? super T,? extends K> classifier, Collector<? super T,A,D> downstream)
import java.util.stream.Collectors;
import java.util.Map;
import java.util.ArrayList;
import java.util.function.Function;

public class SampleClass{

  public static void main(String[] args){
    ArrayList<String> ar = 
    new ArrayList<>();
    
    ar.add("Banana");
    ar.add("Apple");
    ar.add("Carrot");
    ar.add("Banana");
    ar.add("Apple");
    ar.add("Apple");
    
    Map<String,Long> map =
    ar.stream()
      .collect(Collectors.groupingBy(
        Function.identity(),
        Collectors.counting()
      ));
    
    System.out.println(map);
    
  }
}

Result
{Carrot=1, Apple=3, Banana=2}
Function.identity() returns a function that always returns its input argument. This is useful if you don't wanna modify input arguments.

groupingByConcurrent() is a variant of groupingBy() method. These two are the same. Thus, the examples above can be applied to groupingByConcurrent(). However, groupingByConcurrent() is more optimized for concurrency especially for a huge collection of elements.

This example demonstrate groupingByConcurrent().
import java.util.stream.Collectors;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;

public class SampleClass{

  public static void main(String[] args){
    ArrayList<Integer> ar = 
    new ArrayList<>();
    
    ar.add(2);
    ar.add(4);
    ar.add(7);
    ar.add(8);
    ar.add(11);
    
    Map<String,List<Integer>> map =
    ar.parallelStream()
      .collect(Collectors.groupingByConcurrent(
        (t) -> {
          if(t % 2 == 0)
            return "even";
          else
            return "odd";
        }));
    
    System.out.println(map);
  }
}

Result(may vary)
{even=[8, 4, 2], odd=[7, 11]}
joining() Method

Returns a Collector that concatenates the input elements into a String, in encounter order. This method has three forms. I'll demonstrate them one-by-one.

This example demonstrates this form:
public static Collector<CharSequence,?,String> joining()
import java.util.stream.Collectors;
import java.util.ArrayList;

public class SampleClass{

  public static void main(String[] args){
    ArrayList<String> ar = 
    new ArrayList<>();
    
    ar.add("A");
    ar.add(" B");
    ar.add(" C");
    ar.add(" D");
    
    String str = 
    ar.stream()
      .collect(Collectors.joining());
    
    System.out.println("Result: " + str);
  }
}

Result
Result: A B C D
Next, This example demonstrates this form:
public static Collector<CharSequence,?,String> joining(CharSequence delimiter)
import java.util.stream.Collectors;
import java.util.ArrayList;

public class SampleClass{

  public static void main(String[] args){
    ArrayList<String> ar = 
    new ArrayList<>();
    
    ar.add("A");
    ar.add("B");
    ar.add("C");
    ar.add("D");
    
    String str = 
    ar.stream()
      .collect(Collectors.joining("-"));
    
    System.out.println("Result: " + str);
  }
}

Result
Result: A-B-C-D
Next, this example demonstrates this form:
public static Collector<CharSequence,?,String> joining(CharSequence delimiter, CharSequence prefix, CharSequence suffix)
import java.util.stream.Collectors;
import java.util.ArrayList;

public class SampleClass{

  public static void main(String[] args){
    ArrayList<String> ar = 
    new ArrayList<>();
    
    ar.add("A");
    ar.add("B");
    ar.add("C");
    ar.add("D");
    
    String str = 
    ar.stream()
      .collect(Collectors.joining("-","^","*"));
    
    System.out.println("Result: " + str);
  }
}

Result
Result: ^A-B-C-D*
mapping() Method

Method form: public static <T, U, A, R> Collector<T,?,R> mapping(Function<? super T,? extends U> mapper, Collector<? super U,A,R> downstream)
Adapts a Collector accepting elements of type U to one accepting elements of type T by applying a mapping function to each input element before accumulation. More information can be read in the documentation.

This example demonstrates mapping().
import java.util.stream.Collectors;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class SampleClass{

  public static void main(String[] args){
    
    ArrayList<Integer> ar = 
    new ArrayList<>();
    
    ar.add(2);
    ar.add(4);
    ar.add(7);
    ar.add(8);
    ar.add(11);
    
    Map<String, List<Integer>> map = 
    ar.stream()
      .collect(
       Collectors.groupingBy(
       (t) -> {
          if(t % 2 == 0)
            return "even";
          else
            return "odd";
        },
        Collectors.mapping(
          (t) -> t*t,
          Collectors.toList()
        )
      ));
    
    System.out.println(map);
  }
}

Result
{even=[4, 16, 64], odd=[49, 121]}
maxBy() Method

Method Form: public static <T> Collector<T,?,Optional<T>*gt; maxBy(Comparator<? super T> comparator)
Returns a Collector that produces the maximal element according to a given Comparator, described as an Optional<T>. More information can be read in the documentation.

This example demonstrates maxBy().
import java.util.stream.Collectors;
import java.util.ArrayList;
import java.util.Optional;

public class SampleClass{

  public static void main(String[] args){
    ArrayList<String> ar = 
    new ArrayList<>();
    
    ar.add("Joker");
    ar.add("queen");
    ar.add("jack");
    ar.add("King");
    
    Optional<String> opt =
    ar.stream().collect(
    Collectors.maxBy(String.CASE_INSENSITIVE_ORDER));
    
    if(opt.isPresent())
      System.out.println("max(last) index: " + opt.get());
    else
      System.out.println("Empty result!");
  }
}

Result
max(last) index: queen
minBy() Method

Method Form: public static <T> Collector<T,?,Optional<T>> minBy(Comparator<? super T> comparator)
Returns a Collector that produces the minimal element according to a given Comparator, described as an Optional<T>. More information can be read in the documentation.
import java.util.stream.Collectors;
import java.util.ArrayList;
import java.util.Optional;

public class SampleClass{

  public static void main(String[] args){
    ArrayList<String> ar = 
    new ArrayList<>();
    
    ar.add("Joker");
    ar.add("queen");
    ar.add("jack");
    ar.add("King");
    
    Optional<String> opt =
    ar.stream().collect(
    Collectors.minBy(String.CASE_INSENSITIVE_ORDER));
    
    if(opt.isPresent())
      System.out.println("min(first) index: " + opt.get());
    else
      System.out.println("Empty result!");
  }
}

Result
min(first) index: jack
partitioningBy() Method

returns a Collector which partitions the input elements according to a Predicate, and organizes them into a Map. The returned Map always contains mappings for both false and true keys. There are no guarantees on the type, mutability, serializability, or thread-safety of the Map or List returned.

This method is similar to groupBy() method. This method has two forms. I'm gonna demonstrate them one-by-one.

This example demonstrates this form:
public static <T> Collector<T,?,Map<Boolean,List<T>>> partitioningBy(Predicate<? super T> predicate)
import java.util.stream.Collectors;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;

public class SampleClass{

  public static void main(String[] args){
    ArrayList<Integer> ar = 
    new ArrayList<>();
    
    ar.add(2);
    ar.add(4);
    ar.add(7);
    ar.add(8);
    ar.add(11);
    
    Map<Boolean,List<Integer>> map =
    ar.stream()
      .collect(Collectors.partitioningBy(
        (t) -> {
          if(t % 2 == 0)
            return true;
          else
            return false;
        }));
    
    System.out.println(map);
  }
}

Result
{false=[7, 11], true=[2, 4, 8]}
Next, this example demonstrates this form:
public static <T, D, A> Collector<T,?,Map<Boolean,D>> partitioningBy(Predicate<? super T> predicate, Collector<? super T,A,D> downstream)
import java.util.stream.Collectors;
import java.util.Map;
import java.util.Set;
import java.util.ArrayList;

public class SampleClass{

  public static void main(String[] args){
    ArrayList<String> ar = 
    new ArrayList<>();
    
    ar.add("Banana");
    ar.add("Pork");
    ar.add("Bacon");
    ar.add("Quasar");
    ar.add("Bamboo");
    
    Map<Boolean,Set<String>> map =
    ar.stream()
      .collect(Collectors.partitioningBy(
        (t) -> t.startsWith("B"),
        Collectors.toSet()));
    
    System.out.println(map);
  }
}

Result
{false=[Quasar, Pork], true=[Bacon, Bamboo, Banana]}
As you can see, partinioningBy() is closely similar to groupingBy(). Although, in my opinion, It's better to use partinioningBy() if we want to separate elements only into two sections. For separating elements into multiple sections, use groupingBy().

reducing() Method

Method Form: public static <T> Collector<T,?,Optional<T>> reducing(BinaryOperator<T> op)
Returns a Collector which performs a reduction of its input elements under a specified BinaryOperator. The result is described as an Optional<T>. This method has three forms. I'm gonna demonstrate them one-by-one.

This example demonstrates this form:
public static <T> Collector<T,?,Optional<T>> reducing(BinaryOperator<T> op)
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.Optional;

public class SampleClass{

  public static void main(String[] args){
    Stream<Integer> ints = Stream.of(2,4,6);
    
    Optional<Integer> opt =
    ints.collect(
    Collectors.reducing((t,u) -> (t*t)+(u*u)));
    
    if(opt.isPresent())
      //(2*2 + 4*4)*(2*2 + 4*4) + 6*6
      //(20*20) + 36
      //400 + 36
      //=436
      System.out.println("SquaredThenAdd: " + opt.get());
    else
      System.out.println("Empty result!");
  }
}

Result:
SquaredThenAdd: 436
Next, this example demonstrates this form:
public static <T> Collector<T,?,T> reducing(T identity, BinaryOperator<T> op)
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class SampleClass{

  public static void main(String[] args){
    Stream<Integer> ints = Stream.of(4,6);
    
    Integer result =
    ints.collect(
    Collectors.reducing(2, (t,u) -> (t*t)+(u*u)));
    
    //(2*2 + 4*4)*(2*2 + 4*4) + 6*6
    //(20*20) + 36
    //400 + 36
    //=436
    System.out.println("SquaredThenAdd: " + result);
   
  }
}

Result:
SquaredThenAdd: 436
identity parameter denotes initial value.
Next, this example demonstrates this form:
public static <T, U> Collector<T,?,U> reducing(U identity, Function<? super T,? extends U> mapper, BinaryOperator<U> op)
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class SampleClass{

  public static void main(String[] args){
    Stream<Integer> ints = Stream.of(2,4,6);
    
    Integer result =
    ints.collect(
    Collectors
     .reducing(0,
      (t) -> t*t,
      (t,u) -> t+u));
    
    //2*2 + 4*4 + 6*6 = 56
    System.out.println("SquaredThenAdd: " + result);
   
  }
}

Result:
SquaredThenAdd: 56
summarizingInt() and its Variants

Method Form: public static <T> Collector<T,?,IntSummaryStatistics> summarizingInt(ToIntFunction<? super T> mapper)

Returns a Collector which applies an int-producing mapping function to each input element, and returns summary statistics for the resulting values. summarizingInt() returns IntSummaryStatistics. Other variants like summarizingDouble() and summarizingLong() return DoubleSummaryStatistics and LongSummaryStatistics respectively.

This example demonstrates summarizingInt().
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.IntSummaryStatistics;

public class SampleClass{

  public static void main(String[] args){
    Stream<Integer> ints = Stream.of(2,4,6);
    
    IntSummaryStatistics iss = 
    ints.collect(
    Collectors
    .summarizingInt((t) -> t*t));
    
    System.out.println("Records");
    System.out.println(iss + "\n");

  }
}

Result
Records
IntSummaryStatistics
{count=3, sum=56, min=4, average=18.666667, max=36}
summingInt() and its Variants

Method Form: public static <T> Collector<T,?,Integer> summingInt(ToIntFunction<? super T> mapper)

Returns a Collector that produces the sum of a numerical-valued function applied to the input elements. If no elements are present, the result is 0. summingInt() returns Integer. Other variants like summingDouble() and summingLong() return Double and Long respectively.

This example demonstrates summingInt().
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class SampleClass{

  public static void main(String[] args){
    Stream<Integer> ints = Stream.of(2,4,6);
    
    Integer result = 
    ints.collect(
    Collectors
    .summingInt((t) -> t*t));
    
    System.out.println("Value: " + result);
  }
}

Result
Value: 56
teeing() Method

Method Form: public static <T, R1, R2, R> Collector<T,?,R> teeing(Collector<? super T,?,R1> downstream1, Collector<? super T,?,R2> downstream2, BiFunction<? super R1,? super R2,R> merger)

Returns a Collector that is a composite of two downstream collectors. Every element passed to the resulting collector is processed by both downstream collectors, then their results are merged using the specified merge function into the final result.

More information can be read in the documentation.

This example demonstrates teeing().
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class SampleClass{

  public static void main(String[] args){
    Stream<Integer> ints = Stream.of(2,4,6);
    
    Integer result = 
    ints.collect(
    Collectors
    .teeing(
     Collectors.summingInt((t) -> t*t),
     Collectors.summingInt((t) -> t+t),
     (t,u) -> t+u));
    
    //(2*2 + 4*4 + 6*6) + (2+2 + 4+4 + 6+6)
    //= 56 + 24 = 80
    System.out.println("Value: " + result);
  }
}
toMap() Method and other Variants

Returns a Collector that accumulates elements into a Map whose keys and values are the result of applying the provided mapping functions to the input elements. More information can be read in the documentation.

This method has three forms. I'm gonna demonstrate them one-by-one. Also, examples here can be applied to other variants like toConcurrentMap() and toUnmodifiableMap().

This example demonstrates this form:
public static <T, K, U> Collector<T,?,Map<K,U>> toMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper)
import java.util.stream.Collectors;
import java.util.Arrays;
import java.util.Map;

public class SampleClass{

  public static void main(String[] args){
    String[][] strings = 
    new String[][]{{"key1","val1"},
                   {"key2","val2"},
                   {"key3","val3"}};
    
    Map<String,String> map =
    Arrays.stream(strings).collect(
    Collectors
     .toMap((t) -> t[0],
            (t) -> t[1]));
    
    System.out.println(map);
  }
}

Result
{key1=val1, key2=val2, key3=val3}
Next, this example demonstrates this form:
public static <T, K, U> Collector<T,?,Map<K,U>> toMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper, BinaryOperator<U> mergeFunction)
import java.util.stream.Collectors;
import java.util.Arrays;
import java.util.Map;

public class SampleClass{

  public static void main(String[] args){
    String[][] strings = 
    new String[][]{{"key1","val1"},
                   {"key2","val2"},
                   {"key1","val3"}};
    
    Map<String,String> map =
    Arrays.stream(strings).collect(
    Collectors
     .toMap((t) -> t[0],
            (t) -> t[1],
            (t,u) -> t+"-"+u));
    
    System.out.println(map);
  }
}

Result
{key1=val1-val3, key2=val2}
mergeFunction can merge the values of two duplicate keys. In the example above, val1 and val3 had been merged as one value of key1 with "-" delimiter.

Next, this example demonstrates this form:
public static <T, K, U, M extends Map&tl;K, U>> Collector<T,?,M> toMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper, BinaryOperator<U> mergeFunction, Supplier<M> mapFactory)
import java.util.stream.Collectors;
import java.util.Arrays;
import java.util.TreeMap;

public class SampleClass{

  public static void main(String[] args){
    String[][] strings = 
    new String[][]{{"key1","val1"},
                   {"key3","val4"},
                   {"key2","val2"},
                   {"key1","val3"}};
    
    TreeMap<String,String> map =
    Arrays.stream(strings).collect(
    Collectors
     .toMap((t) -> t[0],
            (t) -> t[1],
            (t,u) -> t+"-"+u,
            TreeMap::new));
    
    System.out.println(map);
  }
}

Result
{key1=val1-val3, key2=val2, key3=val4}
This example is not applicable to toUnmodifiableMap() because this form of toMap() doesn't match(closely match) any form of toUnmodifiableMap().

No comments:

Post a Comment