To filter a Map by keys or values in Java, we stream its entrySet(), keep the entries that pass a filter() condition on getKey() or getValue(), and collect the remaining entries into a new map with Collectors.toMap(). The original map stays unchanged, and the condition can test the key, the value or both at once.
We filter maps all the time in app code, for example to keep the products that are in stock, the config properties that start with a prefix or the cache entries that belong to one user. When the keys we want are already known and sit in a list, a lookup is a better fit than a condition, which we cover in filtering a Map by a list of keys.
The following example filters a small fruit stock map by key, by value and by both.
Map<String, Integer> stock = Map.of("apple", 5, "banana", 0, "cherry", 12, "lime", 8, "mango", 3);
Map<String, Integer> shortNames = stock.entrySet().stream()
.filter(e -> e.getKey().length() <= 4)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); // {lime=8}
Map<String, Integer> plenty = stock.entrySet().stream()
.filter(e -> e.getValue() > 10)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); // {cherry=12}
Map<String, Integer> both = stock.entrySet().stream()
.filter(e -> e.getKey().startsWith("m") && e.getValue() > 0)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); // {mango=3}
Notice that all three pipelines are the same except for the lambda inside filter(). We start with how the entry stream behaves, write reusable helpers that take a Predicate, keep the entry order, filter in place with removeIf(), and handle null values that break toMap().
1. Streaming the Entry Set of a Map
A Map is not a Collection, so it has no stream() method of its own. It offers three collection views instead, keySet(), values() and entrySet(), and we stream the view that holds the data we need in the result. For a filtered map, we need both halves of each pair, so we stream entrySet() and work with Map.Entry objects.

The pipeline has three steps, and only the middle one changes from task to task.
- The call entrySet().stream() produces a Stream<Map.Entry<K, V>> with one element per key-value pair.
- The filter() operation keeps the entries for which the lambda returns true.
- The collector Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue) puts each remaining entry into a new HashMap.
The result of the two-argument toMap() is a HashMap in the current JDK, although the Javadoc makes no promise about the type. A HashMap has no defined order, so the printed order of a filtered map can differ from the input order, as we fix in section 6.
2. Filter a Map by Keys
Key filtering is the common case for maps whose keys carry meaning, such as property names, user names or product codes. The condition reads e.getKey() and ignores the value.
Map<String, Integer> stock = Map.of("apple", 5, "banana", 0, "cherry", 12, "lime", 8, "mango", 3);
Map<String, Integer> fromAtoC = stock.entrySet().stream()
.filter(e -> e.getKey().compareTo("c") < 0)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
int count = fromAtoC.size(); // 2
boolean hasBanana = fromAtoC.containsKey("banana"); // true
When the condition is a range of keys, a TreeMap does the same job without a stream. The method headMap(“c”) returns a view of all keys before “c”, and subMap() returns a range between two keys.
TreeMap<String, Integer> sorted = new TreeMap<>(Map.of("apple", 5, "banana", 0, "cherry", 12, "lime", 8));
SortedMap<String, Integer> beforeC = sorted.headMap("c"); // {apple=5, banana=0}
3. Filter a Map by Values
Value filtering keeps the entries whose value passes a test, for example the items with a quantity above zero. The lambda reads e.getValue(), and the result keeps the key because we still collect whole entries.
Map<String, Integer> stock = Map.of("apple", 5, "banana", 0, "cherry", 12, "lime", 8, "mango", 3);
Map<String, Integer> inStock = stock.entrySet().stream()
.filter(e -> e.getValue() > 0)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
int itemsInStock = inStock.size(); // 4
boolean soldOut = inStock.containsKey("banana"); // false
Values are often objects rather than numbers. With a record as the value, the condition calls the record accessor, such as e.getValue().price().
record Item(int qty, double price) {}
Map<String, Item> shelf = Map.of("apple", new Item(5, 0.5), "cherry", new Item(12, 4.0), "lime", new Item(8, 0.3));
Map<String, Item> pricey = shelf.entrySet().stream()
.filter(e -> e.getValue().price() > 1.0)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); // {cherry=Item[qty=12, price=4.0]}
4. Filter a Map by Keys and Values, Both
A condition on both halves of the entry goes into one lambda joined with && or ||. One filter() with a combined condition is easier to read than two chained filter() calls, and it gives the same result for &&.
Map<String, Integer> stock = Map.of("apple", 5, "banana", 0, "cherry", 12, "lime", 8, "mango", 3);
Map<String, Integer> longAndPlenty = stock.entrySet().stream()
.filter(e -> e.getKey().length() > 4 && e.getValue() > 4)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
Set<String> names = new TreeSet<>(longAndPlenty.keySet()); // [apple, cherry]
Map<String, Integer> limeOrEmpty = stock.entrySet().stream()
.filter(e -> e.getKey().equals("lime") || e.getValue() == 0)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
Set<String> picked = new TreeSet<>(limeOrEmpty.keySet()); // [banana, lime]
We copy the keys into a TreeSet in the snippet only to print them in a fixed order. The filtered maps themselves are plain HashMap objects.
5. Reusable byKey(), byValue() and byEntry() Helpers
When several places in an app filter maps, we move the pipeline into small generic methods and pass the condition in. A Predicate tests one half of the entry, and a BiPredicate receives the key and the value together, so the call site never deals with Map.Entry.
static <K, V> Map<K, V> byKey(Map<K, V> map, Predicate<? super K> keyTest) {
return map.entrySet().stream()
.filter(e -> keyTest.test(e.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
static <K, V> Map<K, V> byValue(Map<K, V> map, Predicate<? super V> valueTest) {
return map.entrySet().stream()
.filter(e -> valueTest.test(e.getValue()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
static <K, V> Map<K, V> byEntry(Map<K, V> map, BiPredicate<? super K, ? super V> test) {
return map.entrySet().stream()
.filter(e -> test.test(e.getKey(), e.getValue()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
Map<String, Integer> stock = Map.of("apple", 5, "banana", 0, "cherry", 12, "lime", 8, "mango", 3);
Map<String, Integer> mFruits = byKey(stock, k -> k.startsWith("m")); // {mango=3}
Map<String, Integer> big = byValue(stock, v -> v >= 12); // {cherry=12}
Map<String, Integer> mix = byEntry(stock, (k, v) -> k.contains("i") && v > 5); // {lime=8}
Map<String, Integer> chained = byValue(byKey(stock, k -> k.length() == 5), v -> v > 3); // {apple=5}
The wildcard Predicate<? super K> lets us pass a general predicate, such as a Predicate<Object> or a Predicate<CharSequence>, to a map with String keys. We can also join two conditions with and(), or() and negate() before passing them, which logical operations on predicates covers.
6. Keeping the Original Order of Entries
The two-argument Collectors.toMap() returns a HashMap, so the filtered map loses the order of a LinkedHashMap or TreeMap input. To keep the order, we use the four-argument overload and pass a merge function and a map factory such as LinkedHashMap::new.
Map<String, Integer> menu = new LinkedHashMap<>();
menu.put("soup", 4);
menu.put("salad", 0);
menu.put("pasta", 9);
menu.put("cake", 6);
LinkedHashMap<String, Integer> available = menu.entrySet().stream()
.filter(e -> e.getValue() > 0)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(a, b) -> a, LinkedHashMap::new)); // {soup=4, pasta=9, cake=6}
Map.Entry<String, Integer> first = available.firstEntry(); // soup=4
The merge function (a, b) -> a never runs here, because keys from one map are unique, but the overload needs it before the factory argument. The result type matters too, because LinkedHashMap and TreeMap implement SequencedMap, added in Java 21, so the result also offers firstEntry(), lastEntry() and reversed(). With TreeMap::new as the factory, the result is sorted by key.
7. Filtering a Map in Place With removeIf()
A stream always builds a new map. When we own a mutable map and want to drop entries from it, removeIf() on one of the views is shorter and creates no copy, because the views write through to the map. The method exists on entrySet(), keySet() and values(), so we pick the view that holds the data the condition needs.
Map<String, Integer> cart = new HashMap<>(Map.of("apple", 5, "banana", 0, "cherry", 12, "lime", 0));
boolean changed = cart.values().removeIf(qty -> qty == 0); // true
boolean noBanana = !cart.containsKey("banana"); // true
cart.keySet().removeIf(name -> name.startsWith("ch"));
cart.entrySet().removeIf(e -> e.getKey().equals("apple") && e.getValue() < 3);
Map<String, Integer> left = new TreeMap<>(cart); // {apple=5}
Calling removeIf() on a map from Map.of() throws UnsupportedOperationException, because that map is unmodifiable. Copy it into a HashMap first, as the snippet does.
Map<String, Integer> fixed = Map.of("apple", 5, "banana", 0);
boolean removed = fixed.values().removeIf(qty -> qty == 0); // UnsupportedOperationException
8. Getting Only the Matching Keys or Values
Sometimes the result does not need to be a map. After the filter() step, we map() each entry to its key or value and call toList(), or stream keySet() or values() when the condition needs only one half.
Map<String, Integer> stock = Map.of("apple", 5, "banana", 0, "cherry", 12, "lime", 8, "mango", 3);
List<String> soldOut = stock.entrySet().stream()
.filter(e -> e.getValue() == 0)
.map(Map.Entry::getKey)
.toList(); // [banana]
List<Integer> bigCounts = stock.values().stream().filter(v -> v > 6).sorted().toList(); // [8, 12]
List<String> withA = stock.keySet().stream().filter(k -> k.contains("a")).sorted().toList(); // [apple, banana, mango]
The toList() method was added in Java 16 and returns an unmodifiable list. We add sorted() to get a stable order out of a HashMap source. Other result types, such as an array or a Set, are in converting a Map to a List, Set or array.
9. Null Values and Collectors.toMap()
A HashMap allows null values, but Collectors.toMap() does not. The collector rejects a null value with a NullPointerException, even though the map we filter is valid.
Map<String, Integer> counts = new HashMap<>();
counts.put("apple", 5);
counts.put("kiwi", null);
Map<String, Integer> all = counts.entrySet().stream().filter(e -> true).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); // NullPointerException
Either filter the null values out first or collect with the three-argument collect() and HashMap::put, which accepts null.
Map<String, Integer> counts = new HashMap<>();
counts.put("apple", 5);
counts.put("kiwi", null);
Map<String, Integer> nonNull = counts.entrySet().stream()
.filter(e -> e.getValue() != null)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); // {apple=5}
Map<String, Integer> keepNulls = counts.entrySet().stream()
.filter(e -> e.getKey().length() == 4)
.collect(HashMap::new, (m, e) -> m.put(e.getKey(), e.getValue()), HashMap::putAll); // {kiwi=null}
10. Choosing a Map Filtering Approach
Streams are the default for a filtered copy, but they are not the only tool. Guava adds Maps.filterKeys(), filterValues() and filterEntries(), which return a live view of the original map instead of a copy.
Map<String, Integer> stock = new HashMap<>(Map.of("apple", 5, "banana", 0, "cherry", 12));
Map<String, Integer> view = Maps.filterValues(stock, qty -> qty > 0);
stock.put("lime", 8);
int visible = view.size(); // 3
The view saw the lime entry that we added after creating it, because it filters on every read. The table lists when each approach fits.
| Approach | Result | Original map | Use when |
|---|---|---|---|
| entrySet().stream().filter().collect(toMap()) | New HashMap | Unchanged | We need a filtered copy, the default case |
| Same with toMap(…, LinkedHashMap::new) | New ordered map | Unchanged | The order of the input matters |
| entrySet().removeIf() | Same map, smaller | Changed | We own a mutable map and want to drop entries |
| TreeMap.headMap() or subMap() | Live view | Unchanged | The condition is a key range on a sorted map |
| Guava Maps.filterKeys() | Live view | Unchanged | Callers must always see current data without copying |
| Stream of keySet() or values() | List or Set | Unchanged | Only the keys or the values are needed |
11. Real-World Example of Enabling Beta Feature Flags
Say an app reads its feature flags into a Map<String, Boolean> at startup, with names such as beta.search and beta.dark-mode. The admin page must list only the beta flags that are switched on, in the order they appear in the config file.
static LinkedHashMap<String, Boolean> enabledBetaFlags(Map<String, Boolean> flags) {
return flags.entrySet().stream()
.filter(e -> e.getKey().startsWith("beta.") && Boolean.TRUE.equals(e.getValue()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(a, b) -> a, LinkedHashMap::new));
}
Map<String, Boolean> flags = new LinkedHashMap<>();
flags.put("beta.search", true);
flags.put("checkout.v2", true);
flags.put("beta.dark-mode", false);
flags.put("beta.export", true);
flags.put("beta.chat", null);
LinkedHashMap<String, Boolean> shown = enabledBetaFlags(flags); // {beta.search=true, beta.export=true}
The condition checks the key prefix and the value in one lambda, and Boolean.TRUE.equals() treats the unset null flag as disabled instead of failing in toMap(). The LinkedHashMap factory keeps the config file order on the admin page.
12. Map Filtering FAQs
Readers most often ask about Java versions, in-place changes, entry order, key lists and nested maps.
12.1. How do we filter a HashMap by value in Java 8 and later?
Stream entrySet(), test e.getValue() in filter() and collect with Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue). The code is the same from Java 8 to Java 25, and Java 16 adds toList() for the case where we need only the matching keys or values.
12.2. Can we filter a map without creating a new map?
Yes. Call removeIf() on entrySet(), keySet() or values() of a mutable map, as shown in section 7. The removal changes the original map, so it does not work on maps from Map.of() or Collections.unmodifiableMap().
12.3. Why does the filtered map have a different order than the original?
The two-argument Collectors.toMap() collects into a HashMap, which has no defined order. Pass LinkedHashMap::new or TreeMap::new as the fourth argument to keep insertion order or sort by key.
12.4. How do we filter a map by a list of keys?
Loop over the key list and look each key up, or stream the entries and test keys.contains(e.getKey()) against a Set. On a mutable copy, keySet().retainAll(keys) does the same job in one call.
12.5. How do we filter a nested map such as Map<String, Map<String, Integer>>?
Filter the outer entries by testing the inner map in the lambda, for example e.getValue().containsKey(“price”). To filter the inner maps too, map each outer entry to a new entry whose value is the filtered inner map. More nested map operations are in Java nested maps.
13. Conclusion
Filtering a map with streams always uses the same three steps. We stream entrySet(), test the key, the value or both inside filter(), and collect with Collectors.toMap(). Generic helpers with a Predicate or BiPredicate keep that pipeline in one place.
The code is correct in production only when it handles order, in-place changes and null values. Pass LinkedHashMap::new when order matters, use removeIf() when changing the map in place is the goal, and keep null values away from toMap(). More stream recipes are grouped by task in the Java Streams guide.
14. References
Happy Learning !!