Я получил NPE в java.util.HashSet.isEmpty () в строке 191. Я думаю, эта строка просто вызывает isEmpty () во внутреннем поле карты.
К моему удивлению, это поле временное, поэтому после десериализации оно будет нулевым. Но разве не в этом цель десериализации набора, чтобы вернуть значения? На мой взгляд, десериализация - единственный способ, которым это поле может иметь значение NULL для экземпляра HashSet.
Наверное, мне что-то здесь не хватает. Кто-нибудь может объяснить?
Java 1.8.0_151 в Linux (3.13.0-61-generic) на платформе amd64
Вот реализация HashMap.isEmpty из JDK:
/**
* Returns <tt>true</tt> if this set contains no elements.
*
* @return <tt>true</tt> if this set contains no elements
*/
public boolean isEmpty() {
return map.isEmpty(); // line 191
}
Редактировать / дополнительная информация:
isEmpty(). Но NPE встречается в библиотеке, поэтому этот метод явно был вызван в существующем экземпляре, то есть не в null. Поэтому предоставление этого фрагмента мало поможет.Не могли бы вы сделать небольшой фрагмент кода, демонстрирующий эту проблему? Итак, создайте новый набор, запишите его, а затем прочтите его снова, после чего вызовите isEmpty(), это позволяет нам быстро увидеть проблему.
как насчет предоставления фрагмента кода? потому что я понятия не имею, что происходит в строке 191 (-`
Возможно, ваша установленная ссылка нулевая.
Бросив взгляд на исходный код JDK, я не могу понять, как это могло произойти. Я думал, что если объект может преждевременно выйти из конструктора или readObject, другой поток может вызвать isEmpty до того, как будет установлен map; но я не видел, как это могло случиться.
Это поле должно быть установлено всегда, однако этого может не быть, если оно было записано в одном потоке и получено другим небезопасным для потоков способом.
Правильно, @PeterLawrey, но есть ли способ, чтобы другой поток мог получить ссылку на HashSet до того, как конструктор или readObject установит поле? Или модель памяти Java позволит другому потоку видеть исходный null даже после того, как readObject установил для поля значение, отличное от нуля?
Ульрих Шольц, один эксперимент, который вы можете попробовать, - это синхронизировать доступ к вашему HashSet, чтобы убедиться, что нет проблем с параллелизмом, и увидеть, что проблема исчезнет.
Я также видел бесконечные циклы в HashMap#get. Единственный способ, которым это может произойти, - это совместное использование этих контейнеров между потоками без надлежащей синхронизации. Они могут попадать во всевозможные противоречивые состояния. Не делай этого.
Помимо десериализации, второй путь кода, который сбрасывает map, - это clone.
@Thilo, но во время clone() это никогда не будет null.
@Holger. Я тоже не понимаю, как это могло быть, но это заставило меня задуматься: является ли clone потокобезопасным? Если вы передадите клон в другой поток, все ли поля будут надежно клонированы?
@Holger, Thilo: Может ли быть следующее: исключение во время десериализации / во время super.clone (), оставляя экземпляр HashSet в неинициализированном состоянии?
@ OleV.V. правильно, если не использовались барьеры памяти. Вы можете получить ссылку на HashSet, вызвать readObject и по-прежнему видеть нулевое значение во втором потоке. Это зависит от того, как была передана ссылка на набор.
@UlrichScholz обычно нет, так как в исключительном случае у вас редко есть шанс получить в руки неполный экземпляр. Для этого потребуется очень сложный сценарий. В качестве первого шага, чтобы сузить проблему, замените любой код перехвата исключений кодом регистрации (или повторной генерации) исключений. Вы все равно должны это сделать. В противном случае за углом поджидает следующая проблема ...
@Holger. Я уже положил эту замену на пластину. Посмотрим, назначат ли мне на это время.




Поскольку поле map не является final, нет никакой гарантии увидеть его в инициализированном состоянии при неправильной публикации вновь созданного экземпляра HashSet в многопоточном коде. В то время как «вновь созданный» означает, что никаких других действий, обеспечивающих видимость памяти между задействованными потоками, не произошло с момента создания, что в принципе может быть произвольно долгим.
Во время десериализации объекта метод readObject выполняет повторную инициализацию поля map и возвращает элементы. Это необходимо, чтобы скрыть эти детали реализации от постоянной формы. Кроме того, десериализованные объекты могут иметь хэш-код, отличный от хэш-кода, который был у них при сериализации (например, возьмите хеш-код, унаследованный от java.lang.Object). Так что их все равно придется вставлять заново. Общий принцип большинства коллекций - скрыть детали реализации и просто сериализовать и десериализовать каждый содержащийся элемент один за другим с помощью специальных методов writeObject и readObject.
Таким образом, будущая реализация HashSet может быть настоящим хеш-набором, без использования HashMap за кулисами, без ущерба для совместимости с сериализацией.
Часть об изменении hashCode между запусками JVM интересна. Если вы поместите в этот набор что-либо, зависящее от идентичности экземпляра (вероятно, это в первую очередь плохая идея, но есть некоторые варианты использования), это не пройдет через сериализатор. OTOH, если вы изменили реализацию "hashCode" вашего пользовательского класса между перезапусками (вероятно, еще одна плохая идея), десериализатор, повторно хеширующий все, сохранит его согласованность с новой реализацией.
Даже в рамках одной и той же среды выполнения сериализация с последующей десериализацией подразумевает операцию копирования, которая изменяет идентификаторы объектов, за некоторыми исключениями, такими как константы и одиночные экземпляры enum.
Основная причина - одновременный доступ
private void readObject(java.io.ObjectInputStream s)инициализирует резервную карту, поэтому она не должна быть нулевой после десериализации.