JDK1.8源码-00-深拷贝浅拷贝

JDK1.8源码-00-深拷贝浅拷贝

关于Java的深拷贝和浅拷贝,简单来说就是创建一个和一直对象一模一样的对象。

可能日常编码过程中用的不多,但是这是一个面试经常会问的问题,而且了解深拷贝和浅拷贝的原理,对于Java中的所谓值传递或者引用传递将会有更深的理解。

1. 创建对象的5种方式

①、通过 new 关键字

  这是最常用的一种方式,通过 new 关键字调用类的有参或无参构造方法来创建对象。比如 Object obj = new Object();

②、通过 Class 类的 newInstance() 方法

这种默认是调用类的无参构造方法创建对象。

比如

1
Person p = (Person) Class.forName("com.zhuuu.test.Person").newInstance();

③、通过 Constructor 类的 newInstance 方法

这和第二种方法类时,都是通过反射来实现。

通过java.lang.reflect.Constructor 类的 newInstance()方法指定某个构造器来创建对象。

1
Person p = (Person) Person.class.getConstructors()[0].newInstance();

实际上第二种方法利用 Class 的 newInstance() 方法创建对象,其内部调用还是 Constructor 的 newInstance() 方法。

④、利用 Clone 方法

Clone是Object类中的一个方法,通过对象A.clone()方法会创建一个内容和对象A一模一样的对象B。顾名思义就是创建一个一模一样的对象出来。

1
Person p4 = (Person) p3.clone();

⑤、反序列化

序列化是指把堆内存中的java对象数据,通过某种方式把对象存储到磁盘文件中或者传递给其他网络节点(在网络上传输)。

反序列化则是把磁盘文件中的对象数据或者把网络节点上的对象数据,恢复成java对象模型的过程。

2. Clone() 方法

本篇讲解的是 Java 的深拷贝和浅拷贝,其实现方式正是通过调用 Object 类的 clone() 方法来完成。在 Object.class 类中,源码为:

1
protected native Object clone() throws CloneNotSupportedException;

这是一个用 native 关键字修饰的方法,关于native关键字,只需要知道用 native 修饰的方法就是告诉操作系统,这个方法我不实现了,让操作系统去实现。

3. 基本类型和引用类型

这里再给大家普及一个概念,在 Java 中基本类型和引用类型的区别。

在 Java 中数据类型可以分为两大类:基本类型和引用类型。

基本类型:值类型

①整数类型:long、int、short、byte
②浮点类型:float、double
③字符类型:char
④布尔类型:boolean

引用类型:

  • 类、 接口类型、 数组类型、 枚举类型、 注解类型、 字符串型

例如,String类型就是引用类型。
简单来说,所有的非基本数据类型都是引用数据类型。

Java将内存空间分为堆和栈。

  • 基本类型直接放在栈中存储数值,

  • 引用类型是将引用放在栈中。实际存储的值放在堆中,通过栈中的引用指向堆中存放的数据

mark

4. 浅拷贝

看如下这段代码:

这是我们要进行赋值的原始类Person。下面我们产生一个Person对象,并调用其Clone方法复制一个新的对象。

注意:

  • 调用对象的clone方法,必须要让类实现Cloneable接口,
  • 并且覆写clone方法
  1. Address类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 + "]";
}
}
  1. Person类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.zhuuu;

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;
}
}
  1. 测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.zhuuu;

public class testShallowClone {
public static void main(String[] args) throws CloneNotSupportedException {
Person p1 = new Person("朱酱酱", 24);
p1.setAddress("江苏省","南京市");

// p1 的 clone
Person p2 = (Person) p1.clone();

System.out.println("p1" + p1);
System.out.println("p1.getPname:"+p1.getPname().hashCode());

System.out.println("p2" + p2);
System.out.println("p2.getPname:"+p2.getPname().hashCode());

p1.display("p1");
p2.display("p2");

p2.setAddress("上海省","上海市");
System.out.println("将复制之后的地址修改为:");
p1.display("p1");
p2.display("p2");
}
}

测试结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
首先看原始类Person实现了Cloneable接口,并且重写了clone方法,
还有它的三个属性,
一个引用类型 String定义的 pname,
一个基本类型 int定义的 page,
还有一个引用类型 Address ,(其中:Address这是一个自定义类,这个类也包含两个属性provices和city

接着看测试内容,首先我们创建一个Person对象p1,其pname为朱酱酱,page年龄为24,地址类Address两个属性为江苏省,南京市。接着我们调用clone()方法复制另一个对象p2,接着打印这两个对象的内容。
*/

p1com.zhuuu.Person@4554617c
p1.getPname:26578193
p2com.zhuuu.Person@74a14482
p2.getPname:26578193
p1:pname=朱酱酱, page=24,Address [provices=江苏省, city=南京市]
p2:pname=朱酱酱, page=24,Address [provices=江苏省, city=南京市]
将复制之后的地址修改为:
p1:pname=朱酱酱, page=24,Address [provices=上海省, city=上海市]
p2:pname=朱酱酱, page=24,Address [provices=上海省, city=上海市]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
从第 1 行和第 3 行打印结果:
p1com.zhuuu.Person@4554617c
p2com.zhuuu.Person@74a14482
可以看出这是两个不同的对象。

从第 5 行和第 6 行打印的对象内容看
p1:pname=朱酱酱, page=24,Address [provices=江苏省, city=南京市]
p2:pname=朱酱酱, page=24,Address [provices=江苏省, city=南京市]
原对象p1和克隆出来的对象p2内容完全相同。

但是从第 7 行和第 8 行打印结果来看,
代码中我们只是更改了克隆对象 p2 的属性 Address 。
原对象 p1 和克隆对象 p2 的 Address 属性都被修改了。
p1:pname=朱酱酱, page=24,Address [provices=上海省, city=上海市]
p2:pname=朱酱酱, page=24,Address [provices=上海省, city=上海市]

总结:也就是说对象Person的属性Address,经过clone之后,其实只是复制了其引用,他们的指向还是同一块堆内存空间,当修改其中一个对象的属性Address,另一个也会随之变化。

浅拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,

  • 如果字段是值类型的,那么对该字段进行赋值。

  • 如果该字段是引用类型的话,则复制引用但不复制引用的对象。

因此,原始对象及其副本引用同一个对象。

5. 深拷贝

弄清楚了浅拷贝,那么深拷贝就很容易理解了。

深拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象。无论该字段是值类型还是引用类型,都复制独立的一份。

 那么该如何实现深拷贝呢?Object 类提供的 clone 是只能实现 浅拷贝的。

6. 如何实现深拷贝?

参考博客:https://www.cnblogs.com/xinruyi/p/11537963.html

mark

深拷贝的原理我们知道了,就是要让原始对象和克隆之后的对象所具有的引用类型属性不是指向同一块堆内存,这里有两种实现思路。

①、让每个引用类型属性内部都重写clone() 方法

既然引用类型不能实现深拷贝,那么我们将每个引用类型都拆分成基本类型,分别进行浅拷贝。

比如上面的例子,Person 类有一个引用类型 Address(其实String 也是引用类型,但是String类型有点特殊,后面会详细讲解),

  1. 我们在Address类内部也重写clone方法。如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.zhuuu;

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 + "]";
}

// 实现深拷贝,让每个引用类型的内部都重写clone方法,拆分成一个一个浅拷贝
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
  1. Person.class 的 clone() 方法:
1
2
3
4
5
6
7
8
9
// 拆分成一个一个浅拷贝
// 弊端:Person有一个引用类型Address,Address没有
// 但是,如果Address也有引用类型,那么就要无限重写clone方法下去
@Override
protected Object clone() throws CloneNotSupportedException {
Person p = (Person) super.clone();
p.address = (Address) address.clone();
return p;
}
  1. 测试还是和上面一样,我们会发现更改了p2对象的Address属性,p1 对象的 Address 属性并没有变化。

这种做法有个弊端,这里如果我们Person类只有一个Address引用类型,而Address类没有,所以我们只用重写Address类的clone方法。

但是如果Address类也存在一个引用类型,那么我们也要重写其Clone方法,这样下去,有多少个引用类型,我们就要重写多少次,如果存在很多引用类型,那么代码量会明显增大。所以这种方式不合适、

②、利用序列化

序列化是将对象写到流中便于传输,

而反序列化则是将对象从流中读取出来,

这里写到流中的对象则是原始对象的一个拷贝,因为原始对象还存在JVM中,所以我们可以利用对象的序列化产生克隆对象,然后通过反序列化获取这个对象。

注意每个需要序列化的类都要实现Serializable接口,如果某个属性不需要序列化,可以将其声明为transient,即将其排除在克隆属性之外。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.zhuuu;

import java.io.*;

public class Deepclone implements Serializable {

public Object deepclone() throws Exception{
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);

oos.writeObject(this);

// 反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);

return ois.readObject();
}
}

注意:因为序列化产生的是两个完全独立的对象,所以无论嵌套多少个引用类型,序列化都是能实现深拷贝的。

打赏
  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  • © 2019-2022 Zhuuu
  • PV: UV:

请我喝杯咖啡吧~

支付宝
微信