"Undefined reference: .. ConcurrentHashMap.keySet()" when building in Java 8
Asked Answered
V

2

14

i have a project, and i am build this project with jdk 6,7,8 and my target is 1.6

when i build jdk 8 i get this error:

Undefined reference: java.util.concurrent.ConcurrentHashMap.KeySetView java.util.concurrent.ConcurrentHashMap.keySet()

since i have this code in that line:

   final Iterator<CLASS_NAME> itr = hashMap.keySet().iterator();

how can avoid error, i made some search in internet, and since java 8 changed its return type keyset, i got error. is this any solution. i am using maven, and animal-sniffer-plugin gives this error, with signature error.

Vaticide answered 6/9, 2014 at 22:15 Comment(3)
Think it may be related to this question: #24449223 .Elfrieda
so how can i avoid error, is there any solution?Vaticide
I don't use the animal-sniffer-plugin so am not really any help. I did find this information that may have some relevance: mojo.codehaus.org/animal-sniffer-maven-plugin/examples/…Elfrieda
W
10

Another Answer suggests a modification to your code (using keys() instead of keySet()) so that you can compile your source code on Java 8 and run on Java 7. I think that is a retrograde step.

Instead:

  • If your aim is to create a production build of your software that will run on Java 6, 7 and 8, then your best bet is to do your production builds on JDK 6.

  • If your aim is to do your development builds on Java 8 (but stay backwards compatible at the source code level for now), then change the maven plugin configs for animal-sniffer to ignore these classes; see http://mojo.codehaus.org/animal-sniffer-maven-plugin/examples/checking-signatures.html for an explanation.

    However, there is a risk that animal-sniffer will ignore too much; e.g. it won't tell you if you use new Java 8 methods in ConcurrentHashMap. You will need to allow for that possibility ....

  • If your aim is to move to Java 8 (so that you can start using new Java 8 features in your code), then just do it. Your code won't be backward compatible, but you can't support old versions of Java for ever ...

(These suggestions are not mutually exclusive, if you consider the big picture.)

Wooton answered 6/9, 2014 at 23:13 Comment(5)
i want to build all versions, and i think best answer is your second answer. i have ignored it, thanks.Vaticide
Which answer suggests building on 8 and running on 7? Mine?Capricorn
@StuartMarks - I wrote modifying the code so that you can build on Java 8 and run on Java 7. Did you suggest that? I don't think so!Wooton
Fair enough, the other answer I see suggested modifying the code but didn't talk about 7 vs 8 specifically, so I wasn't sure.Capricorn
+1 to building on JDK 6 in order to run on 6, 7, and 8.Capricorn
C
17

You should be able to use chm.keySet() with ConcurrentHashMap.

Between Java 7 and Java 8, the ConcurrentHashMap.keySet() method did change from returning Set<K> to returning ConcurrentHashMap.KeySetView<K,V>. This is a covariant override, since KeySetView<K,V> implements Set<K>. It's also both source and binary compatible. That is, the same source code should work fine when built and run on 7 and when built and run on 8. The binary built on 7 should also run on 8.

Why the undefined reference? I suspect there is a build configuration problem that mixes bits from Java 7 and Java 8. Typically this occurs because the when attempting to compile for a previous release, the -source and -target options are specified, but the -bootclasspath option isn't specified. For example, consider the following:

/path/to/jdk8/bin/javac -source 1.7 -target 1.7 MyClass.java

If MyClass.java contains any dependencies on JDK 8 APIs, this will not work when run using JDK 7; it will result in a NoSuchMethodError being thrown at runtime.

Note that this error won't occur immediately; it will occur only when an attempt is made to invoke the method in question. This is because linkage in Java is done lazily. Thus, you can have references to nonexistent methods lurking around in your code for an arbitrary amount of time, and errors won't occur unless the execution path attempts to invoke such a method. (This is both a blessing and a curse.)

It's not always easy to discern dependencies on newer JDK APIs by looking at the source code. In this case, if you compile using JDK 8, the call to keySet() will compile in a reference to the method that returns a ConcurrentHashMap.KeySetView because that's the method that's found in the JDK 8 class library. The -target 1.7 option makes the resulting class file compatible with JDK 7, and the -source 1.7 option limits the language level to JDK 7 (which doesn't apply in this case). But the result is actually neither fish nor fowl: the class file format will work with JDK 7, but it contains references to the JDK 8 class library. If you try to run this on JDK 7, of course it can't find the new stuff introduced in 8, so the error occurs.

You can attempt to work around this by modifying the source code to avoid dependencies on newer JDK APIs. So if your code is this:

ConcurrentHashMap<K, V> hashMap = ... ;
final Iterator<CLASS_NAME> itr = hashMap.keySet().iterator();

You could change it to this:

ConcurrentHashMap<K, V> hashMap = ... ;
final Iterator<CLASS_NAME> itr = ((Map<K, V>)hashMap).keySet().iterator();

This can be made to work, but I don't recommend it. First, it garbages up your code. Second, it's not at all obvious where changes like this need to be applied, so it's hard to tell when you've gotten all the cases. Third, it's hard to maintain, as the reason for this cast isn't at all obvious and is easily refactored away.

The correct solution is to make sure you have a build environment that is purely based on the oldest JDK version you want to support. The binary should then run unchanged on later JDKs. For example, to compile for JDK 7 using JDK 8, do this:

/path/to/jdk8/bin/javac -source 1.7 -target 1.7 -bootclasspath /path/to/jdk7/jre/lib/rt.jar MyClass.java

In addition to specifying the -source and -target options, specifying the -bootclasspath option will limit all dependencies to the APIs found at that location, which of course must match the version specified for the other options. This will prevent any inadvertent dependencies on JDK 8 from creeping into the resulting binaries.

In JDK 9 and later, a new option --release has been added. This effectively sets the source, target, and bootclasspath to the same JDK platform level all at once. For example:

/path/to/jdk9/bin/javac --release 7 MyClass.java

When using the --release option, it's no longer necessary to have the older JDK's rt.jar around for build purposes, as javac includes tables of public APIs for earlier releases.

**

More generally, this kind of issue tends to occur when the JDK introduces covariant overrides in a new JDK release. This happened in JDK 9 with the various Buffer subclasses. For example, ByteBuffer.limit(int) was overridden and now returns ByteBuffer, whereas in JDK 8 and earlier this method was inherited from Buffer and returned Buffer. (Several other covariant overrides were added in this area.) Systems compiled on JDK 9 using only -source 8 -target 8 will run into exactly the same issue with NoSuchMethodError. The solution is the same: use --release 8.

Capricorn answered 6/9, 2014 at 23:6 Comment(3)
so if i dont use any new thing in this class from jdk 8, i can use -target 1.7. is this true?Vaticide
@bou Strictly speaking yes, but practically speaking it's impossible to avoid bits of the JDK 8 class library leaking into your built classes. This is a good example, since keySet()'s return type changed to return a Java 8 specific class, there's essentially no way to call keySet() without bringing in a Java 8 dependency.Capricorn
we encountered this problem too when building with JDK 8 with -source 1.6 -target 1.6 then trying to run with JRE 6. I am surprised the JDK 8 API changed for such a fundamental method as keySet() in Java 8 to cause a binary incompatibility with earlier Java versions. If the method signature in Java 8 was kept the same: <code>Set<K> keySet()</code> then it would probably be binary compatible. Our workaround was to us an entrySet() instead - since we have a remove some items as we are iterating through the Map.Kathe
W
10

Another Answer suggests a modification to your code (using keys() instead of keySet()) so that you can compile your source code on Java 8 and run on Java 7. I think that is a retrograde step.

Instead:

  • If your aim is to create a production build of your software that will run on Java 6, 7 and 8, then your best bet is to do your production builds on JDK 6.

  • If your aim is to do your development builds on Java 8 (but stay backwards compatible at the source code level for now), then change the maven plugin configs for animal-sniffer to ignore these classes; see http://mojo.codehaus.org/animal-sniffer-maven-plugin/examples/checking-signatures.html for an explanation.

    However, there is a risk that animal-sniffer will ignore too much; e.g. it won't tell you if you use new Java 8 methods in ConcurrentHashMap. You will need to allow for that possibility ....

  • If your aim is to move to Java 8 (so that you can start using new Java 8 features in your code), then just do it. Your code won't be backward compatible, but you can't support old versions of Java for ever ...

(These suggestions are not mutually exclusive, if you consider the big picture.)

Wooton answered 6/9, 2014 at 23:13 Comment(5)
i want to build all versions, and i think best answer is your second answer. i have ignored it, thanks.Vaticide
Which answer suggests building on 8 and running on 7? Mine?Capricorn
@StuartMarks - I wrote modifying the code so that you can build on Java 8 and run on Java 7. Did you suggest that? I don't think so!Wooton
Fair enough, the other answer I see suggested modifying the code but didn't talk about 7 vs 8 specifically, so I wasn't sure.Capricorn
+1 to building on JDK 6 in order to run on 6, 7, and 8.Capricorn

© 2022 - 2024 — McMap. All rights reserved.