Why some collections cannot be heterogeneous

In this relatively short article, I will discuss an intriguing and noteworthy topic about collections in Java: why some standard Java collections cannot be heterogeneous. In other words, why they cannot contain objects of different types.

Introduction

First of all, it’s essential to clarify that almost all standard Java collections can be heterogeneous. Consider, for example, the following code that uses ArrayList.

ArrayList<Object> arrayList = new ArrayList<Object>();
arrayList.add("hello");    // a java.lang.String
arrayList.add(123);        // a java.lang.Integer
arrayList.add(456L);       // a java.lang.Long
arrayList.add(true);       // a java.lang.Boolean

The above code works correctly without errors. However, it’s also clear that such a collection containing different types may be challenging to use. So ultimately, it all depends on how (and why) you would use that collection. But other than that, ArrayList has nothing to complain about objects of different types. You can do the same with several collections like LinkedList, Vector, HashMap, HashSet, LinkedHashSet, and others.

TreeMap and TreeSet

The Java Collections Framework provides two special collections, TreeMap and TreeSet. Why are these collections so particular? These two collections differ from all the others because they are “sorted” collections. A collection is said to be “sorted” when it keeps the elements ordered according to a specific criterion. This criterion is generally defined using the Comparable or the Comparator interface. When you add an entry into TreeMap or TreeSet, the collection performs one or more comparisons to determine where the entry must be placed.

The Javadoc documentation of these two collections clearly states that all the elements (all the keys for TreeMap) must be mutually comparable. However, comparing objects of different types is generally tricky. For example, what does comparing a String with a Boolean mean? Is a boolean true before or after all string values? You should elaborate a set of custom rules for this. Comparing objects of different types without these rules is challenging and not obvious.

In Java, objects are typically comparable only with objects of the same type. You can find evidence of this by looking at the Javadoc documentation of some common classes:

  • java.lang.String implements Comparable<String>
  • java.lang.Boolean implements Comparable<Boolean>
  • java.lang.Byte implements Comparable<Byte>
  • java.lang.Short implements Comparable<Short>
  • java.lang.Integer implements Comparable<Integer>
  • java.math.BigInteger implements Comparable<BigInteger>
  • java.io.File implements Comparable<File>
  • java.nio.charset.Charset implements Comparable<Charset>
  • java.util.UUID implements Comparable<UUID>
  • java.time.Instant implements Comparable<Instant>

And so on. There are undoubtedly some exceptional cases, for example, the java.time.LocalDate class implements Comparable<ChronoLocalDate>. However, ChronoLocalDate is only a simple “abstraction” interface representing the “local date” concept. Thus, for example, you can compare a java.time.LocalDate object with a java.time.chrono.JapaneseDate object. But you cannot compare a LocalDate with a ZonedDateTime because they are very different.

Most of the time, types implementing Comparable can generally be only compared with objects of the same type. A String object, for example, can only be compared with other String objects, not Integer, not Boolean, and not even java.io.File (even if it contains a simple path String), etc.

Trying objects of different types with TreeSet

So, what happens when you put two or more objects of different types in a TreeSet, or as keys in a TreeMap?

If you try the following:

package sample;

import java.util.TreeSet;

public class Sample {
    public static void main(String[] args) {
        TreeSet<Object> treeSet = new TreeSet<Object>();
        treeSet.add("hello");
        treeSet.add(123);        // Aargh, a ClassCastException here !
        treeSet.add(456L);
        treeSet.add(true);
    }
}

You get the following error at runtime:

Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
        at java.base/java.lang.Integer.compareTo(Integer.java:71)
        at java.base/java.util.TreeMap.put(TreeMap.java:814)
        at java.base/java.util.TreeMap.put(TreeMap.java:534)
        at java.base/java.util.TreeSet.add(TreeSet.java:255)
        at sample.Sample.main(Sample.java:9)

A nasty ClassCastException is thrown at runtime because TreeSet is trying to compare an Integer object with a String object. You can notice, in particular, where the cause of the exception is. TreeSet (to be precise, its internal TreeMap) attempts to call the compareTo method of Integer passing a String object. However, only an Integer is accepted here because Integer implements Comparable<Integer>.

Conclusions

From the above discussion, we can draw a fundamental conclusion that you should always keep in mind:

A “sorted” collection (based on Comparable or Comparator) cannot be a heterogeneous collection … unless you can provide some very specific rules to compare objects of different types.

Similar Posts