Java 8 和 Java 11 之间的不同反序列化行为

我在Java 11中反序列化时遇到了一个问题,导致一个找不到的密钥。如果任何对这个问题有更多了解的人都可以说我提出的解决方法看起来还不错,或者我是否可以做一些更好的事情,我将不胜感激。HashMap

考虑以下人为的实现(实际问题中的关系有点复杂且难以更改):

public class Element implements Serializable {
    private static long serialVersionUID = 1L;

    private final int id;
    private final Map<Element, Integer> idFromElement = new HashMap<>();

    public Element(int id) {
        this.id = id;
    }

    public void addAll(Collection<Element> elements) {
        elements.forEach(e -> idFromElement.put(e, e.id));
    }

    public Integer idFrom(Element element) {
        return idFromElement.get(element);
    }

    @Override
    public int hashCode() {
        return id;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof Element)) {
            return false;
        }
        Element other = (Element) obj;
        return this.id == other.id;
    }
}

然后,我创建一个具有对自身引用的实例,并对其进行序列化和反序列化:

public static void main(String[] args) {
    List<Element> elements = Arrays.asList(new Element(111), new Element(222));
    Element originalElement = elements.get(1);
    originalElement.addAll(elements);

    Storage<Element> storage = new Storage<>();
    storage.serialize(originalElement);
    Element retrievedElement = storage.deserialize();

    if (retrievedElement.idFrom(retrievedElement) == 222) {
        System.out.println("ok");
    }
}

如果我在Java 8中运行此代码,则结果为“ok”,如果我在Java 11中运行它,则结果是a,因为返回.NullPointerExceptionretrievedElement.idFrom(retrievedElement)null

我放了一个断点,注意到:HashMap.hash()

  • 在Java 8中,当被反序列化并被添加到其中时,它是222,所以我以后能够找到它。idFromElementElement(222)id
  • 在Java 11中,没有初始化(0代表或null,如果我把它变成一个),所以当它存储在.后来,当我尝试检索它时,是222,所以返回。idintIntegerhash()HashMapididFromElement.get(element)null

我知道这里的序列是 deserialize(Element(222)) -> deserialize(idFromElement) ->未完成的元素(222) 放入 Map 中。但是,由于某种原因,在Java中,当我们到达最后一步时,8已经初始化,而在Java 11中则不是。id

我想出的解决方案是进行瞬态并编写自定义和方法来强制在以下之后进行反序列化:idFromElementwriteObjectreadObjectidFromElementid

...
transient private Map<Element, Integer> idFromElement = new HashMap<>();
...
private void writeObject(ObjectOutputStream output) throws IOException {
    output.defaultWriteObject();
    output.writeObject(idFromElement);
}

@SuppressWarnings("unchecked")
private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
    input.defaultReadObject();
    idFromElement = (HashMap<Element, Integer>) input.readObject();
}

在序列化/反序列化期间,我能够找到的关于顺序的唯一参考是:

对于可序列化的类,将设置 SC_SERIALIZABLE 标志,字段数计算可序列化字段的数量,后跟每个可序列化字段的描述符。描述符按规范顺序编写。基元类型化字段的描述符首先按字段名称排序,然后按字段名称排序的对象类型化字段的描述符编写。这些名称使用 String.compareTo 进行排序。

这在Java 8Java 11文档中是相同的,并且似乎暗示应该首先编写原始类型字段,所以我预计不会有任何区别。


为完整性,包括的实施:Storage<T>

public class Storage<T> {
    private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();

    public void serialize(T object) {
        buffer.reset();
        try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(buffer)) {
            objectOutputStream.writeObject(object);
            objectOutputStream.flush();
        } catch (Exception ioe) {
            ioe.printStackTrace();
        }
    }

    @SuppressWarnings("unchecked")
    public T deserialize() {
        ByteArrayInputStream byteArrayIS = new ByteArrayInputStream(buffer.toByteArray());
        try (ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayIS)) {
            return (T) objectInputStream.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
}

答案 1

正如注释中提到的和asker所鼓励的,以下是在版本8和版本11之间更改的代码部分,我认为这是不同行为的原因(基于阅读和调试)。

不同之处在于类,在其核心方法之一中。这是 Java 8 中实现的相关部分:ObjectInputStream

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);
}

...

引入了一个内部类 。该方法现在仅读取字段值,并将其作为对象返回。然后,通过将此对象传递给新引入的方法,将返回的值分配给字段,该方法在内部执行最初在读取基元值后立即完成的调用。FieldValuesdefaultReadFieldsFieldValuesFieldValuesdefaultSetFieldValuesdesc.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

Holger已经在评论中给出了为什么这是必要的提示:不能保证反序列化键的哈希代码将与序列化之前相同。因此,盲目地“恢复原始数组”基本上可能导致对象以错误的哈希代码存储在表中。

但在这里,情况恰恰相反:键(即类型的对象)被反序列化。它们包含地图,而地图又包含对象。这些元素被放入映射中,对象仍在使用该方法进行反序列化的过程中。但是由于 中的顺序发生了变化,这是在设置字段的基元值(确定哈希代码)之前完成的。因此,对象使用哈希代码存储,然后分配值(例如值),导致对象最终以它们实际上不再具有的哈希代码在表中。ElementidFromElementElementElementputValObjectInputStreamid0id222


现在,在更抽象的层面上,这已经从观察到的行为中很明显了。因此,最初的问题不是“这里发生了什么???”,而是

如果我提出的解决方法看起来还行,或者如果有更好的事情我可以做。

我认为解决方法可能是可以的,但会犹豫地说那里不会出错。这很复杂。

从第二部分开始:更好的办法是在Java Bug数据库中提交错误报告,因为新行为显然被破坏了。可能很难指出违反的规范,但反序列化的映射肯定是不一致的,这是不可接受的。


(是的,我也可以提交错误报告,但我认为可能需要进行更多的研究,以确保它写得正确,而不是重复,等等。


答案 2

我想在上面的优秀答案中添加一个可能的解决方案:

除了进行瞬态并强制在 之后反序列化 HashMap 之外,您还可以使 Not final 在调用 之前对其进行反序列化。idFromElementididdefaultReadObject()

这使得解决方案更具可扩展性,因为可能还有其他类/对象使用 and 方法或 id 导致您描述的类似循环。hashCodeequals

它还可能导致问题的更通用的解决方案,尽管这还没有完全考虑清楚:在调用之前,在反序列化其他对象时使用的所有信息都需要反序列化。这可能是 id,也可能是类公开的其他字段。defaultReadObject()