Archives January 2022

Sorting a map 2 – Functional style programming – extending API

So, when the stream pipeline needs only the map’s values, we can start from values(), when it needs only the keys, we can start from keyset(), and when it needs both (the values and the keys), we can start from entrySet().For instance, a stream pipeline as Map -> Stream -> Filter -> Map that filters the top 5 cars by key and collects them into a resulting map needs the entrySet() starting point as follows:

Map<Integer, Car> carsTop5a = cars.entrySet().stream()
  .filter(c -> c.getKey() <= 5)
  .collect(Collectors.toMap(
     Map.Entry::getKey, Map.Entry::getValue));
  //or, .collect(Collectors.toMap(
     c -> c.getKey(), c -> c.getValue()));

Here is example that returns a Map of the top 5 cars having more than 100 horsepower:

Map<Integer, Car> hp100Top5a = cars.entrySet().stream()
  .filter(c -> c.getValue().getHorsepower() > 100)
  .sorted(Entry.comparingByValue(
          Comparator.comparingInt(Car::getHorsepower)))
  .collect(Collectors.toMap(
     Map.Entry::getKey, Map.Entry::getValue,
       (c1, c2) -> c2, LinkedHashMap::new));
  //or, .collect(Collectors.toMap(
  //   c -> c.getKey(), c -> c.getValue(),
  //   (c1, c2) -> c2, LinkedHashMap::new));

If we need to express such pipelines quite often then we may prefer to write some helpers. Here is a set of four generic helpers for filtering and sorting a Map<K, V> by key:

public final class Filters {
  private Filters() {
    throw new AssertionError(“Cannot be instantiated”);
  }
  public static <K, V> Map<K, V> byKey(
        Map<K, V> map, Predicate<K> predicate) {
  return map.entrySet()
    .stream()
    .filter(item -> predicate.test(item.getKey()))
    .collect(Collectors.toMap(
       Map.Entry::getKey, Map.Entry::getValue));
  }
  public static <K, V> Map<K, V> sortedByKey(
    Map<K, V> map, Predicate<K> predicate, Comparator<K> c) {
    return map.entrySet()
      .stream()
      .filter(item -> predicate.test(item.getKey()))
      .sorted(Map.Entry.comparingByKey(c))
      .collect(Collectors.toMap(
         Map.Entry::getKey, Map.Entry::getValue,
              (c1, c2) -> c2, LinkedHashMap::new));
  }
  …

And, for filtering and sorting a Map by value:

  public static <K, V> Map<K, V> byValue(
      Map<K, V> map, Predicate<V> predicate) {
    return map.entrySet()
      .stream()
      .filter(item -> predicate.test(item.getValue()))
      .collect(Collectors.toMap(
         Map.Entry::getKey, Map.Entry::getValue));
  }
  public static <K, V> Map<K, V> sortedbyValue(Map<K, V> map,
      Predicate<V> predicate, Comparator<V> c) {
  return map.entrySet()
    .stream()
    .filter(item -> predicate.test(item.getValue()))
    .sorted(Map.Entry.comparingByValue(c))
    .collect(Collectors.toMap(
       Map.Entry::getKey, Map.Entry::getValue,
           (c1, c2) -> c2, LinkedHashMap::new));
  }
}

Now, our code becomes much shorter. For instance, we can filter the top 5 cars by key and collect them into a resulting map as follows:

Map<Integer, Car> carsTop5s
  = Filters.byKey(cars, c -> c <= 5);

Or, we can filter the top 5 cars having more than 100 horsepower as follows:

Map<Integer, Car> hp100Top5s
  = Filters.byValue(cars, c -> c.getHorsepower() > 100);
Map<Integer, Car> hp100Top5d
  = Filters.sortedbyValue(cars, c -> c.getHorsepower() > 100,
      Comparator.comparingInt(Car::getHorsepower));                             

Cool, right?! Feel free to extend Filters with more generic helpers for handling Map processing in stream pipelines.

Sorting a map – Functional style programming – extending API

193. Sorting a map

Let’s assume that we have the following map:

public class Car {
  private final String brand;
  private final String fuel;
  private final int horsepower;
  …
}
Map<Integer, Car> cars = Map.of(
  1, new Car(“Dacia”, “diesel”, 350),
  2, new Car(“Lexus”, “gasoline”, 350),
  3, new Car(“Chevrolet”, “electric”, 150),
  4, new Car(“Mercedes”, “gasoline”, 150),
  5, new Car(“Chevrolet”, “diesel”, 250),
  6, new Car(“Ford”, “electric”, 80),
  7, new Car(“Chevrolet”, “diesel”, 450),
  8, new Car(“Mercedes”, “electric”, 200),
  9, new Car(“Chevrolet”, “gasoline”, 350),
  10, new Car(“Lexus”, “diesel”, 300)
);

Next, we want to sort this map into a List<String> as follows:

If the horsepower values are different then sort in descending order by horsepower

If the horsepower values are equal then sort in ascending order by the map keys

The result, List<String>, should contain items of type key(horsepower)

Under these statements, sorting the cars map will result in:[7(450), 1(350), 2(350), 9(350), 10(300), 5(250), 8(200), 3(150), 4(150), 6(80)]Obviously, this problem requires a custom comparator. Having two map entries (c1, c2), we elaborate the following logic:

Check if c2’s horsepower is equal to c1’s horsepower If they are equal, then compare c1’s key with c2’key Otherwise, compare c2’s horsepower with c1’s horsepower Collect the result into a List

In code lines, this can be expressed as follows:

List<String> result = cars.entrySet().stream()
  .sorted((c1, c2) -> c2.getValue().getHorsepower()
        == c1.getValue().getHorsepower()
     ? c1.getKey().compareTo(c2.getKey())
     : Integer.valueOf(c2.getValue().getHorsepower())
        .compareTo(c1.getValue().getHorsepower()))
  .map(c -> c.getKey() + “(“
                       + c.getValue().getHorsepower() + “)”)
  .toList();

Or, if we rely on Map.Entry.comparingByValue(), comparingByKey(), and java.util.Comparator then we can write it as follows:

List<String> result = cars.entrySet().stream()
  .sorted(Entry.<Integer, Car>comparingByValue(
            Comparator.comparingInt(
              Car::getHorsepower).reversed())
  .thenComparing(Entry.comparingByKey()))
  .map(c -> c.getKey() + “(“
    + c.getValue().getHorsepower() + “)”)
  .toList();

This approach is more readable and expressive.

194. Filtering a map

Let’s consider the following map:

public class Car {
  private final String brand;
  private final String fuel;
  private final int horsepower;
  …
}
Map<Integer, Car> cars = Map.of(
  1, new Car(“Dacia”, “diesel”, 100),
  …
  10, new Car(“Lexus”, “diesel”, 300)
);

In order to stream a map, we can start from Map’s entrySet(), values(), or keyset() followed by a stream() call. For instance, if we want to express a pipeline as Map -> Stream -> Filter -> String that returns a List<String> containing all the electric brands then we can rely on entrySet() as follows:

String electricBrands = cars.entrySet().stream()
  .filter(c -> “electric”.equals(c.getValue().getFuel()))
  .map(c -> c.getValue().getBrand())
  .collect(Collectors.joining(“, “));

But, as you can see, this stream pipeline doesn’t use the map’s keys. This means that we can better express it via values() instead of entrySet() as follows:

String electricBrands = cars.values().stream()
  .filter(c -> “electric”.equals(c.getFuel()))
  .map(c -> c.getBrand())
  .collect(Collectors.joining(“, “));

This is more readable and it clearly expresses its intention.Here is another example that you should be able to follow without further details:

Car newCar = new Car(“No name”, “gasoline”, 350);
String carsAsNewCar1 = cars.entrySet().stream()
 .filter(c -> (c.getValue().getFuel().equals(newCar.getFuel())
   && c.getValue().getHorsepower() == newCar.getHorsepower()))
 .map(map -> map.getValue().getBrand())
 .collect(Collectors.joining(“, “));
      
String carsAsNewCar2 = cars.values().stream()
 .filter(c -> (c.getFuel().equals(newCar.getFuel())
   && c.getHorsepower() == newCar.getHorsepower()))
 .map(map -> map.getBrand())
 .collect(Collectors.joining(“, “));