You are here: Home ยป Incompatible Java 8 changes in the Collections runtime classes

Incompatible Java 8 changes in the Collections runtime classes

Posted on Tuesday, December 09 2014 at 13:15 (Age: 5 yrs) | Category: Java

Suppose that we want to implement a HashMap (let's call it OrderedHashMap) where the values can also retrieved by their index, where the index refers to the order in which the key/value pairs have been inserted. The following unit test shows how such a class shall be used:

@Test
public void testPutGet() {
    OrderedHashMap<String, String> ohm = new OrderedHashMap<>();
    ohm.put("Z", "LetterZ");    // 0
    ohm.put("A", "LetterA");    // 1
    ohm.put("D", "LetterD");    // 2
    ohm.put("K", "LetterK");    // 3

    assertEquals(ohm.get(0), "LetterZ");
    assertEquals(ohm.get(3), "LetterK");
}

The straight forward implementation of the class OrderedHashMap is to inherit from one of the existing Collection classes and simply implement the additional functionality, e.g. like

public class OrderedHashMap<K, V> extends LinkedHashMap<K, V> {
    private static final long serialVersionUID = 6354582314971513369L;

    private List<V> items = new ArrayList<>();

    public V get(int index) {
        return items.get(index);
    }

    @Override
    public V put(K key, V value) {
        items.add(value);
        return super.put(key, value);
    }
}

With this code, the above unit test succeeds. Lets add an additional unit tests for one of the other methods inherited from LinkedHashMap, putAll():

@Test
public void testPutAll() {
    OrderedHashMap<String, String> ohm = new OrderedHashMap<>();
    ohm.put("Z", "LetterZ");    // 0
    ohm.put("A", "LetterA");    // 1
    ohm.put("D", "LetterD");    // 2
    ohm.put("K", "LetterK");    // 3

    OrderedHashMap<String, String> ohm2 = new OrderedHashMap<&ht;();
    ohm2.putAll(ohm);

    assertEquals(ohm2.get(0), "LetterZ");
    assertEquals(ohm2.get(3), "LetterK");
}

This unit test also succeeds and shows that we can copy the elements from one OrderedHashMap to a second one, using putAll().

Unless we switch to Java 8.

If we run the above sample with the Java 8 runtime, the test fails with an exception:

java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
	at java.util.ArrayList.rangeCheck(Unknown Source)
	at java.util.ArrayList.get(Unknown Source)
	at com.example.OrderedHashMap.get(OrderedHashMap.java:13)
	at com.example.test.OrderedHashMapTest.testPutAll(OrderedHashMapTest.java:37)
	...

This is an issue I came across when I was trying to translate a grammar with ANTLR (not using the latest version) - see also https://github.com/antlr/antlr4/issues/337.

The background is that the LinkedHashMap class (or one of its super classes) changed the implementation of putAll() so that it no longer calls put() to insert the new elements in the destination map. Hence, our own implementation is no longer able to catch this call to update the List at the same time a new key/value pair is added to the map.

If you do similar things, you should check if there are such differences between the Java 7 and the Java 8 runtime which might affect your code. 

In general, this is a good example to Favor Composition over Inheritance in Java and Object Oriented Programming. The main problem is that the sub class depends on the behaviour of the superclass, and hence becomes fragile (and breaks when the superclass behaviour changes in an incompatible way). Collections are always a bad candidate for subclassing - instead, create a new class which implements the desired interfaces and then delegates to the necessary Collection classes. This is more effort at the beginning (to implement all the necessary interface methods), but makes the class much more resistent against modifications in the runtime classes. See also Effective Java, Second Edition: Item 16: Favor composition over inheritance.


No comments

Be the first to post a comment!