目录
1. Java中的赋值
在Java中对于8种基本类型变量的赋值是通过拷贝值的形式实现的(即将原有变量的值拷贝到新变量中,两者独立,互不干扰)。
而Java对于对象的拷贝一共有三种实现方式:分别是1)直接赋值、2)浅拷贝、3)深拷贝。
1.1 直接赋值
1)概述:
基本类型变量的直接赋值就是值拷贝,这里不再赘述。
对于引用类型变量的直接赋值时发生的是引用的拷贝,即将原引用(对象的内存地址)拷贝一份,然后传递给新的引用。此时,新/旧引用类型变量指向的是同一个对象。
2)示例:对Person对象进行直接赋值
public class Person {
public static void main(String[] args) {
// 直接赋值
Person firstPerson = new Person(25,"Clark");
Person newPerson = firstPerson;
// 检查地址
System.out.println(firstPerson.hashCode());
System.out.println(newPerson.hashCode());
}
// 内部成员变量
private int age;
private String name;
public Person(Integer age, String name) {
super();
this.age = age;
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
控制台输出的结果为:
可以看出引用类型变量(对象/实例)指向内存中的同一个对象,因此直接赋值发生的是引用的拷贝。
1.2 clone()
clone()方法在Java中起到了复制对象的作用,所谓复制对象,需要首先在堆中分配一个和原对象大小相同的空间以创建一个同类型的新对象,再对数据域进行处理。
注意:在Java中有两种创建新对象的方式,1)通过new关键字在堆中开辟内存空间一创建一个对象;2)通过clone()方法返回一个新的Objcet对象。
clone()与new的区别:两者第一步都是开辟内存空间,但new关键字需要调用构造方法实现数据域的初始化,clone()方法则是通过原对象来填充数据域以完成初始化操作,并且clone()方法返回的是Object对象。
Object类中的clone()方法:
/**
* native方法,默认是实现的是浅拷贝,返回一个object对象。
*/
protected native Object clone() throws CloneNotSupportedException;
Cloneable接口是一个标记接口,只有实现这个接口,然后重写Object类中的clone()方法,然后通过类实例调用clone()方法才能克隆成功,如果不实现这个接口,则会抛出CloneNotSupportException异常。如果想要实现浅拷贝,直接在重写的clone()方法中调用Object类的clone()方法就好了;如果想要实现深拷贝,需要重写clone()方法。
1.3 浅拷贝(Shallow Copy)
1)概述:
· 对于基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据(值拷贝)。
· 对于引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值(引用拷贝)。
注意:无论是通过new关键字(利用构造方法),还是通过Object类中的clone()方法(浅拷贝的默认实现)实现浅拷贝,都必然会创建一个新的对象(与原对象有不同的内存地址),但两者并不是完全独立的,其引用类型的成员变量必然指向同一个对象。
2)示例:通过Person类与clone()实现浅复制
package com.bjpowernode;
public class Person implements Cloneable{
public static void main(String[] args) throws CloneNotSupportedException {
// 直接赋值
Person firstPerson = new Person(25,"Clark");
Person newPerson = (Person) firstPerson.clone();
// 检查地址
System.out.println("firstPerson's address:" + firstPerson.hashCode());
System.out.println("newPerson's address:" + newPerson.hashCode());
// 检查引用类型变量的地址
System.out.println("firstPerson's name address:" + firstPerson.name.hashCode());
System.out.println("newPerson's name address:" + newPerson.name.hashCode());
}
// 内部成员变量
private int age;
private String name;
public Person(Integer age, String name) {
super();
this.age = age;
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
控制台输出结果为:
可以看出使用clone()确实发生了浅拷贝,创建了一个新的对象,但原/新对象的引用类型数据指向同一个String对象。
注意:虽然浅拷贝中修改原/新对象的引用类型变量会影响到另一方的数据域,但是String类型有些独特,其虽然是引用类型,但是String对象是一个存放在方法区中字符串常量池的常量 ,通过一个引用对String对象进行修改不会影响到其余指向该String对象的引用,因为JVM会在字符串常量池中重新new一个String对象,用修改值初始化并将地址赋给引用。
1.4 深拷贝(Deep Copy)
1)概述:
· 对于基本数据类型的成员变量,深拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据(值拷贝)。
· 对于引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么深拷贝会进行对象传递,也就是创建一个与该成员变量同类型的新成员变量对象,然后使用原对象的引用数据类型数据域填充新对象的引用数据类型数据域。发生深拷贝的对象与原对象之间完全独立在这种情况下,在一个对象中修改该成员变量不会影响到另一个对象的该成员变量值(对象拷贝)。
注意:因为深拷贝需要创建内存空间并拷贝所有引用类型的成员变量,所以深拷贝相较于浅拷贝速度较慢且花销较大。
2)深拷贝的实现:
· 重写clone()方法:
与通过重写clone方法实现浅拷贝的基本思路一样,只需要为对象图的每一层的每一个对象都实现Cloneable接口并重写clone方法,最后在最顶层的类的重写的clone方法中调用所有的clone方法即可实现深拷贝(注意:传递的是对象,不是引用)。
通过重写clone()方法实现深拷贝是一种繁琐且通用性差的方法,一种便捷的实现方法是通过序列化对象:将对象序列化为字节序列后,默认会将该对象的整个对象图进行序列化,在反序列化对象时对数据域进行的是值传递而非引用传递。
· 序列化对象:
示例:
import java.io.*;
public class Person implements Serializable {
public static void main(String[] args){
// 序列化实现深拷贝
Person firstPerson = new Person(25,"Clark");
Person newPerson = null;
// 创建IO流对象
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
try {
// 序列化
oos = new ObjectOutputStream(new FileOutputStream("test/test.txt"));
oos.writeObject(firstPerson);
oos.flush();
// 反序列化
ois = new ObjectInputStream(new FileInputStream("test/test.txt"));
newPerson = (Person) ois.readObject();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (oos != null){
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (ois != null){
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 检查地址
System.out.println("firstPerson's address:" + firstPerson.hashCode());
System.out.println("newPerson's address:" + newPerson.hashCode());
}
// UID
private static final long serialVersionUID = 15L;
// 内部成员变量
private int age;
private String name;
public Person(Integer age, String name) {
super();
this.age = age;
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
控制台输出结果为:
反序列化确实创建了一个新的对象,但由于String的特殊性,暂不探究引用类型成员变量的地址,大家只要知道对于引用类型成员变量(方法区常量除外)其会创建一个有相同数据域的新的对象即可。