Generics trouble implementing Map.entrySet()

2.9k views Asked by At

I'm trying to extend AbstractMap to create a MapTreeNode class (a tree node where children are accessed by key rather than by index).

I already have a method for getting a set of the children which works fine:

public class MapTreeNode<K,V> implements Map.Entry<K,V> {
    private Map<K,MapTreeNode<K,V>> children = new HashMap<K,MapTreeNode<K,V>>();
    private Set<MapTreeNode<K,V>> child_set = null;

    public Set<MapTreeNode<K,V>> children() {
        if (child_set == null)
            child_set = new ChildSet();

        return child_set;
    }

    ...

    private final class ChildSet extends AbstractSet<MapTreeNode<K,V>> {
        @Override
        public Iterator<MapTreeNode<K,V>> iterator() {
            return children.values().iterator();
        }

        @Override
        public int size() {
            return MapTreeNode.this.childCount();
        }
        ...
    }

}

I'd like to create a map view of a node (Map<K,V>) and reuse child_set but I'm not sure it's possible with Java's generics:

public Map<K,V> asMap() {
    return new AbstractMap<K,V>() {
        @Override
        public Set<Map.Entry<K,V>> entrySet() {
            return child_set; // line 166
        }
    };
}

this of course gives

MapTreeNode:166: incompatible types
found   : java.util.Set<MapTreeNode<K,V>>
required: java.util.Set<java.util.MapEntry<K,V>>

Is there a way I can reuse my ChildSet class for this?

5

There are 5 answers

0
newacct On BEST ANSWER

The problem is with entrySet()'s return type. It is Set<Map.Entry<K,V>>. And as you know, Foo<A> is not compatible with Foo<B> for different A and B no matter how they're related.

I would argue that this was a design mistake in the API. The return type of entrySet() should really be Set<? extends Map.Entry<K,V>>. Here is why: If you read the documentation for entrySet(), it says that things can be read from the Set, things can be removed from the Set (which causes changes to the underlying map), but things cannot be added to the Set. This exactly fits the role of a Producer -- you do not add things to it. Per the PECS rule, an extends-wildcard collection type should be used.

6
JoshDM On

Unless you need something specific from a MapTreeNode method, treat it as a Map.Entry, meaning declare child_set as

private Set<Map.Entry<K,V>> child_set = null;

Since MapTreeNode extends Map.Entry, you should be fine.

0
Brad Mace On

Here's the best I've been able to do so far at avoiding code duplication:

private abstract class AbstractChildSet<T extends Map.Entry<K,V>> extends AbstractSet<T> {
    @Override
    public boolean remove(Object o) {
        if (o == null || !(o instanceof Map.Entry)) {
            return false;
        }

        MapTreeNode<K,V> node;
        if (o instanceof MapTreeNode)
            node = (MapTreeNode<K,V>) o;
        else
            node = MapTreeNode.this.child(((Map.Entry<K,V>) o).getKey());

        if (node == null || !isParentOf(node))
            return false;

        node.removeFromParent();
        return true;
    }

    @Override
    public int size() {
        return MapTreeNode.this.childCount();
    }

    @Override
    public void clear() {
        MapTreeNode.this.removeAllChildren();
    }
}

private final class ChildSet extends AbstractChildSet<MapTreeNode<K,V>> {
    @Override       
    public boolean add(MapTreeNode<K,V> node) {
        if (MapTreeNode.this.containsKey(node.getKey()))
            return false;

        MapTreeNode.this.addChild(node);
        return true;
    }

    @Override
    public Iterator<MapTreeNode<K,V>> iterator() {
        return children.values().iterator();
    }
}

private final class EntrySet extends AbstractChildSet<Map.Entry<K,V>> {
    @Override
    public boolean add(Map.Entry<K,V> entry) {
        if (MapTreeNode.this.containsKey(entry.getKey()))
            return false;

        MapTreeNode new_child = new HashMapTreeNode(MapTreeNode.this, entry.getKey(), entry.getValue());

        MapTreeNode.this.addChild(new_child);
        return true;
    }

    @Override
    public Iterator<Map.Entry<K,V>> iterator() {
        return new EntryIterator();
    }
}
0
Sachin Thapa On

What I understood that you have implemented new class to get children which are of type Entry. To build a map again I would iterate over the set of entries and rebuild the map.

Not sure I helped here as like others I am unable to fully read what else this class contains.

2
Syed Muhammad Humayun On

Why don't you simply cast it like this:

public Map<K,V> asMap() {
    return (Map<K,V>) this;
}