0

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.

Andi
  • 888
  • 2
  • 10
  • 24
  • "Before I used this" what was the problem with that? – Andy Turner Aug 31 '19 at 18:56
  • Map says nothing about the meaning of the data. It can be a map for (lastName, firstName), (autor, title) or similar. In first place I would like to have better self-documenting code. And in second place, less characters to type. – Andi Aug 31 '19 at 19:04

2 Answers2

2

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.

Andy Turner
  • 137,514
  • 11
  • 162
  • 243
  • LSP violation is an interesting point, indeed. But I'm not sure this is the real reason? In case of using Map, this instance has also a put() method and other modifying methods. The naive user can try to modify but will get an exception. For me this look also like LSP. – Andi Aug 31 '19 at 18:47
  • @Andi that is not a violation of LSP: the method is [documented as optional](https://docs.oracle.com/javase/8/docs/api/java/util/Map.html#put-K-V-): it puts the value into the map, or throws an exception. As such, throwing an exception is a totally a acceptable thing for a subclass to do. The bad thing is that the Map interface gives you no means of knowing without invoking the method. What this basically means is that you should never invoke mutation methods on a Map that you don't know for definite to support those methods. – Andy Turner Aug 31 '19 at 18:50
  • I see your point, yes. But for my understanding, "optional" is just a text in the documentation, not in the interface definition. I'm not convinced abuot that, this implementation looks a little bit vague to me. – Andi Aug 31 '19 at 19:08
  • @Andi yes, optional is merely text. It's the fact that it says "Throws:UnsupportedOperationException - if the put operation is not supported by this map" that makes it part of the interface definition. – Andy Turner Aug 31 '19 at 19:21
1

Collections.unmodifiableMap returns a Map<K,V> so you would have to use it like this :

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

And you are not able to cast the returned object to Attributes like:

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

because Collections.unmodifiableMap returns instance of private static UnmodifiableMap so you will get a ClassCastException. And Attributes is not subtype of UnmodifiableMap.

Also I think that in your case it would be easier to use LinkedHashMap directly instead of creating a derived class from it, since as I see the functionality does not differ from the original one. And then use the object returned from Collections.unmodifiableMap as Map.

Michał Krzywański
  • 15,659
  • 4
  • 36
  • 63
  • Yeah that is what I found out so far. It is not my primary intent to subclass, I just want to have a shorter way to define variables and method parameters. As mentioned in my question, in C++ the "using" keyword helps a lot to make the source code more readable without adding extra code. But it seems there is no such simple way in Java? – Andi Aug 31 '19 at 18:51