【发布时间】:2019-10-27 08:15:40
【问题描述】:
我在 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 中运行它,结果是 NullPointerException,因为 retrievedElement.idFrom(retrievedElement) 返回 null。
我在HashMap.hash() 处设置了一个断点并注意到:
- 在Java 8中,当
idFromElement被反序列化并添加Element(222)时,它的id是222,所以我以后可以找到它。 - 在 Java 11 中,
id未初始化(int为 0,如果我将其设为Integer,则为 null),因此当它存储在HashMap中时,hash()为 0。后来,当我尝试检索它时,id是 222,所以idFromElement.get(element)返回null。
我知道这里的顺序是反序列化(元素(222))->反序列化(idFromElement)->将未完成的元素(222)放入Map中。但是,由于某种原因,在 Java 8 中,id 在我们进行到最后一步时已经初始化,而在 Java 11 中则没有。
我想出的解决方案是使idFromElement 瞬态并编写自定义writeObject 和readObject 方法来强制idFromElement 在id 之后反序列化:
...
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 8 和Java 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;
}
}
【问题讨论】:
-
在比较了 8 和 11 的
ObjectInputStream实现之后,我很想将其视为一个错误:在 8 中,它读取原始字段值,然后 设置它们,然后它读取对象字段值,然后设置它们。在11中,它读取原始字段值,然后读取对象字段值,然后设置原始字段值,然后设置对象字段值。当我建议它确实是错误(因此是一个错误)时,我会四处走动,但如果你认为它有帮助,我会将相关的代码部分分割成一个答案. -
引用部分仅说明了描述符的写入顺序。它没有说明读取字段值的顺序。
-
@Marco13 序列化通常是有缺陷的。这种圆形对象图有很多问题。
HashMap的策略并非完全错误,因为无法保证反序列化后对象的哈希码相同。想想所有没有被覆盖的对象Object.hashCode()。它可以通过注册ObjectInputValidation来推迟重新散列,但即使这样也有问题。协议说值是按描述符的顺序写入的,但这并没有说它们是分别读取的。按此顺序恢复。 -
@Marco13 只是为了防止误解:即使没有明确的定义,我同意应该保留以前的行为,除非有充分的理由进行更改。再增加一个问题是没有意义的。如果他们弃用并删除它,那很好。否则,它应该保持在兼容性维护模式。
-
请注意,JDK 8 的行为是在 JDK 1.4 中建立的,similarly breaking compatibility。当时建议的解决方法看起来很像这个问题中的解决方法。 has been recognized JDK 9 也做出了兼容性破坏性更改,尽管错误报告的情况略有不同。
标签: java serialization java-8 deserialization java-11