2

Based on the following question: filter Map in Java 8 Streams

public void filterStudents(Map<Integer, Student> studentsMap){
    Map<Integer, Student> filteredStudentsMap = 
        studentsMap.entrySet()
                   .stream()
                   .filter(s -> s.getValue().getAddress().equalsIgnoreCase("delhi"))
                   .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

This filter students leaving in dehli. How could I filter students leaving in dehli, amsterdam or new york?

Is there a better way than filtering three times the original map and merging the three outputs together?

Nakrule
  • 511
  • 8
  • 24
  • The part after the `->` can by any Java expression that you want. So write an arbitrarily complex statement that checks if the address is any of the desired values and returns `true` in that case. – Joachim Sauer Sep 12 '19 at 09:01

5 Answers5

4

There is Predicate#or(Predicate) to logically compose two Predicates.

Predicate<Student> livesInDelhi = student -> "delhi".equalsIgnoreCase(student.getAddress());
Predicate<Student> livesInAmsterdam = student -> "amsterdam".equalsIgnoreCase(student.getAddress());
Predicate<Student> livesInNewYork = student -> "new york".equalsIgnoreCase(student.getAddress());

Predicate<Student> livesInAnyOfTheseThreeCities = livesInDelhi.or(livesInAmsterdam).or(livesInNewYork);

A filter call would look like

.filter(e -> livesInAnyOfTheseThreeCities.test(e.getValue()))

How could I adapt the fourth lines where you're chaining filtering parameters?

Assuming we have an array of cities

final String[] cities = {"delhi", "amsterdam", "new york"};

for each Student, we could write a Predicate<Student> and reduce them by Predicate::or

Predicate<Student> livesInAnyOfGivenCities = 
  Arrays.stream(cities)
    .map(city -> (Predicate<Student>) student -> city.equalsIgnoreCase(student.getAddress()))
    .reduce(Predicate::or)
    .orElseGet(() -> student -> false);

student -> false is used when there are no cities given.

Andrew Tobilko
  • 48,120
  • 14
  • 91
  • 142
  • @Naman your assumption is also nice, it would reduce repetitive `student.getAddress()` calls. However, I wanted to emphasise the transition from a `Student` to a `boolean` (not `String->boolean` because I believe `address` should be more complex than a `String`) – Andrew Tobilko Sep 12 '19 at 09:18
  • In you example you have three predicates, but let's assume I don't know how many I would have (like I receive an array of cities, sometimes with x cities, sometimes with y). How could I adapt the fourth lines where you're chaining filtering parameters? – Nakrule Sep 12 '19 at 09:20
2

Of course, you should use plain old object oriented programming here and create a separate method with meaningful name:

public void filterStudents(Map<Integer, Student> studentsMap){
Map<Integer, Student> filteredStudentsMap = 
    studentsMap.entrySet()
               .stream()
               .filter(s -> s.getValue().liveIn("delhi", "amsterdam", "new york"))
               .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

Of course this requires creating a corresponding method in Student class, but why else do we need objects and OOP?)

public class Student {

   // other methods of Student

   public boolean liveIn(String... cities) {
     return Arrays.stream(cities).anyMatch(this.city::equals);
   }
}

Array is just for example - you can use set, list or whatever you want. The point here is to create a meaningful methods that could be used in stream api.

star67
  • 1,505
  • 7
  • 16
  • “plain old object oriented programming” would be `Arrays.asList(cities).contains(city)` instead of `Arrays.stream(cities).anyMatch(this.city::equals)` – Holger Sep 12 '19 at 11:26
1

Use one filter with multiple conditions:

public void filterStudents(Map<Integer, Student> studentsMap){
    Map<Integer, Student> filteredStudentsMap = 
        studentsMap.entrySet()
                   .stream()
                   .filter(s -> s.getValue().getAddress().equalsIgnoreCase("delhi") ||
                                s.getValue().getAddress().equalsIgnoreCase("amsterdam") ||
                                s.getValue().getAddress().equalsIgnoreCase("new york"))
                   .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

Or you can simplify with a Set:

public void filterStudents(Map<Integer, Student> studentsMap){
    Map<Integer, Student> filteredStudentsMap = 
        studentsMap.entrySet()
                   .stream()
                   .filter(s -> Set.of("delhi","amsterdam","new york").contains(s.getValue().getAddress().toLowerCase()))
                   .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
Eran
  • 387,369
  • 54
  • 702
  • 768
1
List<String> toFilter = Arrays.asList("delhi", "amsterdam", "new york");
Map<Integer, Student> filteredStudentsMap =
        studentsMap.entrySet()
                .stream()
                .filter(s -> toFilter.stream().anyMatch(f -> s.getValue().getAddress().equalsIgnoreCase(f)))
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
lczapski
  • 4,026
  • 3
  • 16
  • 32
  • Like with other similar answers, [this approach is incorrect](https://stackoverflow.com/questions/57903213/apply-multiple-filter-to-a-map-in-java/57903284#comment102227286_57903397) and [using `List` as well as unnecessarily creating new strings is inefficient](https://stackoverflow.com/questions/57903213/apply-multiple-filter-to-a-map-in-java/57903284#comment102231035_57903397)… – Holger Sep 12 '19 at 11:24
1

You could try this one too, if you are unsure, how much of the cities could come in the filter condition in near future..

public void filterStudents(Map<Integer, Student> studentsMap){
final List<String> includedCities = List.of("DELHI", "NEW YORK", "AMSTERDEM", "SOME MORE");

Map<Integer, Student> filteredStudentsMap = 
    studentsMap.entrySet()
               .stream()
               .filter(s -> includedCities.contains(s.toUpperCase()))
               .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

UPDATE (after comments by @JoachimSauer):

This should be much finer..

public void filterStudents(Map<Integer, Student> studentsMap){
final List<String> includedCities = List.of("DELHI", "NEW YORK", "AMSTERDEM", "SOME MORE");

Map<Integer, Student> filteredStudentsMap = 
    studentsMap.entrySet()
               .stream()
               .filter(s -> exclusiveCities.stream().anyMatch(s::equalsIgnoreCase))
               .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

UPDATE (after comments by @Holger):

Even Better..

public void filterStudents(Map<Integer, Student> studentsMap){
final Set<String> includedCities = new TreeSet<>(CASE_INSENSITIVE_ORDER);
Collections.addAll( includedCities , "DELHI", "NEW YORK", "AMSTERDEM", "SOME MORE");

Map<Integer, Student> filteredStudentsMap = 
    studentsMap.entrySet()
               .stream()
               .filter(includedCities::contains))
               .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
miiiii
  • 1,580
  • 1
  • 16
  • 29
  • 2
    Just make sure your code [never runs in Turkey](https://blog.codinghorror.com/whats-wrong-with-turkey/): `"delhi".toUpperCase().equals("DELHI")` will evaluate to `false` in any Turkish locale, because `"delhi".toUpperCase()` will return `"DELHİ"` (yes, that's a capital-I with a dot on top of it). Don't use `toUpperCase()` or `toLowerCase()` when you really want to do `equalsIgnoreCase()`! – Joachim Sauer Sep 12 '19 at 09:25
  • @JoachimSauer thanks.. I wasn't aware of this. :) – miiiii Sep 12 '19 at 09:47
  • 2
    The better solution would be `Set excludedCities=new TreeSet<>(CASE_INSENSITIVE_ORDER ); Collections.addAll(excludedCities, "DELHI", "NEW YORK", "AMSTERDEM", "SOME MORE");`, followed by using `.filter(exclusiveCities::contains)` in the stream. That’s avoiding the the problem @JoachimSauer mentioned, as well as the creation of new strings via `toUpperCase` or `toLowerCase` and the linear search through a `List`. In other words, is more efficient. – Holger Sep 12 '19 at 11:18
  • Yup. I had thought abt using `Set` instead of `List` as data-structure after posting the answer. But never thought of this. This is the reason why I love StackOverflow. Thanks @Holger. :) – miiiii Sep 12 '19 at 11:41
  • Just noticed that `excludedCities` should actually be named `includedCities`… – Holger Sep 12 '19 at 11:52
  • Updated.. but it wasn't `excludedCities` initially, it was `exclusiveCities`.. got messed up in some frequent edits. – miiiii Sep 12 '19 at 11:55