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
implementsComparable<
String
>
java.lang.Boolean
implementsComparable<
Boolean
>
java.lang.Byte
implementsComparable<
Byte
>
java.lang.Short
implementsComparable<
Short
>
java.lang.Integer
implementsComparable<
Integer
>
java.math.BigInteger
implementsComparable<
BigInteger
>
java.io.File
implementsComparable<
File
>
java.nio.charset.Charset
implementsComparable<
Charset
>
java.util.UUID
implementsComparable<
UUID
>
java.time.Instant
implementsComparable<
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:
Comparable
or Comparator
) cannot be a heterogeneous collection … unless you can provide some very specific rules to compare objects of different types.