对象的拷贝与序列化
1.对象的拷贝
对象的拷贝我们常见的几种方式大概分为引用拷贝、浅克隆、深克隆,在实际开发中,当我们想把一个对象中的所有属性的值复制到另一个对象中时,我们经常会用到对象的拷贝,在很多代码中都使用的是引用拷贝,如下面代码
public static void main(String[] args) {
Customer c1 = new Customer();
c1.setId(1L);
c1.setNickname("aaa");
System.out.println(c1);
Customer c2 = new Customer();
c2 = c1;
System.out.println(c2);
}
打印结果为:
这种以赋值的形式,将对象c1赋值给新的对象c2,但这里面赋值的并不是c1的值,而其实是c1的引用地址值,这也就是为什么称他为引用拷贝,原因就是这种方式,其实就是把一个对象的引用地址赋值给了另一个新的对象,而这两个对象在堆内存中引用的其实是同一个内存体现,下面我们将toString方法注释掉再来看,代码示例如下:
public static void main(String[] args) {
Customer c1 = new Customer();
c1.setId(1L);
c1.setNickname("aaa");
System.out.println(c1);
Customer c2 = new Customer();
c2 = c1;
System.out.println(c2);
System.out.println(c2 == c1);
}
打印结果为:
如图,但是这种方式在多线程中或在多个方法操作同一个用户的情况下会出现很大的问题,因为我们是赋值的地址值操作的同一个堆内存,所以在当我们其中一个方法中使用了这个对象,改变了数据,那么在其他方法中也都会被改变。这样,数据就出现了极大的问题:
public static void main(String[] args) {
Customer c1 = new Customer();
c1.setId(1L);
c1.setNickname("aaa");
System.out.println(c1);
Customer c2 = new Customer();
c2 = c1;//赋值地址值
useCustomer(c1);//操作c1
updateById(c2);//操作c2
System.out.println(c2);
}
public static void useCustomer(Customer c1){
customer.setNickname("bbbb");
customer.setId(18L);
}
public static void updateById(Customer c2){
if (customer.getId() == 1l){//此时我们想要的是id为1的用户
customer.setNickname("测试");
}else {
throw new RuntimeException("错误的用户:{}"+customer);
}
}
输出结果:
我们想要的结果应该是输出改变了值的c2,但此时结果却是报错了,原因就是我们在操作c2时,其实已经被上一个方法改变了值,因为c1和c2是一个引用地址,改变了c1的同时,c2就也已经发生了变化。
而如果我们想要解决这个问题,c1和c2只是值相等,而地址值不同的话,有两种办法,第一种是将c1中的各个属性get出来,再set到c2中:
public static void main(String[] args) {
Customer c1 = new Customer();
c1.setId(1L);
c1.setNickname("aaa");
System.out.println(c1);
Customer c2 = new Customer();
c2.setId(c1.getId());
c2.setNickname(c1.getNickname());
System.out.println(c1);
System.out.println(c2);
System.out.println(c1 == c2);
}
输出结果:
但是这种方式如果实体类的属性值过多的话代码量太过冗余,于是便引出第二种拷贝方式,浅克隆,克隆的是将一个对象的所有属性值复制给另一个对象,而不是引用地址,但要实现浅克隆,必须要将需要克隆的类,实现Cloneable接口,并重写Object类中的clone方法:
public static void main(String[] args) throws CloneNotSupportedException {
Customer c1 = new Customer();
c1.setId(1L);
c1.setNickname("aaa");
System.out.println(c1);
Customer c2 = c1.deepClone();
System.out.println(c1);
System.out.println(c2);
System.out.println(c1 == c2);
}
输出结果:
使用浅克隆要注意的是,如果被克隆的类的成员变量中有其他引用对象,那么该变量依然会使用引用拷贝的方式,此时我们需要将其他类同样实现Cloneable接口,实现clone方法,但是如果嵌套的引用对象过多,而我们确认需要将所有值全部拷贝的话,那么我们需要用深克隆的另一种方式:
// 深克隆
public Customer deepClone() throws Exception {
// 将对象写入流中
ByteArrayOutputStream bao = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bao);
oos.writeObject(this);
// 将对象从流中取出
ByteArrayInputStream bis = new ByteArrayInputStream(bao.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (Customer) ois.readObject();
}
public static void main(String[] args) throws CloneNotSupportedException {
Customer c1 = new Customer();
c1.setId(1L);
c1.setNickname("aaa");
System.out.println(c1);
Customer c2 = c1.deepClone();
System.out.println(c1);
System.out.println(c2);
System.out.println(c1 == c2);
}
输出结果:
这个方式需要实体类实现Serializable序列化接口,将对象先序列化到流中,再从流中反序列化回来,会得到两个不同的对象。
这里提到了序列化和反序列化,这是实现深克隆的基础,那什么是序列化,什么又是反序列化呢?
2.序列化、反序列化
序列化是将对象状态转换为可保持或传输的格式的过程。与序列化相对的是反序列化,它将流转换为对象。简单来说,就是将一个对象变成一种可以在网络或磁盘传输的格式(字节,json,xml等。。),通过流存储或者网络传输,反序列化就是将这个格式读取,并按照指定的格式转变为对象。举个例子就是如果我们搬家时想要将一个大玩具运到新家,我们就可以把它拆散,用一个箱子装起来运到新家,然后再通过说明书,将这个玩具重新组装好,这个拆散又重组的过程也就是序列化和反序列化了。
如何使Java类可序列化?
通过实现java.io.Serializable接口,可以在Java类中启用可序列化。它是一个标记接口,意味着它不包含任何方法或字段,仅用于标识可序列化的语义。
对于实现Java的序列化接口注意事项;
1.java中的序列化时transient变量(这个关键字的作用就是告知JAVA我不可以被序列化)和静态变量不会被序列化。
class Student1 implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private transient String password;
private static int count = 0;
public Student1(String name, String password) {
System.out.println("调用Student的带参的构造方法");
this.name = name;
this.password = password;
count++;
}
public String toString() {
return "人数: " + count + " 姓名: " + name + " 密码: " + password;
}
}
public static void main(String args[]) {
try {
FileOutputStream fos = new FileOutputStream("test.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
Student1 s1 = new Student1("张三", "12345");
Student1 s2 = new Student1("王五", "54321");
oos.writeObject(s1);
oos.writeObject(s2);
oos.close();
FileInputStream fis = new FileInputStream("test.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
Student1 s3 = (Student1) ois.readObject();
Student1 s4 = (Student1) ois.readObject();
System.out.println(s3);
System.out.println(s4);
ois.close();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e1) {
e1.printStackTrace();
}
}
public static void main(String args[]){
try {
FileInputStream fis = new FileInputStream("test.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
Student1 s3 = (Student1) ois.readObject();
Student1 s4 = (Student1) ois.readObject();
System.out.println(s3);
System.out.println(s4);
ois.close();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e1) {
e1.printStackTrace();
}
}
输出结果:
2.如果你先序列化对象A后序列化B,那么在反序列化的时候一定记着JAVA规定先读到的对象 是先被序列化的对象,不要先接收对象B,那样会报错.尤其在使用上面的Externalizable的时候一定要注意读取的先后顺序。
3.实现序列化接口的对象并不强制声明唯一的serialVersionUID,是否声明serialVersionUID对于对象序列化的向上向下的兼容性有很大的影响,如果没有明确指定serialVersionUID,序列化的时候会根据字段和特定的算法生成一个serialVersionUID,当属性有变化时这个id发生了变化,所以反序列化的时候就会失败。抛出“本地classd的唯一id和流中class的唯一id不匹配”。
jdk文档关于serialVersionUID的描述:
如果可序列化类未显式声明 serialVersionUID,则序列化运行时将基于该类的各
个方面计算该类的默认 serialVersionUID 值,如“Java(TM) 对象序列化规范”中所述。
不过,强烈建议 所有可序列化类都显式声明 serialVersionUID 值,原因是计算默
认的 serialVersionUID 对类的详细信息具有较高的敏感性,根据编译器实现的不
同可能千差万别,这样在反序列化过程中可能会导致意外的 InvalidClassException。
因此,为保证 serialVersionUID 值跨不同 java 编译器实现的一致性,序列化类必
须声明一个明确的 serialVersionUID 值。还强烈建议使用 private 修饰符显示声明
serialVersionUID(如果可能),原因是这种声明仅应用于直接声明类
-- serialVersionUID 字段作为继承成员没有用处。数组类不能声明一个明确的
serialVersionUID,因此它们总是具有默认的计算值,但是数组类没有匹配
serialVersionUID 值的要求。
其中三种序列化方式的对比选型: