Filter a Map by a List of Keys in Java: Lookup vs Stream

Learn to filter a map by a list of keys in Java with get() lookups, streams and retainAll(), keep the key order and report the keys that are missing.

A list of keys looked up in a map, with found keys copied to the result map and a missing key skipped

Filtering a Map by a list of keys in Java means looking up each key of the list with get() and copying the hits into a new map, or streaming the map entries and keeping those whose key is in a Set built from the list. Both ways return a smaller map that holds only the requested keys, and keys from the list that the map does not contain are skipped.

We need this whenever a request, a cart or a report names the exact IDs it wants, for example the user profiles for five user IDs or the prices for the products in a cart. When the condition is a rule instead of a fixed set of keys, such as “price above 10”, see filtering a Map by keys and values with streams.

The following example selects three users out of six by their IDs, first with a lookup loop and after that with a stream.

Map<Integer, String> users = Map.of(1, "Alex", 2, "Allen", 3, "Brian", 4, "Bob", 5, "Charles", 6, "David");
List<Integer> ids = List.of(5, 1, 3, 7);

Map<Integer, String> picked = new LinkedHashMap<>();
for (Integer id : ids) {
    String name = users.get(id);
    if (name != null) {
        picked.put(id, name);
    }
}
Map<Integer, String> byLookup = picked;                       // {5=Charles, 1=Alex, 3=Brian}

Set<Integer> idSet = Set.copyOf(ids);
Map<Integer, String> byStream = users.entrySet().stream()
        .filter(e -> idSet.contains(e.getKey()))
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
                (a, b) -> a, TreeMap::new));                // {1=Alex, 3=Brian, 5=Charles}

Notice that the ID 7 is in the list but not in the map, so neither result contains it, and that the lookup loop keeps the order of the key list. We compare the two approaches, filter a mutable map in place with retainAll(), remove a list of keys, collect only the values and report the keys that were not found.

1. Lookup or Scan to Filter a Map by a List of Keys

There are two ways to pick entries by key, and they differ in which collection drives the loop. A lookup walks the key list and asks the map for each key, so it touches only the requested keys. A scan walks every entry of the map and asks the key collection whether it contains the entry key.

A list of keys looked up in a map, with found keys copied to the result map and a missing key skipped
A lookup asks the map once per requested key; key 7 is not in the map, so it is skipped

The cost of each approach depends on the sizes, where k is the number of keys in the list and n is the size of the map.

ApproachWork doneResult orderBest for
Loop over the key list with get()k map lookupsOrder of the key listFew keys, large map (the common case)
Stream entries, test Set.contains()n set lookupsMap order, or sorted with TreeMap::newKey list about as big as the map
Stream entries, test List.contains()n times k comparisonsMap orderNever for large inputs
keySet().retainAll(keys)n lookups in keysMap orderChanging a mutable map in place

Calling List.contains() inside filter() scans the whole list for every map entry, so we convert the list to a Set first or loop over the list instead. For 10 keys and a 100,000-entry map, the lookup loop does 10 hash lookups, while the scan visits all 100,000 entries.

2. Looking Up Each Key in the Map

The lookup approach is the most direct translation of the task, “give me these keys”. We iterate the key list, keep the keys for which containsKey() is true and read the value with get(). A LinkedHashMap keeps the result in the order of the key list, which is what a caller that sent the IDs in a specific order expects.

Map<Integer, String> users = Map.of(1, "Alex", 2, "Allen", 3, "Brian", 4, "Bob", 5, "Charles", 6, "David");
List<Integer> ids = List.of(6, 2, 7, 2);

Map<Integer, String> selected = ids.stream()
        .filter(users::containsKey)
        .collect(Collectors.toMap(id -> id, users::get, (first, dup) -> first, LinkedHashMap::new));   // {6=David, 2=Allen}

The merge function (first, dup) -> first matters in the stream version. The key list contains 2 twice, and without a merge function Collectors.toMap() throws an IllegalStateException for the duplicate key.

Map<Integer, String> users = Map.of(1, "Alex", 2, "Allen");
List<Integer> ids = List.of(2, 2);
Map<Integer, String> dupes = ids.stream().filter(users::containsKey).collect(Collectors.toMap(id -> id, users::get));   // IllegalStateException: Duplicate key 2

The plain loop from the intro does not have that problem, because a second put() with the same key replaces the value. If the map can hold null values, test with containsKey() instead of get() != null, and collect with the loop, since toMap() rejects null values.

3. Streaming the Entries Against a Set of Keys

The scan approach streams entrySet() and keeps entries whose key is in the key collection. It reads well and fits into longer pipelines, for example when the same stream also maps or sorts the entries. We copy the key list into a HashSet or call Set.copyOf(), so each contains() call is a constant-time hash lookup.

Map<Integer, String> users = Map.of(1, "Alex", 2, "Allen", 3, "Brian", 4, "Bob", 5, "Charles", 6, "David");
List<Integer> ids = List.of(4, 6, 9);

Set<Integer> wanted = new HashSet<>(ids);
Map<Integer, String> found = users.entrySet().stream()
        .filter(e -> wanted.contains(e.getKey()))
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
int size = found.size();                                     // 2
String fourth = found.get(4);                                // "Bob"

Without a map factory, toMap() collects into a HashMap, so the order of found is undefined. Pass TreeMap::new as in the intro for keys sorted ascending, or LinkedHashMap::new to keep the order of a LinkedHashMap source.

The scan also works when the key list does not hold the map keys themselves but values derived from the entries. For example, to keep users whose name starts with one of several letters, test letters.contains(e.getValue().charAt(0)) in the same filter().

4. Keeping Only the Listed Keys With retainAll()

When we own a mutable map and want to shrink it, we do not need a stream at all. The keySet() view writes through to the map, so keySet().retainAll(keys) removes every entry whose key is not in the collection.

Map<Integer, String> users = new HashMap<>(Map.of(1, "Alex", 2, "Allen", 3, "Brian", 4, "Bob"));
Set<Integer> keep = Set.of(1, 3, 7);

boolean changed = users.keySet().retainAll(keep);             // true
Map<Integer, String> left = new TreeMap<>(users);             // {1=Alex, 3=Brian}

The method retainAll() calls contains() on its argument for each map key, so pass a Set here too. On a map from Map.of() or Collections.unmodifiableMap(), the call throws UnsupportedOperationException, so we copy the map into a HashMap first.

Map<Integer, String> fixed = Map.of(1, "Alex", 2, "Allen");
boolean shrunk = fixed.keySet().retainAll(Set.of(1));         // UnsupportedOperationException

5. Removing a List of Keys From a Map

The opposite task, dropping a list of keys and keeping the rest, uses the same two tools. On a mutable map, keySet().removeAll(keys) deletes the listed keys. For a new map, the stream condition becomes !keys.contains(e.getKey()).

Map<Integer, String> users = new HashMap<>(Map.of(1, "Alex", 2, "Allen", 3, "Brian", 4, "Bob"));
Set<Integer> blocked = Set.of(2, 4);

Map<Integer, String> allowed = users.entrySet().stream()
        .filter(e -> !blocked.contains(e.getKey()))
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, TreeMap::new));   // {1=Alex, 3=Brian}
users.keySet().removeAll(blocked);
Map<Integer, String> afterRemove = new TreeMap<>(users);      // {1=Alex, 3=Brian}

6. Getting Only the Values for a List of Keys

Often the caller needs the values, not a map, for example the names to show in a list. We stream the key list, map each key to its value with users::get and drop the null results of missing keys before collecting with toList().

Map<Integer, String> users = Map.of(1, "Alex", 2, "Allen", 3, "Brian", 4, "Bob", 5, "Charles", 6, "David");
List<Integer> ids = List.of(3, 8, 1);

List<String> names = ids.stream().map(users::get).filter(Objects::nonNull).toList();   // [Brian, Alex]
List<String> withDefault = ids.stream().map(id -> users.getOrDefault(id, "unknown")).toList();   // [Brian, unknown, Alex]

The first line keeps the order of the key list and skips 8 without a trace, whereas getOrDefault() keeps one value per requested key, which suits a table that must show a row for every ID.

7. Finding the Keys That Are Missing

Skipping unknown keys without a log line hides bugs, such as a stale product ID in a cart or a user ID that was deleted. Before we filter, we can split the key list into keys the map knows and keys it does not with Collectors.partitioningBy().

Map<Integer, String> users = Map.of(1, "Alex", 2, "Allen", 3, "Brian");
List<Integer> ids = List.of(1, 9, 3, 12);

Map<Boolean, List<Integer>> split = ids.stream().collect(Collectors.partitioningBy(users::containsKey));
List<Integer> known = split.get(true);                        // [1, 3]
List<Integer> missing = split.get(false);                     // [9, 12]

Depending on the use case, the app can log the missing keys, return them to the client in the response or throw an exception for the first one.

8. Filtering by Keys With Guava

Projects that already use Guava can call Maps.filterKeys() with the key set’s contains method as the predicate. The result is a live view of the original map rather than a copy, so later changes to the map show up in the view. Wrap it in Map.copyOf() or ImmutableMap.copyOf() when a snapshot is needed.

Map<Integer, String> users = new HashMap<>(Map.of(1, "Alex", 2, "Allen", 3, "Brian"));
Set<Integer> wanted = Set.of(1, 3, 5);

Map<Integer, String> view = Maps.filterKeys(users, wanted::contains);
users.put(5, "Charles");
int visible = view.size();                                    // 3

9. Pricing a Shopping Cart From a Price Catalog

Say an online shop keeps its price catalog in a Map<String, BigDecimal> with thousands of product codes, and a cart holds a short list of codes. To price the cart, we need the prices for the cart items only, in cart order, and we must reject the cart if an item is no longer sold.

record PricedCart(Map<String, BigDecimal> prices, List<String> unknown) {}
static PricedCart priceCart(Map<String, BigDecimal> catalog, List<String> cartCodes) {
    Map<String, BigDecimal> prices = new LinkedHashMap<>();
    List<String> unknown = new ArrayList<>();
    for (String code : cartCodes) {
        BigDecimal price = catalog.get(code);
        if (price == null) {
            unknown.add(code);
        } else {
            prices.put(code, price);
        }
    }
    return new PricedCart(prices, List.copyOf(unknown));
}
Map<String, BigDecimal> catalog = Map.of("tea", new BigDecimal("4.50"), "mug", new BigDecimal("9.99"), "jam", new BigDecimal("3.20"));
PricedCart cart = priceCart(catalog, List.of("mug", "tea", "cake"));
Map<String, BigDecimal> cartPrices = cart.prices();          // {mug=9.99, tea=4.50}
List<String> notSold = cart.unknown();                        // [cake]

The lookup loop fits the job, because the cart has a handful of codes and the catalog is large. It reads the catalog once per cart item and collects the unknown codes in the same pass, so the checkout can tell the customer that cake is no longer available.

10. Filter Map by Keys FAQs

Submaps, slow filters, missing keys, in-place changes and value lists are the usual follow-ups to key-list filtering.

10.1. How do we get a submap for a list of keys?

Loop over the keys and copy the hits into a new LinkedHashMap, as in section 2. The method TreeMap.subMap() is different, because it returns a range of keys between two bounds, not an arbitrary set of keys.

10.2. Is List.contains() inside Stream.filter() slow?

Yes, for large inputs. Each contains() call on an ArrayList scans the list, so the filter does about n times k comparisons. Convert the list with Set.copyOf(list) or new HashSet<>(list) before the stream.

10.3. What happens if a key from the list is not in the map?

Every approach shown above skips it. The method get() returns null for a missing key, so a lookup loop must check the result, and getOrDefault() or partitioningBy() help when we want to show or report the missing keys.

10.4. Does keySet().retainAll() change the original map?

Yes. The keySet() view writes through, so retainAll() removes the entries from the map itself. Call it on a copy, such as new HashMap<>(map), when the original must stay intact.

10.5. How do we filter a map by a list of values instead of keys?

Stream entrySet() and test valueSet.contains(e.getValue()) in filter(). Values are not indexed, so a lookup loop is not possible, and the scan is the only option without an inverted map. To build one, see inverting a map.

11. Conclusion

Filtering a map by a list of keys is a lookup problem first. A loop over the key list with get() or containsKey() touches only the requested keys, keeps the list order in a LinkedHashMap and lets us report missing keys in the same pass.

A stream over entrySet() is the better fit when the filter is part of a longer pipeline, as long as the keys sit in a Set. For mutable maps, keySet().retainAll() and removeAll() do the same work in place. Related stream recipes are grouped in the Java Streams guide.

12. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.