Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save exceptionplayer/36abd06398e152081fb079895b5ec466 to your computer and use it in GitHub Desktop.
Save exceptionplayer/36abd06398e152081fb079895b5ec466 to your computer and use it in GitHub Desktop.
Exception in thread "main" java.lang.NoSuchMethodError: java.util.concurrent.ConcurrentHashMap.keySet()Ljava/util/concurrent/ConcurrentHashMap$KeySetView;

Interaction of Convariance and Java Cross-compile

Here is a Java class with a compatibility problem:

HelloCovariance.java:

import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public class HelloConvariance {
  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:

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:

#!/usr/bin/env bash
# Tested on Ubuntu with WebUpd8 install of Oracle JDK 8
/usr/lib/jvm/java-8-oracle/bin/javac -source 1.7 -target 1.7 HelloCovariance.java
/usr/lib/jvm/java-1.7.0-openjdk-amd64/bin/java HelloCovariance
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class HelloConvariance {
public static void main(String[] args) {
ConcurrentHashMap<String, String> properties = new ConcurrentHashMap<>();
Set<String> keySet = properties.keySet();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment