Java 里 Hashmap 序列化的一个坑

Posted 开发者技术前线

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 里 Hashmap 序列化的一个坑相关的知识,希望对你有一定的参考价值。

属于你的文章第一时间送达!


发现问题

在做业务需求的过程中,遇到一个非常奇怪的问题。在一个继承了Serializable接口的 java bean 里按照常规操作添加了一个 hashmap 和与之对应的 getter、setter,就像这样:

1
2
3
4
5
6
7
8
9
10
11
...
private HashMap<String, String> mChooseMap;

public HashMap<String, String> getChooseMap() {
       return mChooseMap;
   }

   public void setChooseMap(HashMap<String, String> chooseMap) {
       mChooseMap = chooseMap;
   }
...


然后我在某种情况下对含有这个 hashmap 的 java bean 进行了 deep clone 操作,就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
    * 深度拷贝 要求data对象及其引用对象都实现了Serializable接口才可以用
    *
    * @param o 要深拷贝的对象
    */
   public static <T> T deepClone(T o) throws IOException, ClassNotFoundException {
       //将对象写到流里
       ByteArrayOutputStream bo = new ByteArrayOutputStream();
       ObjectOutputStream oo = new ObjectOutputStream(bo);
       oo.writeObject(o);
       //从流里读出来
       ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
       ObjectInputStream oi = new ObjectInputStream(bi);
       return (T) oi.readObject();
   }


很简单,对吧?在我的意料之中,这件事简直可以小的忽略不计,完全就是一个非常常规的操作。但是我被打脸了,啪啪的打,因为我发现了一个以前没遇到过的问题…… 为毛我的数据不见了?本来这个 bean 里的数据应该依照列表的形式在 listview 里加载出来,但是我发现我的 listview 一片空白。最关键的问题是,控制台并没有报错信息啊!

查错

面对这个问题,我先怀疑了一会儿人生。错误还是要查的,于是我只能先猜猜为什么会出现这个问题。我和之前的代码版本做了对比,发现我只多写了一个 hashmap,就出现了问题,于是我怀疑是这个 hashmap 出现了一个我以前不太了解的坑,导致了现在的问题。

Debug

首先我要庆幸的是,我的 listview 是应该拿到后端接口数据之后渲染的,既然控制台没有相关的日志输出,我就 debug 了一下,看看是不是后端的数据问题。结果我发现了一个让我惊讶的现象,在我处理请求返回的代码中,我正好做了 deep clone 操作,结果出现了如下错误:


咦,控制台毛错没报,为毛一 debug 就出现了这个问题?在一开始的代码里,我在方法注释里已经写了,深度拷贝 要求 data 对象及其引用对象都实现了 Serializable 接口才可以用。这个错太打脸了,我竟然传了一个没有实现序列化接口的对象?再根据刚才的推断,难道说 hashmap 没有实现序列化接口?

追根溯源

非常震惊的我赶紧点开 hashmap 看了一眼:

1
public class HashMap<K, V> extends AbstractMap<K, V> implements Cloneable, Serializable


我的脸有点疼。代码清清楚楚的写着,明明是序列化了…… 我不甘心,再次 debug 发现 crash 出现在这里:

1
2
3
...
oo.writeOject(o);
...


有意思,这不是就是写对象吗,没想明白为什么挂在这里,于是我瞅了一眼 hashmap 的 writeObject 方法:

复制

1
2
3
4
5
6
7
8
9
10
11
12
13
private void writeObject(ObjectOutputStream stream) throws IOException {
       // Emulate loadFactor field for other implementations to read
       ObjectOutputStream.PutField fields = stream.putFields();
       fields.put("loadFactor", DEFAULT_LOAD_FACTOR);
       stream.writeFields();

       stream.writeInt(table.length); // Capacity
       stream.writeInt(size);
       for (Entry<K, V> e : entrySet()) {
           stream.writeObject(e.getKey());
           stream.writeObject(e.getValue());
       }
   }


private的?为什么不是 public?于是我从 crash 出现的地方一层层点进去看代码,边看边想是哪里出了问题。看着看着我发现这么一段代码,在ObjectOutputStream类的writeObjectInternal方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
if (clDesc.hasMethodWriteReplace()){
                   Method methodWriteReplace = clDesc.getMethodWriteReplace();
                   Object replObj;
                   try {
                       replObj = methodWriteReplace.invoke(object, (Object[]) null);
                   } catch (IllegalAccessException iae) {
                       replObj = object;
                   } catch (InvocationTargetException ite) {
                       // WARNING - Not sure this is the right thing to do
                       // if we can't run the method
                       Throwable target = ite.getTargetException();
                       if (target instanceof ObjectStreamException) {
                           throw (ObjectStreamException) target;
                       } else if (target instanceof Error) {
                           throw (Error) target;
                       } else {
                           throw (RuntimeException) target;
                       }
                   }
          ...
                   }
               }


这下我看懂了,这不就是说,如果要这对象有自己的 writeObject 方法,就在这里会用反射的方式执行对象自己的 writeObject 方法么,这么一来 hashmap 里的private void writeObject就可以理解了。那我这个问题也就简单了,我在这里加个断点,我看看 hashmap 到底 writeObject 除了什么问题不就可以了么。

于是再次 debug。不看不知道,一看吓一跳!我发现原来问题出现在这里:


What?! 原来错误是从这里报出来的。我仔细看了一下报错后面的信息,居然是一个其他的类,确实不是一个可以序列化的类。

此时此刻我的脸真的很疼。

代码不会骗人,我确实传进来了一个没有序列化的类。于是我赶紧点进去报错提示的类里查看,我发现了我是在这个类里往 bean 中 set 我的 map。我保证这是我第一次这么 set:

1
2
3
4
5
6
contactItem.setChooseMap(new LinkedHashMap() {
  {
                      put("男", "1");
                       put("女", "2");
                 
    }

  });


明明是 new 了一个 hashmap,但是 writeObject 的时候传进去的却是当前的这个类。看来确实是我姿势不对。

在我疑惑的时候,我搜到这么一篇文章:hashmap-not-serializable和我差不多的问题嘛,我瞅了一眼回答。

The exception message tells you exactly what the problem is: you are trying to serialize an instance of class SimpleSerializationTest, and that class is not serializable.

Why? Well, you have created an anonymous inner class of SimpleSerializationTest, one that extends HashMap, and you are trying to serialize an instance of that class. Inner classes always have references to the relevant instance of their outer class, and by default, serialization will try to traverse those.

嗯?这话大概意思就是,在一个类里创建一个匿名内部类,相当于扩展了 hashmap 和要序列化的类的实例。内部类会持有外部类的引用,默认情况下会遍历这些进行序列化。

卧槽!原来是这个原因!怪不得我用来 new hashmap 的这个类被 writeObject 然后报错了!因为它根本就不能被序列化嘛。

解决问题

最后我改成了这样:

1
2
3
4
LinkedHashMap<String, String> genderMap = new LinkedHashMap<>();
               genderMap.put("男", "1");
               genderMap.put("女", "2");
               contactItem.setChooseMap(genderMap);


问题完美解决。

以后再也不会犯这个错误了,脸疼,这回深刻的记住了。

猜你喜欢




以上是关于Java 里 Hashmap 序列化的一个坑的主要内容,如果未能解决你的问题,请参考以下文章

今天踩的Dubbo的序列化对象的一个坑

今天踩的Dubbo的序列化对象的一个坑

Android Parcelable反序列化报错笔记:java.lang.RuntimeException: Unmarshalling unknown type code at offset(代码片

JAligner的一个坑

Java踩坑笔记:ObjectIOStream与IOStream的各种装饰器(先挖个坑,以后再来详细填)

HashMap(key,Object)中的Java Gson序列化和反序列化对象[重复]