• Java 如何复制 List ?


      List 复制在项目开发时,使用到的频率还是比较高的。List 复制有浅拷贝和深拷贝两种方式。在陈述复制方法前,先总结下什么是浅拷贝和深拷贝(以下内容均站在 Java 语言基础上进行讨论)。

    一、什么是浅拷贝(Shallow Copy)和深拷贝(Deep Copy)?

      浅拷贝只复制某个对象的引用,而不复制对象本身,新旧对象还是共享同一块内存。深拷贝会创造一个一模一样的对象,新对象和原对象不共享内存,修改新对象不会改变原对象。
      假设 B 复制了 A,当修改 A 时,看 B 是否会发生变化。如果 B 也跟着变了,说明这是浅拷贝,如果 B 没变,那就是深拷贝。

    1.1 浅拷贝

      对于数据类型是基本数据类型(整型:byte、short、int、long;字符型:char;浮点型:float、double;布尔型:boolean)的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据。

      对于数据类型是引用数据类型(比如说成员变量是某个数组、某个类的对象等)的成员变量,浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例,在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。

    1.2 深拷贝

      相对于浅拷贝而言,深拷贝对于引用类型的修改,并不会影响到对应的拷贝对象的值。

      备注:一般在讨论深拷贝和浅拷贝时,通常是针对引用数据类型而言的。因为基本数据类型在进行赋值操作时(也就是拷贝)是直接将值赋给了新的变量,也就是该变量是原变量的一个副本,这个时候你修改两者中的任何一个的值都不会影响另一个。而对于引用数据类型来说,在进行浅拷贝时,只是将对象的引用复制了一份,也就是内存地址,即两个不同的变量指向了同一个内存地址,那么改变任一个变量的值,都是改变这个内存地址所存储的值,所以两个变量的值都会改变。
      Java 对对象和基本数据类型的处理是不一样的。在 Java 中,用对象作为入口参数传递时,缺省为 “引用传递”,也就是说仅仅传递了对象的一个”引用”。当方法体对输入变量修改时,实质上就是直接操作这个对象。 除了在函数传值的时候是”引用传递”,在任何用 ”=” 向对象变量赋值的时候都是”引用传递”。
      将对象序列化为字节序列后,再通过反序列化即可完美地实现深拷贝。

    1.3 对象如何实现深拷贝?

      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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

      Object 的 clone() 方法本身是一个浅拷贝的方法,但是我们可以通过实现 Cloneable 接口,重写该方法来实现深拷贝,除了调用父类中的 clone() 方法得到新的对象, 还要将该类中的引用变量也 clone 出来。如果只是用 Object 中默认的 clone() 方法,是浅拷贝的。
      如下代码所示,我们创建一个测试类 TestClone,实现深拷贝。

    package com.example.test;
    
    import lombok.Data;
    import lombok.SneakyThrows;
    
    @Data
    public class TestClone implements Cloneable {
        private String a;
    
        // 构造函数
        TestClone(String str) {
            this.a = str;
        }
    
        @Override
        protected TestClone clone() throws CloneNotSupportedException {
            TestClone newTestClone = (TestClone) super.clone();
            newTestClone.setA(this.a);
            return newTestClone;
        }
    
        @SneakyThrows
        public static void main(String[] args) {
            TestClone clone1 = new TestClone("a");
    
            TestClone clone2 = clone1.clone();
            System.out.println(clone2.a);     // a
            clone2.setA("b");
    
            System.out.println(clone1.a);     // a
            System.out.println(clone2.a);     // b
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    二、List 复制

      List 复制时有深拷贝和浅拷贝两类方式,分述如下。

    2.1 浅拷贝

      List 其本质就是数组,而其存储的形式是地址,如下图所示。
    在这里插入图片描述

      将 listA 列表浅拷贝为 listB 时,listA 与 listB 指向同一地址,造成的后果就是,改变 listB 的同时也会改变 listA,因为改变listB 就是改变 listB 所指向的地址的内容,由于 listA 也指向同一地址,所以 listA 与 listB 一起改变。其常见的实现方式有如下几种(以上述代码中的 TestClone 为测试类)。

    2.1.1 循环遍历复制(含测试方法)
    	@SneakyThrows
        public static void main(String[] args) {
            List<TestClone> listA = new ArrayList<>();
            TestClone testClone = new TestClone("a");
            listA.add(testClone);
            System.out.println(listA);  // [TestClone(a=a)]
    
            List<TestClone> listB = new ArrayList<>(listA.size());
            for (TestClone clone : listA) {
                listB.add(clone);
            }
            System.out.println(listB);  // [TestClone(a=a)]
    
            listA.get(0).setA("b");
            System.out.println(listA);  // [TestClone(a=b)]
            System.out.println(listB);  // [TestClone(a=b)]
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    2.1.2 使用 List 实现类的构造方法
    	@SneakyThrows
        public static void main(String[] args) {
            List<TestClone> listA = new ArrayList<>();
            TestClone testClone = new TestClone("a");
            listA.add(testClone);
            List<TestClone> listB = new ArrayList<>(listA);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    2.1.3 使用 list.addAll() 方法
    	@SneakyThrows
        public static void main(String[] args) {
            List<TestClone> listA = new ArrayList<>();
            TestClone testClone = new TestClone("a");
            listA.add(testClone);
    
            List<TestClone> listB = new ArrayList<>();
            listB.addAll(listA);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    2.1.4 使用 java.util.Collections.copy() 方法
    	public static void main(String[] args) {
            List<TestClone> listA = new ArrayList<>();
            TestClone clone = new TestClone("a");
            listA.add(clone);
    
            List<TestClone> listB = new ArrayList<>();
            listB.add(new TestClone("c"));
            System.out.println(listB);      // [TestClone(a=c)]
            Collections.copy(listB, listA);
            System.out.println(listB);      // [TestClone(a=a)]
            listA.get(0).setA("b");
            System.out.println(listA);      // [TestClone(a=b)]
            System.out.println(listB);      // [TestClone(a=b)]
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    2.1.5 使用 Java 8 Stream API 将 List 复制到另一个 List 中
    	public static void main(String[] args) {
            List<TestClone> listA = new ArrayList<>();
            TestClone clone = new TestClone("a");
            listA.add(clone);
    
            List<TestClone> listB = listA.stream().collect(Collectors.toList());
            System.out.println(listB); // [TestClone(a=a)]
    
            listA.get(0).setA("b");
            System.out.println(listA); // [TestClone(a=b)]
            System.out.println(listB); // [TestClone(a=b)]
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    2.1.6 在 JDK 10 中的使用方式

      JDK 10 在线调试网址:https://lightly.teamcode.com/

    	public static void main(String[] args) {
            List<TestClone> listA = new ArrayList<>();
            TestClone clone = new TestClone("a");
            listA.add(clone);
    
            List<TestClone> listB = List.copyOf(listA);
        
            listA.get(0).setA("b");
            System.out.println(listA);                  // [TestClone@76ed5528]
            System.out.println(listA.get(0).getA());    // b
            System.out.println(listB);                  // [TestClone@76ed5528]
            System.out.println(listB.get(0).getA());    // b
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    2.2 深拷贝

    在这里插入图片描述

      如图,深拷贝就是将 listA 复制给 listB 的同时,给 listB 创建新的地址,再将地址 A 的内容传递到地址 B。listA 与 listB 内容一致,但是由于所指向的地址不同,所以各自改变内容不会影响对方。深拷贝时,向新列表添加的是原列表中的元素执行 clone() 方法后的新对象。

    	@SneakyThrows
        public static void main(String[] args) {
            List<TestClone> listA = new ArrayList<>();
            TestClone testClone = new TestClone("a");
            listA.add(testClone);
    
            List<TestClone> listB = new ArrayList<>();
            listB.add(listA.get(0).clone());
            System.out.println(listB);  // [TestClone(a=a)]
    
            listA.get(0).setA("b");
            System.out.println(listA);  // [TestClone(a=b)]
            System.out.println(listB);  // [TestClone(a=a)]
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

      本文提到的几种浅拷贝的方法对于 List<String> 可以实现深拷贝的效果,其测试代码和原因分析见 《Java 如何实现 List<String> 的深拷贝?》。

    文章参考:
  • 相关阅读:
    Opengl ES之YUV数据渲染
    使用openai-whisper实现语音转文字
    C#面:可以在C#静态类中使用“this”吗 ?
    朴素贝叶斯简介
    autojs实现自动答题、复诵答案、100%正确率
    元宇宙改变人类工作模式的四种方式
    string的模拟实现——C++
    《自卑与超越》生活对你应有的意义
    LeetCode581:最短无序连续子数组
    【MySQL事务篇】多版本并发控制(MVCC)
  • 原文地址:https://blog.csdn.net/piaoranyuji/article/details/125440931