考慮以下代碼:
Hashmap map = new HashMap();
map.put("key", "value")
service.doSomething(map);
map.clear()
測試時想要驗證doSomething調(diào)用時的參數(shù)內(nèi)容(狀態(tài)),使用Mockito的ArgumentCaptor,capture到的都將是空的map,因為capture到的對象,在調(diào)用doSomething后,又被修改了(clear)。
一方面目前Mockito的實現(xiàn),并非capture時立即創(chuàng)建參數(shù)的副本,而是直接持有其引用,所以后面的修改在將會生效,即修改操作發(fā)生后,再進行驗證,參與驗證的將是修改后的值。
但是另一方面來看,這也是設(shè)計上的缺陷(code/design smell),更優(yōu)雅的方式應(yīng)該是在函數(shù)調(diào)用時傳入不可變的對象,這樣也會避免隱藏的bug,例如:被調(diào)用函數(shù)并未立即使用參數(shù),而是在回調(diào)中/異步線程中使用參數(shù),因此即便函數(shù)調(diào)用是同步進行的,后續(xù)的修改也會導(dǎo)致被調(diào)用函數(shù)使用參數(shù)時值發(fā)生了變化。
更好的方式是這樣的:
Map map = new HashMap();
map.put("key", "value")
service.doSomething(map);
Map mapTwo = new HashMap();
mapTwo.put("key2", "value2");
serviceTwo.doSomethingElse(mapTwo);
另外,使用AutoValue/AutoParcel可以很方便的創(chuàng)建不可變的對象,但是在使用過程中還是容易“入坑”,例如使用了Collection類,即便元素對象是不可變的,但是collection并不是,如果按照上面的方式去實現(xiàn),依然會導(dǎo)致問題,一方面,新new一個Map是一種解決方式,另一方面,如果是使用List,可以通過變長參數(shù)的方式來傳遞,這樣就能避免這一問題,變長參數(shù)使用時仍然可能會有問題,例如:調(diào)用時傳遞的是一個數(shù)組對象,而非手動傳遞多個參數(shù),那么如果多次調(diào)用之間傳遞的是同一個數(shù)組對象,那還是存在上面的問題,所以,無需變成傳遞變長參數(shù),而是在調(diào)用時保證之后不再修改參數(shù)對象(TODO:go語言中有把數(shù)組打散之后傳遞的語法,是否能避免此問題?)。
參考: Google網(wǎng)上論壇
update at 2015. 09. 15
經(jīng)過更多的實踐與思考,我對上述問題有了新的認識,上述問題的解決,只能放到函數(shù)調(diào)用方來做,即便在被調(diào)用方的第一行代碼對傳入?yún)?shù)進行一次深拷貝,還是無法保證深拷貝這一操作會早于調(diào)用方后續(xù)的修改。只能通過限制/強制保證調(diào)用方傳進來的數(shù)據(jù)就是不會且不可改變的數(shù)據(jù),才能避免此問題。
而如何保證這一點,可以通過調(diào)用方傳參時進行深拷貝,或者unmodifiable+程序員保證調(diào)用后不再讀寫該數(shù)據(jù),來實現(xiàn)。
更多閱讀