Java里Hashmap序列化的一個坑
在做業務需求的過程中,遇到一個非常奇怪的問題。在一個繼承了 Serializable 接口的java bean里按照常規操作添加了一個hashmap和與之對應的getter、setter,就像這樣:
...
private HashMap<String, String> mChooseMap;
publicHashMap<String, String>getChooseMap(){
return mChooseMap;
}
publicvoidsetChooseMap(HashMap<String, String> chooseMap){
mChooseMap = chooseMap;
}
...
然后我在某種情況下對含有這個hashmap的java bean進行了deep clone操作,就像這樣:
/**
* 深度拷貝 要求data對象及其引用對象都實現了Serializable接口才可以用
*
* @param o 要深拷貝的對象
*/
public static <T> TdeepClone(T o)throwsIOException, 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看了一眼:
public classHashMap<K,V>extendsAbstractMap<K,V>implementsCloneable,Serializable
我的臉有點疼。代碼清清楚楚的寫著,明明是序列化了……我不甘心,再次debug發現crash出現在這里:
...
oo.writeObject(o);
...
有意思,這不是就是寫對象嗎,沒想明白為什么掛在這里,于是我瞅了一眼hashmap的writeObject方法:
privatevoidwriteObject(ObjectOutputStream stream)throwsIOException{
// 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 方法中:
...
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:
contactItem.setChooseMap(new LinkedHashMap() {
{
put("男", "1");
put("女", "2");
}
});
明明是new了一個hashmap,但是writeObject的時候傳進去的卻是當前的這個類。看來確實是我姿勢不對。
在我疑惑的時候,我搜到這么一篇文章:
和我差不多的問題嘛,我瞅了一眼回答。
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然后報錯了!因為它根本就不能被序列化嘛。
解決問題
最后我改成了這樣:
LinkedHashMap<String, String> genderMap = new LinkedHashMap<>();
genderMap.put("男", "1");
genderMap.put("女", "2");
contactItem.setChooseMap(genderMap);
問題完美解決。
以后再也不會犯這個錯誤了,臉疼,這回深刻的記住了。
來自:http://richardcao.me/2017/05/15/Java-Hashmap-Serializable/