题记:项目中用到对象的复制功能,自己写了一个工具类,使用的浅克隆(当时根本不懂什么浅克隆,深克隆),后期代码评审被替换,虽潜心研究!特总结如下!
对象复制可以分为:(地址复制),(实现Cloneable的方法),(使用BeanUtils.copyProperties() ),(PropertyUtils.copyProperties()),(序列化),(反射)
这里先总结一下浅克隆和深克隆:
Java的数据类型分为:基本数据类型(byte,short,int,long,float,double,boolean,char)和引用数据类型(数组,类,接口)。
在数据做为参数传递的时候,基本数据类型是“值传递”,引用数据类型是“引用传递”(地址传递)。
这里有一个特例:String是一个类,类是引用数据类型,做为参数传递的时候,应该是引用传递。但是从结果看起来却是值传递。
原因:String是被final修饰的,String的API中有这么一句话:“their values cannot be changed after they are created”,
意思是:String的值在创建之后不能被更改。
API中还有一段:
String str = "abc";
等效于:
char data[] = {'a', 'b', 'c'};
String str = new String(data);
也就是说:对String对象str的任何修改 等同于 重新创建一个对象,并将新的地址值赋值给str。
浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制。这里说的复制是吧引用类型全部复制给对象,而不是把对象地址复制一份给对象。
一、浅克隆(地址复制)
public class ShallowCopy {
@Data
class Student {
private String name;
}
//直接将对象的地址复制给另一个对象,但是这样改变一个另一个也会改变!
@Test
public void test1() {
Student stu1 = new Student();
stu1.setName("aaaa");
Student stu2 = stu1;
stu1.setName("bbbb");
stu2.setName("cccc");
System.out.println("学生1:" + stu1.getName());
System.out.println("学生2:" + stu2.getName());
}
}
打印结果:
学生1:cccc
学生2:cccc
Disconnected from the target VM, address: '127.0.0.1:0', transport: 'socket'
Process finished with exit code 0
总结:这里stu1和stu2在对内存中创建了两个对象,两个对象地址不同,Student stu2=stu1;这段代码将stu1的地址赋值给stu2,这是stu1和stu2指向同一个地址。当改变任意一个时两个对象的值都会被改变。
二、深克隆(实现Cloneable的方法)
@Data
class Student3 implements Cloneable {
private int name;
@Override
public Object clone() {
Student3 stu = null;
try {
stu = (Student3) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return stu;
}
}
@Test
public void test3() {
Student3 stu1 = new Student3();
stu1.setName(12345);
Student3 stu2 = (Student3) stu1.clone();
System.out.println("学生1:" + stu1.getName());
System.out.println("学生2:" + stu2.getName());
stu2.setName(54321);
System.out.println("学生1:" + stu1.getName());
System.out.println("学生2:" + stu2.getName());
}
打印结果:
Connected to the target VM, address: '127.0.0.1:0', transport: 'socket'
学生1:12345
学生2:12345
学生1:12345
学生2:54321
Disconnected from the target VM, address: '127.0.0.1:0', transport: 'socket'
Process finished with exit code 0
总结:可以看到这里复制完以后对str2进行修改,但stu1的值没有被修改,说明复制stu2时进行了深克隆,而不是地址复制。
这里Student3实体实现了一个标记接口CloneAble,复写的时Object方法的clone()方法。
延申:
如果对象中包含对象使用clone方法克隆。
@Data
class Address implements Cloneable {
private String add;
@Override
public Object clone() {
Address stu = null;
try {
stu = (Address) super.clone(); //浅复制
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return stu;
}
}
@Data
class Student2 implements Cloneable {
private int name;
private Address addr;
@Override
public Object clone() {
Student2 stu = null;
try {
stu = (Student2) super.clone(); //浅复制
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
//这里需要添加这一条才能实现深克隆,否则即便实现了CloneAble接口依然是浅克隆
stu.addr = (Address) addr.clone(); //深度复制
return stu;
}
}
@Test
public void test4() {
Address addr = new Address();
addr.setAdd("北京");
Student2 stu1 = new Student2();
stu1.setName(123);
stu1.setAddr(addr);
Student2 stu2 = (Student2) stu1.clone();
System.out.println("学生1:" + stu1.getName() + ",地址:" + stu1.getAddr().getAdd());
System.out.println("学生2:" + stu2.getName() + ",地址:" + stu2.getAddr().getAdd());
addr.setAdd("上海");
System.out.println("学生1:" + stu1.getName() + ",地址:" + stu1.getAddr().getAdd());
System.out.println("学生2:" + stu2.getName() + ",地址:" + stu2.getAddr().getAdd());
}
总结:也就是两个实体类对象都要实现serizable接口,另外在包含对象中要添加被包含对象的复制语句,否则深克隆不生效
//这里需要添加这一条才能实现深克隆,否则即便实现了CloneAble接口依然是浅克隆
stu.addr = (Address) addr.clone(); //深度复制
三、深克隆(使用BeanUtils.copyProperties() )
@Data
class Student {
private String name;
}
@Test
public void test5() {
Student stu1 = new Student();
stu1.setName("aaaa");
Student stu2 = new Student();
BeanUtils.copyProperties(stu1, stu2);
// stu2.setNumber(3456);
System.out.println("stu1:" + stu1.getName());
System.out.println("stu2:" + stu2.getName());
}
四、深克隆(PropertyUtils.copyProperties())这里对象复制完为什么String类型为null,int类型为0,有知道的留个言!
@Data
class Student {
private String name;
}
@Test
public void test6() throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Student stu1 = new Student();
stu1.setName("aaaa");
Student stu2 = new Student();
PropertyUtils.copyProperties(stu2, stu1);
// stu2.setNumber(2222);
System.out.println("stu1:" + stu1.getName());
System.out.println("stu2:" + stu2.getName());
}
五、深克隆(序列化)
@SuppressWarnings("unchecked")
public static <T extends Serializable> T clone(T obj) {
T cloneObj = null;
try {
//写入字节流
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream obs = new ObjectOutputStream(out);
obs.writeObject(obj);
obs.close();
//分配内存,写入原始对象,生成新对象
ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray());
ObjectInputStream ois = new ObjectInputStream(ios);
//返回生成的新对象
cloneObj = (T) ois.readObject();
ois.close();
} catch (Exception e) {
e.printStackTrace();
}
return cloneObj;
}
@Test
public void test2() {
Country country = new Country(1, "china");
Person person = new Person(country, 1, "test");
//引用传递
Country country1 = country;
Person person1 = person;
//序列化和反序列化
Person person2 = (Person) CloneUtils.clone(person);
Country country2 = (Country) CloneUtils.clone(country);
person1.setName("bbbbPerson");
country1.setName("bbbContry");
//修改序列化复制对象
person2.setName("aaaaPerson");
country2.setName("aaaaContry");
System.out.println("创建国家 :" + country);
System.out.println("引用传递国家 :" + country1);
System.out.println("序列化复制国家 :" + country2);
System.out.println("创建人 :" + person);
System.out.println("引用传递人:" + person1);
System.out.println("序列化复制人:" + person2);
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
int id;
String name;
int age;
Country country;
public Person(Country country, int age, String name) {
this.country = country;
this.age = age;
this.name = name;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Country implements Serializable {
private static final long serialVersionUID = 1L;
int code;
String name;
}
总结:序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。
Java序列化是指把Java对象转换为字节序列的过程;而Java反序列化是指把字节序列恢复为Java对象的过程。字节码可以存储,无状态,而对象在内存中开辟空间,有地址。
由此,可以把对象序列化后反序列化。相当于破碎重组。
前提是:实体类需要实现序列化接口
六、深克隆(反射)
@Test
public void test1(){
//2.创建一个静态的Student对象
Student stu1=new Student("ST20161282","Henry",22);
try {
Student stu2=(Student)ReflactCopyUtils.copyObj(stu1);
stu2.setName("aaaa");
System.out.println("复制对象成功");
System.out.println(stu1.toString());
System.out.println(stu2.toString());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public class ReflactCopyUtils {
public static Object copyObj(Object obj) throws Exception {
//3. 获取已有Student对象的Class对象
Class<?> classType = obj.getClass();
//4. 通过Class.newinstance方法动态构建一个副本对象
Object stu1 = classType.newInstance();
//由于不知道源对象的具体属性和属性值,通过反射机制,先得到属性名称,再拼接字段得到属性对应的get,set方法
for (Field field : classType.getDeclaredFields()) {
//5. 获取副本对象的get,set方法
String getMethodName = "get" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1);
String setMethodName = "set" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1);
//6. 调用源对象的get方法获取属性值
Method getMethod = classType.getDeclaredMethod(getMethodName, new Class[]{});
Object value = getMethod.invoke(obj, new Object[]{});
//7. 调用副本对象的set方法把属性值复制过来
Method setMethod = classType.getDeclaredMethod(setMethodName, new Class[]{field.getType()});
setMethod.invoke(stu1, new Object[]{value});
}
return stu1;
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
private String id;
private String name;
private int age;
}
总结:
案例介绍:
Student类,有属性id, name, age, 还有对应的get,set方法和构造方法。
现产生一个Student对象。通过反射复制此Student对象。复制时,并不知道源对象具体的属性名称。
案例设计
通过反射机制来获取类的属性和方法
通过反射生成对象,并通过反射来调用其set方法对属性进行复制以达到复制对象的目的
最后对赋值成功的对象信息进行打印
方案实施
创建一个Student类
创建一个静态的Student对象
获取已有Student对象的Class对象
通过Class.newinstance方法动态构建一个副本对象
获取副本对象的get,set方法
调用源对象的get方法获取属性值
调用副本对象的set方法把属性值复制过来
打印输出