Java ☞ 对象的克隆
我喜欢用自己的想法去阐述一个在项目中用到的技术,虽然网上到处都可以搜到,但我还是决定自己写下来,就像记笔记一样,记录在自己的博客中,这就是我的每一篇博文诞生的初衷。
不是为了抄袭,也不是为了比别人写的更好,仅仅是为了记录,正因为有了记录,我有了自己的想法,这样很棒!
--- 2018年9月27日14:04:41
一、为什么要用到对象的克隆?
项目中有个地方需要借助某个对象进行某个算法的计算,假设这个对象是A,而这个算法需要对A中的某些信息进行诸如合并之类的操作,比如A中有两个成员变量,假设一个叫"m",另一个叫"n",合并后自然成了"mn",如果A这个对象在后续操作中不再使用(针对一个线程,也就是A始终贯彻整个业务线),则我门完全不用担心,但事实是,A在后续的操作中仍然会使用到,也就是对象在一个业务线中会多次使用到;
由于A在结束一段算法的时候,其内部信息已经发生了改变(对象的引用),而我再次使用对象A时,发现原来的成员变量m已经不是m了,n也不是n了? 如何是好?
这种情况下,就只能借助对象的克隆技术了,也就是在对A进行算法之前,对A先进行一个克隆,克隆出一个"一模一样"的对象B,这时候算法的操作过程针对的只是对象B(独立的内存空间,但是信息和A是一样的),这样一来,我即可以拿到我想要的mn,又可以在再次使用A的时候,拿到我想要的m和n,一举两得,互不干扰。
(注:如果需要用到克隆技术的,耗费一点内存空间也是值得的,前提是,克隆的对象不多,内存足够)
二、克隆有类型之分吗?
根据克隆程度的深浅(即对象是完全克隆还是部分克隆),我们将Java中的clone分为深拷贝和浅拷贝
这是什么意思呢 ? 我举个例子
假设有一份食谱,我们称作A,这个A的作者叫B,我们用Java类表示就是:有一个类是A,还有一个类是B,A中包含了B,也就是A和B之间是组合的关系
(1)浅拷贝
如果我只想要食谱,而不关心其创作者是谁的话,我就用浅拷贝,我们将这种克隆方式得到的新对象称作A+,A+的内存地址是独立的,但是其内部的对象B却不是独立的内存,其仍然指向先前A对象中的B对象的内存地址,因此,改变A+的成员变量并不会影响原有的A对象(因为二者是两个完全独立的内存空间),但是改变A+中的B对象的成员变量却会影响原有A对象中的B对象
这就好比,我食谱拿来后,我可以在原有的食谱上加以修改,但是不会破坏最开始的食谱,但是作者我却可以改变,因为如果我改变后得到一个新的食谱,理应将新食谱的作者改成我自己,所谓吃水不忘挖井人,源食谱的内容我是不会动的。
实现方法:实现Cloneable接口,重写基类Object的clone方法
注意:这是一个native方法,其要比对象之间的直接引用性能要高
什么是native方法?【摘自网络】
native关键字说明其修饰的方法是一个原生态方法,方法对应的实现不是在当前文件,而是在用其他语言(如C和C++)实现的文件中。Java语言本身不能对操作系统底层进行访问和操作,但是可以通过JNI接口调用其他语言来实现对底层的访问。
什么是对象间的直接引用呢?
如: Object A = new Object(); Obejct B = A;
(2)深拷贝
即对象完全克隆;不管A对象中包含了多少个B对象,B对象里面又套了其他对象(嵌套克隆)...etc,纵使情况如此复杂,我的目的只有一个,就是完全将A对象进行"复制",复制到什么程度呢:即复制后得到的新对象A+,不管我对A+及其内部对象做了什么操作,都不会改变原有的对象A,这就叫做深度克隆
实现方法:实现Serializable接口,自定义克隆方法,利用对象的字节流进行序列化和反序列化得到新的对象
三、克隆的实现
(1)基于SpringBoot框架的项目,文章最后会提供GitHub演示demo下载地址
(1)构造用户类User
package com.appleyk.pojo;
import java.io.Serializable;
/**
* @Description 用户表
* @Author Appleyk
* @Blob https://blog.****.net/appleyk
* @Date Created on 下午 12:45 2018-9-27
*/
public class User implements Serializable {
/**
* 用户ID
*/
private Long uid ;
/**
* 用户名称
*/
private String name;
/**
* 用户年龄
*/
private Integer age;
public User(Long uid,String name,Integer age){
this.uid = uid;
this.name = name;
this.age = age;
}
public Long getUid() {
return uid;
}
public void setUid(Long uid) {
this.uid = uid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "姓名:"+this.name+",工号:"+this.uid+",年龄:"+this.age;
}
}
注意:为了实现深度克隆,必须让嵌套的所有关联的对象也都实现Serializable接口
(2)构建食谱类
package com.appleyk.pojo;
import com.appleyk.utils.DateUtils;
import java.io.*;
import java.util.Date;
/**
* @Description -- 食谱类
* @Author Appleyk
* @Blob https://blog.****.net/appleyk
* @Date Created on 上午 11:03 2018-9-27
*/
public class CookBook implements Cloneable,Serializable {
// 食谱名称
private String memu;
// 食谱创建时间
private Date ctime;
// 用户信息
private User user;
public CookBook(){
ctime = new Date();
}
public String getMemu() {
return memu;
}
public void setMemu(String memu) {
this.memu = memu;
}
public Date getCtime() {
return ctime;
}
public void setCtime(Date ctime) {
this.ctime = ctime;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
@Override
public String toString(){
return "食谱名称:"+this.memu+",食谱作者信息:--【"+user+"】 --- "+DateUtils.dateTostring(this.ctime);
}
/**
* 对象之间的浅克隆【只负责copy对象本身,不负责深度copy其内嵌的成员对象】
* @return
*/
@Override
public Object clone() {
try{
return (CookBook) super.clone();
}catch (CloneNotSupportedException ex){
System.out.println(ex.getClass()+":"+ ex.getMessage());
}
return null;
}
/**
* 实现对象间的深度克隆【从外形到内在细胞,完完全全深度copy】
* @return
*/
public Object deepClone(){
// Anything 都是可以用字节流进行表示,记住是任何!
CookBook cookBook = null;
try{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
// 将当前的对象写入baos【输出流 -- 字节数组】里
oos.writeObject(this);
// 从输出字节数组缓存区中拿到字节流
byte[] bytes = baos.toByteArray();
// 创建一个输入字节数组缓冲区
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
// 创建一个对象输入流
ObjectInputStream ois = new ObjectInputStream(bais);
// 下面将反序列化字节流 == 重新开辟一块空间存放反序列化后的对象
cookBook = (CookBook) ois.readObject();
}catch (Exception e){
System.out.println(e.getClass()+":"+e.getMessage());
}
return cookBook;
}
}
注意:为了区别浅克隆和深克隆,食谱类同时实现了Cloneable和Serializable接口
(Java只能继承一个父类,但是可以实现多个接口)
(3)初始化一个食谱对象
package com.appleyk;
import com.appleyk.pojo.CookBook;
import com.appleyk.pojo.User;
/**
* @Description
* @Author Appleyk
* @Blob https://blog.****.net/appleyk
* @Date Created on 下午 1:52 2018-9-27
*/
public class ObjectFactory {
/**
* 创建一个食谱对象(实例化类)
* @return
*/
public static CookBook createCookBook(){
CookBook cookBook = new CookBook();
cookBook.setMemu("Appleyk'食谱大全");
cookBook.setUser(new User(1001L,"appleyk",18));
return cookBook;
}
}
(4)拷贝测试☞ 对象引用【共用一块内存地址】
/**
* 对象之间的引用
* Object a = new Object();
* Object b = a;
*/
@Test
public void copy1(){
CookBook cookBookSrc = ObjectFactory.createCookBook();
// 我们这里打印下原始食谱信息
System.out.println("源"+cookBookSrc);
// 然后我们需要复制一份食谱,给另外一个人使用
CookBook cookBookNew = cookBookSrc;
// 由于上述是对象之间的直接"赋值"或"复制",因此,新的食谱在内存中的地址引用其实和原始食谱是一样的
System.out.println("新"+cookBookNew);
System.out.println("新食谱不做修改之前======================新食谱修改之后");
cookBookNew.getUser().setAge(27);
cookBookNew.setMemu("豫菜食谱大全");
System.out.println("源"+cookBookSrc);
System.out.println("新"+cookBookNew);
System.out.println("源 == 新:"+(cookBookSrc.equals(cookBookNew)));
}
上述代码,得到的新食谱,就是对原有食谱的一个彻底的对象引用,因此,改变新食谱的内容,其实就是同步改变原有食谱对象
输出效果如下:
(5)浅拷贝测试☞ 对象浅克隆【部分内存地址完全独立】
/**
* 对象之间的克隆
* Object a = new Object();
* Object b = a;
*/
@Test
public void copy2(){
CookBook cookBookSrc = ObjectFactory.createCookBook();
// 我们这里打印下原始食谱信息
System.out.println("源"+cookBookSrc);
// 然后我们需要克隆一份食谱,给另外一个人使用
CookBook cookBookNew = (CookBook) cookBookSrc.clone();
// 由于上述是对象之间的直接"赋值"或"复制",因此,新的食谱在内存中的地址引用其实和原始食谱是一样的
System.out.println("新"+cookBookNew);
System.out.println("新食谱不做修改之前======================新食谱修改之后");
cookBookNew.getUser().setAge(27);
cookBookNew.setMemu("豫菜食谱大全");
System.out.println("源"+cookBookSrc);
System.out.println("新"+cookBookNew);
System.out.println("源 == 新:"+(cookBookSrc.equals(cookBookNew)));
}
上述代码,我们利用重写基类Object的clone方法,克隆出了一份新食谱,由于是浅克隆,因此当我们修改新食谱里面的user信息时,源食谱中的user信息也会同步进行改变,但是修改新食谱的内容时,源食谱的内容是不会改变的,这就是所谓的不完全克隆
输出效果如下:
(6)深拷贝测试☞ 对象深克隆【内存地址完全独立】
@Test
public void copy3(){
CookBook cookBookSrc = ObjectFactory.createCookBook();
// 我们这里打印下原始食谱信息
System.out.println("源"+cookBookSrc);
// 然后我们需要克隆一份食谱,给另外一个人使用
CookBook cookBookNew = (CookBook) cookBookSrc.deepClone();
// 由于上述是对象之间的直接"赋值"或"复制",因此,新的食谱在内存中的地址引用其实和原始食谱是一样的
System.out.println("新"+cookBookNew);
System.out.println("新食谱不做修改之前======================新食谱修改之后");
cookBookNew.getUser().setAge(27);
cookBookNew.setMemu("豫菜食谱大全");
System.out.println("源"+cookBookSrc);
System.out.println("新"+cookBookNew);
System.out.println("源 == 新:"+(cookBookSrc.equals(cookBookNew)));
}
上述代码,利用我们自定义的deepClone方法(对象字节流的序列与反序列化),深度克隆了一份新的食谱,既然是深度克隆,那么就意味着,无论我怎么修改新食谱的内容(包括修改其内部嵌套的对象的信息),都不会改变源食谱对象的信息
输出效果如下:
四、GitHub项目地址
Java 实现对象的浅拷贝和深拷贝:https://github.com/kobeyk/Spring-Boot-Clone.git