33

I have a HashMap and I would like to get a new HashMap that contains only the elements from the first HashMap where K belongs to a specific List.

I could look through all the keys and fillup a new HashMap but I was wondering if there is a more efficient way to do it?

thanks

aregnier
  • 1,574
  • 2
  • 14
  • 19

12 Answers12

36

With Java8 streams, there is a functional (elegant) solution. If keys is the list of keys to keep and map is the source Map.

keys.stream()
    .filter(map::containsKey)
    .collect(Collectors.toMap(Function.identity(), map::get));

Complete example:

    List<Integer> keys = new ArrayList<>();
    keys.add(2);
    keys.add(3);
    keys.add(42); // this key is not in the map

    Map<Integer, String> map = new HashMap<>();
    map.put(1, "foo");
    map.put(2, "bar");
    map.put(3, "fizz");
    map.put(4, "buz");

    Map<Integer, String> res = keys.stream()
        .filter(map::containsKey)
        .collect(Collectors.toMap(Function.identity(), map::get));

    System.out.println(res.toString());

Prints: {2=bar, 3=fizz}

EDIT add a filter for keys that are absent from the map

T.Gounelle
  • 5,953
  • 1
  • 22
  • 32
11

Yes there is a solution:

Map<K,V> myMap = ...;
List<K> keysToRetain = ...;
myMap.keySet().retainAll(keysToRetain);

The retainAll operation on the Set updates the underlying map. See java doc.

Edit Be aware this solution modify the Map.

T.Gounelle
  • 5,953
  • 1
  • 22
  • 32
6

With a help of Guava.

Suppose you have a map Map<String, String> and want to submap with a values from List<String> list.

Map<String, String> map = new HashMap<>();
map.put("1", "1");
map.put("2", "2");
map.put("3", "4");

final List<String> list = Arrays.asList("2", "4");

Map<String, String> subMap = Maps.filterValues(
                map, Predicates.in(list));

Update / Note: As @assylias mentioned in the comment, you will have O(n) when using contains(). So if you have large list, this could have huge impact in performance.

On the other side HashSet.contains() is constant time O(1), so if there is a possibility to have Set instead of List, this could be a nice approach (note that converting List to Set will cost O(n) anyway, so better not to convert :))

Louis Wasserman
  • 191,574
  • 25
  • 345
  • 413
vtor
  • 8,989
  • 7
  • 51
  • 67
  • Same comment as for the view: this could be awfully slow if the list is large-ish. – assylias Mar 04 '15 at 16:37
  • Very good point. Updated my answer to mention the performance impact on huge lists. – vtor Mar 04 '15 at 17:01
  • I'd love to use Guava on this project but I can't. Also the solution is interesting but I'd like to know if there is a solution without external library. – aregnier Mar 06 '15 at 17:07
  • 1
    The question is about filtering based on keys, not values, so Maps.filterKeys() would be correct. – tkruse Mar 30 '21 at 10:25
5

If you have Map m1 and List keys, then try following

Map m2 = new HashMap(m1);
m2.keySet().retainAll(keys);
Evgeniy Dorofeev
  • 133,369
  • 30
  • 199
  • 275
3

Depending on your usage, this may be a more efficient implementation

public class MapView implements Map{
  List ak;
  Map map;
  public MapView(Map map, List allowableKeys) {
     ak = allowableKeys;
     map = map;
  }
  public Object get(Object key) {
    if (!ak.contains(key)) return null;
    return map.get(key);
  }
}
ControlAltDel
  • 33,923
  • 10
  • 53
  • 80
1

If your keys have an ordering, you can use a TreeMap.

Look at TreeMap.subMap()

It does not let you do this using a list, though.

user1717259
  • 2,717
  • 6
  • 30
  • 44
1

Copy the map and remove all keys not in the list:

Map map2 = new Hashmap(map);
map2.keySet().retainAll(keysToKeep);
kapex
  • 28,903
  • 6
  • 107
  • 121
1

Instead of looking through all keys you could loop over the list and check if the HashMap contains a mapping. Then create a new HashMap with the filtered entries:

List<String> keys = Arrays.asList('a', 'c', 'e');

Map<String, String> old = new HashMap<>();
old.put('a', 'aa');
old.put('b', 'bb');
old.put('c', 'cc');
old.put('d', 'dd');
old.put('e', 'ee');

// only use an inital capacity of keys.size() if you won't add
// additional entries to the map; anyways it's more of a micro optimization
Map<String, String> newMap = new HashMap<>(keys.size(), 1f);

for (String key: keys) {
    String value = old.get(key);
    if (value != null) newMap.put(key, value);
}
helpermethod
  • 59,493
  • 71
  • 188
  • 276
  • 3
    You cannot use `new` as variable name. – Bubletan Mar 04 '15 at 14:30
  • @Bubletan You are correct, renaming the variable to newMap. – helpermethod Mar 04 '15 at 14:32
  • 1
    @helpermethod you should use `new HashMap<> (keys.size(), 1f);`, otherwise a load fatcor of 0.75 will be used and the map may be resized once – assylias Mar 04 '15 at 14:46
  • 1
    `ìf (value != null)` is not a valid substitue for `containsKey` with HashMap. One could have written `hashMap.put("key", null)`, and then your `if (value != null)` test would be fooled. (Granted, this is such an frequent design at this point that setting a null value in a Map seems like a bad idea in the first place) – GPI Mar 04 '15 at 15:05
  • 1
    there are cases where you may want to store null values in your HashMap (you can still verify if it contains the key to disambiguate between a mapping with null or the absence of value for that specific key)... for example when you use it to cache results of computer intensive functions (where the result could be null)... so that would not work. – aregnier Mar 06 '15 at 17:16
1

You could even grow your own:

public class FilteredMap<K, V> extends AbstractMap<K, V> implements Map<K, V> {

    // The map I wrap.
    private final Map<K, V> map;
    // The filter.
    private final Set<K> filter;

    public FilteredMap(Map<K, V> map, Set<K> filter) {
        this.map = map;
        this.filter = filter;
    }

    @Override
    public Set<Entry<K, V>> entrySet() {
        // Make a new one to break the bond with the underlying map.
        Set<Entry<K, V>> entries = new HashSet<>(map.entrySet());
        Set<Entry<K, V>> remove = new HashSet<>();
        for (Entry<K, V> entry : entries) {
            if (!filter.contains(entry.getKey())) {
                remove.add(entry);
            }
        }
        entries.removeAll(remove);
        return entries;
    }

}

public void test() {
    Map<String, String> map = new HashMap<>();
    map.put("1", "One");
    map.put("2", "Two");
    map.put("3", "Three");
    Set<String> filter = new HashSet<>();
    filter.add("1");
    filter.add("2");
    Map<String, String> filtered = new FilteredMap<>(map, filter);
    System.out.println(filtered);

}

If you're concerned about all of the copying you could also grow a filtered Set and a filterd Iterator instead.

public interface Filter<T> {

    public boolean accept(T t);
}

public class FilteredIterator<T> implements Iterator<T> {

    // The Iterator
    private final Iterator<T> i;
    // The filter.
    private final Filter<T> filter;
    // The next.
    private T next = null;

    public FilteredIterator(Iterator<T> i, Filter<T> filter) {
        this.i = i;
        this.filter = filter;
    }

    @Override
    public boolean hasNext() {
        while (next == null && i.hasNext()) {
            T n = i.next();
            if (filter.accept(n)) {
                next = n;
            }
        }
        return next != null;
    }

    @Override
    public T next() {
        T n = next;
        next = null;
        return n;
    }
}

public class FilteredSet<K> extends AbstractSet<K> implements Set<K> {

    // The Set
    private final Set<K> set;
    // The filter.
    private final Filter<K> filter;

    public FilteredSet(Set<K> set, Filter<K> filter) {
        this.set = set;
        this.filter = filter;
    }

    @Override
    public Iterator<K> iterator() {
        return new FilteredIterator(set.iterator(), filter);
    }

    @Override
    public int size() {
        int n = 0;
        Iterator<K> i = iterator();
        while (i.hasNext()) {
            i.next();
            n += 1;
        }
        return n;
    }

}

public class FilteredMap<K, V> extends AbstractMap<K, V> implements Map<K, V> {

    // The map I wrap.
    private final Map<K, V> map;
    // The filter.
    private final Filter<K> filter;

    public FilteredMap(Map<K, V> map, Filter<K> filter) {
        this.map = map;
        this.filter = filter;
    }

    @Override
    public Set<Entry<K, V>> entrySet() {
        return new FilteredSet<>(map.entrySet(), new Filter<Entry<K, V>>() {

            @Override
            public boolean accept(Entry<K, V> t) {
                return filter.accept(t.getKey());
            }

        });
    }

}

public void test() {
    Map<String, String> map = new HashMap<>();
    map.put("1", "One");
    map.put("2", "Two");
    map.put("3", "Three");
    Set<String> filter = new HashSet<>();
    filter.add("1");
    filter.add("2");
    Map<String, String> filtered = new FilteredMap<>(map, new Filter<String>() {

        @Override
        public boolean accept(String t) {
            return filter.contains(t);
        }
    });
    System.out.println(filtered);

}
OldCurmudgeon
  • 64,482
  • 16
  • 119
  • 213
0

you can use the clone() method on the K HashMap returned.

something like this:

import java.util.HashMap;

public class MyClone {    
     public static void main(String a[]) {    
        Map<String, HashMap<String, String>> hashMap = new HashMap<String, HashMap<String, String>>();    
        Map hashMapCloned = new HashMap<String, String>();    

        Map<String, String> insert = new HashMap<String, String>();

        insert.put("foo", "bar");
        hashMap.put("first", insert);

        hashMapCloned.put((HashMap<String, String>) hashMap.get("first").clone());

    }    
}

It may have some syntax errors because I haven't tested, but try something like that.

Bruno Paulino
  • 5,611
  • 1
  • 41
  • 40
0

No, because HashMap doesn't maintain an order of it's entries. You can use TreeMap if you need a sub map between some range. And also, please look at this question; it seems to be on the similar lines of yours.

Community
  • 1
  • 1
geekprogrammer
  • 1,108
  • 1
  • 13
  • 39
0

You asked for a new HashMap. Since HashMap does not support structure sharing, there is no better approach than the obvious one. (I have assumed here that null cannot be a value).

Map<K, V> newMap = new HashMap<>();
for (K k : keys) {
    V v  = map.get(k);
    if (v != null)
        newMap.put(k, v);
}

If you don't absolutely require that new object created is a HashMap you could create a new class (ideally extending AbstractMap<K, V>) representing a restricted view of the original Map. The class would have two private final fields

Map<? extends K, ? extends V> originalMap;
Set<?> restrictedSetOfKeys;

The get method for the new Map would be something like this

@Override
public V get(Object k) {
    if (!restrictedSetOfKeys.contains(k))
        return null;
    return originalMap.get(k);
}

Notice that it is better if the restrictedSetOfKeys is a Set rather than a List because if it is a HashSet you would typically have O(1) time complexity for the get method.

Paul Boddington
  • 37,127
  • 10
  • 65
  • 116