Вы должны иметь возможность использовать chm.keySet()
с ConcurrentHashMap
.
Между Java 7 и Java 8 метод ConcurrentHashMap.keySet()
действительно изменился с возврата Set<K>
на возврат ConcurrentHashMap.KeySetView<K,V>
. Это ковариантное переопределение, поскольку KeySetView<K,V>
реализует Set<K>
. Он также совместим как с исходным кодом, так и с двоичным кодом. То есть один и тот же исходный код должен нормально работать при сборке и запуске на 7 и при сборке и запуске на 8. Бинарный файл, построенный на 7, также должен работать на 8.
Почему неопределенная ссылка? Я подозреваю, что существует проблема конфигурации сборки, которая смешивает биты из Java 7 и Java 8. Обычно это происходит из-за того, что при попытке компиляции для предыдущего выпуска указаны параметры -source
и -target
, но параметр -bootclasspath
не указан. Например, рассмотрите следующее:
/path/to/jdk8/bin/javac -source 1.7 -target 1.7 MyClass.java
Если MyClass.java содержит какие-либо зависимости от API-интерфейсов JDK 8, этот не будет работать при запуске с использованием JDK 7; это приведет к выдаче NoSuchMethodError
во время выполнения.
Обратите внимание, что эта ошибка возникнет не сразу; это произойдет только тогда, когда будет сделана попытка вызвать рассматриваемый метод. Это потому, что связывание в Java выполняется лениво. Таким образом, вы можете иметь ссылки на несуществующие методы, скрывающиеся в вашем коде в течение произвольного периода времени, и ошибки не возникнут, если путь выполнения не попытается вызвать такой метод. (Это одновременно и благословение, и проклятие.)
Не всегда легко определить зависимости от новых API JDK, взглянув на исходный код. В этом случае, если вы компилируете с использованием JDK 8, вызов keySet()
будет компилироваться со ссылкой на метод, который возвращает ConcurrentHashMap.KeySetView
, потому что этот метод находится в библиотеке классов JDK 8. Параметр -target 1.7
делает результирующий файл класса совместимым с JDK 7, а параметр -source 1.7
ограничивает уровень языка JDK 7 (что в данном случае не применяется). Но в результате получается ни рыба, ни мясо: формат файла класса будет работать с JDK 7, но он содержит ссылки на библиотеку классов JDK 8. Если вы попытаетесь запустить это на JDK 7, конечно, он не сможет найти новые вещи, представленные в 8, поэтому возникает ошибка.
Вы можете попытаться обойти это, изменив исходный код, чтобы избежать зависимости от новых API JDK. Итак, если ваш код такой:
ConcurrentHashMap<K, V> hashMap = ... ;
final Iterator<CLASS_NAME> itr = hashMap.keySet().iterator();
Вы можете изменить его на это:
ConcurrentHashMap<K, V> hashMap = ... ;
final Iterator<CLASS_NAME> itr = ((Map<K, V>)hashMap).keySet().iterator();
Это можно заставить работать, но я не рекомендую это делать. Во-первых, он засоряет ваш код. Во-вторых, совсем не очевидно, где нужно применять подобные изменения, поэтому трудно сказать, когда у вас есть все кейсы. В-третьих, его трудно поддерживать, поскольку причина такого приведения совсем не очевидна и легко устраняется рефакторингом.
Правильное решение - убедиться, что у вас есть среда сборки, основанная исключительно на самой старой версии JDK, которую вы хотите поддерживать. Затем двоичный файл должен работать без изменений на более поздних JDK. Например, чтобы скомпилировать JDK 7 с использованием JDK 8, сделайте следующее:
/path/to/jdk8/bin/javac -source 1.7 -target 1.7 -bootclasspath /path/to/jdk7/jre/lib/rt.jar MyClass.java
В дополнение к указанию параметров -source
и -target
, указание параметра -bootclasspath
ограничит все зависимости API, найденными в этом месте, которые, конечно, должны соответствовать версии, указанной для других параметров. Это предотвратит попадание любых непреднамеренных зависимостей от JDK 8 в результирующие двоичные файлы.
В JDK 9 и более поздних версиях была добавлена новая опция --release
. Это фактически устанавливает для источника, цели и пути к загрузочному классу один и тот же уровень платформы JDK одновременно. Например:
/path/to/jdk9/bin/javac --release 7 MyClass.java
При использовании параметра --release
больше не требуется иметь старый rt.jar
JDK для целей сборки, поскольку javac
включает таблицы общедоступных API для более ранних выпусков.
**
В более общем плане проблема такого рода обычно возникает, когда JDK вводит ковариантные переопределения в новом выпуске JDK. Это произошло в JDK 9 с различными Buffer
подклассами. Например, ByteBuffer.limit(int)
. был переопределен и теперь возвращает ByteBuffer
, тогда как в JDK 8 и ранее этот метод был унаследован от Buffer
и возвращал Buffer
. (В этой области было добавлено несколько других ковариантных переопределений.) Системы, скомпилированные на JDK 9 с использованием только -source 8 -target 8
, столкнутся с точно такой же проблемой с NoSuchMethodError
. Решение то же: используйте --release 8
.
person
Stuart Marks
schedule
06.09.2014