27

I have a Map<String, String> and a List<String>. I'd like to partition the Map based on the condition

foreach(map.key -> list.contains(map.key))

and produce two Map(s). What's the most elegant way to do so? I'm on Java 11, so you can throw everything you want in the answers.

What I came up to for now is:

map.entrySet()
   .stream()
   .collect(partitioningBy(e -> list.contains(o.getKey())));

but that gives a Map<Boolean, List<Entry<String, String>>>.

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
LppEdd
  • 20,274
  • 11
  • 84
  • 139
  • 1
    Please read https://stackoverflow.com/questions/27993604/whats-the-purpose-of-partitioningby – Torben Feb 06 '19 at 09:09
  • 4
    Could you give an example with real data what you want to achieve? And I wouldnt necessarily go full stream here: a stream creates ONE result, but you want TWO maps?! – GhostCat Feb 06 '19 at 09:16
  • See related stackoverflow.com/questions/28856781 . The best approach depends on the sizes of the Map and the list, and how the result should be used. As an example Guava Maps can simply and quickly provide 2 filtered views on the original map to avoid additional memory usage, but may have very bad performance if contains() is called a lot on the results. – tkruse Mar 29 '21 at 10:53

6 Answers6

31

You can reduce each group using toMap (as a downstream collector):

Map<String, String> myMap = new HashMap<>();
myMap.put("d", "D");
myMap.put("c", "C");
myMap.put("b", "B");
myMap.put("A", "A");

List<String> myList = Arrays.asList("a", "b", "c");

Map<Boolean, Map<String, String>> result = myMap.entrySet()
        .stream()
        .collect(Collectors.partitioningBy(
                            entry -> myList.contains(entry.getKey()),
                            Collectors.toMap(Entry::getKey, Entry::getValue)
                    )
        );

And for this example, that produces {false={A=A, d=D}, true={b=B, c=C}}

ernest_k
  • 44,416
  • 5
  • 53
  • 99
9

Though partitioningBy is the way to go when you need both the alternatives as an output based on the condition. Yet, another way out (useful for creating map based on a single condition) is to use Collectors.filtering as :

Map<String, String> myMap = Map.of("d", "D","c", "C","b", "B","A", "A");
List<String> myList = List.of("a", "b", "c");
Predicate<String> condition = myList::contains;

Map<String, String> keysPresentInList = myMap.keySet()
        .stream()
        .collect(Collectors.filtering(condition,
                Collectors.toMap(Function.identity(), myMap::get)));
Map<String, String> keysNotPresentInList = myMap.keySet()
        .stream()
        .collect(Collectors.filtering(Predicate.not(condition),
                Collectors.toMap(Function.identity(), myMap::get)));

or alternatively, if you could update the existing map in-place, then you could retain entries based on their key's presence in the list using just a one-liner:

myMap.keySet().retainAll(myList);
Naman
  • 27,789
  • 26
  • 218
  • 353
7

You can have filtered map by applying filtering on the original map, e.g.:

List<String> list = new ArrayList<>(); //List of values
Map<String, String> map = new HashMap<>();

Map<String, String> filteredMap = map.entrySet()
.stream()
.filter(e -> list.contains(e.getKey()))
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));

You can then compare the filteredMap contents with original map to extract the entries that are not present in the filteredMap.

Darshan Mehta
  • 30,102
  • 11
  • 68
  • 102
  • This approach has horrible performance O(n * m), when for HashMaps and input, given get() and contains() have O(1), a solution of O(m) is possible. See stackoverflow.com/questions/28856781 – tkruse Mar 29 '21 at 10:43
6

You can't really produce two separate maps using streams (at least not in the most elegant way). Don't be afraid to use the old regular forEach, I think it's quite a clean version:

Map<String, String> contains = new HashMap<>();
Map<String, String> containsNot = new HashMap<>();

for(Map.Entry<String, String> entry : yourMap.entrySet()) {
    if (yourList.contains(entry.getKey())) {
        contains.put(entry.getKey(), entry.getValue());
    } else {
        containsNot.put(entry.getKey(), entry.getValue());
    }
}
Schidu Luca
  • 3,897
  • 1
  • 12
  • 27
6

You could iterate the map and use the goodies introduced in Java 8+:

Map<Boolean, Map<String, String>> result = Map.of(true, new LinkedHashMap<>(), 
                                                  false, new LinkedHashMap<>());
Set<String> set = new HashSet<>(list);
map.forEach((k, v) -> result.get(set.contains(k)).put(k, v));

First, we create a result map with two entries, one for each partition. The values are LinkedHashMaps so that insertion order is preserved.

Then, we create a HashSet from the list, so that invoking set.contains(k) is a O(1) operation (otherwise, if we did list.contains(k), this would be O(n) for each entry of the map, thus yielding a total time complexity of O(n^2), which is bad).

Finally, we iterate the input map and put the (k, v) entry in the corresponding partition, as per the result of invoking set.contains(k).

fps
  • 33,623
  • 8
  • 55
  • 110
2

As a supplement to @ernest_k 's answer you can use a function and groupingBy:

Map<String, String> myMap = new HashMap<>();
myMap.put("d", "D");
myMap.put("c", "C");
myMap.put("b", "B");
myMap.put("A", "A");
List<String> myList = Arrays.asList("a", "b", "c");

Function<Entry<String, String> , Boolean> myCondition =  i -> myList.contains(i.getKey());

Map<Boolean,List<Entry<String, String>>>  myPartedMap = myMap.entrySet()
        .stream().collect(Collectors.groupingBy(myCondition));

System.out.println(myPartedMap);
Gaurang Tandon
  • 6,504
  • 11
  • 47
  • 84
Eritrean
  • 15,851
  • 3
  • 22
  • 28