Java Object的clone方法分析

学习视频:https://study.163.com/course/introduction/1006177009.htm

学习目标


  1. 能够理解clone方法的由来
  2. 能够使用clone方法创建对象
  3. 能够理解克隆对象和原对象的关系
  4. 能够理解clone方法创建对象与new关键字和反射创建对象的不同
  5. 能够理解浅表复制和深层复制的含义
  6. 能够探寻对象的复制必须实现Cloneable接口的底层源码

1. 克隆方法的由来


问题一:什么是克隆(clone)方法?
答:创建并返回此对象的一个副本–按照原对象,创建一个新对象(复制原对象的内容)。

问题二:已经存在new关键字反射技术都可以创建对象,为什么还需要一个Object的clone方法呢?
答:必然是new关键字和 反射技术,存在一些弊端。看下面的例子体会弊端在哪。

1.1 new关键字和反射创建对象的弊端


我们来看一个需求:使用new关键字和反射创建内容一模一样的对象,并且打印它们的哈希值。
演示素材–Person:

package com.yynm.pojo;

/**
 * @Description: Person实体类
 * @Author: yeyulemon
 * @Date: 2021-12-16 21:06
 **/
public class Person {
    private String name;
    private int age;

    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;
    }
}

演示Person类使用new关键字和反射创建内容一模一样的对象,并且打印它们的哈希值

/**
 * @Description: 使用new关键字和反射创建内容一模一样的对象,并且打印它们的哈希值
 * @Param: []
 * @return: void
 * @Author: yeyulemon
 * @Date: 2021/12/16 21:12
 */
@Test
public void test01() throws Exception {
    Person person1 = new Person();
    person1.setName("张三");
    person1.setAge(18);

    Person person2 = new Person();
    person2.setName("张三");
    person2.setAge(18);

    System.out.println(person1 + ":" + person1.hashCode());
    System.out.println(person2 + ":" + person2.hashCode());

    Class clazz = Class.forName("com.yynm.pojo.Person");
    Person person3 = (Person) clazz.getConstructor().newInstance();
    person3.setName("张三");
    person3.setAge(18);

    Person person4 = (Person) clazz.getConstructor().newInstance();
    person4.setName("张三");
    person4.setAge(18);

    System.out.println(person3 + ":" + person3.hashCode());
    System.out.println(person4 + ":" + person4.hashCode());
}

效果

com.yynm.pojo.Person@dc24521:230835489
com.yynm.pojo.Person@10bdf5e5:280884709
com.yynm.pojo.Person@6e1ec318:1847509784
com.yynm.pojo.Person@7e0b0338:2114650936

总结:通过new和反射可以创建内容一模一样的对象。但是,创建对象之后,通过setter方法设置一模一样的内容,如果需要创建更多内容一致的对象,那么就需要调用非常多的setter方法。

接下来,使用Object的clone方法演示,更加简便快捷,复制对象的操作!

1.2 使用clone方法创建对象


1.2.1 使用步骤
  1. 在需要clone方法的类上实现Cloneable接口
  2. 重写clone方法,在自己的clone方法中调用父类的clone方法,将返回值类型强转成本类类型,将当前clone方法修饰符改为public
  3. 在测试中调用对象的clone方法
1.2.2 代码演示
  1. 在需要clone方法的类上实现Cloneable接口
  2. 重写clone方法,在自己的clone方法中调用父类的clone方法,将返回值类型强转成本类类型,将当前clone方法修饰符改为public
package com.yynm.pojo;

/**
 * @Description: Person实体类
 * @Author: yeyulemon
 * @Date: 2021-12-16 21:06
 **/
public class Person implements Cloneable {
    private String name;
    private int age;

    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 Person clone() throws CloneNotSupportedException {
        return (Person) super.clone();
    }
}

  1. 在测试中调用对象的clone方法
/** 
* @Description: 在测试中调用对象的clone方法
* @Param: [] 
* @return: void 
* @Author: yeyulemon 
* @Date: 2021/12/16 21:33 
*/
@Test
public void test02() throws Exception {
    Person person1 = new Person();
    person1.setName("张三");
    person1.setAge(18);

    Person person2 = person1.clone();

    System.out.println(person1 + ":" + person1.hashCode());
    System.out.println(person2 + ":" + person2.hashCode());
}

效果

com.yynm.pojo.Person@dc24521:230835489
com.yynm.pojo.Person@10bdf5e5:280884709

总结:通过使用clone方法,我们发现大大的减少了创建重复对象代码。这也就是clone方法存在的意义。

2. 克隆出来的对象和原来的对象有什么关系

通过上面的测试,我们已经知道了,克隆出来的对象内容一致,但是对象哈希值不一样,所以是不同对象。那么两个对象的内容之间有什么关联呢?两个对象的内容是彼此独立,还是,两个对象底层使用的同一个内容呢?
素材(新Person):

package com.yynm.pojo;

/**
 * @Description: Person实体类
 * @Author: yeyulemon
 * @Date: 2021-12-16 21:06
 **/
public class Person implements Cloneable {
    private String name;
    private Integer age;
    private Children children;

    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;
    }

    public Children getChildren() {
        return children;
    }

    public void setChildren(Children children) {
        this.children = children;
    }

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

素材(新Children):

package com.yynm.pojo;

/**
 * @Description: Children实体类
 * @Author: yeyulemon
 * @Date: 2021-12-16 21:49
 **/
public class Children {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

测试代码:

/**
* @Description: 测试克隆对象和原对象之间成员变量的联系
 * @Param: []
 * @return: void
 * @Author: yeyulemon
 * @Date: 2021/12/16 21:51
 */
@Test
public void test03() throws Exception {
    Person person1 = new Person();
    person1.setName("张三");
    person1.setAge(18);
    Children children = new Children();
    children.setName("李四");
    children.setAge(18);
    person1.setChildren(children);

    Person person2 = person1.clone();

    System.out.println(person1 + ":" + person1.hashCode() + ";" + children + ":" +person1.getChildren().hashCode());
    System.out.println(person2 + ":" + person2.hashCode() + ";" + children + ":" +person2.getChildren().hashCode());
}

效果:

com.yynm.pojo.Person@10bdf5e5:280884709;com.yynm.pojo.Children@6e1ec318:1847509784
com.yynm.pojo.Person@7e0b0338:2114650936;com.yynm.pojo.Children@6e1ec318:1847509784

结论:通过测试发现克隆出来的对象虽然不一致,但是底层的成员变量的哈希值是一致的。
这种复制我们称之为:浅表复制
浅表复制内存结构:
浅表复制内存结构

3. 能不能让克隆出来的对象其中成员变量也变成新对象

3.1 浅表复制的弊端

由于浅表复制导致克隆的对象中成员变量的底层哈希值一致,如果我们操作其中一个对象的成员变量内容,就会导致所有的克隆对象的成员变量内容发生改变。
测试代码:

@Test
public void test04() throws Exception {
    Person person1 = new Person();
    Children children = new Children();
    children.setName("张三");
    children.setAge(18);
    person1.setChildren(children);
    Person person2 = person1.clone();
    System.out.println(person1 + ":" + person1.hashCode() + ";" + person1.getChildren().getName());
    System.out.println(person2 + ":" + person2.hashCode() + ";" + person2.getChildren().getName());
    children.setName("李四");
    children.setAge(18);
    System.out.println(person1 + ":" + person1.hashCode() + ";" + person1.getChildren().getName());
    System.out.println(person2 + ":" + person2.hashCode() + ";" + person2.getChildren().getName());
}

效果:

com.yynm.pojo.Person@573fd745:1463801669;张三
com.yynm.pojo.Person@15327b79:355629945;张三
com.yynm.pojo.Person@573fd745:1463801669;李四
com.yynm.pojo.Person@15327b79:355629945;李四

结论:clone方法默认的赋值操作是浅表复制,浅表复制存在弊端–仅仅创建新的对象,对象的成员内容底层哈希值是一致的,因此,不管是原对象还是克隆对象,只有其中一个修改了成员的数据,就会影响所有的原对象和克隆对象。
要解决浅表复制的问题:进行深层的复制。

3.2 深层复制

目的:不仅在执行克隆的时候,克隆对象是一个新对象,而且,克隆对象中的成员变量,也要求是一个新的对象。

3.2.1 开发步骤
  1. 修改Children类实现Cloneable接口
  2. 修改Children类重写clone方法
  3. 修改Person类重写clone方法,在clone方法中调用children的clone方法
3.2.2 代码实现
  1. 修改Children类实现Cloneable接口
  2. 修改Children类实现clone方法
    Children类:
package com.yynm.pojo;

/**
 * @Description: Children实体类
 * @Author: yeyulemon
 * @Date: 2021-12-16 21:49
 **/
public class Children implements Cloneable {
    private String name;
    private Integer age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

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

Person类:

package com.yynm.pojo;

/**
 * @Description: Person实体类
 * @Author: yeyulemon
 * @Date: 2021-12-16 21:06
 **/
public class Person implements Cloneable {
    private String name;
    private Integer age;
    private Children children;

    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;
    }

    public Children getChildren() {
        return children;
    }

    public void setChildren(Children children) {
        this.children = children;
    }

    @Override
    public Person clone() throws CloneNotSupportedException {
         Person personClone = (Person) super.clone();
         personClone.setChildren(this.children.clone());
        return personClone;
    }
}

测试代码:

@Test
public void test05() throws Exception {
    Person person1 = new Person();
    Children children = new Children();
    children.setName("张三");
    children.setAge(18);
    person1.setChildren(children);
    Person person2 = person1.clone();
    System.out.println(person1 + ":" + person1.hashCode() + ";" + person1.getChildren().hashCode());
    System.out.println(person2 + ":" + person2.hashCode() + ";" + person2.getChildren().hashCode());
}

效果:

com.yynm.pojo.Person@573fd745:1463801669;355629945
com.yynm.pojo.Person@4f2410ac:1327763628;1915503092

深层复制内存结构:
在这里插入图片描述

4. 使用clone接口实现深层复制的弊端

4.1 使用clone接口实现深层复制的弊端

以上的方法虽然完成了深层复制,但是修改类中成员变量对应的源码,如果成员变量特别多,那么就需要修改多个类的源码。
例如以下代码,我们就需要修改两个成员变量对应类的源码(Children,Grandson):

package com.yynm.pojo;

/**
 * @Description: Person实体类
 * @Author: yeyulemon
 * @Date: 2021-12-16 21:06
 **/
public class Person implements Cloneable {
    private String name;
    private Integer age;
    private Children children;
    private Grandson grandson;

    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;
    }

    public Children getChildren() {
        return children;
    }

    public void setChildren(Children children) {
        this.children = children;
    }

    public Grandson getGrandson() {
        return grandson;
    }

    public void setGrandson(Grandson grandson) {
        this.grandson = grandson;
    }

    @Override
    public Person clone() throws CloneNotSupportedException {
        Person personClone = (Person) super.clone();
        personClone.setChildren(this.children.clone());
        personClone.setGrandson(this.grandson.clone());
        return personClone;
    }
}

结论:使用克隆接口完成深度复制的弊端:

1. 重复实现Cloneable接口
2. 重复实现clone方法
3. 重复改写Person类的clone方法

可以使用IO流的方式进行复制操作(深度复制),可以解决重复修改源代码的问题。

4.2 使用IO进行克隆复制(深度复制)

4.2.1 使用IO复制相关的API介绍

1、ByteArrayOutputStream
ByteArrayOutputStream API
构造方法:
ByteArrayOutputStream 构造方法
2、ByteArrayInputStream
ByteArrayInputStream API
构造方法:
ByteArrayInputStream 构造方法
3、ObjectOutputStream
ObjectOutputStream API
构造方法:
ObjectOutputStream 构造方法
将对象写入流的方法:
将对象写入流的方法
4、ObjectInputStream
ObjectInputStream API
构造方法:
ObjectInputStream 构造方法
要调用的方法:
要调用的方法
简单演示:一个对象的复制。
开发步骤:

  1. 创建ByteArrayOutputStream,将数据可以转换成字节
  2. 创建ObjectOutputStream,关联ByteArrayOutputStream
  3. 使用ObjectOutputStream的writeObject方法,读取要复制的对象
  4. 使用ByteArrayInputStream读取ByteArrayOutputStream的转化的对象字节数据
  5. 创建ObjectInputStream并用readObject读取对象字节数据返回新对象
    素材User类:
package com.yynm.pojo;

import java.io.Serializable;

/**
 * @Description:
 * @Author: yeyulemon
 * @Date: 2021-12-19 20:09
 **/
public class User implements Serializable {
    private String username;
    private String password;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

测试:

@Test
public void test06() throws Exception {
    User user1 = new User();
    user1.setUsername("张三");
    user1.setPassword("123456");

    // 1. 创建ByteArrayOutputStream,将数据可以转换成字节
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    // 2. 创建ObjectOutputStream,关联ByteArrayOutputStream
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
    // 3. 使用ObjectOutputStream的writeObject方法,读取要复制的对象
    objectOutputStream.writeObject(user1);
    // 4. 使用ByteArrayInputStream读取ByteArrayOutputStream的转化的对象字节数据
    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
    // 5. 创建ObjectInputStream并用readObject读取对象字节数据返回新对象
    ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
    User user2 = (User) objectInputStream.readObject();

    System.out.println(user1 + ":" + user1.hashCode());
    System.out.println(user2 + ":" + user2.hashCode());
}

效果:

com.yynm.pojo.User@2b05039f:721748895
com.yynm.pojo.User@77468bd9:2001112025

4.3 使用IO改写Person的clone方法

4.3.1 开发步骤
  1. 克隆涉及的所有的类实现Serializable
  2. 修改Person类的clone方法,使用IO复制对象
  3. 测试演示
4.3.2 代码实现
  1. 克隆涉及的所有的类实现Serializable
  2. 修改Person类的clone方法,使用IO复制对象
package com.yynm.pojo;

import java.io.*;

/**
 * @Description: Person实体类
 * @Author: yeyulemon
 * @Date: 2021-12-16 21:06
 **/
public class Person implements Cloneable, Serializable {
    private String name;
    private Integer age;
    private Children children;
    private Grandson grandson;

    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;
    }

    public Children getChildren() {
        return children;
    }

    public void setChildren(Children children) {
        this.children = children;
    }

    public Grandson getGrandson() {
        return grandson;
    }

    public void setGrandson(Grandson grandson) {
        this.grandson = grandson;
    }

    @Override
    public Person clone() throws CloneNotSupportedException {
        try {
            // 1. 创建ByteArrayOutputStream,将数据可以转换成字节
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            // 2. 创建ObjectOutputStream,关联ByteArrayOutputStream
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            // 3. 使用ObjectOutputStream的writeObject方法,读取要复制的对象
            oos.writeObject(this);
            // 4. 使用ByteArrayInputStream读取ByteArrayOutputStream的转化的对象字节数据
            ByteArrayInputStream bAIS = new ByteArrayInputStream(baos.toByteArray());
            // 5. 创建ObjectInputStream并用readObject读取对象字节数据返回新对象
            ObjectInputStream oIS = new ObjectInputStream(bAIS);
            Person personClone = (Person) oIS.readObject();
            return personClone;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

测试:

@Test
public void test07() throws Exception {
    Person person1 = new Person();
    Person person2 = person1.clone();
    System.out.println(person1 + ":" + person1.hashCode());
    System.out.println(person2 + ":" + person2.hashCode());
}

效果:

com.yynm.pojo.Person@4dcbadb4:1305193908
com.yynm.pojo.Person@77468bd9:2001112025

5. 为什么使用clone方法需要实现Cloneable接口

答:源代码就是这么设定的,实现接口仅仅是一个可以使用clone方法的标记。
那么源代码在哪里设定的呢?
查看jdk源码我们发现:
Onject clone方法
因此,我们需要查看native修饰的背后的源码,这个一直要追溯到jdk底层C,C++源码。

5.1 下载完整jdk源码

下载地址:http://jdk.java.net/java-se-ri/7
查看步骤
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
源码展示:

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_javaArray()) {
    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_javaArray()) {
    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

校验当前类是否实现克隆接口的代码:

// 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());
}

注释翻译:

数组类型默认可以直接克隆,而其它对象实现clone需要先实现Cloneable接口,否则抛出:
CloneNotSupportedException异常

结论,对象使用clone方法必须实现Cloneable接口。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值