前言

​ Java语言的一个优点就是取消了指针的概念,但也导致了许多程序员在编程中常常忽略了对象与引用的区别,本文会试图澄清这一概念。并且由于Java不能 通过简单的赋值来解决对象复制的问题,在开发过程中,也常常要要应用clone()方法来复制对象。本文会让你了解什么是影子clone与深度 clone,认识它们的区别、优点及缺点。
​ 看到这个标题,是不是有点困惑:Java语言明确说明取消了指针,因为指针往往是在带来方便的同时也是导致代码不安全的根源,同时也会使程序的变得非常复 杂难以理解,滥用指针写成的代码不亚于使用早已臭名昭著的”GOTO”语句。Java放弃指针的概念绝对是极其明智的。但这只是在Java语言中没有明确 的指针定义,实质上每一个new语句返回的都是一个指针的引用,只不过在大多时候Java中不用关心如何操作这个”指针”,更不用象在操作C++的指针那 样胆战心惊。唯一要多多关心的是在给函数传递对象的时候。

我们先来“自欺”一下

实体类User

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.mytest.copy.entities;

import lombok.Builder;
import lombok.Data;

import java.time.LocalDateTime;

/**
* @author YoungDream
* @since 2019/4/1 10:48
*/
@Data
@Builder
public class User {
private String id;
private String email;
private int age;
}

lombok的@Data注解可以使我们省略实体类getter/setter、toString、equals和hashCode方法,其他更多注解可以参考lombok常用注解一文。

测试类

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

import com.mytest.copy.entities.User;

import java.time.LocalDateTime;

/**
* @author YoungDream
* @since 2019/4/1 10:44
*/
public class CloneTest {
public static void main(String[] args) {
User u1 = User.builder().id("a").email("aa@yd.com").age(11).build();
User u2 = u1;
System.out.println(u1);
System.out.println(u2);
System.out.println(u2 == u1);
System.out.println(u2.equals(u1));
}
}

控制台打印

1
2
3
4
User(id=a, email=aa@yd.com, age=11)
User(id=a, email=aa@yd.com, age=11)
true
true

这里我们自定义了一个User类,新建了一个学生实例,然后将该值赋值给u2实例:User u2 = u1

再看看打印结果,作为一个新手,拍了拍胸腹,对象复制不过如此,

难道真的是这样吗?

我们试着改变u2实例的age字段,再打印结果看看:

1
2
3
4
5
6
User u2 = u1;
u2.setAge(22);
System.out.println(u1);
System.out.println(u2);
System.out.println(u2 == u1);
System.out.println(u2.equals(u1));
1
2
3
4
User(id=a, email=aa@yd.com, age=22)
User(id=a, email=aa@yd.com, age=22)
true
true

啊呀呀,u1也一起改变了,其实从==方法就可以得知引用的地址一样,指向堆内的对象肯定也是同一个

那么,如何拷贝呢?

是否记得Object类11个方法中的clone()

1
protected native Object clone() throws CloneNotSupportedException;

因为每个类直接或间接的父类都是Object,因此它们都含有clone()方法,但是因为该方法是protected,所以都不能在类外进行访问。

clone()方法

浅复制

要想对一个对象进行复制,就需要对clone方法覆盖。

一般步骤是(浅复制):

  1. 被复制的类需要实现Clonenable接口(不实现的话在调用clone方法会抛出CloneNotSupportedException异常) 该接口为标记接口(不含任何方法)
  2. 覆盖clone()方法,访问修饰符设为public。方法中调用super.clone()方法得到需要的复制对象,(native为本地方法)

开始对User类添加clone方法

1
2
3
4
5
6
7
8
9
10
public class User implements Cloneable {
private String id;
private String email;
private int age;

@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
public class CloneTest {
public static void main(String[] args) throws CloneNotSupportedException {
User u1 = User.builder().id("a").email("aa@yd.com").age(11).build();
User u2 = (User) u1.clone();
u2.setAge(22);
System.out.println(u1);
System.out.println(u2);
System.out.println(u2 == u1);
System.out.println(u2.equals(u1));
}
}

控制台

1
2
3
4
User(id=a, email=aa@yd.com, age=11)
User(id=a, email=aa@yd.com, age=22)
false
false

哟呵,成功了!上面的复制被称为浅复制(Shallow Copy),还有一种稍微复杂的深度复制(deep copy)

深复制

我们在User类里再加一个Address类

Address类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.mytest.copy.entities;

import lombok.Builder;
import lombok.Data;

/**
* @author YoungDream
* @since 2019/4/1 11:55
*/
@Data
@Builder
public class Address {
private int id;
private String add;
}

User类

1
2
3
4
5
6
public class User implements Cloneable {
private String id;
private String email;
private int age;
private Address address;
}

测试类

1
2
3
4
User u2 = (User) u1.clone();
u2.getAddress().setAdd("广州市");
System.out.println(u1);
System.out.println(u2);

控制台

1
2
User(id=a, email=aa@yd.com, age=11, address=Address(id=1, add=广州市))
User(id=b, email=bb@yd.com, age=22, address=Address(id=1, add=广州市))

二者同时改变了,说明浅复制只是复制了address变量的引用,并没有真正的开辟另一块空间,将值复制后再将引用返回给新对象

为了达到真正的复制对象,而不是纯粹引用复制。我们需要将Address类可复制化,并且修改clone方法。

Address类

1
2
3
4
5
6
7
8
9
public class Address implements Cloneable {
private int id;
private String add;

@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

User类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class User implements Cloneable {
private String id;
private String email;
private int age;
private Address address;

@Override
public Object clone() throws CloneNotSupportedException {
User user = (User) super.clone();
user.address = (Address) address.clone();
return user;
}
}

再进行测试

控制台

1
2
User(id=a, email=aa@yd.com, age=11, address=Address(id=1, add=深圳市))
User(id=a, email=aa@yd.com, age=11, address=Address(id=1, add=广州市))

这样结果就符合我们的想法了。

最后我们可以看看API里其中一个实现了clone方法的类

Date类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Date implements java.io.Serializable, Cloneable, Comparable<Date>{
...
/**
* Return a copy of this object.
*/
public Object clone() {
Date d = null;
try {
d = (Date)super.clone();
if (cdate != null) {
d.cdate = (BaseCalendar.Date) cdate.clone();
}
} catch (CloneNotSupportedException e) {} // Won't happen
return d;
}
...
}

说明Date类其实也属于深度复制

参考内容:Java的clone():深复制与浅复制