上篇《Java创建对象方式》介绍了对象创建的5中方式,本篇介绍下Java对象拷贝方式:浅拷贝、深拷贝。
在介绍拷贝之前,先说下Java基本复制方法 = 。
基本复制方法
public class CopyObject {
public static void main(String[] args){
Score score1 = new Score("math", "100");
// 基本复制方法
Score score2 = score1;
System.out.println("score1=" + score1 + ",hashCode="+score1.hashCode());
System.out.println("score2=" + score2 + ",hashCode="+score2.hashCode());
change(score2);
System.out.println("score1=" + score1 + ",hashCode="+score1.hashCode());
System.out.println("score2=" + score2 + ",hashCode="+score2.hashCode());
}
public static void change(Score score){
score.grade = "90";
}
}
输出:
score1=Score{course='math', grade='100'},hashCode=1625635731
score2=Score{course='math', grade='100'},hashCode=1625635731
score1=Score{course='math', grade='90'},hashCode=1625635731
score2=Score{course='math', grade='90'},hashCode=1625635731
Java中只有值传递。= 复制的是对象引用,并没有在堆空间中重新新建。对原先对象属性进行修改,复制对象的属性也随之修改。(引用指向同一块堆内存,理论上就是同一个内存空间。)
浅拷贝、深拷贝概念
Java语言中,数据类型 分为 基本数据类型 和 引用类型。
- 基本数据类型(值类型)
- 字符类型 char
- 布尔类型 boolean
- 数值类型 byte、short、int、long、float、double
- 引用类型
- 类、接口、数组、枚举等
浅拷贝和深拷贝的主要区别在于是否支持引用类型的成员变量的复制,下面将对两者进行详细介绍。
浅拷贝
clone方法
Object类的clone方法,访问限定符为protected。
protected native Object clone() throws CloneNotSupportedException;
这是一个native方法,大家都知道native方法是非Java语言实现的代码,供Java程序调用的,因为Java程序是运行在JVM虚拟机上面的,要想访问到比较底层的与操作系统相关的就没办法了,只能由靠近操作系统的语言来实现。
因为每个类直接或间接的父类都是Object,因此它们都含有clone()方法,但是因为该方法是protected,所以都不能在类外进行访问。要想对一个对象进行复制,就需要对clone方法覆盖。
修改基本类型
- 类需要实现Clonenable接口,该接口为标记接口。不实现的话在调用clone方法会抛出CloneNotSupportedException异常
- 重写clone()方法,访问修饰符设为public(方便类对象调用)。
public class Score implements Cloneable {
public String course;
public String grade;
public Score() {
}
public Score(String course, String grade) {
this.course = course;
this.grade = grade;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "Score{" + "course='" + course + '\'' + ", grade='" + grade + '\'' + '}';
}
}
调用:
public class CopyObject {
public static void main(String[] args) throws CloneNotSupportedException {
Score score1 = new Score("math", "100");
// clone方法
Score score2 = (Score) score1.clone();
System.out.println("score1=" + score1 + ",hashCode="+score1.hashCode());
System.out.println("score2=" + score2 + ",hashCode="+score2.hashCode());
change(score2);
System.out.println("score1=" + score1 + ",hashCode="+score1.hashCode());
System.out.println("score2=" + score2 + ",hashCode="+score2.hashCode());
}
public static void change(Score score){
// 修改基本类型
score.grade = "90";
}
}
输出:
score1=Score{course='math', grade='100'},hashCode=1625635731
score2=Score{course='math', grade='100'},hashCode=1580066828
score1=Score{course='math', grade='100'},hashCode=1625635731
score2=Score{course='math', grade='90'},hashCode=1580066828
结果看出,原对象与新对象是两个不同的对象,拷贝出来的新对象与原对象内容一致。
而且,新对象的 基本类型 跟 String类型 的改变不会影响到原始对象。
如果改变引用类型呢?
修改引用类型
新增级别类Level:
public class Level {
public String classLevel;
public String gradeLevel;
public Level(String classLevel, String gradeLevel) {
this.classLevel = classLevel;
this.gradeLevel = gradeLevel;
}
@Override
public String toString() {
return "Level{" +"classLevel='" + classLevel + '\'' +", gradeLevel='" + gradeLevel + '\'' +'}';
}
}
类Score 属性增加类Level对象:
public class Score implements Cloneable {
public String course;
public String grade;
public Level level;
public Score() {
}
public Score(String course, String grade, Level level) {
this.course = course;
this.grade = grade;
this.level = level;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "Score{" +"course='" + course + '\'' +", grade='" + grade + '\'' +", level=" + level +'}';
}
}
调用:
public class CopyObject {
public static void main(String[] args) throws CloneNotSupportedException {
Score score1 = new Score("math", "95", new Level("5","30"));
// clone方法
Score score2 = (Score) score1.clone();
System.out.println("score1=" + score1 + ",hashCode="+score1.hashCode());
System.out.println("score2=" + score2 + ",hashCode="+score2.hashCode());
change(score2);
System.out.println("score1=" + score1 + ",hashCode="+score1.hashCode());
System.out.println("score2=" + score2 + ",hashCode="+score2.hashCode());
}
public static void change(Score score){
// 修改基本类型
score.grade = "90";
// 修改引用类型
score.level.classLevel = "10";
}
}
输出:
score1=Score{course='math', grade='95', level=Level{classLevel='5', gradeLevel='30'}},hashCode=1625635731
score2=Score{course='math', grade='95', level=Level{classLevel='5', gradeLevel='30'}},hashCode=1580066828
score1=Score{course='math', grade='95', level=Level{classLevel='10', gradeLevel='30'}},hashCode=1625635731
score2=Score{course='math', grade='90', level=Level{classLevel='10', gradeLevel='30'}},hashCode=1580066828
新对象score2的修改影响到原始对象score1,说明引用类型的改变会影响到原始数据。进而说明clone()方法是浅拷贝,只是复制了level变量的引用,并没有真正的开辟另一块空间,将值复制后再将引用返回给新对象。
深拷贝
那如何实现深拷贝呢?
有两个方法:
- 需要拷贝的引用类型也实现Cloneable接口、并覆写clone方法
- 利用序列化
1、需要拷贝的引用类型也实现Cloneable接口、并重写clone方法
Level实现Cloneable接口、重写clone方法:
public class Level implements Cloneable {
...
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
...
}
Score增加引用对象的clone调用:
public class Score implements Cloneable {
...
@Override
public Object clone() throws CloneNotSupportedException {
Score score = (Score) super.clone(); //浅复制
Level level = (Level) this.level.clone(); //深度复制
score.level = level;
return score;
}
...
}
调用结果:
score1=Score{course='math', grade='95', level=Level{classLevel='5', gradeLevel='30'}},hashCode=1625635731
score2=Score{course='math', grade='95', level=Level{classLevel='5', gradeLevel='30'}},hashCode=1580066828
score1=Score{course='math', grade='95', level=Level{classLevel='5', gradeLevel='30'}},hashCode=1625635731
score2=Score{course='math', grade='90', level=Level{classLevel='10', gradeLevel='30'}},hashCode=1580066828
结果看出,对象score2的修改没有影响原对象score1。
多层引用类型拷贝问题
但是这种做法有个弊端,这里我们Score 类只有一个 Level 引用类型,而 Level 类没有,所以我们只用重写 Level 类的clone 方法。
但是如果 Level 类也存在一个引用类型,那么我们也要重写其clone 方法,这样下去,有多少个引用类型,我们就要重写多少次,如果存在很多引用类型,那么代码量显然会很大,所以这种方法不太合适。
所以,可以考虑用序列化。
2、序列化
序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。
通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深拷贝。
需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作,如果有某个属性不需要序列化,可以将其声明为 transient,即将其排除在克隆属性之外。
类Score实现Serializable、新增serialClone()方法:
public class Score implements Serializable {
public static final long serialVersionUID = 1L;
public String course;
public String grade;
public Level level;
public Score() { }
public Score(String course, String grade, Level level) {
this.course = course;
this.grade = grade;
this.level = level;
}
public Score serialClone() throws IOException, ClassNotFoundException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
oos.close();
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
Score score = (Score) ois.readObject();
ois.close();
return score;
}
@Override
public String toString() {
return "Score{" +"course='" + course + '\'' +", grade='" + grade + '\'' +", level=" + level +'}';
}
}
Level也必须实现Serializable,否则无法序列化:
public class Level implements Serializable {
public static final long serialVersionUID = 1L;
public String classLevel;
public String gradeLevel;
public Level(String classLevel, String gradeLevel) {
this.classLevel = classLevel;
this.gradeLevel = gradeLevel;
}
@Override
public String toString() {
return "Level{" +"classLevel='" + classLevel + '\'' +", gradeLevel='" + gradeLevel + '\'' +'}';
}
}
调用:
public class CopyObject {
public static void main(String[] args) throws CloneNotSupportedException {
Score score1 = new Score("math", "95", new Level("5","30"));
// 序列化
Score score2 = null;
try {
score2 = (Score) score1.serialClone();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("score1=" + score1 + ",hashCode="+score1.hashCode());
System.out.println("score2=" + score2 + ",hashCode="+score2.hashCode());
change(score2);
System.out.println("score1=" + score1 + ",hashCode="+score1.hashCode());
System.out.println("score2=" + score2 + ",hashCode="+score2.hashCode());
}
public static void change(Score score){
// 修改基本类型
score.grade = "90";
// 修改引用类型
score.level.classLevel = "10";
}
}
输出结果:
score1=Score{course='math', grade='95', level=Level{classLevel='5', gradeLevel='30'}},hashCode=723074861
score2=Score{course='math', grade='95', level=Level{classLevel='5', gradeLevel='30'}},hashCode=664223387
score1=Score{course='math', grade='95', level=Level{classLevel='5', gradeLevel='30'}},hashCode=723074861
score2=Score{course='math', grade='90', level=Level{classLevel='10', gradeLevel='30'}},hashCode=664223387
结果看出,对象score2的修改没有影响原对象score1。
小结
- 如果需要创建一个对象的副本,可以使用clone(浅拷贝),因为clone是一个native方法,底层实现,效率高。
- 浅拷贝中,如果原型对象的成员变量是值类型,将复制一份给拷贝对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给拷贝对象,也就是说原型对象和拷贝对象的 引用类型 变量指向相同的内存地址。
- 深拷贝中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给拷贝对象,深拷贝将原型对象的所有引用对象也复制一份给拷贝对象。
- 如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,可以用序列化的方式来实现对象的深拷贝。