Java深克隆和浅克隆

一、为什么要克隆?

使用场景: 当使用一个对象的属性时,需要进行一些修改,但是又不能直接修改该对象,此时我们就可以使用克隆来拷贝一个对象,进行操作。不然就需要new一个对象,对属性赋值。
总的来说为了保证引用类型的参数不被其他方法修改,可以使用克隆后的值作为参数传递。
一般情况下,我们实际需要使用的是深克隆

二、如何实现克隆

  1. 对象的类实现Cloneable接口;
  2. 重写Object类的clone()方法 ;
  3. 在clone()方法中调用super.clone();

三、两种不同的克隆方法,浅克隆(ShallowClone)和深克隆(DeepClone)。

浅克隆是指拷贝对象时仅仅拷贝对象本身(包括对象中的基本变量),而不拷贝对象里面包含的引用对象。

深克隆不仅拷贝对象本身,而且拷贝对象里面包含引用指向的所有对象。
浅克隆
在这里插入图片描述
浅克隆

@Data
@EqualsAndHashCode(callSuper = false)
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Account implements Cloneable {
    /** 主键 */
    private Long id;
    /** 账户名称 */
    private String name;
    /** 账户详情 */
    private AccountDetail detail;
    @Override
    public Account clone() {
        Account account = null;
        try {
            account = (Account) super.clone();            
            return account;
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return account;
    }
}
@Data
@EqualsAndHashCode(callSuper = false)
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class AccountDetail{
    /** 账户ID */
    private Long accountId;
    /** 邮箱 */
    private String email;
}

测试代码

 public static void main(String[] args) {
        // 浅克隆证明
        AccountDetail detail = new AccountDetail(1L, "1048791780@qq.com");
        Account account = new Account(1L, "小何", detail);

        // 克隆
        Account clone = (Account) account.clone();

        // 判断详情对对象是否相同,预期值(true)
        log.debug("对象是否相同:{}", clone.getDetail() == account.getDetail());

        log.debug("原始对象的地址:{}", System.identityHashCode(account.getDetail()));
        log.debug("克隆对象的地址:{}", System.identityHashCode(clone.getDetail()));
        clone.getDetail().setAccountId(2L);
        log.debug("原始对象:{}", account);
        log.debug("克隆对象:{}", clone);
    }

这里我们使用到System.identityHashCode来打印对象的地址(不完全等同地址,但是可以看做是)。hashcode通过@EqualsAndHashCode重写了。在这里插入图片描述
验证结果跟预期一样。浅克隆出来的对象,修改引用对象会影响到原始对象。
由于里面的引用对象AccountDetail并没有实现克隆。
我们需要对代码改造一下,将AccountDetail也实现克隆,手动赋值。进行深度克隆。
在这里插入图片描述修改完后在执行测试代码,在验证,发现达到深度克隆了。
在这里插入图片描述
此时,我们发现实现深度克隆,有一个麻烦之处,对象里面的引用对象也需要实现克隆,手动赋值。此时我们需要使用更方便的深度克隆方式,序列化克隆。
这种方式为:所有对象都实现克隆方法

序列化克隆(JDK 自带的字节流实现深克隆)

 /**
     * 使用ObjectStream序列化实现深克隆
     * @return Object obj
     */
    public static <T extends Serializable> T deepClone(T t) throws CloneNotSupportedException {
        // 保存对象为字节数组
        try {
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            try(ObjectOutputStream out = new ObjectOutputStream(bout)) {
                out.writeObject(t);
            }
            // 从字节数组中读取克隆对象
            try(InputStream bin = new ByteArrayInputStream(bout.toByteArray())) {
                ObjectInputStream in = new ObjectInputStream(bin);
                return (T)(in.readObject());
            }
        }catch (IOException | ClassNotFoundException e){
            CloneNotSupportedException cloneNotSupportedException = new CloneNotSupportedException();
            e.initCause(cloneNotSupportedException);
            throw cloneNotSupportedException;
        }
    }

第三方工具实现

列举几个常用的工具类

 // fastJson实现克隆
    Account clone = JSONObject.parseObject(JSONObject.toJSONBytes(account), Account.class);
    //commons-beanutils
    Account cloneObject = new Account();
    BeanUtils.copyProperties(account,cloneObject);
    // 调用 apache.commons.lang 克隆对象
    Account a = (Account) SerializationUtils.clone(account);

疑问

  1. 在 java.lang.Object 中对 clone() 方法的限制有哪些?
  2. Java 中的克隆为什么要设计成,既要实现空接口 Cloneable,还要重写 Object 的 clone() 方法?

1、分析clone() 源码,发现注释里面有说到

  • 对于所有对象来说,x.clone() !=x 应当返回 true,因为克隆对象与原对象不是同一个对象;
  • 对于所有对象来说,x.clone().getClass() == x.getClass() 应当返回 true,但不是绝对要相等。
    这里我解读应该跟继承有关。
  • 对于所有对象来说,x.clone().equals(x) 应当返回 true,因为使用 equals 比较时,它们的值都是相同的。

除了注释信息外,发现 clone() 是使用 native 修饰的本地方法,因此执行的性能会很高,并且它返回的类型为 Object,因此在调用克隆之后要把对象强转为目标类型才行。

protected修饰,只能同包名和子类内部调用。这样你就不能瞎鸡儿调用

2、空接口 Cloneable,看注释是JDK1.0就存在。
一个类实现Cloneable接口,表明调用Object.clone()方法进行该类实例的field-for-field(属性复制)是合法的。
在未实现Cloneable接口的实例上调用Object的clone方法会导致抛出CloneNotSupportedException异常。
由于Object.clone是native方法。我大胆猜测是历史原因。简单看一下native源码

JVM_ENTRY(jobject, JVM_Clone(JNIEnv* env, jobject handle))
  JVMWrapper("JVM_Clone");
  Handle obj(THREAD, JNIHandles::resolve_non_null(handle));
  const KlassHandle klass (THREAD, obj->klass());
  JvmtiVMObjectAllocEventCollector oam;

#ifdef ASSERT
  // Just checking that the cloneable flag is set correct
  if (obj->is_array()) {
    guarantee(klass->is_cloneable(), "all arrays are cloneable");
  } else {
    guarantee(obj->is_instance(), "should be instanceOop");
    bool cloneable = klass->is_subtype_of(SystemDictionary::Cloneable_klass());
    guarantee(cloneable == klass->is_cloneable(), "incorrect cloneable flag");
  }
#endif

  // Check if class of obj supports the Cloneable interface.
  // All arrays are considered to be cloneable (See JLS 20.1.5)
  if (!klass->is_cloneable()) {
    ResourceMark rm(THREAD);
    THROW_MSG_0(vmSymbols::java_lang_CloneNotSupportedException(), klass->external_name());
  }

  // Make shallow object copy
  const int size = obj->size();
  oop new_obj = NULL;
  if (obj->is_array()) {
    const int length = ((arrayOop)obj())->length();
    new_obj = CollectedHeap::array_allocate(klass, size, length, CHECK_NULL);
  } else {
    new_obj = CollectedHeap::obj_allocate(klass, size, CHECK_NULL);
  }
  // 4839641 (4840070): We must do an oop-atomic copy, because if another thread
  // is modifying a reference field in the clonee, a non-oop-atomic copy might
  // be suspended in the middle of copying the pointer and end up with parts
  // of two different pointers in the field.  Subsequent dereferences will crash.
  // 4846409: an oop-copy of objects with long or double fields or arrays of same
  // won't copy the longs/doubles atomically in 32-bit vm's, so we copy jlongs instead
  // of oops.  We know objects are aligned on a minimum of an jlong boundary.
  // The same is true of StubRoutines::object_copy and the various oop_copy
  // variants, and of the code generated by the inline_native_clone intrinsic.
  assert(MinObjAlignmentInBytes >= BytesPerLong, "objects misaligned");
  Copy::conjoint_jlongs_atomic((jlong*)obj(), (jlong*)new_obj,
                               (size_t)align_object_size(size) / HeapWordsPerLong);
  // Clear the header
  new_obj->init_mark();

  // Store check (mark entire object and let gc sort it out)
  BarrierSet* bs = Universe::heap()->barrier_set();
  assert(bs->has_write_region_opt(), "Barrier set does not have write_region");
  bs->write_region(MemRegion((HeapWord*)new_obj, size));

  // Caution: this involves a java upcall, so the clone should be
  // "gc-robust" by this stage.
  if (klass->has_finalizer()) {
    assert(obj->is_instance(), "should be instanceOop");
    new_obj = InstanceKlass::register_finalizer(instanceOop(new_obj), CHECK_NULL);
  }

  return JNIHandles::make_local(env, oop(new_obj));
JVM_END

发现其中一段代码,好熟悉的CloneNotSupportedException

  // Check if class of obj supports the Cloneable interface.
  // All arrays are considered to be cloneable (See JLS 20.1.5)
  if (!klass->is_cloneable()) {
    ResourceMark rm(THREAD);
    THROW_MSG_0(vmSymbols::java_lang_CloneNotSupportedException(), klass->external_name());
  }

再回过来看,我觉得是历史原因了。本身克隆功能就不是java写的,而且是早期版本实现的。使用频率也不高。而且也满足了任何对象都能实现克隆能力。

所以空接口Cloneable实际上就是一个标记。光实现Cloneable接口也没啥用,啥也不是。
真实的克隆功能是Object.clone()实现的。
默认都给你实现了(可能你并不需要而且也不是必要功能),所以就产生了Cloneable接口和protected修饰的限制。

思考:为什么不直接调用Object.clone

在明白克隆的实现后,我们就会想为什么不直接调用Object.clone。因为你不能直接去调用Object.clone(),该方法是protected修饰的不是public。导致你只能通过子类内部调用去实现。

  • 至于同包名内调用实现这个方式,放到跟Object同包下,我想你应该不会想不开。这条路给你堵死了。

实验一下,新建一个跟Object同包名。伪装一下,放到同包下。

package java.lang;
 
public class Test {
    public static void main(String[] args) throws CloneNotSupportedException {
        Object o = new Object();
        Object clone = o.clone();
    }
}

直接给你报错,类加载器进行了检查,休想欺骗他。

> java.lang.SecurityException: Prohibited package name: java.lang
> 			at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
			at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
  • 子类内部调用实现
    我们都会进行重写clone方法。必须改成使用public修饰。不然就会遇到跟Object.clone一样的调用困境。只能同包名和子类内部调用了。
    拿出上面例子来看。现在就知道为什么要调用super.clone了。
    在这里插入图片描述

拓展

至于为什么没有用类似class,final这样的关键字,来个clone的关键字。
为什么不用,我认为是不想维护这么多关键字。
像goto 保留关键字,没有具体含义,这个对java而已完全就是多余的关键字。native方法有没有用到,就不得而知了

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值