2019年1月27日
目录
Java中的对象拷贝主要分为:浅拷贝(Shallow Copy)、深拷贝(Deep Copy)。
深拷贝与浅拷贝的本质是开辟新的内存空间,是用来存放另一个对象的引用,还是对象的值。
工作中,忽然收到测试提的一个奇怪的bug(当然只是浅层原因),经过log定位,发现对象竟然被修改了,顺藤摸瓜,我找到了问题根源:那就是某处地方对参数对象进行了一个浅拷贝,然后修改了对象里面的hashMap属性,导致了问题。
(总结:hashmap属性clone属于浅拷贝,Converthelper是公司封装的一个工具类,看过源码才明白也是浅拷贝,cloneable接口通过被实现,达到重写clone方法的目的)
概念
浅拷贝(Shallow Copy):①对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据。②对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。
浅拷贝的问题就是两个对象并非独立的。如果你修改了其中一个 Person 对象的 Name 对象,那么这次修改也会影响奥另外一个 Person 对象。
深拷贝:首先介绍对象图的概念。设想一下,一个类有一个对象,其成员变量中又有一个对象,该对象指向另一个对象,另一个对象又指向另一个对象,直到一个确定的实例。这就形成了对象图。那么,对于深拷贝来说,不仅要复制对象的所有基本数据类型的成员变量值,还要为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象图进行拷贝!
深拷贝是一个整个独立的对象拷贝。如果我们对整个 Person对象进行深拷贝,我们会对整个对象的结构都进行拷贝。
简单地说,深拷贝对引用数据类型的成员变量的对象图中所有的对象都开辟了内存空间;而浅拷贝只是传递地址指向,新的对象并没有对引用数据类型创建内存空间。
Test1
公司的一个工具类:
public class testCopy {
private static final Logger LOGGER = LoggerFactory.getLogger(testCopy.class);
public static void main(String [] args) throws CloneNotSupportedException {
A a = new A();
a.setAge(11);
a.setChineseName("中国梦");
a.setEngName("Chinese Dream");
HashMap<String,String> map = new HashMap<>();
map.put("a","a");
map.put("b","b");
a.setMeta(map);
System.out.println("[1] --- a={}"+JsonHelper.toJson(a));
//test ConvertHelper;
B b = ConvertHelper.convert(a, B.class);
System.out.println("[2] --- b={}"+JsonHelper.toJson(b));
b.getMeta().put("a","a");
b.setAge(22);
if (a.getMeta() == b.getMeta())
System.out.println("meta: a.hashCode="+a.getMeta().hashCode()+" b.hashCode="+b.getMeta().hashCode());
System.out.println("[3] --- a={}"+JsonHelper.toJson(a)+",b={}"+JsonHelper.toJson(b));
}
}
package org.tempuri;
import java.util.HashMap;
/**
* Date: 2019/1/27 11 :12
*
*/
public class A implements Cloneable{
private int age;
private String ChineseName;
private String EngName;
private HashMap<String,String> meta;
public HashMap<String, String> getMeta() {
return meta;
}
public void setMeta(HashMap<String, String> meta) {
this.meta = meta;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getChineseName() {
return ChineseName;
}
public void setChineseName(String chineseName) {
ChineseName = chineseName;
}
public String getEngName() {
return EngName;
}
public void setEngName(String engName) {
EngName = engName;
}
public A() {
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
package org.tempuri;
import java.util.HashMap;
/**
* Date: 2019/1/27 11 :13
*
*/
public class B implements Cloneable{
private int age;
private String EngName;
private HashMap<String,String> meta;
public HashMap<String, String> getMeta() {
return meta;
}
public void setMeta(HashMap<String, String> meta) {
this.meta = meta;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getEngName() {
return EngName;
}
public void setEngName(String engName) {
EngName = engName;
}
public B() {
}
}
测试结果:
[1] --- a={}{"age":11,"ChineseName":"中国梦","EngName":"Chinese Dream","meta":{"a":"a","b":"b"}}
[2] --- b={}{"age":11,"EngName":"Chinese Dream","meta":{"a":"a","b":"b"}}
meta: a.hashCode=0 b.hashCode=0
[3] --- a={}{"age":11,"ChineseName":"中国梦","EngName":"Chinese Dream","meta":{"a":"a","b":"b"}},b={}{"age":22,"EngName":"Chinese Dream","meta":{"a":"a","b":"b"}}
GG,我的引用变量修改,居然影响到了原来的对象!!
分析一下源码(ConvertHelper.convert()):
public static <D> D convert(Object src, Class<D> clzDst) {
if (src == null) {
return null;
} else {
assert clzDst != null;
Object dst = null;
try {
dst = clzDst.newInstance();
} catch (InstantiationException var4) {
LOGGER.error("Unexpected exception", var4);
throw new RuntimeException("Unexpected exception", var4);
} catch (IllegalAccessException var5) {
LOGGER.error("Unexpected exception", var5);
throw new RuntimeException("Unexpected exception", var5);
}
BeanUtils.copyProperties(src, dst);
return dst;
}
}
上面两个步骤很重要:1是 clzDst.newInstance() ,2是 BeanUtils.copyProperties(src, dst);
先看1:
@CallerSensitive
public T newInstance()
throws InstantiationException, IllegalAccessException
{
if (System.getSecurityManager() != null) {
checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), false);
}
// NOTE: the following code may not be strictly correct under
// the current Java memory model.
// Constructor lookup
if (cachedConstructor == null) {
if (this == Class.class) {
throw new IllegalAccessException(
"Can not call newInstance() on the Class for java.lang.Class"
);
}
try {
Class<?>[] empty = {};
final Constructor<T> c = getConstructor0(empty, Member.DECLARED);
// Disable accessibility checks on the constructor
// since we have to do the security check here anyway
// (the stack depth is wrong for the Constructor's
// security check to work)
java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction<Void>() {
public Void run() {
c.setAccessible(true);
return null;
}
});
cachedConstructor = c;
} catch (NoSuchMethodException e) {
throw (InstantiationException)
new InstantiationException(getName()).initCause(e);
}
}
Constructor<T> tmpConstructor = cachedConstructor;
// Security check (same as in java.lang.reflect.Constructor)
int modifiers = tmpConstructor.getModifiers();
if (!Reflection.quickCheckMemberAccess(this, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
if (newInstanceCallerCache != caller) {
Reflection.ensureMemberAccess(caller, this, null, modifiers);
newInstanceCallerCache = caller;
}
}
// Run constructor
try {
return tmpConstructor.newInstance((Object[])null);
} catch (InvocationTargetException e) {
Unsafe.getUnsafe().throwException(e.getTargetException());
// Not reached
return null;
}
}
再看2:
public static void copyProperties(Object source, Object target) throws BeansException {
copyProperties(source, target, null, (String[]) null);
}
private static void copyProperties(Object source, Object target, Class<?> editable, String... ignoreProperties)
throws BeansException {
Assert.notNull(source, "Source must not be null");
Assert.notNull(target, "Target must not be null");
Class<?> actualEditable = target.getClass();
if (editable != null) {
if (!editable.isInstance(target)) {
throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
"] not assignable to Editable class [" + editable.getName() + "]");
}
actualEditable = editable;
}
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);
for (PropertyDescriptor targetPd : targetPds) {
Method writeMethod = targetPd.getWriteMethod();
if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
if (sourcePd != null) {
Method readMethod = sourcePd.getReadMethod();
if (readMethod != null &&
ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
try {
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
}
Object value = readMethod.invoke(source);
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
writeMethod.invoke(target, value);
}
catch (Throwable ex) {
throw new FatalBeanException(
"Could not copy property '" + targetPd.getName() + "' from source to target", ex);
}
}
}
}
}
}
仔细看到了:
Method readMethod = sourcePd.getReadMethod();
是的,这个就是用原来的对象的getter和setter进行一个属性设置,因此属于浅拷贝!!也是bug的最终原因。
Test2
那么使用clone能不能解决呢?做个测试:
A a = new A();
a.setAge(11);
a.setChineseName("中国梦");
a.setEngName("Chinese Dream");
HashMap<String,String> mapC = new HashMap<>();
mapC.put("a","a");
mapC.put("b","b");
a.setMeta(mapC);
A b = (A) a.clone();
B c = ConvertHelper.convert(b, B.class);
System.out.println("[1] --- a={}"+JsonHelper.toJson(a));
System.out.println("[2] --- b={}"+JsonHelper.toJson(b));
System.out.println("[3] --- c={}"+JsonHelper.toJson(c));
//test Clone
c.getMeta().put("a","c");
c.setAge(22);
if (c.getMeta() == b.getMeta())
System.out.println("c.getMeta() == b.getMeta() !!");
if (c.getMeta() == a.getMeta())
System.out.println("c.getMeta() == a.getMeta() !!");
if (b.getMeta() == a.getMeta())
System.out.println("b.getMeta() == a.getMeta() !!");
System.out.println("[4] --- a={}"+JsonHelper.toJson(a));
System.out.println("[5] --- b={}"+JsonHelper.toJson(b));
System.out.println("[6] --- c={}"+JsonHelper.toJson(c));
测试结果:
[1] --- a={}{"age":11,"ChineseName":"中国梦","EngName":"Chinese Dream","meta":{"a":"a","b":"b"}}
[2] --- b={}{"age":11,"ChineseName":"中国梦","EngName":"Chinese Dream","meta":{"a":"a","b":"b"}}
[3] --- c={}{"age":11,"EngName":"Chinese Dream","meta":{"a":"a","b":"b"}}
b.getMeta() == a.getMeta() !!
[4] --- a={}{"age":11,"ChineseName":"中国梦","EngName":"Chinese Dream","meta":{"a":"a","b":"b"}}
[5] --- b={}{"age":11,"ChineseName":"中国梦","EngName":"Chinese Dream","meta":{"a":"a","b":"b"}}
[6] --- c={}{"age":22,"EngName":"Chinese Dream","meta":{"a":"c","b":"b"}}
GG,还是一样,因为clone本身就是浅拷贝啊!!
Solution
如果想要实现深拷贝,推荐使用 clone() 方法,这样只需要每个类自己维护自己即可,而无需关心内部其他的对象中,其他的参数是否也需要 clone() 。
抛砖引玉
个人觉得,除了这种硬编码开辟一个对象的存储空间,然后重复设置进新的变量,这种方法很low;
由于上线很急,我使用了临时解决方案(既然是hashMap被引用拷贝了,那么对症下药吧,新建个put进去),待往后有了新的解决方法再分享。如果你有好的思路,烦请留言,不吝赐教 ~~
//solution:create new Object
HashMap<String, String> metaClone = new HashMap<String, String>();
metaClone.putAll(b.getMeta());
c.setMeta(metaClone);
参考文章
1、https://blog.csdn.net/wangbiao007/article/details/52625099