Как уже упоминалось в комментариях и ободрено спрашивающим, вот части кода, которые изменились между версией 8 и версией 11, которые я предполагаю как причину различного поведения (основано на чтении и отладка).
Разница заключается в классе ObjectInputStream
, в одном из его основных методов. Это соответствующая часть реализации в Java 8:
private void readSerialData(Object obj, ObjectStreamClass desc)
throws IOException
{
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
for (int i = 0; i < slots.length; i++) {
ObjectStreamClass slotDesc = slots[i].desc;
if (slots[i].hasData) {
if (obj == null || handles.lookupException(passHandle) != null) {
...
} else {
defaultReadFields(obj, slotDesc);
}
...
}
}
}
/**
* Reads in values of serializable fields declared by given class
* descriptor. If obj is non-null, sets field values in obj. Expects that
* passHandle is set to obj's handle before this method is called.
*/
private void defaultReadFields(Object obj, ObjectStreamClass desc)
throws IOException
{
Class<?> cl = desc.forClass();
if (cl != null && obj != null && !cl.isInstance(obj)) {
throw new ClassCastException();
}
int primDataSize = desc.getPrimDataSize();
if (primVals == null || primVals.length < primDataSize) {
primVals = new byte[primDataSize];
}
bin.readFully(primVals, 0, primDataSize, false);
if (obj != null) {
desc.setPrimFieldValues(obj, primVals);
}
int objHandle = passHandle;
ObjectStreamField[] fields = desc.getFields(false);
Object[] objVals = new Object[desc.getNumObjFields()];
int numPrimFields = fields.length - objVals.length;
for (int i = 0; i < objVals.length; i++) {
ObjectStreamField f = fields[numPrimFields + i];
objVals[i] = readObject0(f.isUnshared());
if (f.getField() != null) {
handles.markDependency(objHandle, passHandle);
}
}
if (obj != null) {
desc.setObjFieldValues(obj, objVals);
}
passHandle = objHandle;
}
...
Метод вызывает defaultReadFields
, который считывает значения полей. Как упомянуто в цитируемой части спецификации, он сначала обрабатывает дескрипторы полей примитивных полей. Значения, которые считываются для этих полей , устанавливаются сразу после их чтения , с
desc.setPrimFieldValues(obj, primVals);
и, что важно: это происходит за до вызова readObject0
для каждого из не -примитивных полей.
В отличие от этого, вот соответствующая часть реализации Java 11:
private void readSerialData(Object obj, ObjectStreamClass desc)
throws IOException
{
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
...
for (int i = 0; i < slots.length; i++) {
ObjectStreamClass slotDesc = slots[i].desc;
if (slots[i].hasData) {
if (obj == null || handles.lookupException(passHandle) != null) {
...
} else {
FieldValues vals = defaultReadFields(obj, slotDesc);
if (slotValues != null) {
slotValues[i] = vals;
} else if (obj != null) {
defaultCheckFieldValues(obj, slotDesc, vals);
defaultSetFieldValues(obj, slotDesc, vals);
}
}
...
}
}
...
}
private class FieldValues {
final byte[] primValues;
final Object[] objValues;
FieldValues(byte[] primValues, Object[] objValues) {
this.primValues = primValues;
this.objValues = objValues;
}
}
/**
* Reads in values of serializable fields declared by given class
* descriptor. Expects that passHandle is set to obj's handle before this
* method is called.
*/
private FieldValues defaultReadFields(Object obj, ObjectStreamClass desc)
throws IOException
{
Class<?> cl = desc.forClass();
if (cl != null && obj != null && !cl.isInstance(obj)) {
throw new ClassCastException();
}
byte[] primVals = null;
int primDataSize = desc.getPrimDataSize();
if (primDataSize > 0) {
primVals = new byte[primDataSize];
bin.readFully(primVals, 0, primDataSize, false);
}
Object[] objVals = null;
int numObjFields = desc.getNumObjFields();
if (numObjFields > 0) {
int objHandle = passHandle;
ObjectStreamField[] fields = desc.getFields(false);
objVals = new Object[numObjFields];
int numPrimFields = fields.length - objVals.length;
for (int i = 0; i < objVals.length; i++) {
ObjectStreamField f = fields[numPrimFields + i];
objVals[i] = readObject0(f.isUnshared());
if (f.getField() != null) {
handles.markDependency(objHandle, passHandle);
}
}
passHandle = objHandle;
}
return new FieldValues(primVals, objVals);
}
...
Введен внутренний класс, FieldValues
. Метод defaultReadFields
теперь только читает значения полей и возвращает их как FieldValues
объект. После этого возвращенные значения присваиваются полям путем передачи этого объекта FieldValues
во вновь введенный метод defaultSetFieldValues
, который внутренне выполняет вызов desc.setPrimFieldValues(obj, primValues)
, который первоначально был выполнен сразу после считывания примитивных значений.
Чтобы еще раз подчеркнуть это: метод defaultReadFields
сначала читает значения примитивного поля. Затем он читает значения непримитивного поля. Но это так до значения примитивных полей были установлены!
Этот новый процесс сталкивается с методом десериализации HashMap
. Опять же, соответствующая часть показана здесь:
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
...
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) { // (if zero, use defaults)
...
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}
Он считывает объекты ключа и значения, один за другим, и помещает их в таблицу, вычисляя хэш ключа и используя внутренний метод putVal
. Это тот же метод, который используется при заполнении карты вручную (т. Е. Когда она заполняется программно и не десериализуется).
Хольгер уже дал подсказку в комментариях, почему это необходимо: нет гарантии, что хеш-код десериализованных ключей будет таким же, как и до сериализации. Таким образом, вслепую «восстановление исходного массива» может привести к тому, что объекты будут храниться в таблице с неверным хеш-кодом.
Но здесь происходит обратное: ключи (то есть объекты типа Element
) десериализованы. Они содержат карту idFromElement
, которая в свою очередь содержит объекты Element
. Эти элементы помещаются в карту, , в то время как объекты Element
все еще находятся в процессе десериализации, используя метод putVal
. Но из-за измененного порядка в ObjectInputStream
это делается до того, как будет установлено примитивное значение поля id
(которое определяет хэш-код). Таким образом, объекты хранятся с использованием хеш-кода 0
, а затем присваиваются значения id
(например, значение 222
), в результате чего объекты оказываются в таблице под хеш-кодом, которого у них больше нет .
Теперь, на более абстрактном уровне, это было уже ясно из наблюдаемого поведения. Поэтому первоначальный вопрос был не «Что здесь происходит ???», а
если мой предложенный обходной путь выглядит нормально, или если есть что-то лучшее, что я мог бы сделать.
Я думаю, что обходной путь мог бы быть в порядке, но не решился бы сказать, что ничего не может пойти не так. Это сложно.
Что касается второй части: Что-то лучше может быть в том, чтобы подать отчет об ошибке в Java Bug Database , потому что новое поведение явно нарушено. Может быть трудно указать на спецификацию, которая нарушена, но десериализованная карта определенно несовместима , и это неприемлемо.
(Да, я также мог бы подать отчет об ошибке, но подумал, что может потребоваться больше исследований, чтобы убедиться, что он написан правильно, а не дубликат и т. Д ....)