前言
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;
@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;
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方法覆盖。
一般步骤是(浅复制):
- 被复制的类需要实现
Clonenable
接口(不实现的话在调用clone方法会抛出CloneNotSupportedException
异常) 该接口为标记接口(不含任何方法)
- 覆盖
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;
@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>{ ...
public Object clone() { Date d = null; try { d = (Date)super.clone(); if (cdate != null) { d.cdate = (BaseCalendar.Date) cdate.clone(); } } catch (CloneNotSupportedException e) {} return d; } ... }
|
说明Date类其实也属于深度复制
参考内容:Java的clone():深复制与浅复制