Search code examples
javagenericscollectionsunmodifiable

How to make a specialization of Map unmodifiable?


I'm currently refreshing my Java knowledge with an mid-sized coding example. I have a data structure Map<String, String>, and initialize it typically with new LinkedHashMap<>() to preserve insertion order. I use this very often in my code, and I want to get rid of the declaration repetition. In C++ I would alias the map, but in Java there is no alias, as far as I know.

So I came up with the idea to subclass the generic like this:

public class Attributes extends LinkedHashMap<String, String> {

    public Attributes() {
        super();
    }

    public Attributes(Map<? extends String, ? extends String> map) {
        super(map);
    }

}

This looks good so far, but now I want to create an unmodifiable copy of this, because attributes should be part of immutable/unmodifiable data structures. Before I used this:

Map<String, String> unmodifiableAttributes = Collections.unmodifiableMap(
        new LinkedHashMap<>(attributes)
);

This doesn't work for a derived class, I tried this:

Attributes unmodifiableAttributes = Collections.unmodifiableMap(
        new Attributes(attributes)
);

The compiler rejects it with Incompatible types.

Is there an easy way to get an unmodifiable (or immutable) copy of such a subclass? Or is my idea completely wrong? I don't want to write a full featured decorator, just a few lines of code.

UPDATE

So far, it looks like there is no good solution for what I want to do. I looked at the source code of the Java Collections class, and there are internal classes for unmodifiable maps and similar collections. These are used to wrap the input collection and are returned by the correcponding static methods. One could reimplement this, but I think would be too much overhead.

We had more discussion about LSP violation instead of the original question, which is indeed an interesting question too.


Solution

  • You can't make a subclass LinkedHashMap unmodifiable because it would violate Liskov substitutability: LinkedHashMap is documented as being mutable, so all subclasses must also be.

    You have the additional problem that it's actually rather a lot of work to make the map unmodifiable: not only do you have obvious methods like put and remove, you also have things like clear, putAll, putIfAbsent, computeIfAbsent, computeIfPresent. And then you have to worry about the view-returning methods: entrySet, keySet, values all have to return unmodifiable views. I'm sure I've missed several methods that also need overriding, but my point remains that it is not trivial to make a mutable map unmodifiable.

    You can have unmodifiable Map implementations, however. The easiest way to do this would be to extend AbstractMap, and delegate to an actual LinkedHashMap:

    public class Attributes extends AbstractMap<String, String> {
        private final LinkedHashMap<String, String> delegate;
    
        public Attributes() {
            this(Collections.emptyMap());
        }
    
        public Attributes(Map<? extends String, ? extends String> map) {
            this.delegate = new LinkedHashMap<>(map);
        }
    
        // Override the methods you need to, and that are required by AbstractMap.
        // Details of methods to override in AbstractMap are given in Javadoc.
    }
    

    But I would also question whether your Attributes class really needs to implement something as general as the Map interface - if you need that generality, you could simply use a Map directly.