精华内容
下载资源
问答
  • 主要介绍了Java中对象的深复制(深克隆)和浅复制(浅克隆) ,需要的朋友可以参考下
  • 惰性深克隆,高性能的js克隆 传统的深拷贝做法 JSON.parse(JSON.stringify(data)) 递归浅拷贝 第一种做法存在一些局限,很多情况下并不能使用,因此这里就不提了;第二种做法一般是工具库中的深拷贝函数实现方式,...
  • 本文详细介绍了Java中的浅克隆和深克隆的概念,及案例演示如何实现深克隆

      本文详细介绍了Java中的浅克隆和深克隆的概念,及案例演示如何实现深克隆!

    1 克隆概述

    Java中实现对象的克隆分两种一种是浅克隆一种是深克隆。首先java中Clone方法对于对象克隆的机制是:对象的基本数据类型的成员变量会被全部复制,引用类型的成员变量不会复制,只会复制该变量的引用,这样被克隆对象的引用类型的成员变量还是指向了原对象的同名引用类型的成员变量的堆内存空间,对其中一个对象的引用类型成员变量的修改会影响到另外一个被克隆对象或者源对象的引用类型的成员变量。

    浅克隆:最普遍的克隆,即对象实现cloneable接口和重写clone方法,然后调用一次内部不做改写的clone方法克隆出一个对象,如果源对象内部存在引用类型的成员变量,那么就说该克隆是浅克隆,即对于引用类型属性,只克隆引用,两个对象的引用指向同一块内存地址,即同一个对象。

    深克隆:基本数据类型变量和引用类型变量指向的对象都会被复制,即针对引用类型的成员变量真正的复制一份,重新开辟空间保存,这样两个引用类型属性互不影响。

    2 深克隆实现

    实现深克隆的方法有三种:

    1. 重写clone方法,clone中嵌套clone

      1. 这种方法的原理其实就是在需要克隆的对象以及该对象的引用类型的变量的类中全部实现cloneable接口,否则抛出CloneNotSupportedException将引用类型的变量也克隆一份。实际的操作上就是改写源对象的clone方法,在其内部嵌套克隆方法。
        首先让源对象调用克隆方法获得克隆的对象,然后获得被克隆对象的引用类型的成员变量对象,对该成员变量对象调用克隆方法,此时成员变量对象也被克隆了一份,最后将该克隆的成员变量对象,设置为克隆对象的新的成员变量,再返回该被克隆的对象,即可实现深克隆。
    2. 使用序列化流

      1. 其原理是:首先使要序列化的对象和该对象的引用类型成员变量对象的类都实现Serializable接口,将对象序列化到输出流中,然后再反序列化为对象就完成了完全的复制操作了,反序列化对象类似于运行新对象的构造方法。一般序列化到外部文件,此时只需要克隆对象,并不需要外部文件,因此我们的序列化和反序列化也应该在内存中进行最好,因此还使用到在内存操作数据的流ByteArrayOutputStream和ByteArrayInputStream,他们的输出和读取都默认是在内存的数组中操作。
      2. 首先创建一个ByteArrayOutputStream内存数组输出流,创建一个ObjectOutputStream序列化流,并传入内存数组输出流,使用序列化流的writeobject方法将要序列化的对象写入内部数组中,然后创建一个ByteArrayInputStream内存数组读取流,传入一个读取数据的数组,这个数组通过内存数组输出流的toByteArray方法获得,这个数组里面的数据其实就是已经被序列化成二进制数据的对象。最后创建一个ObjectInputStream反序列化流,并传入内存数组读取流,使用反序列化流的readobject方法将数组中的对象的信息,反序列化出来。反序列化出的对象就是一个新的对象,完成了深克隆。当然还可以固定要被序列化对象的版本号,定义一个private static final long serialVersionUID,但需要注意静态的成员和transient关键字修饰的成员不能被序列化
    3. 使用开源工具类

      1. 比如Json工具类(先转换为Json字符串,然后再转换为对象)、Spring的BeanUtils(Spring项目中使用比较方便)、Cglib的BeanCopier(速度最快)、Apache的BeanUtils(该工具类比较慢,谨慎使用!)

    3 案例

    这里为了方便,没有使用get、set方法。

    3.1 测试普通clone方法–浅克隆

    public class Teacher implements Cloneable {
        private String name;
        private int age;
        private Student stu;
    
        public static void main(String[] args) throws CloneNotSupportedException {
            Student stu = new Student("李四", 24);
            Teacher tea = new Teacher("张三", 30, stu);
            //使用未做改变的clone方法
            Teacher teaClone = (Teacher) tea.clone();
    
            /*clone之后改变原对象的数据*/
            //改变stu的数据
            stu.name="李四改";
            //改变tea的数据
            tea.name="张三改";
    
            //结果被克隆的数据的内部类的stu数据也受到了影响,说明未重写的clone方法,实现的只是浅克隆,tea的对象类型属性stu还是指同一个对象
            System.out.println(teaClone);
            System.out.println(tea);
        }
    
    
        public Teacher(String name, int age, Student stu) {
            super();
            this.name = name;
            this.age = age;
            this.stu = stu;
        }
    
        public static class Student implements Cloneable {
            private String name;
            private int age;
    
            public Student(String name, int age) {
                super();
                this.name = name;
                this.age = age;
            }
    
    
            @Override
            public String toString() {
                return "Student{" +
                        "name='" + name + '\'' +
                        ", age=" + age +
                        '}';
            }
        }
    
        @Override
        public String toString() {
            return "Teacher{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", stu=" + stu +
                    '}';
        }
    }
    

    3.2 使用重写后的clone方法–深克隆

    先让外部类和内部类都重写clone方法,然后改写clone方法:

    public class Teacher implements Cloneable {
        private String name;
        private int age;
        private Student stu;
    
    
        public static void main(String[] args) throws CloneNotSupportedException {
            Student stu = new Student("李四", 24);
            Teacher tea = new Teacher("张三", 30, stu);
            //使用重写的的clone方法
            Teacher teaClone = (Teacher) tea.clone();
    
            /*clone之后改变原对象的数据*/
            //改变stu的数据
            stu.name="李四改";
            //改变tea的数据
            tea.name="张三改";
    
            //结果被克隆的数据的内部类的stu数据没受到了影响,说明重写的clone方法,实现的是深克隆,tea的对象类型属性stu是指不同对象
            System.out.println(teaClone);
            System.out.println(tea);
        }
    
        @Override
        protected Object clone() throws CloneNotSupportedException {
            //改写clone方法
            Teacher tea = (Teacher) super.clone();
            //获取属性对象,再clone一次,让后设置到被克隆的对象中,返回
            tea.stu = ((Student) tea.stu.clone());
            return tea;
        }
    
    
        public Teacher(String name, int age, Student stu) {
            super();
            this.name = name;
            this.age = age;
            this.stu = stu;
        }
    
        public static class Student implements Cloneable {
            private String name;
            private int age;
    
            public Student(String name, int age) {
                super();
                this.name = name;
                this.age = age;
            }
    
    
            @Override
            public String toString() {
                return "Student{" +
                        "name='" + name + '\'' +
                        ", age=" + age +
                        '}';
            }
    
            @Override
            protected Object clone() throws CloneNotSupportedException {
                return super.clone();
            }
        }
    
        @Override
        public String toString() {
            return "Teacher{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", stu=" + stu +
                    '}';
        }
    }
    

    这种方法对于不能重写的类,比如数组不适用!

    3.3 使用序列化流–深克隆

    使得两个类实现Serialzable接口,clone方法可以不要了,使用序列化流。

    public class Teacher implements Serializable {
        private String name;
        private int age;
        private Student stu;
    
    
        public static void main(String[] args) throws IOException, ClassNotFoundException {
            Student stu = new Student("李四", 24);
            Teacher tea = new Teacher("张三", 30, stu);
            //内存数组输出流
            ByteArrayOutputStream bao = new ByteArrayOutputStream();
            //序列化流
            ObjectOutputStream oos = new ObjectOutputStream(bao);
            //将数据tea写入序列化流中,随后会被传递到内存数组输出流中,将对象序列化为byte[]类型的数据
            oos.writeObject(tea);
            //从内存数组输出流中获取到tea的byte[]类型的数据,传入内存数组输入流
            ByteArrayInputStream bai = new ByteArrayInputStream(bao.toByteArray());
            //将内存数组输入流传给反序列化流,这样也实现了byte[]类型的数据的传递
            ObjectInputStream ois = new ObjectInputStream(bai);
            //使用readObject,从反序列化流中读取数据,将byte[]类型的数据反序列化成Teacher对象
            Teacher teaClone = (Teacher) ois.readObject();
            //改变stu的数据
            stu.name = "李四改";
            //改变tea的数据
            tea.name = "张三该";
            //结果被克隆的数据的内部类的stu数据没有受到了影响,说明重写后的clone方法,实现了深克隆
            System.out.println(teaClone);
            System.out.println(tea);
        }
    
    
        public Teacher(String name, int age, Student stu) {
            super();
            this.name = name;
            this.age = age;
            this.stu = stu;
        }
    
        public static class Student implements Serializable {
            private String name;
            private int age;
    
            public Student(String name, int age) {
                super();
                this.name = name;
                this.age = age;
            }
    
            @Override
            public String toString() {
                return "Student{" +
                        "name='" + name + '\'' +
                        ", age=" + age +
                        '}';
            }
    
        }
    
        @Override
        public String toString() {
            return "Teacher{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", stu=" + stu +
                    '}';
        }
    }
    

    3.4 使用开源工具

    这里只介绍Json工具:Gson的使用。

     //maven依赖
     <!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
    <dependency>
        <groupId>com.google.code.gson</groupId>
        <artifactId>gson</artifactId>
        <version>2.8.5</version>
    </dependency>
    

    也可以使用jar包形式,引入Gson工具:Gson jar包下载

    public class Teacher {
        private String name;
        private int age;
        private Student stu;
    
        public static void main(String[] args) {
            Student stu = new Student("李四", 24);
            Teacher tea = new Teacher("张三", 30, stu);
            /*使用Gson工具*/
            Gson gson = new Gson();
            //将对象序列化为json字符串
            String teaStr = gson.toJson(tea);
            //然后将字符串反序列化为对象
            Teacher GsonTea = gson.fromJson(teaStr, Teacher.class);
    
            /*clone之后改变原对象的数据*/
            //改变stu的数据
            stu.name = "李四改";
            //改变tea的数据
            tea.name = "张三改";
    
            /*结果被克隆的数据的内部类的stu数据没受到了影响,说明使用JSON工具,实现的是深克隆,tea的对象类型属性stu不是指向同一个对象*/
            System.out.println(GsonTea);
            System.out.println(tea);
        }
    
        public Teacher(String name, int age, Student stu) {
            super();
            this.name = name;
            this.age = age;
            this.stu = stu;
        }
    
        public static class Student {
            private String name;
            private int age;
    
            public Student(String name, int age) {
                super();
                this.name = name;
                this.age = age;
            }
    
    
            @Override
            public String toString() {
                return "Student{" +
                        "name='" + name + '\'' +
                        ", age=" + age +
                        '}';
            }
        }
    
        @Override
        public String toString() {
            return "Teacher{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", stu=" + stu +
                    '}';
        }
    }
    

    从结果来看,我们并没有实现Cloneable和Serializable接口,但是对结果并没有影响。

    展开全文
  • 关于拷贝和浅拷贝的概念和区别在这里就不再赘述了, 而常规的JSON.parse(JSON.stringfy(data)方式存在很多缺陷,例如无法处理undefined、function、特殊...(定义源数据为target,克隆后的数据为result) 1、数据类型

    关于深拷贝和浅拷贝的概念和区别在这里就不再赘述了,
    而常规的JSON.parse(JSON.stringfy(data)方式存在很多缺陷,例如无法处理undefined、function、特殊引用类型、循环引用等。

    最近尝试手写一下深拷贝的实现,分享一下思路和代码。(完整代码见文章末尾)

    一、整体思路

    深拷贝要考虑的点还是挺复杂的,数据类型太多需要一一处理,具体我是怎么一步步手写以及修改填坑的过程就不多说了,就大概说一下我的代码流程吧。
    (定义源数据为target,克隆后的数据为result)

    1、数据类型划分处理

    • 数据类型的判断就通过这个方法即可:

      const type = Object.prototype.toString.call(target).match(/\s(\w+)\]/)[1]
      

      返回数据target的类型,首字母大写,之所以返回首字母大写的格式也是为了后面通过new构造函数的方式生成拷贝实例。

    • 先区分基本数据类型和复杂数据类型,基本数据类型直接返回结果:

      if (typeof target !== 'object' || target === null) {
        // 基本数据类型
        result = target
      }
      
    • 针对复杂数据类型再划分成两类,一类是需要递归拷贝的类型:

      if (['Array', 'Set', 'Map', 'Object', 'Arguments'].includes(type)) {}
      
    • 另一类是不需要递归的类型,也就是其他所有类型,逐个做处理赋值。

    2、递归处理

    针对'Array', 'Set', 'Map', 'Object', 'Arguments'这五中类型需要做递归深度遍历,写递归的核心就是做好变量传参和递归出口,将已处理的结果作为参数递归传入,然后定义递归的出口防止死循环。
    只是不同的类型遍历的方式稍有不同:

      // Array
      target.forEach(v => {
        result.push(clone(v))
      })
    
      // Set
      target.forEach(v => {
        result.add(clone(v))
      })
    
      // Map
      target.forEach((v, k) => {
        result.set(k, clone(v))
      })
    
      // Object Arguments
      Object.keys(target).forEach(k => {
        result[k] = clone(target[k])
      })
    

    不要使用for…in遍历,因为它会遍历对象的所有可枚举属性,包括原型链上的,原型链上的属性不应该拷贝到新对象自身属性上。

    二、特殊处理

    1、拷贝结果的初始化

    意思就是拷贝赋值时是通过什么方式,比如拷贝一个对象,推荐不使用result = {}的字面量方式,而是采用new构造函数的方式来初始化result,即result = new Object()
    这样做的好处就是能保持原始构造函数的原型和继承信息,比如通过es6的class形式创造的Person类,通过对象字面量初始化拷贝时,访问它的构造函数是Object,而通过new初始化拷贝后访问构造函数是Person。

    const Constr = target.constructor
    result = new Constr()
    

    2、循环引用的处理

    循环引用即源数据中可能存在内部数据互相引用的问题,如不处理在递归的时候会导致死循环。

    处理循环引用可以利用map或weakMap数据结构很巧妙的实现,即每次处理需递归的类型时都把当前要递归的子数据作为key、把result结果作为value写进map里,然后在递归之前先检查一下要递归的数据是否已存在于map中,如果已存在就直接取出value返回。

    if (map.get(target)) {
      result = map.get(target)
    } else {
      const Constr = target.constructor
      result = new Constr()
      map.set(target, result)
      // 先给map赋值,在下面写常规的递归逻辑
    }
    

    3、不需要的递归的一些特殊类型

    (1)正则

    if (type === 'RegExp') {
     // RegExp
      result = new Constr(target.source, /\w*$/.exec(target))
      result.lastIndex = target.lastIndex
    }
    

    (2)函数

    函数暂时没发现有需要拷贝的场景,再加上函数柯里化的形式难以处理,所以简单点直接赋值返回。

    if (type.includes('Function')) {
      // Function AsyncFunction GeneratorFunction
      result = target
    }
    

    (3)错误对象

    if (type === 'Error') {
      // Error
      result = new Constr(target.message)
      result.stack = target.stack
    }
    

    (4)包装过的基本数据类型

    例如new Number(1)、Object(Symbol(1))等,包括未具体判断的类型都统一处理。

    else {
      try {
        // 包装过的 Number String Symbol BigInt
        const val = Constr.prototype.valueOf.call(target)
        result = Object(val)
      } catch (err) {
        // other
        console.warn(`Uncatched type:${type}`)
        console.warn(err)
      }
    }
    

    可能还有一些没有考虑到的数据类型需要做特殊处理,以后遇到了再更。

    三、完整代码

    1、函数实现

    /** 
     * @description: 深克隆方法
     * @param {any} target 源数据
     * @return {any} 克隆后的数据
     */
    function deepClone (target) {
      function clone (target, map = new WeakMap()) {
        let result
        const type = Object.prototype.toString.call(target).match(/\s(\w+)\]/)[1]
      
        if (typeof target !== 'object' || target === null) {
          // 基本数据类型
          result = target
        } else {
          if (['Array', 'Set', 'Map', 'Object', 'Arguments'].includes(type)) {
            // 可递归遍历的类型处理
            // 循环引用处理
            if (map.get(target)) {
              result = map.get(target)
            } else {
              const Constr = target.constructor
              result = new Constr()
              map.set(target, result)
              
              if (type === 'Array') {
                // Array
                target.forEach(v => {
                  result.push(clone(v, map))
                })
              } else if (type === 'Set') {
                // Set
                target.forEach(v => {
                  result.add(clone(v, map))
                })
              } else if (type === 'Map') {
                // Map
                target.forEach((v, k) => {
                  result.set(k, clone(v, map))
                })
              } else {
                // Object Arguments
                Object.keys(target).forEach(k => {
                  result[k] = clone(target[k], map)
                })
              }
            }
          } else {
            // 不可递归遍历的类型处理
            const Constr = target.constructor
            if (type === 'RegExp') {
              // RegExp
              result = new Constr(target.source, /\w*$/.exec(target))
              result.lastIndex = target.lastIndex
            } else if (type.includes('Function')) {
              // Function AsyncFunction GeneratorFunction
              result = target
            } else if (['Date'].includes(type)) {
              // Date
              result = new Constr(target)
            } else if (type === 'Error') {
              // Error
              result = new Constr(target.message)
              result.stack = target.stack
            } else if (type === 'URL') {
              // URL
              result = new Constr(target.href)
            } else if (type.includes('Array')) {
              // ArrayBuffer TypeArray BigArray ...
              result = target.slice()
            } else if (type === 'DataView') {
              // DataView
              result = new Constr(target.buffer.slice(0), target.byteOffset, target.byteLength)
            } else {
              try {
                // 包装过的 Number String Symbol BigInt
                const val = Constr.prototype.valueOf.call(target)
                result = Object(val)
              } catch (err) {
                // other
                console.warn(`Uncatched type:${type}`)
                console.warn(err)
              }
            }
          }
        }
        return result
      }
    
      const res = clone(target)
      return res
    }
    

    2、测试代码

    /**
     * 测试
     */
    const target = {
      a: {
        b1: 2,
        b2: 5,
      },
      b: [1 , 2, { c1: 3, c2: 4 }],
      c: 1,
      d: Symbol(123),
      e: undefined,
      f: null,
      g: () => {},
      h: new Date(),
      i: /123/gi,
      j: Object(Symbol(45)),
      k: new Error('wrong'),
      l: new URL('https://www.baidu.com:80/#/index'),
      m: new Number(12),
      n: new String('23'),
      o: new Boolean('23'),
      p: BigInt('23'),
      q: Object(BigInt('23')),
      r: new ArrayBuffer(10),
      s: new Int8Array(10),
    }
    target.z = { ref: target }
    
    const result = deepClone(target)
    console.log(result)
    

    参考链接:https://juejin.cn/post/6844903929705136141

    展开全文
  • 深克隆和浅克隆有什么区别?它的实现方式有哪些? 浅克隆(Shadow Clone)是把原型对象中成员变量为值类型的属性都复制给克隆对象,把原型对象中成员变量为引用类型的引用地址也复制给克隆对象,也就是原型对象中...

    深克隆和浅克隆有什么区别?它的实现方式有哪些?

    浅克隆(Shadow Clone)是把原型对象中成员变量为值类型的属性都复制给克隆对象,把原型对象中成员变量为引用类型的引用地址也复制给克隆对象,也就是原型对象中如果有成员变量为引用对象,则此引用对象的地址是共享给原型对象和克隆对象的

    简单来说就是浅克隆只会复制原型对象,但不会复制它所引用的对象,如下图所示

    深克隆(Deep Clone)是将原型对象中的所有类型,无论是值类型还是引用类型,都复制一份给克隆对象,也就是说深克隆会把原型对象和原型对象所引用的对象,都复制一份给克隆对象,如下图所示

    在 Java 语言中要实现克隆则需要实现 Cloneable 接口,并重写 Object 类中的 clone() 方法,实现代码如下

    public class CloneExample {
        public static void main(String[] args) throws CloneNotSupportedException {
            // 创建被赋值对象
            People p1 = new People();
            p1.setId(1);
            p1.setName("Java");
            // 克隆 p1 对象
            People p2 = (People) p1.clone();
            // 打印名称
            System.out.println("p2:" + p2.getName());
        }
        static class People implements Cloneable {
            // 属性
            private Integer id;
            private String name;
            /**
             * 重写 clone 方法
             * @throws CloneNotSupportedException
             */
            @Override
            protected Object clone() throws CloneNotSupportedException {
                return super.clone();
            }
            public Integer getId() {
                return id;
            }
            public void setId(Integer id) {
                this.id = id;
            }
            public String getName() {
                return name;
            }
            public void setName(String name) {
                this.name = name;
            }
        }
    }

    以上程序执行的结果为:p2:Java

     

    clone() 源码分析

    要想真正的了解克隆,首先要从它的源码入手,代码如下

    /**
     * Creates and returns a copy of this object.  The precise meaning
     * of "copy" may depend on the class of the object. The general
     * intent is that, for any object {@code x}, the expression:
     * <blockquote>
     * <pre>
     * x.clone() != x</pre></blockquote>
     * will be true, and that the expression:
     * <blockquote>
     * <pre>
     * x.clone().getClass() == x.getClass()</pre></blockquote>
     * will be {@code true}, but these are not absolute requirements.
     * While it is typically the case that:
     * <blockquote>
     * <pre>
     * x.clone().equals(x)</pre></blockquote>
     * will be {@code true}, this is not an absolute requirement.
     * <p>
     * By convention, the returned object should be obtained by calling
     * {@code super.clone}.  If a class and all of its superclasses (except
     * {@code Object}) obey this convention, it will be the case that
     * {@code x.clone().getClass() == x.getClass()}.
     * <p>
     * By convention, the object returned by this method should be independent
     * of this object (which is being cloned).  To achieve this independence,
     * it may be necessary to modify one or more fields of the object returned
     * by {@code super.clone} before returning it.  Typically, this means
     * copying any mutable objects that comprise the internal "deep structure"
     * of the object being cloned and replacing the references to these
     * objects with references to the copies.  If a class contains only
     * primitive fields or references to immutable objects, then it is usually
     * the case that no fields in the object returned by {@code super.clone}
     * need to be modified.
     * <p>
     * ......
     */
    protected native Object clone() throws CloneNotSupportedException;

    从源码的注释信息中我们可以看出,Object 对 clone() 方法的约定有三条

    • 对于所有对象来说,x.clone() !=x 应当返回 true,因为克隆对象与原对象不是同一个对象

    • 对于所有对象来说,x.clone().getClass() == x.getClass() 应当返回 true,因为克隆对象与原对象的类型是一样的

    • 对于所有对象来说,x.clone().equals(x) 应当返回 true,因为使用 equals 比较时,它们的值都是相同的

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

     

    Arrays.copyOf()分析

    如果是数组类型,我们可以直接使用 Arrays.copyOf() 来实现克隆,实现代码如下

    People[] o1 = {new People(1, "Java")};
    People[] o2 = Arrays.copyOf(o1, o1.length);
    // 修改原型对象的第一个元素的值
    o1[0].setName("Jdk");
    System.out.println("o1:" + o1[0].getName());
    System.out.println("o2:" + o2[0].getName());

    以上程序的执行结果为

    o1:Jdk
    o2:Jdk

    从结果可以看出,我们在修改克隆对象的第一个元素之后,原型对象的第一个元素也跟着被修改了,这说明 Arrays.copyOf() 其实是一个浅克隆

    因为数组比较特殊,数组本身就是引用类型,因此在使用 Arrays.copyOf() 其实只是把引用地址复制了一份给克隆对象,如果修改了它的引用对象,那么指向它的(引用地址)所有对象都会发生改变,因此看到的结果是,修改了克隆对象的第一个元素,原型对象也跟着被修改了

     

    深克隆实现方式有几种?

    深克隆的实现方式有很多种,大体可以分为以下几类

    • 所有对象都实现克隆方法;

    • 通过构造方法实现深克隆;

    • 使用 JDK 自带的字节流实现深克隆;

    • 使用第三方工具实现深克隆,比如 Apache Commons Lang

    • 使用 JSON 工具类实现深克隆,比如 Gson、FastJSON 等

    接下来分别来实现以上这些方式,在开始之前先定义一个公共的用户类,代码如下

    /**
     * 用户类
     */
    public class People {
        private Integer id;
        private String name;
        private Address address; // 包含 Address 引用对象
        // 忽略构造方法、set、get 方法
    }
    
    /**
     * 地址类
     */
    public class Address {
        private Integer id;
        private String city;
        // 忽略构造方法、set、get 方法
    }

    1.所有对象都实现克隆

    这种方式我们需要修改 People 和 Address 类,让它们都实现 Cloneable 的接口,让所有的引用对象都实现克隆,从而实现 People 类的深克隆,代码如下

    public class CloneExample {
        public static void main(String[] args) throws CloneNotSupportedException {
              // 创建被赋值对象
              Address address = new Address(110, "北京");
              People p1 = new People(1, "Java", address);
              // 克隆 p1 对象
              People p2 = p1.clone();
              // 修改原型对象
              p1.getAddress().setCity("西安");
              // 输出 p1 和 p2 地址信息
              System.out.println("p1:" + p1.getAddress().getCity() +
                      " p2:" + p2.getAddress().getCity());
        }
        /**
         * 用户类
         */
        static class People implements Cloneable {
            private Integer id;
            private String name;
            private Address address;
            /**
             * 重写 clone 方法
             * @throws CloneNotSupportedException
             */
            @Override
            protected People clone() throws CloneNotSupportedException {
                People people = (People) super.clone();
                people.setAddress(this.address.clone()); // 引用类型克隆赋值
                return people;
            }
            // 忽略构造方法、set、get 方法
        }
        /**
         * 地址类
         */
        static class Address implements Cloneable {
            private Integer id;
            private String city;
            /**
             * 重写 clone 方法
             * @throws CloneNotSupportedException
             */
            @Override
            protected Address clone() throws CloneNotSupportedException {
                return (Address) super.clone();
            }
            // 忽略构造方法、set、get 方法
        }
    }

    以上程序的执行结果为

    p1:西安 p2:北京

    从结果可以看出,当我们修改了原型对象的引用属性之后,并没有影响克隆对象,这说明此对象已经实现了深克隆

    2.通过构造方法实现深克隆

    《Effective Java》中推荐使用构造器(Copy Constructor)来实现深克隆,如果构造器的参数为基本数据类型或字符串类型则直接赋值,如果是对象类型,则需要重新 new 一个对象,实现代码如下

    public class SecondExample {
        public static void main(String[] args) throws CloneNotSupportedException {
            // 创建对象
            Address address = new Address(110, "北京");
            People p1 = new People(1, "Java", address);
    
            // 调用构造函数克隆对象
            People p2 = new People(p1.getId(), p1.getName(),
                    new Address(p1.getAddress().getId(), p1.getAddress().getCity()));
    
            // 修改原型对象
            p1.getAddress().setCity("西安");
    
            // 输出 p1 和 p2 地址信息
            System.out.println("p1:" + p1.getAddress().getCity() +
                    " p2:" + p2.getAddress().getCity());
        }
    
        /**
         * 用户类
         */
        static class People {
            private Integer id;
            private String name;
            private Address address;
            // 忽略构造方法、set、get 方法
        }
    
        /**
         * 地址类
         */
        static class Address {
            private Integer id;
            private String city;
            // 忽略构造方法、set、get 方法
        }
    }

    以上程序的执行结果为

    p1:西安 p2:北京

    从结果可以看出,当我们修改了原型对象的引用属性之后,并没有影响克隆对象,这说明此对象已经实现了深克隆

    3.通过字节流实现深克隆

    通过 JDK 自带的字节流实现深克隆的方式,是先将要原型对象写入到内存中的字节流,然后再从这个字节流中读出刚刚存储的信息,来作为一个新的对象返回,那么这个新对象和原型对象就不存在任何地址上的共享,这样就实现了深克隆,代码如下

    import java.io.*;
    
    public class ThirdExample {
        public static void main(String[] args) throws CloneNotSupportedException {
            // 创建对象
            Address address = new Address(110, "北京");
            People p1 = new People(1, "Java", address);
    
            // 通过字节流实现克隆
            People p2 = (People) StreamClone.clone(p1);
    
            // 修改原型对象
            p1.getAddress().setCity("西安");
    
            // 输出 p1 和 p2 地址信息
            System.out.println("p1:" + p1.getAddress().getCity() +
                    " p2:" + p2.getAddress().getCity());
        }
    
        /**
         * 通过字节流实现克隆
         */
        static class StreamClone {
            public static <T extends Serializable> T clone(People obj) {
                T cloneObj = null;
                try {
                    // 写入字节流
                    ByteArrayOutputStream bo = new ByteArrayOutputStream();
                    ObjectOutputStream oos = new ObjectOutputStream(bo);
                    oos.writeObject(obj);
                    oos.close();
                    // 分配内存,写入原始对象,生成新对象
                    ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());//获取上面的输出字节流
                    ObjectInputStream oi = new ObjectInputStream(bi);
                    // 返回生成的新对象
                    cloneObj = (T) oi.readObject();
                    oi.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return cloneObj;
            }
        }
    
        /**
         * 用户类
         */
        static class People implements Serializable {
            private Integer id;
            private String name;
            private Address address;
            // 忽略构造方法、set、get 方法
        }
    
        /**
         * 地址类
         */
        static class Address implements Serializable {
            private Integer id;
            private String city;
            // 忽略构造方法、set、get 方法
        }
    }

    以上程序的执行结果为

    p1:西安 p2:北京

    此方式需要注意的是,由于是通过字节流序列化实现的深克隆,因此每个对象必须能被序列化,必须实现 Serializable 接口,标识自己可以被序列化,否则会抛出异常 (java.io.NotSerializableException)

    4.通过第三方工具实现深克隆

    使用 Apache Commons Lang 来实现深克隆,实现代码如下

    import org.apache.commons.lang3.SerializationUtils;
    
    import java.io.Serializable;
    
    /**
     * 深克隆实现方式四:通过 apache.commons.lang 实现
     */
    public class FourthExample {
        public static void main(String[] args) throws CloneNotSupportedException {
            // 创建对象
            Address address = new Address(110, "北京");
            People p1 = new People(1, "Java", address);
    
            // 调用 apache.commons.lang 克隆对象
            People p2 = (People) SerializationUtils.clone(p1);
    
            // 修改原型对象
            p1.getAddress().setCity("西安");
    
            // 输出 p1 和 p2 地址信息
            System.out.println("p1:" + p1.getAddress().getCity() +
                    " p2:" + p2.getAddress().getCity());
        }
    
        /**
         * 用户类
         */
        static class People implements Serializable {
            private Integer id;
            private String name;
            private Address address;
            // 忽略构造方法、set、get 方法
        }
    
        /**
         * 地址类
         */
        static class Address implements Serializable {
            private Integer id;
            private String city;
            // 忽略构造方法、set、get 方法
        }
    }

    以上程序的执行结果为

    p1:西安 p2:北京

    可以看出此方法和第三种实现方式类似,都需要实现 Serializable 接口,都是通过字节流的方式实现的,只不过这种实现方式是第三方提供了现成的方法,让我们可以直接调用

    5.通过 JSON 工具类实现深克隆

    使用 Google 提供的 JSON 转化工具 Gson 来实现,其他 JSON 转化工具类也是类似的,实现代码如下

    import com.google.gson.Gson;
    
    /**
     * 深克隆实现方式五:通过 JSON 工具实现
     */
    public class FifthExample {
        public static void main(String[] args) throws CloneNotSupportedException {
            // 创建对象
            Address address = new Address(110, "北京");
            People p1 = new People(1, "Java", address);
    
            // 调用 Gson 克隆对象
            Gson gson = new Gson();
            People p2 = gson.fromJson(gson.toJson(p1), People.class);
    
            // 修改原型对象
            p1.getAddress().setCity("西安");
    
            // 输出 p1 和 p2 地址信息
            System.out.println("p1:" + p1.getAddress().getCity() +
                    " p2:" + p2.getAddress().getCity());
        }
    
        /**
         * 用户类
         */
        static class People {
            private Integer id;
            private String name;
            private Address address;
            // 忽略构造方法、set、get 方法
        }
    
        /**
         * 地址类
         */
        static class Address {
            private Integer id;
            private String city;
            // 忽略构造方法、set、get 方法
        }
    }

    以上程序的执行结果为

    p1:西安 p2:北京

    使用 JSON 工具类会先把对象转化成字符串,再从字符串转化成新的对象,因为新对象是从字符串转化而来的,因此不会和原型对象有任何的关联,这样就实现了深克隆,其他类似的 JSON 工具类实现方式也是一样的

     

    克隆设计理念猜想

    对于克隆为什么要这样设计,官方没有直接给出答案,Java 中实现克隆需要两个主要的步骤,一是 实现 Cloneable 空接口,二是重写 Object 的 clone() 方法再调用父类的克隆方法 (super.clone())那为什么要这么做?

    从源码中可以看出 Cloneable 接口诞生的比较早,JDK 1.0 就已经存在了,因此从那个时候就已经有克隆方法了,那我们怎么来标识一个类级别对象拥有克隆方法呢?克隆虽然重要,但我们不能给每个类都默认加上克隆,这显然是不合适的,那我们能使用的手段就只有这几个了

    • 在类上新增标识,此标识用于声明某个类拥有克隆的功能,像 final 关键字一样;

    • 使用 Java 中的注解;

    • 实现某个接口;

    • 继承某个类

    以上方法对比分析:先说第一个,为了一个重要但不常用的克隆功能, 单独新增一个类标识,这显然不合适;再说第二个,因为克隆功能出现的比较早,那时候还没有注解功能,因此也不能使用;第三点基本满足我们的需求,第四点和第一点比较类似,为了一个克隆功能需要牺牲一个基类,并且 Java 只能单继承,因此这个方案也不合适。采用排除法,无疑使用实现接口的方式是那时最合理的方案了,而且在 Java 语言中一个类可以实现多个接口

     

    那为什么要在 Object 中添加一个 clone() 方法呢?

    因为 clone() 方法语义的特殊性,因此最好能有 JVM 的直接支持,既然要 JVM 直接支持,就要找一个 API 来把这个方法暴露出来才行,最直接的做法就是把它放入到一个所有类的基类 Object 中,这样所有类就可以很方便地调用到了

    展开全文
  • 面试题:深克隆和浅克隆的实现方式 面试官考察点 考察目的:深克隆和浅克隆,考察的是Java基础知识的理解。 考察人群:2到5年开发经验。 背景知识详解 先了解下浅克隆和深克隆的定义: 浅克隆:被复制对象...

    面试题:深克隆和浅克隆的实现方式

    面试官考察点

    考察目的:深克隆和浅克隆,考察的是Java基础知识的理解。

    考察人群:2到5年开发经验。

    背景知识详解

    先了解下浅克隆和深克隆的定义:

    1. 浅克隆:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。

    2. 深克隆:除去那些引用其他对象的变量,被复制对象的所有变量都含有与原来的对象相同的值。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深复制把要复制的对象所引用的对象都复制了一遍。

    如何实现克隆

    我么先不管深克隆、还是浅克隆。首先,要先了解如何实现克隆,实现克隆需要满足以下三个步骤

    1. 对象的类实现Cloneable接口;

    2. 覆盖Object类的clone()方法(覆盖clone()方法,访问修饰符设为public,默认是protected,但是如果所有类都在同一个包下protected是可以访问的);

    3. 在clone()方法中调用super.clone();

    实现一个克隆

    先定义一个score类,表示分数信息。

    public class Score {
        private String category;
        private double fraction;
    
        public Score() {
        }
    
        public Score(String category, double fraction) {
            this.category = category;
            this.fraction = fraction;
        }
    
        //getter/setter省略
    
        @Override
        public String toString() {
            return "Score{" +
                    "category='" + category + '\'' +
                    ", fraction=" + fraction +
                    '}';
        }
    }

    定义一个Person,其中包含Score属性,来表示这个人的考试分数。

    需要注意,Person类是实现了Cloneable接口的,并且重写了clone()这个方法。

    public class Person implements Cloneable{
        private String name;
        private int age;
        private List<Score> score;
    
        public Person() {
        }
    
    
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }

    克隆代码测试,代码逻辑不复杂,就是初始化一个对象mic,然后基于mic使用clone方法克隆出一个对象dylan

    接着通过修改被克隆对象mic的成员属性,打印出这两个对象的状态信息。

    public class CloneMain {
    
        public static void main(String[] args) throws CloneNotSupportedException {
            Person mic=new Person();
            Score s1=new Score();
            s1.setCategory("语文");
            s1.setFraction(90);
            Score s2=new Score();
            s2.setCategory("数学");
            s2.setFraction(100);
            mic.setAge(18);
            mic.setName("Mic");
            mic.setScore(Arrays.asList(s1,s2));
            System.out.println("person对象初始化状态:"+mic);
    
            Person dylan=(Person)mic.clone(); //克隆一个对象
            System.out.println("打印克隆对象:dylan:"+dylan);
            mic.setAge(20);
            mic.getScore().get(0).setFraction(70); //修改mic语文分数为70
            System.out.println("打印mic:"+mic);
            System.out.println("打印dylan:"+dylan);
        }
    }

    执行结果如下:

    person对象初始化状态:Person{name='Mic', age=18, score=[Score{category='语文', fraction=90.0}, Score{category='数学', fraction=100.0}]}
    打印克隆对象:dylan:Person{name='Mic', age=18, score=[Score{category='语文', fraction=90.0}, Score{category='数学', fraction=100.0}]}
    打印mic:Person{name='Mic', age=20, score=[Score{category='语文', fraction=70.0}, Score{category='数学', fraction=100.0}]}
    打印dylan:Person{name='Mic', age=18, score=[Score{category='语文', fraction=70.0}, Score{category='数学', fraction=100.0}]}
    

    从结果中可以发现:

    1. 修改mic对象本身的普通属性age,发现该属性的修改只影响到mic对象本身的实例。

    2. 当修改mic对象的语文成绩时,dylan对象的语文成绩也发生了变化。

    为什么会导致这个现象?回过头看一下浅克隆的定义:

    浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址

    需要特别强调非基本类型,对于非基本类型,传递的是值,所以新的dylan对象会对该属性创建一个副本。同样,对于final修饰的属性,由于它的不可变性,在浅克隆时,也会在内存中创建副本。

    如图所示,dylan对象从mic对象克隆过来后,dylan对象的内存地址指向的是同一个。因此当mic这个对象中的属性发生变化时,dylan对象的属性也会发生变化。

    图片

    clone方法的源码分析

    经过上述案例演示可以发现,如果对象实现Cloneable并重写clone方法不进行任何操作时,调用clone是进行的浅克隆,那clone方法是如何实现的呢?它默认情况下做了什么?

    clone方法是Object中默认提供的,它的源码定义如下

    protected native Object clone() throws CloneNotSupportedException;

    从源码中我们可以看到几个关键点:

    1.clone方法是native方法,native方法的效率远高于非native方法,因此如果我们需要拷贝一个对象,建议使用clone,而不是new。

    2.该方法被protected修饰。这就意味着想要使用,则必须重写该方法,并且设置成public。

    3.返回值是一个Object对象,因此通过clone方法克隆一个对象,需要强制转换。

    4.如果在没有实现Cloneable接口的实例上调用Object的clone()方法,则会导致抛出CloneNotSupporteddException;

    再来看一下Object.clone方法上的注释,注释的内容有点长。

        /**
         * Creates and returns a copy of this object.  The precise meaning
         * of "copy" may depend on the class of the object. The general
         * intent is that, for any object {@code x}, the expression:
         * <blockquote>
         * <pre>
         * x.clone() != x</pre></blockquote>
         * will be true, and that the expression:
         * <blockquote>
         * <pre>
         * x.clone().getClass() == x.getClass()</pre></blockquote>
         * will be {@code true}, but these are not absolute requirements.
         * While it is typically the case that:
         * <blockquote>
         * <pre>
         * x.clone().equals(x)</pre></blockquote>
         * will be {@code true}, this is not an absolute requirement.
         * <p>
         * By convention, the returned object should be obtained by calling
         * {@code super.clone}.  If a class and all of its superclasses (except
         * {@code Object}) obey this convention, it will be the case that
         * {@code x.clone().getClass() == x.getClass()}.
         * <p>
         * By convention, the object returned by this method should be independent
         * of this object (which is being cloned).  To achieve this independence,
         * it may be necessary to modify one or more fields of the object returned
         * by {@code super.clone} before returning it.  Typically, this means
         * copying any mutable objects that comprise the internal "deep structure"
         * of the object being cloned and replacing the references to these
         * objects with references to the copies.  If a class contains only
         * primitive fields or references to immutable objects, then it is usually
         * the case that no fields in the object returned by {@code super.clone}
         * need to be modified.
         * <p>
         * The method {@code clone} for class {@code Object} performs a
         * specific cloning operation. First, if the class of this object does
         * not implement the interface {@code Cloneable}, then a
         * {@code CloneNotSupportedException} is thrown. Note that all arrays
         * are considered to implement the interface {@code Cloneable} and that
         * the return type of the {@code clone} method of an array type {@code T[]}
         * is {@code T[]} where T is any reference or primitive type.
         * Otherwise, this method creates a new instance of the class of this
         * object and initializes all its fields with exactly the contents of
         * the corresponding fields of this object, as if by assignment; the
         * contents of the fields are not themselves cloned. Thus, this method
         * performs a "shallow copy" of this object, not a "deep copy" operation.
         * <p>
         * The class {@code Object} does not itself implement the interface
         * {@code Cloneable}, so calling the {@code clone} method on an object
         * whose class is {@code Object} will result in throwing an
         * exception at run time.
         *
         * @return     a clone of this instance.
         * @throws  CloneNotSupportedException  if the object's class does not
         *               support the {@code Cloneable} interface. Subclasses
         *               that override the {@code clone} method can also
         *               throw this exception to indicate that an instance cannot
         *               be cloned.
         * @see java.lang.Cloneable
         */
        protected native Object clone() throws CloneNotSupportedException;

    上述方法中的注释描述中,对于clone方法关于复制描述,提出了三个规则,也就是说,”复制“的确切定义取决于对象本身,它可以满足以下任意一条规则:

    • 对于所有对象,x.clone () !=x 应当返回 true,因为克隆对象与原对象不是同一个对象。

    • 对于所有对象,x.clone ().getClass () == x.getClass () 应当返回 true,因为克隆对象与原对象的类型是一样的。

    • 对于所有对象,x.clone ().equals (x) 应当返回 true,因为使用 equals 比较时,它们的值都是相同的。

    因此,从clone方法的源码中可以得到一个结论,clone方法是深克隆还是浅克隆,取决于实现克隆方法对象的本身实现。

    深克隆

    理解了浅克隆,我们就不难猜测到,所谓深克隆的本质,应该是如下图所示。

    图片

    dylan这个对象实例从mic对象克隆之后,应该要分配一块新的内存地址,从而实现在内存地址上的隔离。

    深拷贝实现的是对所有可变(没有被final修饰的引用变量)引用类型的成员变量都开辟独立的内存空间,使得拷贝对象和被拷贝对象之间彼此独立,因此一般深拷贝对于浅拷贝来说是比较耗费时间和内存开销的。

    深克隆实现

    修改Person类中的clone()方法,代码如下。

    @Override
    protected Object clone() throws CloneNotSupportedException {
      Person p=(Person)super.clone(); //可以直接使用clone方法克隆,因为String类型中的属性是final修饰,而int是基本类型,都会创建副本
      if(this.score!=null&&this.score.size()>0){ //如果score不为空时,才做深度克隆
        //由于`score`是引用类型,所以需要重新分配内存空间
        List<Score> ls=new ArrayList<>();
        this.score.stream().forEach(score->{
          Score s=new Score();
          s.setFraction(score.getFraction());
          s.setCategory(score.getCategory());
          ls.add(s);
        });
        p.setScore(ls);
      }
      return p;
    }

    再次执行,运行结果如下

    person对象初始化状态:Person{name='Mic', age=18, score=[Score{category='语文', fraction=90.0}, Score{category='数学', fraction=100.0}]}
    打印克隆对象:dylan:Person{name='Mic', age=18, score=[Score{category='语文', fraction=90.0}, Score{category='数学', fraction=100.0}]}
    打印mic:Person{name='Mic', age=20, score=[Score{category='语文', fraction=70.0}, Score{category='数学', fraction=100.0}]}
    打印dylan:Person{name='Mic', age=18, score=[Score{category='语文', fraction=90.0}, Score{category='数学', fraction=100.0}]}
    
    Process finished with exit code 0
    

    从结果可以看到,这两个对象之间并没有相互影响,因为我们在clone方法中,对于Person这个类的成员属性Score使用new创建了一个新的对象,这样就使得两个对象分别指向不同的内存地址。

    创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。总之深浅克隆都会在堆中新分配一块区域,区别在于对象属性引用的对象是否需要进行克隆(递归性的)

    深克隆的其他实现方式

    深克隆的实现方式很多,总的来说有以下几种:

    • 所有对象都实现克隆方法。

    • 通过构造方法实现深克隆。

    • 使用 JDK 自带的字节流。

    • 使用第三方工具实现,比如:Apache Commons Lang。

    • 使用 JSON 工具类实现,比如:Gson,FastJSON 等等。

    其实,深克隆既然是在内存中创建新的对象,那么任何能够创建新实例对象的方式都能完成这个动作,因此不局限于这些方法。

    所有对象都实现克隆方法

    由于浅克隆本质上是因为引用对象指向同一块内存地址,如果每个对象都实现克隆方法,意味着每个对象的最基本单位是基本数据类型或者封装类型,而这些类型在克隆时会创建副本,从而避免了指向同一块内存地址的问题。

    修改代码如下。

    public class Person implements Cloneable {
        private String name;
        private int age;
        private List<Score> score;
    
        public Person() {
        }
    
        @Override
        protected Object clone() throws CloneNotSupportedException {
            Person p=(Person)super.clone();
            if(this.score!=null&&this.score.size()>0){ //如果score不为空时,才做深度克隆
                //由于`score`是引用类型,所以需要重新分配内存空间
                List<Score> ls=new ArrayList<>();
                this.score.stream().forEach(score->{
                    try {
                        ls.add((Score)score.clone()); //这里用了克隆方法
                    } catch (CloneNotSupportedException e) {
                        e.printStackTrace();
                    }
                });
                p.setScore(ls);
            }
            return p;
        }
    }

    修改Score对象

    public class Score implements Cloneable {
        private String category;
        private double fraction;
    
        public Score() {
        }
    
        public Score(String category, double fraction) {
            this.category = category;
            this.fraction = fraction;
        }
    
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
    Person dylan=(Person)mic.clone(); //克隆一个对象

    运行结果如下

    person对象初始化状态:Person{name='Mic', age=18, score=[Score{category='语文', fraction=90.0}, Score{category='数学', fraction=100.0}]}
    打印克隆对象:dylan:Person{name='Mic', age=18, score=[Score{category='语文', fraction=90.0}, Score{category='数学', fraction=100.0}]}
    打印mic:Person{name='Mic', age=20, score=[Score{category='语文', fraction=70.0}, Score{category='数学', fraction=100.0}]}
    打印dylan:Person{name='Mic', age=18, score=[Score{category='语文', fraction=90.0}, Score{category='数学', fraction=100.0}]}

    通过构造方法实现深克隆。

    构造方法实现深克隆,其实是我们经常使用的方法,就是使用new关键字来实例化一个新的对象,然后通过构造参数传值来实现数据拷贝。

    public class Person implements Cloneable {
        private String name;
        private int age;
        private List<Score> score;
    
        public Person() {
        }
    
        public Person(String name, int age, List<Score> score) {
            this.name = name;
            this.age = age;
            this.score = score;
        }
    }

    克隆的时候,我们这么做

     Person dylan=new Person(mic.getName(),mic.getAge(),mic.getScore()); //克隆一个对象

    基于ObjectStream实现深克隆

    在Java中,对象流也可以实现深克隆,大家可能对对象流这个名词有点陌生,它的定义如下:

    • ObjectOutputStream, 对象输出流,把一个对象转换为二进制格式数据

    • ObjectInputStream,对象输入流,把一个二进制数据转换为对象。

    这两个对象,在Java中通常用来实现对象的序列化。

    创建一个工具类,使用ObjectStream来实现对象的克隆,代码实现逻辑不难:

    1. 使用ObjectOutputStream,把一个对象转换为数据流存储到对象ByteArrayOutputStream中。

    2. 再从内存中读取该数据流,使用ObjectInputStream,把该数据流转换为目标对象。

    public class ObjectStreamClone {
    
        public static <T extends Serializable> T clone(T t){
            T cloneObj = null;
            try {
                // bo,存储对象输出流,写入到内存
                ByteArrayOutputStream bo = new ByteArrayOutputStream();
                //对象输出流,把对象转换为数据流
                ObjectOutputStream oos = new ObjectOutputStream(bo);
                oos.writeObject(t);
                oos.close();
                // 分配内存,写入原始对象,生成新对象
                ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
                ObjectInputStream oi = new ObjectInputStream(bi);
                // 返回生成的新对象
                cloneObj = (T) oi.readObject();
                oi.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return cloneObj;
        }
    
    }

    Person对象和Score对象均需要实现Serializable接口,

    public class Person implements Serializable {
    }
    public class Score implements Serializable {}

    修改测试类的克隆方法.

     Person dylan=(Person)ObjectStreamClone.clone(mic); //克隆一个对象

    运行结果如下:

    person对象初始化状态:Person{name='Mic', age=18, score=[Score{category='语文', fraction=90.0}, Score{category='数学', fraction=100.0}]}
    打印克隆对象:dylan:Person{name='Mic', age=18, score=[Score{category='语文', fraction=90.0}, Score{category='数学', fraction=100.0}]}
    打印mic:Person{name='Mic', age=20, score=[Score{category='语文', fraction=70.0}, Score{category='数学', fraction=100.0}]}
    打印dylan:Person{name='Mic', age=18, score=[Score{category='语文', fraction=90.0}, Score{category='数学', fraction=100.0}]}

    通过对象流能够实现深克隆,其根本原因还是在于对象的序列化之后,已经脱离了JVM内存对象的范畴,毕竟一个对象序列化之后,是可以通过文件、或者网络跨JVM传输的,因此对象在反序列化时,必然需要基于该数据流重新反射生成新的对象。

    问题解答

    问题:深克隆和浅克隆的实现方式

    回答:

    1. 浅克隆是指被复制对象中属于引用类型的成员变量的内存地址和被克隆对象的内存地址相同,也就是克隆对象只实现了对被克隆对象基本类型的副本克隆。

      浅克隆的实现方式,可以实现Cloneable接口,并重写clone方法,即可完成浅克隆。

      浅克隆的好处是,避免了引用对象的内存分配和回收,提高对象的复制效率。

    2. 深克隆是指实现对于基本类型和引用类型的完整克隆,克隆对象和被克隆对象中的引用对象的内存地址完全隔离。

      深克隆的实现方式:

      • 基于Cloneable接口重写clone方法,但是我们需要在clone方法中,针对应用类型的成员变量,使用new关键字分配独立的内存空间。

      • 基于Java中对象流的方式实现

      • 基于构造方法实现深度克隆

      • 被克隆的对象中所有涉及到引用类型变量的对象,全部实现克隆方法,并且在被克隆对象的clone方法中,需要调用所有成员对象的clone方法实现对象克隆

    问题总结

    深克隆的本质,其实是保证被克隆对象中所有应用对象以及引用所嵌套的引用对象,全部分配一块独立的内存空间,避免克隆对象和被克隆对象指向同一块内存地址,造成数据错误等问题。

    所以,深克隆,表示对象拷贝的深度,因为在Java中对象的嵌套是非常常见的。理解了这个知识点,才能避免在开发过程中遇到一些奇奇怪怪的问题。

    往期好文推荐:

    看了这篇文章后,面试官再也不敢问你非结构化存储的原理了

    吹爆阿里P8的SQL优化笔记,由于太全直接被GitHub下架

    展开全文
  • JAVA浅克隆与深克隆

    2019-04-07 01:03:55
    NULL 博文链接:https://gegeyi.iteye.com/blog/1562016
  • 我们本课时的面试题是,什么是浅克隆和深克隆?如何实现克隆? 典型回答 浅克隆(Shadow Clone)是把原型对象中成员变量为值类型的属性都复制给克隆对象,把原型对象中成员变量为引用类型的引用地址也复制给克隆...
  • 必须覆盖Object的clone方法clone分为浅克隆和深克隆。Object对象的clone方法是浅克隆,即待克隆的对象若存在对象实例域,则仅仅clone其引用。深克隆即clone对象本体。# 实现深克隆的方式1. 先使用待克隆对象的clone...
  • 浅克隆与深克隆的区别及特点

    千次阅读 2020-06-29 10:29:20
    浅克隆与深克隆的区别 1、浅克隆:对当前对象进行克隆,并克隆该对象所包含的8种基本数据类型和String类型属性(拷贝一份该对象并重新分配内存,即产生了新的对象);但如果被克隆的对象中包含除8中数据类型和String...
  • 深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址; 异同点: 深浅克隆都会在堆中新分配一块区域; 区别在于对象属性引用的对象是否需要进行克隆(递归性的)。 示例 pos:...
  • “老沉,什么是深克隆,什么是浅克隆?” “哈,迷茫了? 这深深浅浅的体验是不是把你搞晕了?” “嗯,这都是啥程序员黑话吗?” “这是专业术语!因为有“引用类型”这个概念,所以才引申出来深克隆和浅克隆的名词...
  • 克隆克隆是指:只克隆数组/对象的第一层级内容(开辟新的堆内存),而第二层级及以上层级的内容则直接引用(使用原来第二层级及以上层级的堆内存)。如果对克隆后对象的二级或以上层级进行修改,那么克隆前...
  • 目录为什么要使用克隆(clone)? 为什么要使用克隆(clone)? 当拷贝一个对象时,原始变量与拷贝变量引用同一个对象,也就是说,改变一个变量所引用的对象将会对另一个变量产生影响。先来看一个例子 ...
  • 易懂js的浅克隆和深克隆原理

    千次阅读 2020-05-02 18:01:59
    什么是克隆 其实js里的克隆跟生活里说的克隆是一样的。有一个本体,克隆出跟本体一摸一样的副体,当本体“受伤”时副体是不会“受伤”的,js的克隆也是如此。 来看下面代码 <script> var benti = { top: ...
  • 一、两大区别,浅克隆和深克隆 浅克隆:是指拷贝对象时仅仅拷贝对象本身(包括对象中的基本变量),而不拷贝对象包含的引用指向的对象,也就是说对象虽然克隆出来了,但是引用的对象还是那个。 深克隆:不仅拷贝...
  • Java中如何实现深克隆

    2021-03-06 14:23:49
    实现深克隆的方法有三种:一、重写clone方法,clone中嵌套clone这种方法的原理其实就是在需要克隆的对象以及该对象的引用类型的变量的类中全部实现cloneable接口,否则抛出CloneNotSupportedException将引用类型的...
  • Java对象克隆——浅克隆和深克隆的区别

    万次阅读 多人点赞 2017-08-06 00:51:35
    在Java中对象的克隆有深克隆和浅克隆之分。有这种区分的原因是Java中分为基本数据类型和引用数据类型,对于不同的数据类型在内存中的存储的区域是不同的。基本数据类型存储在栈中,引用数据类型存储在堆中。 ...
  • 对于Java的克隆技术,标准的方式是:首先实现Cloneable接口,然后重写clone方法,调用父类clone进行克隆。介绍另外一种方法,该方法的原理:利用流将序列化的对象写在流里,因为写在流里面的对象就是原对象的一份...
  • 在说Cloneable接口前,先将一下什么是浅克隆,什么是深克隆 浅克隆 浅克隆,顾名思义就是把表层的东西复制一份,对于基础数据类型来说,就是把值克隆一份(栈中的值),而对引用数据类型而言,则是复制了一份引用...
  • 真正快速的深克隆 用法 const clone = require ( 'rfdc' ) ( ) clone ( { a : 1 , b : { c : 2 } } ) // => {a: 1, b: {c: 2}} 原料药 require('rfdc')(opts = { proto: false, circles: false }) => clone(obj) => ...
  • Java 浅/深克隆、克隆数组

    千次阅读 2019-05-30 13:55:10
    Java 浅/深克隆、克隆数组。及,示例代码演示。
  • 克隆克隆是指在克隆对象时,对于基本数据类型的变量会重新复制一份,而对于引用类型的变量只是对引用进行克隆。就是将栈中的值复制一份给新的变量,但是两个对象指向都是同一个地址,一个发生改变另外一个也...
  • 前端-深克隆与浅克隆

    2020-03-30 23:13:07
    深克隆与浅克隆
  • vue数组的深克隆和浅克隆

    千次阅读 2020-01-02 14:48:31
    在开发过程中,前端获取到后端的数据之后,通常直接复制后就使用,比如: ...如果我们接下来的代码对 _data 进行的处理操作,res.data也会一起改变,这是因为浅克隆克隆的是一个地址, 下面我用...
  • 当浅克隆中存在引用类型的数据时,修改克隆后的值会影响原来的值,因此出现了深克隆。 前期准备: 1.学校类(需要拷贝),实现Cloneable接口并重写clone方法。 2.学生类 一、浅克隆 对于八大基本数据类型(byte,...
  • 深克隆(深拷贝) 指的是在复制java对象的时候java对象中的值不共享 浅克隆(浅拷贝) 指的是在复制java对象的时候java对象中的值共享 也就是说,如果我们需要完全复制出一个新的对象,我们需要重写clone方法 ...
  • 克隆 let obj = { a: 100, b: [10, 20, 30], c: { x: 10 }, d: /^\d+$/ }; let obj2 = {}; 现在想把obj里的每一项都复制给obj2,循环实现浅克隆 for ( let key in obj ) { /*obj.hasOwnProperty(key)...
  • Java实现深克隆的三种方式

    千次阅读 2019-11-15 17:14:13
    大家都知道,Java中的克隆有深克隆和浅克隆,今天我们谈谈深克隆的几种实现方式。 首先,我们先谈谈浅克隆的实现 一、浅克隆 Java中实现浅克隆主要就是要实现Cloneable接口,然后返回克隆对象。 假设,现在我们...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 50,951
精华内容 20,380
关键字:

深克隆