Here is a Java class with a compatibility problem:
HelloCovariance.java:
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class HelloCovariance {
public static void main(String[] args) {
ConcurrentHashMap<String, String> properties = new ConcurrentHashMap<>();
Set<String> keySet = properties.keySet();
}
}
Here is a session log that seems implausible:
$ /usr/lib/jvm/java-8-oracle/bin/javac -source 1.7 -target 1.7 HelloCovariance.java
warning: [options] bootstrap class path not set in conjunction with -source 1.7
1 warning
$ /usr/lib/jvm/java-1.7.0-openjdk-amd64/bin/java HelloCovariance
Exception in thread "main" java.lang.NoSuchMethodError: java.util.concurrent.ConcurrentHashMap.keySet()Ljava/util/concurrent/ConcurrentHashMap$KeySetView;
at HelloCovariance.main(HelloCovariance.java:7)
Why does this NoSuchMethodError happen?
Compare the JavaDoc for ConcurrentHashMap#keySet() in Java 1.7 and 1.8:
- http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ConcurrentHashMap.html#keySet()
- http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ConcurrentHashMap.html#keySet--
Notably the Java 1.7 ConcurrentHashMap#keySet() returns a Set<K>
while the 1.8 ConcurrentHashMap#keySet() returns a ConcurrentHashMap.KeySetView<K,V>
.
How can this work? It turns out that a overriding method is allowed to return a sub-type of the parent methods return type. This is due to covariance. This is useful in certain cases where you know the concrete types and want to avail of them somehow.
You'll notice that there is a warning about bootstrap classpath. It turns out those bootstrap classpath warnings have a lot of merit. The safest fix is clearly to compile with a bootstrap classpath.
Failing that you can avoid the unnecessary use of a concrete type for a variable here.
Using the general Map interface in place of the concrete ConcurrentHashMap type here side-steps the coupling to the Java 8 return type and will allow this code to be compiled with Java 8 and run on Java 7.
HelloSidestep.java:
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class HelloSidestep {
public static void main(String[] args) {
Map<String, String> properties = new ConcurrentHashMap<>();
Set<String> keySet = properties.keySet();
}
}
$ /usr/lib/jvm/java-8-oracle/bin/javac -source 1.7 -target 1.7 HelloCovariance.java
warning: [options] bootstrap class path not set in conjunction with -source 1.7
1 warning
$ /usr/lib/jvm/java-1.7.0-openjdk-amd64/bin/java HelloCovariance
Hey look! No crash. This is not really the right fix, and you will be playing whack-a-mole with similar bugs in a large system. The correct and recommended fix is to use a bootstrap classpath.
$ /usr/lib/jvm/java-8-oracle/bin/javac -source 1.7 -target 1.7 HelloCovariance.java -bootclasspath /usr/lib/jvm/java-1.7.0-openjdk-amd64/jre/lib/rt.jar
$ /usr/lib/jvm/java-1.7.0-openjdk-amd64/bin/java HelloCovariance
Now we get no warnings and no crash.
This is not a new problem, but it seems like there is still a lot of confusion around it.
Here are some useful resources to consider:
Java 9 has a good solution for this: http://openjdk.java.net/jeps/247. In the meantime, one can also simply compile with Java 7 if that is the minimum supported version. It doesn't seem like there is much of an advantage in compiling with Java 8 if one still has to pass Java 7's
rt.jar
in the bootclasspath.