How to create a Java Map<String,String> with unmodifiable keys?

吃可爱长大的小学妹 提交于 2019-12-11 01:59:59

问题


In java, how should I create a Map<String,String> that has unmodifiable keys, while keeping the values modifiable.

I'd like to hand this Map<String,String> through an interface for someone else to add/change the Map values, but not be able to change the Map keys.

The background on higher level problem is that I have list/set of variable names (with tree like structure) (represented as java String) that I'd like code on the other side of the java interface to be able to populate aliases (also Strings) for each of the variable names. I'd like to have multiple implementations of this interface so naming tree hierarchy can be aliases different ways to fit different situations. Having the interface implementation populate a Map<String,String> with bunch of keys already set-in-stone (and maybe containing defaults for the values) and allowing it to modify the values (but not the keys), seems like the best approach. I'm creating a mapping between names and alias, so Map<> makes sense.

Back to the lower level problem. I'd like my code to resemble:

    public class MyClass
    {
        public interface IMyMapper
        {
            void build(Map<String,String> mapping);
        }

        IMyMapper mapper;

        // How I'd like to use it
        void work()
        {
            Map<String,String> map ;
            // Magic something like Collections unmodifiableMap, but only for keys
            // Maybe my question should be how this magic for UnmodifiableMap works, so I could reproduce it??
            mapper.build(map); 
            // Because Maps<> are by reference, changed they made (to the values) would be reflected here
        }
    }

    public class TheirClass implements MyClass.IMyMapper
    {
        @Override
        public void build(Map<String,String> mapping)
        {
            // use mapping like Map<String,String> without extra/foreign classes
            // but not be able to modify the Map keys
            // only be able to change the Map values
            // Should be able to use all of the awesome Map stuff, like foreach, values, compute
        }
    }

I know there is Collections unmodifiableMap(Map<> m) but that also makes the values unmodifiable. If my values were mutable objects, then I could modify them but I'd like to stick with Strings (avoiding creating Class with set/get for single String member, or creating Structure-like-class with public String member).

AKA, I'd like to avoid creating my own mutable class-values, and use Collections unmodifiableMap() to make the keys and value references unmodifiable:

    // mutable reference to a String
    public class ExtraWorkForForEveryone
    {
        public String value;

        public void setValue(String value) { ... }
        public String getValue() { ... }
    }

    // and then use:
    void work()
    {            
        Map<String,ExtraWorkForEveryone> map;
        map = Collections.unmodifiableMap( ... );
        // because Collections.unmodifiableMap() only stops them from changing the Map references,
        // the interfacer could still change the ExtraWorkForEveryone internals.
        // so they could not change keys refs or value refs, but they could change value data.
        mapper.build(map); 
        // Because Maps<> are by reference, changed they made (to the values) would be reflected here
    }

I could extend or implement my own Map, then (like how Collections unmodifiableMap()) override all methods that could change the keys throw UnsupportedOperationException. But with Java 8, there has been a large number of methods added using Lambda functions, which would be nice for Interface implementers to have access to, as long as they could not change the keys.

AKA, I'd like to avoid this lengthy and error-prone technique:

    public final class FinalHashMap extends HashMap
    {
        @Override // anything that might be able to change the Map Keys
        so_many_methods_and_edge_cases()
        { throws UnsupportedOperationException }
    }

Is there existing interface that only allows changing the data of values of Maps<>?

What are my other options for creating something resembling a Map<String,String> that has unmodifiable keys, but modifiable values? I am interested in good coding practices, if possible.


回答1:


Seems like you're looking for the Proxy Pattern.


Detailed answer:

The idea is to use what's called a proxy to interact with the map. The proxy will intercept all calls to the map; you should only be able to interact with the map through the proxy. It acts as an interface between the client and the map.

A proxy is a skeleton of what you are "wrapping". Since you are creating a proxy for a map, the proxy should implement the Map interface:

class ImmutableMap<K, V> implements Map<K, V> {
    private Map<K, V> map;

    public ImmutableMap(Map<K, V> map) {
        this.map = new HashMap<>(map); // detach reference
    }

    //implement methods from Map
}

Most methods will simply telescope to map. Modify the methods you need to prevent removing keys or adding new keys to the map, such as put, putAll and remove:

final class ImmutableMap<K, V> implementsMap<K, V> {
    private Map<K, V> map;

    public ImmutableMap(Map<K, V> map) {
        this.map = new HashMap<>(map);
    }

    @Override
    public int size() {
        return map.size();
    }

    @Override
    public boolean isEmpty() {
        return map.isEmpty();
    }

    @Override
    public boolean containsKey(Object key) {
        return map.containsKey(key);
    }

    @Override
    public boolean containsValue(Object value) {
        return map.containsValue(value);
    }

    @Override
    public V get(Object key) {
        return map.get(key);
    }

    @Override
    public V put(K key, V value) {
        if(!map.containsKey(key)) {
            throw new IllegalArgumentException("Cannot add new keys!");
        }

        return map.put(key, value);
    }

    @Override
    public V remove(Object key) {
        throw new UnsupportedOperationException("You cannot remove entries from this map!");
    }

    @Override
    public void putAll(Map<? extends K, ? extends V> map) {
        for(K key : map.keySet()) {
            if(!this.map.containsKey(key)) {
                throw new IllegalArgumentException("Cannot add new keys to this map!");
            }
        }

        this.map.putAll(map);
    }

    @Override
    public void clear() {
        throw new UnsupportedOperationException("You cannot remove entries from this map!");
    }

    @Override
    public Set<K> keySet() {
        return Collections.unmodifiableSet(map.keySet());
    }

    @Override
    public Collection<V> values() {
        return Collections.unmodifiableSet(map.values()); //prevebt changing values to null
    }

    @Override
    public Set<Map.Entry<K, V>> entrySet() {
        //to allow modification of values, create your own ("immutable") entry set and return that
        return Collections.unmodifiableSet(map.entrySet()); 
    }
}

Keynotes:

  1. Collections.unmodifiableSet should be used when returning sets from the map. This ensures that if a person attempts to modify a set returned from the map, it'll throw an UnsupportedOperationException

  2. Creating a new Map containing the values of the map passed into the constructor prevents the client from modifying the ImmutableMap using the map they passed into it.




回答2:


you may want to limit the size of your map

In over riding your put method you may use

if (map.size() == maxEntries) {
             throw some exception;


来源:https://stackoverflow.com/questions/29547853/how-to-create-a-java-mapstring-string-with-unmodifiable-keys

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!