Sunday, September 24, 2017

Using Comparators with Java 8 Streams

This post gives a few examples of how to use Comparator with Java 8 streams to Sort and to find the Maximum and Minimum elements in a stream.

Comparing Simple types with Comparator.comparing* methods

The Comparator.comparingDouble, Comparator.comparingInt and Comparator.comparingLong etc. methods can be used to do sorting or finding max and min from stream. The following example uses the comparingDouble method to compare and sort a stream of doubles generated using the Stream.generate() method. The Stream.generate has been explained my earlier post "Java 8 Streams"
  // Sort a stream of Doubles
  Stream.generate(Math::random).limit(10).sorted(Comparator.comparingDouble(Double::valueOf)).forEach(System.out::println);

Comparator.comparing method

The Comparator.comparing method gives a more flexible implementation which can be used to compare Objects of any type. There are a couple of variations of this method which we will see in the following examples.

Comparator.comparing method to compare Double

In the following example, we collect a stream of doubles generated using the Stream.generate method and print the numbers followed by using the Comparator.comparing method.
  // Max from a list of Doubles
  List<Double> doubles = Stream.generate(Math::random).limit(10).collect(Collectors.toList());
  doubles.stream().forEach(System.out::println);
  System.out
    .println("Max from random number : " + doubles.stream().max(Comparator.comparing(Function.identity())).get());

Comparator.comparing method to compare Dates

In the following example, we use the Comparator.comparing method to compare a list of LocalDate objects.
  // Max and min from a stream of dates
  LocalDate start = LocalDate.now();
  List<LocalDate> dates = Stream.iterate(start, date -> date.plusDays(1)).limit(100).collect(Collectors.toList());

Using Comparators on Streams with User Defined Classes

For the following few examples, we will use the Country Class defined below, and corresponding list shown below. The Country class consists of the name, Captial city and GDP (in trillions) of the country. I have changed a couple of values which are close enough to be equal in order to demonstrate a few variations of the Comparator.comparable method. This class also defines a natural ordering on the GDP by implementing the Comparable interface.
package beans;

public class Country implements Comparable<Country> {
 public String name;
 public String capital;
 public Double gdp;

 public Country(String name, String capital, Double gdp) {
  this.name = name;
  this.capital = capital;
  this.gdp = gdp;
 }

 @Override
 public int compareTo(Country o) {
  return gdp.compareTo(o.getGdp());
 }
// ... Getters Setters and toString
  List countries = new ArrayList();
  countries.add(new Country("United States", "Washington DC", 18.56));
  countries.add(new Country("China", "Beijing", 11.21));
  countries.add(new Country("Japan", "Tokyo", 4.93));
  countries.add(new Country("Germany", "Berlin", 3.46));
  countries.add(new Country("United Kingdom", "London", 2.62));
  countries.add(new Country("France", "Paris", 2.46));
  countries.add(new Country("India", "New Delhi", 2.46));
  countries.add(new Country("Italy", "Rome", 1.85));
  countries.add(new Country("Brazil", "Brasília", 1.85));

Comparator.comparing with Natural Ordering

In the first variation, the comparing method takes a Function which returns a Comparable as a parameter. Since our Country class implements the Comparable method, we will use the Function.identity() method to return the Country object. The Function.identity method behaves similar to the lambda x->x. In the following piece of code, we use the comparing method to print the country with the max and min GDP from the list
  // Countries with Max and Min GDPs from a list of countries
  Comparator<Country> compareNatural = Comparator.comparing(Function.identity());
  Country maxGdp = countries.stream().max(compareNatural).get();
  Country minGdp = countries.stream().min(compareNatural).get();

  System.out.println("Country with Max GDP in the list : " + maxGdp.getName());
  System.out.println("Country with Min GDP in the list : " + minGdp.getName());

Comparator.comparing using specific fields in the class

In the following few examples, we use the comparing method to sort the list of countries using different fields. Since all the fields in the Country class are comparable, we can use the get methods to return Comparable objects as shown below.
  // Sort Countries by Capital
  Comparator<Country> compareCapital = Comparator.comparing(Country::getCapital);
  countries.stream().sorted(compareCapital)
    .forEach(x -> System.out.println(x.getName() + " : " + x.getCapital()));

  // Sort Countries by Name
  Comparator<Country> compareName = Comparator.comparing(Country::getName);
  countries.stream().sorted(compareName).forEach(x -> System.out.println(x.getName()));

  // Sort by GDP
  Comparator<Country> compareGdp = Comparator.comparing(Country::getGdp);
  countries.stream().sorted(compareGdp)
    .forEach(x -> System.out.println(x.getName() + " : " + x.getGdp()));

Using Comparator.comparing method with custom comparators

The Comparator.comparing method has a variation that takes in a custom Comparator as an additional parameter to override the natural ordering. In the following example, we use the custom comparator to see if the Countries have equal GDP, in which case we will go to the second ordering which is by the Country name.
  // Compare with custom comparator different from natural ordering
  Comparator<? super Country> customComparator = (c1, c2) -> {
   return (c1.compareTo(c2) != 0) 
     ? c1.compareTo(c2)       // Compare by natural ordering if not equal
     : ((Country) c2).getName().compareTo(((Country) c1).getName());  // Compare reverse by name if equal by natural ordering
  };
  countries.stream().sorted(Comparator.comparing(Function.identity(), customComparator)).forEach(System.out::println);

The thenComparing method

The thenComparing method allows using multiple sort keys on the stream. The functionality is similar to the above example where we manually compare the fields one after the other.
  // Compare and then
  Comparator<Country> thenComparing = Comparator
    .comparing(Country::getGdp)
    .thenComparing(Country::getName);
  
  countries.stream()
    .sorted(thenComparing)
    .forEach(System.out::println);

Here is the full code

streams.StreamsComparator

package streams;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import beans.Country;

public class StreamsComparator {

 public static void main(String[] args) {

  // Sort a stream of Doubles
  Stream.generate(Math::random).limit(10).sorted(Comparator.comparingDouble(Double::valueOf)).forEach(System.out::println);

  // Max from a list of Doubles
  List<Double> doubles = Stream.generate(Math::random).limit(10).collect(Collectors.toList());
  doubles.stream().forEach(System.out::println);
  System.out
    .println("Max from random number : " + doubles.stream().max(Comparator.comparing(Function.identity())).get());

  // Max and min from a stream of dates
  LocalDate start = LocalDate.now();
  List<LocalDate> dates = Stream.iterate(start, date -> date.plusDays(1)).limit(100).collect(Collectors.toList());

  LocalDate maxDate = dates.stream().max(Comparator.comparing(Function.identity())).get();
  LocalDate minDate = dates.stream().min(Comparator.comparing(Function.identity())).get();

  System.out.println("Last Date = " + maxDate);
  System.out.println("First Date = " + minDate);

  List<Country> countries = new ArrayList<Country>();
  countries.add(new Country("United States", "Washington DC", 18.56));
  countries.add(new Country("China", "Beijing", 11.21));
  countries.add(new Country("Japan", "Tokyo", 4.93));
  countries.add(new Country("Germany", "Berlin", 3.46));
  countries.add(new Country("United Kingdom", "London", 2.62));
  countries.add(new Country("France", "Paris", 2.46));
  countries.add(new Country("India", "New Delhi", 2.46));
  countries.add(new Country("Italy", "Rome", 1.85));
  countries.add(new Country("Brazil", "Brasília", 1.85));

  // Countries with Max and Min GDPs from a list of countries
  Comparator<Country> compareNatural = Comparator.comparing(Function.identity());
  Country maxGdp = countries.stream().max(compareNatural).get();
  Country minGdp = countries.stream().min(compareNatural).get();

  System.out.println("Country with Max GDP in the list : " + maxGdp.getName());
  System.out.println("Country with Min GDP in the list : " + minGdp.getName());

  // Sort Countries by Capital
  Comparator<Country> compareCapital = Comparator.comparing(Country::getCapital);
  countries.stream().sorted(compareCapital)
    .forEach(x -> System.out.println(x.getName() + " : " + x.getCapital()));

  // Sort Countries by Name
  Comparator<Country> compareName = Comparator.comparing(Country::getName);
  countries.stream().sorted(compareName).forEach(x -> System.out.println(x.getName()));

  // Sort by GDP
  Comparator<Country> compareGdp = Comparator.comparing(Country::getGdp);
  countries.stream().sorted(compareGdp)
    .forEach(x -> System.out.println(x.getName() + " : " + x.getGdp()));

  // Compare with custom comparator different from natural ordering
  Comparator<? super Country> customComparator = (c1, c2) -> {
   return (c1.compareTo(c2) != 0) 
     ? c1.compareTo(c2)       // Compare by natural ordering if not equal
     : ((Country) c2).getName().compareTo(((Country) c1).getName());  // Compare reverse by name if equal by natural ordering
  };
  countries.stream().sorted(Comparator.comparing(Function.identity(), customComparator)).forEach(System.out::println);;

  // Compare and then
  Comparator<Country> thenComparing = Comparator
    .comparing(Country::getGdp)
    .thenComparing(Country::getName);
  
  countries.stream()
    .sorted(thenComparing)
    .forEach(System.out::println);
  
 }

}

beans.Country

package beans;

public class Country implements Comparable<Country> {
 public String name;
 public String capital;
 public Double gdp;

 public Country(String name, String capital, Double gdp) {
  this.name = name;
  this.capital = capital;
  this.gdp = gdp;
 }

 @Override
 public int compareTo(Country o) {
  return gdp.compareTo(o.getGdp());
 }

 public String getName() {
  return name;
 }

 public void setName(String name) {
  this.name = name;
 }

 public String getCapital() {
  return capital;
 }

 public void setCapital(String capital) {
  this.capital = capital;
 }

 public Double getGdp() {
  return gdp;
 }

 public void setGdp(Double gdp) {
  this.gdp = gdp;
 }

 @Override
 public String toString() {
  return "Country [name=" + name + ", capital=" + capital + ", gdp=" + gdp + "]";
 }

}

No comments:

Post a Comment

Popular Posts