Backwards compatibility has long been a hallmark of Java: there are many classes and methods still in the standard library even though they've been deprecated since the 1990s. Imagine my surprise, then, when I saw this exception:
java.lang.NoSuchMethodError: java.util.concurrent.ConcurrentHashMap.keySet()Ljava/util/concurrent/ConcurrentHashMap$KeySetView; at net.sf.kdgcommons.util.Counters.keySet(Counters.java:179) ...
To understand why, you need some back-story. I maintain a couple of open-source libraries that I claim are compatible with Java 1.6. To ensure that I don't accidentally use methods that didn't exist in 1.6, I have a 1.6 JVM installed on my development machine, and tell Eclipse to use it to compile the project. Ditto for the library that was running this test.
However, I don't use the real 1.6 JDK for producing the released versions of these
libraries. I use Maven for releases, and the newer versions of Maven won't run on anything
older than JDK 1.8 (and there are enough backwards-incompatibilites between older versions
of Maven and its plugins that trying to use one is an exercise in frustration). This means
that, even though I'm setting my target
property to 1.6, it's compiling against
the 1.8 standard library.
And in 1.8, the Java maintainers changed the return type of ConcurrentHashMap.keySet()
:
previously, it returned a Set
, as defined by the contract
of java.util.Map
. In
1.8, it returns a ConcurrentHashMap.KeySetView
. This concrete
class implements Set
, so it doesn't break the contract at the source level.
However things are different at bytecode level: the actual return type is retained in the
method's descriptor, and the
invokevirtual operation attempts to find a defined method that
matches that descriptor. It will accept a method definition that returns a subtype of the
specified return type, but not one that returns a supertype. So, when presented with a version
of that standard library in which keySet()
returns a Set
, the JVM
throws NoSuchMethodError
.
There is a work-around: cast the map reference to java.util.Map
. This causes the
compiler to use the method definition from the interface, which fortunately has not changed
since it was released in 1.2.
ConcurrentHashMapmap = // something Set keys = ((Map )map).keySet();
There is a bug report for this behavior, which was closed with “Won't Fix”. Unfortunately, that's the correct resolution: reverting the method signature would not only break any code that relies on it, but also any deployed applications compiled with the old signature but running on a JVM with the reverted signature.
I can only hope that, in reviewing the bug report, the JVM maintainers realized that changing the public API of a class — any class — is a mistake. Unfortunately, some of the changes happening with newer releases of Java indicate this is to be a more frequent occurrence. Which is probably why 1.8 remains (in my experience) the primary deployment platform for Java apps, even though we're now at Java 12.
No comments:
Post a Comment