并对比通过赋值、浅拷贝、深拷贝三种不同方式弄出来的对象之间的equals()和hashCode()的差别
通过 new 关键字
通过 Class 类的 newInstance() 方法
- 这种默认是调用类的无参构造方法创建对象。比如 Person p2 = (Person) Class.forName(“com.ys.test.Person”).newInstance();
通过 Constructor 类的 newInstance 方法
利用 Clone 方法
反序列化
本篇博客涉及的是 Java 的深拷贝和浅拷贝,其实现方式正是通过调用 Object 类的 clone() 方法来完成。在 Object.class 类中,源码为:
protected native Object clone() throws CloneNotSupportedException;
这是一个用 native 关键字修饰的方法,关于native(native 用来修饰方法,用 native 声明的方法表示告知 JVM 调用,该方法在外部定义,我们可以用任何语言去实现它,一般对于java来说,调用native修饰的方法都是用c来写的。 简单地讲,一个native Method就是一个 Java 调用非 Java 代码的接口。),不理解也没关系,只需要知道用 native 修饰的方法就是告诉操作系统,这个方法我不实现了,让操作系统去实现。具体怎么实现我们不需要了解,只需要知道 clone方法的作用就是复制对象,产生一个新的对象。那么这个新的对象和原对象是什么关系呢?
在 Java 中基本类型和引用类型的区别。
在 Java 中数据类型可以分为两大类:基本类型和引用类型。
Java 将内存空间分为堆和栈。基本类型直接在栈中存储数值,而引用类型是将引用放在栈中,实际存储的值是放在堆中,通过栈中的引用指向堆中存放的数据。
上图定义的 a 和 b 都是基本类型,其值是直接存放在栈中的;而 c 和 d 是 String 声明的,这是一个引用类型,引用地址是存放在 栈中,然后指向堆的内存空间。
下面 d = c;这条语句表示将 c 的引用赋值给 d,那么 c 和 d 将指向同一块堆内存空间。
深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。
深拷贝和浅拷贝的示意图大致如下:
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存(这句话有待商榷)。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
浅拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。因此,原始对象及其副本 引用同一个对象。
通过代码实例来看一下浅拷贝,代码如下:
package com.wlw.testjava8.testCopy;
public class Address {
private String provices;
private String city;
public void setAddress(String provices,String city){
this.provices = provices;
this.city = city;
}
@Override
public String toString() {
return "Address [provices=" + provices + ", city=" + city + "]";
}
}
package com.wlw.testjava8.testCopy;
public class Person implements Cloneable{
public String pname;
public int page;
public Address address;
public Person() {}
public Person(String pname,int page){
this.pname = pname;
this.page = page;
this.address = new Address();
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public void setAddress(String provices,String city ){
address.setAddress(provices, city);
}
public void display(String name){
System.out.println(name+":"+"pname=" + pname + ", page=" + page +","+ address);
}
public String getPname() {
return pname;
}
public void setPname(String pname) {
this.pname = pname;
}
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
}
这是一个我们要进行赋值的原始类 Person。下面我们产生一个 Person 对象,并调用其 clone 方法复制一个新的对象。
注意:调用对象的 clone 方法,必须要让类实现 Cloneable 接口,并且覆写 clone 方法。
测试浅拷贝:
package com.wlw.testjava8.testCopy;
public class testCopy {
public static void main(String[] args) throws CloneNotSupportedException {
Person p1 = new Person("wlw",21);
p1.setAddress("湖北省", "武汉市");
Person p2 = (Person) p1.clone();
System.out.println("p1:"+p1);
System.out.println("p1.getPname.hashCode: "+p1.getPname().hashCode());
System.out.println("p2:"+p2);
System.out.println("p2.getPname.hashCode: "+p2.getPname().hashCode());
System.out.println("------------------------------------");
System.out.println("地址修改之前:");
p1.display("p1");
p2.display("p2");
p2.setAddress("湖北省", "荆州市");
System.out.println("将复制之后的对象(p2)地址修改之后,对比变化:");
p1.display("p1");
p2.display("p2");
}
}
//输出结果:
p1:com.wlw.testjava8.testCopy.Person@610455d6
p1.getPname.hashCode: 117826
p2:com.wlw.testjava8.testCopy.Person@511d50c0
p2.getPname.hashCode: 117826
------------------------------------
地址修改之前:
p1:pname=wlw, page=21,Address [provices=湖北省, city=武汉市]
p2:pname=wlw, page=21,Address [provices=湖北省, city=武汉市]
将复制之后的对象(p2)地址修改之后,对比变化:
p1:pname=wlw, page=21,Address [provices=湖北省, city=荆州市]
p2:pname=wlw, page=21,Address [provices=湖北省, city=荆州市]
首先看原始类 Person 实现 Cloneable 接口,并且覆写 clone 方法,它还有三个属性,一个引用类型 String定义的 pname,一个基本类型 int定义的 page,还有一个引用类型 Address ,Address是一个自定义类,这个类也包含两个属性 pprovices 和 city 。
接着看测试内容,首先我们创建一个Person 类的对象 p1,其pname 为wlw,page为21,地址类 Address 两个属性为湖北省和武汉市。接着我们调用 clone() 方法复制另一个对象 p2,接着打印这两个对象的内容。
从第 1 行和第 3 行打印结果:
可以看出这是两个不同的对象。
从第 7 行和第 8 行打印的对象内容看,原对象 p1 和克隆出来的对象 p2 内容完全相同。
代码中我们只是更改了克隆对象 p2 的属性 Address 为湖北省荆州市(原对象 p1 是湖北省武汉市) ,但是从第 10 行和第 11 行打印结果来看,原对象 p1 和克隆对象 p2 的 Address 属性都被修改了。
也就是说对象 Person 的属性 Address,经过 clone 之后,其实只是复制了其引用,他们(p1,p2)指向的还是同一块堆内存空间,当修改其中一个对象的属性 Address,另一个也会跟着变化。
浅拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。因此,原始对象及其副本 引用同一个对象。
弄清楚了浅拷贝,那么深拷贝就很容易理解了。
深拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,无论该字段是值类型的还是引用类型,都复制独立的一份。当你修改其中一个对象的任何内容时,都不会影响另一个对象的内容。
那么该如何实现深拷贝呢?Object 类提供的 clone 是只能实现 浅拷贝的。
深拷贝的原理我们知道了,就是要让原始对象和克隆之后的对象所具有的引用类型属性不是指向同一块堆内存,这里有三种实现思路。
既然引用类型不能实现深拷贝,那么我们将每个引用类型都拆分为基本类型,分别进行浅拷贝。比如上面的例子,Person 类有一个引用类型 Address(其实String 也是引用类型,但是String类型有点特殊,字符常量池),我们在 Address 类内部也重写 clone 方法。如下:
Address.class:
package com.wlw.testjava8.testCopy;
public class Address implements Cloneable{
private String provices;
private String city;
public void setAddress(String provices,String city){
this.provices = provices;
this.city = city;
}
@Override
public String toString() {
return "Address [provices=" + provices + ", city=" + city + "]";
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
然后更改 Person.class 的 clone() 方法:
@Override
protected Object clone() throws CloneNotSupportedException {
Person p = (Person)super.clone();
p.address = (Address) address.clone();
return p;
}
测试还是和上面一样,我们会发现更改了p2对象的Address属性,p1 对象的 Address 属性并没有变化。
//测试结果
p1:com.wlw.testjava8.testCopy.Person@610455d6
p1.getPname.hashCode: 117826
p2:com.wlw.testjava8.testCopy.Person@511d50c0
p2.getPname.hashCode: 117826
------------------------------------
地址修改之前:
p1:pname=wlw, page=21,Address [provices=湖北省, city=武汉市]
p2:pname=wlw, page=21,Address [provices=湖北省, city=武汉市]
将复制之后的对象(p2)地址修改之后,对比变化:
p1:pname=wlw, page=21,Address [provices=湖北省, city=武汉市]
p2:pname=wlw, page=21,Address [provices=湖北省, city=荆州市]
这种方法对于不能重写的类,比如数组不适用!
但是这种做法有个弊端,这里我们Person 类只有一个 Address 引用类型,而Address类中没有用到引用类型,所以我们只用重写 Address 类的clone 方法,但是如果 Address 类也存在一个引用类型,那么我们也要重写其clone 方法,这样下去,有多少个引用类型,我们就要重写多少次,如果存在很多引用类型,那么代码量显然会很大,所以这种方法不太合适。
序列化是将对象写到流中便于传输,而反序列化则是把对象从流中读取出来。这里写到流中的对象则是原始对象的一个拷贝,因为原始对象还存在 JVM 中,所以我们可以利用对象的序列化产生克隆对象,然后通过反序列化获取这个对象。
原理:
注意每个需要序列化的类都要实现 Serializable 接口,如果有某个属性不需要序列化,可以将其声明为 transient,即将其排除在克隆属性之外。
package com.wlw.testjava8.testCopy;
import java.io.Serializable;
public class Address2 implements Serializable {
private String provices;
private String city;
public void setAddress(String provices, String city){
this.provices = provices;
this.city = city;
}
@Override
public String toString() {
return "Address [provices=" + provices + ", city=" + city + "]";
}
}
package com.wlw.testjava8.testCopy;
import java.io.Serializable;
public class Person2 implements Serializable {
public String pname;
public int page;
public Address2 address;
public Person2() {}
public Person2(String pname, int page){
this.pname = pname;
this.page = page;
this.address = new Address2();
}
public void setAddress(String provices,String city ){
address.setAddress(provices, city);
}
public void display(String name){
System.out.println(name+":"+"pname=" + pname + ", page=" + page +","+ address);
}
public String getPname() {
return pname;
}
public void setPname(String pname) {
this.pname = pname;
}
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
}
package com.wlw.testjava8.testCopy;
import java.io.*;
public class testCopy2 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person2 p1 = new Person2("wlw",21);
p1.setAddress("湖北省", "武汉市");
//序列化
//内存数组输出流
ByteArrayOutputStream bos = new ByteArrayOutputStream();
//序列化流
ObjectOutputStream oos = new ObjectOutputStream(bos);
//将数据写入序列化流中,随后会被传递到内存数组输出流中,将对象序列化为byte[]类型的数据
oos.writeObject(p1);
//反序列化
//从内存数组输出流中获取到p1的byte[]类型的数据,传入内存数组输入流
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
//将内存数组输入流传给反序列化流,这样也实现了byte[]类型的数据的传递
ObjectInputStream ois = new ObjectInputStream(bis);
//使用readObject,从反序列化流中读取数据,将byte[]类型的数据反序列化成Person2对象
Person2 p2 = (Person2)ois.readObject();
System.out.println("p1.hashCode: "+p1.hashCode());
System.out.println("p2.hashCode: "+p2.hashCode());
System.out.println("p1:"+p1);
System.out.println("p1.getPname.hashCode: "+p1.getPname().hashCode());
System.out.println("p2:"+p2);
System.out.println("p2.getPname.hashCode: "+p2.getPname().hashCode());
System.out.println("------------------------------------");
System.out.println("地址修改之前:");
p1.display("p1");
p2.display("p2");
p2.setAddress("湖北省", "荆州市");
System.out.println("将复制之后的对象(p2)地址修改之后,对比变化:");
p1.display("p1");
p2.display("p2");
}
}
//输出结果:
p1.hashCode: 1581781576
p2.hashCode: 1534030866
p1:com.wlw.testjava8.testCopy.Person2@5e481248
p1.getPname.hashCode: 117826
p2:com.wlw.testjava8.testCopy.Person2@5b6f7412
p2.getPname.hashCode: 117826
------------------------------------
地址修改之前:
p1:pname=wlw, page=21,Address [provices=湖北省, city=武汉市]
p2:pname=wlw, page=21,Address [provices=湖北省, city=武汉市]
将复制之后的对象(p2)地址修改之后,对比变化:
p1:pname=wlw, page=21,Address [provices=湖北省, city=武汉市]
p2:pname=wlw, page=21,Address [provices=湖北省, city=荆州市]
因为序列化产生的是两个完全独立的对象,所有无论嵌套多少个引用类型,序列化都是能实现深拷贝的。
这里介绍一下Json工具:Gson的使用。Gson的下载
这里复制粘贴出Address3、Person3,这两个类什么都没继承,里面也只有属性和常用的方法(也没有clone)
package com.wlw.testjava8.testCopy;
public class Address3 {
private String provices;
private String city;
public void setAddress(String provices, String city){
this.provices = provices;
this.city = city;
}
@Override
public String toString() {
return "Address [provices=" + provices + ", city=" + city + "]";
}
}
package com.wlw.testjava8.testCopy;
import java.io.Serializable;
public class Person3 {
public String pname;
public int page;
public Address3 address;
public Person3() {}
public Person3(String pname, int page){
this.pname = pname;
this.page = page;
this.address = new Address3();
}
public void setAddress(String provices,String city ){
address.setAddress(provices, city);
}
public void display(String name){
System.out.println(name+":"+"pname=" + pname + ", page=" + page +","+ address);
}
public String getPname() {
return pname;
}
public void setPname(String pname) {
this.pname = pname;
}
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
}
package com.wlw.testjava8.testCopy;
import com.google.gson.Gson;
public class testCopy3 {
public static void main(String[] args) {
Person3 p1 = new Person3("wlw",21);
p1.setAddress("湖北省", "武汉市");
/*使用Gson工具*/
Gson gson = new Gson();
//将对象序列化为json字符串
String p1Str = gson.toJson(p1);
//然后将字符串反序列化为对象
Person3 p2 = gson.fromJson(p1Str, Person3.class);
System.out.println("p1.hashCode: "+p1.hashCode());
System.out.println("p2.hashCode: "+p2.hashCode());
System.out.println("p1:"+p1);
System.out.println("p1.getPname.hashCode: "+p1.getPname().hashCode());
System.out.println("p2:"+p2);
System.out.println("p2.getPname.hashCode: "+p2.getPname().hashCode());
System.out.println("------------------------------------");
System.out.println("地址修改之前:");
p1.display("p1");
p2.display("p2");
p2.setAddress("湖北省", "荆州市");
System.out.println("将复制之后的对象(p2)地址修改之后,对比变化:");
p1.display("p1");
p2.display("p2");
}
}
//输出结果:
p1.hashCode: 1288354730
p2.hashCode: 1274370218
p1:com.wlw.testjava8.testCopy.Person3@4ccabbaa
p1.getPname.hashCode: 117826
p2:com.wlw.testjava8.testCopy.Person3@4bf558aa
p2.getPname.hashCode: 117826
------------------------------------
地址修改之前:
p1:pname=wlw, page=21,Address [provices=湖北省, city=武汉市]
p2:pname=wlw, page=21,Address [provices=湖北省, city=武汉市]
将复制之后的对象(p2)地址修改之后,对比变化:
p1:pname=wlw, page=21,Address [provices=湖北省, city=武汉市]
p2:pname=wlw, page=21,Address [provices=湖北省, city=荆州市]
创建一个Person对象:p1,利用语句(=)、浅拷贝、深拷贝分别根据p1得到三个p2,对比p1与三个p2的equals(),hashCode();【前提是:equals()方法没用重写】
equals():比较的是对象引用
hashCode():得到的是对象的hash值,hash值就是对象在哈希表中的索引位置(也是地址)
一般使用=
号做赋值操作的时候,对于基本数据类型
,实际上是拷贝它的值
;对于引用数据类型
而言,其实赋值的只是这个对象的引用,将原对象的引用传递过去,原对象和新的对象实际上还是指向同一个对象
。
p1与p2的equals()相等,hashCode()相等
package com.wlw.testjava8.testCopy;
public class testEqualsAndHashCode {
public static void main(String[] args) {
Person3 p1 = new Person3("wlw",21);
p1.setAddress("湖北省", "武汉市");
Person3 p2 = p1;
System.out.println("p1.equals(p2): "+p1.equals(p2));
System.out.println("p1.hashCode: "+p1.hashCode());
System.out.println("p2.hashCode: "+p2.hashCode());
System.out.println("p1:"+p1);
System.out.println("p2:"+p2);
System.out.println("------------------------------------");
System.out.println("地址修改之前:");
p1.display("p1");
p2.display("p2");
p2.setAddress("湖北省", "荆州市");
System.out.println("将复制之后的对象(p2)地址修改之后,对比变化:");
p1.display("p1");
p2.display("p2");
}
}
//输出结果:
p1.equals(p2): true
p1.hashCode: 1625635731
p2.hashCode: 1625635731
p1:com.wlw.testjava8.testCopy.Person3@60e53b93
p2:com.wlw.testjava8.testCopy.Person3@60e53b93
------------------------------------
地址修改之前:
p1:pname=wlw, page=21,Address [provices=湖北省, city=武汉市]
p2:pname=wlw, page=21,Address [provices=湖北省, city=武汉市]
将复制之后的对象(p2)地址修改之后,对比变化:
p1:pname=wlw, page=21,Address [provices=湖北省, city=荆州市]
p2:pname=wlw, page=21,Address [provices=湖北省, city=荆州市]
package com.wlw.testjava8.testCopy;
public class testCopy4 {
public static void main(String[] args) throws CloneNotSupportedException {
Person4 p1 = new Person4("wlw",21);
p1.setAddress("湖北省", "武汉市");
Person4 p2 = (Person4) p1.clone();
System.out.println("p1.equals(p2): "+p1.equals(p2));
System.out.println("p1.hashCode: "+p1.hashCode());
System.out.println("p2.hashCode: "+p2.hashCode());
System.out.println("p1:"+p1);
System.out.println("p2:"+p2);
System.out.println("------------------------------------");
System.out.println("地址修改之前:");
p1.display("p1");
p2.display("p2");
p2.setAddress("湖北省", "荆州市");
System.out.println("将复制之后的对象(p2)地址修改之后,对比变化:");
p1.display("p1");
p2.display("p2");
}
}
//输出结果:
p1.equals(p2): false
p1.hashCode: 1625635731
p2.hashCode: 1580066828
p1:com.wlw.testjava8.testCopy.Person4@60e53b93
p2:com.wlw.testjava8.testCopy.Person4@5e2de80c
------------------------------------
地址修改之前:
p1:pname=wlw, page=21,Address [provices=湖北省, city=武汉市]
p2:pname=wlw, page=21,Address [provices=湖北省, city=武汉市]
将复制之后的对象(p2)地址修改之后,对比变化:
p1:pname=wlw, page=21,Address [provices=湖北省, city=荆州市]
p2:pname=wlw, page=21,Address [provices=湖北省, city=荆州市]
package com.wlw.testjava8.testCopy;
import com.google.gson.Gson;
public class testCopy3 {
public static void main(String[] args) {
Person3 p1 = new Person3("wlw",21);
p1.setAddress("湖北省", "武汉市");
/*使用Gson工具*/
Gson gson = new Gson();
//将对象序列化为json字符串
String p1Str = gson.toJson(p1);
//然后将字符串反序列化为对象
Person3 p2 = gson.fromJson(p1Str, Person3.class);
System.out.println("p1.equals(p2): "+p1.equals(p2));
System.out.println("p1.hashCode: "+p1.hashCode());
System.out.println("p2.hashCode: "+p2.hashCode());
System.out.println("p1:"+p1);
System.out.println("p2:"+p2);
System.out.println("------------------------------------");
System.out.println("地址修改之前:");
p1.display("p1");
p2.display("p2");
p2.setAddress("湖北省", "荆州市");
System.out.println("将复制之后的对象(p2)地址修改之后,对比变化:");
p1.display("p1");
p2.display("p2");
}
}
//输出结果:
p1.equals(p2): false
p1.hashCode: 1288354730
p2.hashCode: 1274370218
p1:com.wlw.testjava8.testCopy.Person3@4ccabbaa
p2:com.wlw.testjava8.testCopy.Person3@4bf558aa
------------------------------------
地址修改之前:
p1:pname=wlw, page=21,Address [provices=湖北省, city=武汉市]
p2:pname=wlw, page=21,Address [provices=湖北省, city=武汉市]
将复制之后的对象(p2)地址修改之后,对比变化:
p1:pname=wlw, page=21,Address [provices=湖北省, city=武汉市]
p2:pname=wlw, page=21,Address [provices=湖北省, city=荆州市]
赋值语句(=):p1与p2的equals()相等,hashCode()相等
浅拷贝:p1与p2的equals()不相等,hashCode()不相等
深拷贝:p1与p2的equals()不相等,hashCode()不相等
这三者中,赋值语句与深拷贝很容易理解,对于浅拷贝,新旧对象共享的是同一块内存,注意:也是有新对象产生的,这就造成hashCode()不相同。(这里容易产生误解,两个对象如果hashCode()相同,两个对象不一定相同,因为有hash冲突存在。)(浅拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。因此,原始对象及其副本 引用同一个对象。)