Java list 深拷贝,浅拷贝分析以及原理讲解

x33g5p2x  于2021-08-22 转载在 Java  
字(10.6k)|赞(0)|评价(0)|浏览(1303)

定义

深拷贝:创建了新的对象,分配新的内存空间给对象,拷贝后的list改变的东西不会影响原始list

在这里插入图片描述

浅拷贝:只拷贝引用,没有创建新的对象,没有分配新的内存空间,拷贝后的list变动会影响到原始list

在这里插入图片描述

代码实现
首先新建一个项目,然后创建一个学生类,只建一个name属性

import java.io.Serializable;

/** * 学生类 */
public class Student implements Serializable {
    /** * 姓名 */
    private String name;

    public String getName() {
        return name;
    }

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

然后建一个主方法,这里ShallowCopy为浅拷贝类,DeepCopy为深拷贝类

import java.util.ArrayList;
import java.util.List;

public class ListCopyTest {
    public static void main(String[] args) {
        List<Student> originalList = new ArrayList<>();
        Student student = new Student();
        student.setName("张三");
        originalList.add(student);
        List<Student> copyList = ShallowCopy.copy5(originalList);
        //List<Student> copyList = DeepCopy.copy1(originalList);
        System.out.println("originalList:" + originalList.get(0).getName());
        System.out.println("copyList:" + copyList.get(0).getName());
        copyList.get(0).setName("李四");
        System.out.println("originalList:" + originalList.get(0).getName());
        System.out.println("copyList:" + copyList.get(0).getName());
    }
}

浅拷贝代码实现

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/** * 浅拷贝 */
public class ShallowCopy {

    /** * 浅拷贝方法1(循环赋值) */
    public static List<Student> copy1(List<Student> originalList) {
        List<Student> copyList = new ArrayList<>();
        for (Student student : originalList) {
            copyList.add(student);
        }
        return copyList;
    }

    /** * 浅拷贝方法2(list add) */
    public static List<Student> copy2(List<Student> originalList) {
        List<Student> copyList = new ArrayList<>();
        copyList.addAll(originalList);
        return copyList;
    }

    /** * 浅拷贝方法3(stream) */
    public static List<Student> copy3(List<Student> originalList) {
        List<Student> copyList = originalList.stream().collect(Collectors.toList());
        return copyList;
    }

    /** * 浅拷贝方法4(Arrays copyOf) */
    public static List<Student> copy4(List<Student> originalList) {
        Student[] student = originalList.toArray(new Student[0]);
        Student[] copyStudent = Arrays.copyOf(student, student.length);
        return Arrays.asList(copyStudent);
    }

    /** * 浅拷贝方法5(System arraycopy) */
    public static List<Student> copy5(List<Student> originalList) {
        Student[] student = originalList.toArray(new Student[0]);
        Student[] copyStudent = new Student[student.length];
        System.arraycopy(student, 0, copyStudent, 0, student.length);
        return Arrays.asList(copyStudent);
    }
}

深拷贝代码实现

import com.alibaba.fastjson.*;
import java.util.List;

/** * 深拷贝 */
public class DeepCopy {
    /** * 深拷贝方法1(序列化) */
    public static List<Student> copy1(List<Student> originalList) {
        List<Student> copyList = JSONArray.parseArray(JSONArray.toJSONString(originalList), Student.class);
        return copyList;
    }
}

运行结果
浅拷贝中的任何一个方法运行完结果都是

Connected to the target VM, address: '127.0.0.1:9706', transport: 'socket'
originalList:张三
copyList:张三
originalList:李四
copyList:李四
Disconnected from the target VM, address: '127.0.0.1:9706', transport: 'socket'

深拷贝运行完结果是

Connected to the target VM, address: '127.0.0.1:5731', transport: 'socket'
originalList:张三
copyList:张三
originalList:张三
copyList:李四
Disconnected from the target VM, address: '127.0.0.1:5731', transport: 'socket'

分析
这里集合中数据在浅拷贝后,数据发生变动,2个集合中的数据都会变化。而深拷贝后,数据发生变化,只会影响到当前那个集合,另一个没有变化。
浅拷贝中copy4和copy5方法是平时开发过程中很少用到的,但是却是非常重要的2个方法,因为集合的浅拷贝,最终基本上都是用到这2个方法。
ShallowCopy中copy1和copy2方法中使用的是平常比较常用的集合复制方法,可能大家看到new ArrayList后,都会感觉是新建了一个完全新的集合,和之前的集合没有任何关联了,但实际结果不是,它们只是存放了原始集合的引用,并非完全克隆了集合,接下来,看看方法中的add和addAll的实现原理。

add源码分析
首先打上断点,看看里面怎么运行的。

在这里插入图片描述

发现调用的是

在这里插入图片描述

中的add接口,接下来找到对应的实现方法。
实现方法在java.util.ArrayList中

/** * Appends the specified element to the end of this list. * * @param e element to be appended to this list * @return <tt>true</tt> (as specified by {@link Collection#add}) */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

elementData[size++] = e;这里代表的是对象数组,将新的对象e赋值给elementData数组,这里elementData代表的就是我们的集合
进入ensureCapacityInternal方法,这里是判断集合是否需要扩容

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

进入到calculateCapacity方法

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

如果是初始化集合,会默认赋值DEFAULT_CAPACITY=10 大小的容量

在这里插入图片描述

接下来进入ensureExplicitCapacity方法

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

初始集合的elementData.length=0,minCapacity=10,会进入grow方法

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

这里newCapacity如果小于最小容量,直接赋值minCapacity,如果超过最大容量,则只赋值最大容量Integer.MAX_VALUE - 8或者是Integer.MAX_VALUE(0x7fffffff,2的31次方-1)
接下来看到elementData = Arrays.copyOf(elementData, newCapacity);这行代码,使用到了Arrays.copyOf方法,是不是跟浅拷贝的demo中的copy4方法一样,接下来看下该方法如何实现的

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    @SuppressWarnings("unchecked")
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
    return copy;
}

先是判断是数组还是集合,然后三目运算去新建对象数组或者集合,接下来最关键的就是用到的System.arraycopy方法,这个方法跟浅拷贝的demo中的copy5方法一样,所以add最终用到的是System.arraycopy方法,

在这里插入图片描述

看到这里整体分析一下,集合是如何拷贝的,首先ArrayList用到的是数组添加元素的方式,先计算数组长度,默认长度是10,然后将新增的元素直接赋值给数组,数组长度不够的时候在扩容。那么具体这里在赋值的时候是在原有内存空间上新增元素的还是新开辟空间新增,需要看下System.arraycopy的实现方法
addAll源码分析

public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}

看完add的实现方法,在来看addAll的方法就比较好懂了,这里同样最终都是去调用了System.arraycopy方法
add和addAll的都是先计算和扩容,使用ensureCapacityInternal(内部使用System.arraycopy去开辟空间)方法,然后赋值的时候add用的elementData[size++] = e;,而addAll用的还是System.arraycopy方法
System.arraycopy源码分析

arraycopy方法是JNI(Java Native Interface)方法,调用的是C++代码,这里只能去下载open jdk8来看下源码,具体下载过程不介绍了

源码下载下来后,找到copy代码hotspot\src\share\vm\prims\jvm.cpp

在这里插入图片描述

这里调用了一个copy_array函数,这里会根据是基本类型的还是引用类型的调用不同的类中的方法,基本类型调用TypeArrayKlass,引用类型调用ObjArrayKlass。
找到ObjArrayKlass中的copy_array方法,可以看到方法前面都是一些验证,检测代码,最终用的是do_copy这个方法。

void ObjArrayKlass::copy_array(arrayOop s, int src_pos, arrayOop d,
                               int dst_pos, int length, TRAPS) {
  assert(s->is_objArray(), "must be obj array");
  if (!d->is_objArray()) {
    THROW(vmSymbols::java_lang_ArrayStoreException());
  }
  // Check is all offsets and lengths are non negative
  if (src_pos < 0 || dst_pos < 0 || length < 0) {
    THROW(vmSymbols::java_lang_ArrayIndexOutOfBoundsException());
  }
  // Check if the ranges are valid
  if  ( (((unsigned int) length + (unsigned int) src_pos) > (unsigned int)  s->length())
     || (((unsigned int) length + (unsigned int) dst_pos) > (unsigned int)  d->length()) ) {
    THROW(vmSymbols::java_lang_ArrayIndexOutOfBoundsException());
  }
  // Special case. Boundary cases must be checked first
  // This allows the following call: copy_array(s, s.length(), d.length(), 0).
  // This is correct, since the position is supposed to be an 'in between point', i.e., s.length(),
  // points to the right of the last element.
  if (length==0) {
    return;
  }
  if (UseCompressedOops) {
    narrowOop* const src = objArrayOop(s)->obj_at_addr<narrowOop>(src_pos);
    narrowOop* const dst = objArrayOop(d)->obj_at_addr<narrowOop>(dst_pos);
    do_copy<narrowOop>(s, src, d, dst, length, CHECK);
  } else {
    oop* const src = objArrayOop(s)->obj_at_addr<oop>(src_pos);
    oop* const dst = objArrayOop(d)->obj_at_addr<oop>(dst_pos);
    do_copy<oop> (s, src, d, dst, length, CHECK);
  }
}

do_copy方法中,先判断copy的2个集合是否一致,不一致需要转换,一致的话调用conjoint_oops_atomic方法。

// Either oop or narrowOop depending on UseCompressedOops.
template <class T> void ObjArrayKlass::do_copy(arrayOop s, T* src,
                               arrayOop d, T* dst, int length, TRAPS) {
  BarrierSet* bs = Universe::heap()->barrier_set();
  // For performance reasons, we assume we are that the write barrier we
  // are using has optimized modes for arrays of references. At least one
  // of the asserts below will fail if this is not the case.
  assert(bs->has_write_ref_array_opt(), "Barrier set must have ref array opt");
  assert(bs->has_write_ref_array_pre_opt(), "For pre-barrier as well.");
    //判源和目标对象是否一致,
  if (s == d) {
    // since source and destination are equal we do not need conversion checks.
    assert(length > 0, "sanity check");
    bs->write_ref_array_pre(dst, length);
    Copy::conjoint_oops_atomic(src, dst, length);
  } else {
    // We have to make sure all elements conform to the destination array
    Klass* bound = ObjArrayKlass::cast(d->klass())->element_klass();
    Klass* stype = ObjArrayKlass::cast(s->klass())->element_klass();
    if (stype == bound || stype->is_subtype_of(bound)) {
      // elements are guaranteed to be subtypes, so no check necessary
      bs->write_ref_array_pre(dst, length);
      Copy::conjoint_oops_atomic(src, dst, length);
    } else {
      // slow case: need individual subtype checks
      // note: don't use obj_at_put below because it includes a redundant store check
      T* from = src;
      T* end = from + length;
      for (T* p = dst; from < end; from++, p++) {
        // XXX this is going to be slow.
        T element = *from;
        // even slower now
        bool element_is_null = oopDesc::is_null(element);
        oop new_val = element_is_null ? oop(NULL)
                                      : oopDesc::decode_heap_oop_not_null(element);
        if (element_is_null ||
            (new_val->klass())->is_subtype_of(bound)) {
          bs->write_ref_field_pre(p, new_val);
          *p = *from;
        } else {
          // We must do a barrier to cover the partial copy.
          const size_t pd = pointer_delta(p, dst, (size_t)heapOopSize);
          // pointer delta is scaled to number of elements (length field in
          // objArrayOop) which we assume is 32 bit.
          assert(pd == (size_t)(int)pd, "length field overflow");
          bs->write_ref_array((HeapWord*)dst, pd);
          THROW(vmSymbols::java_lang_ArrayStoreException());
          return;
        }
      }
    }
  }
  bs->write_ref_array((HeapWord*)dst, length);
}

assert_params_ok检测参数是否能用,调用pd_conjoint_jints_atomic方法,该方法在不同操作系统中有不同实现,可以看下linux下的实现方法

// overloaded for UseCompressedOops
  static void conjoint_oops_atomic(narrowOop* from, narrowOop* to, size_t count) {
    assert(sizeof(narrowOop) == sizeof(jint), "this cast is wrong");
    assert_params_ok(from, to, LogBytesPerInt);
    pd_conjoint_jints_atomic((jint*)from, (jint*)to, count);
  }

os_linux_zero.cpp下的_Copy_conjoint_jints_atomic方法,这里C++代码看的不是太懂,可能分析的不对,但是原理还是复制的是指向的地址,并不是值

/// <param name="from">源数组指针</param>
/// <param name="to">目标数组指针</param>
/// <param name="count">数组长度</param>
void _Copy_conjoint_jints_atomic(jint* from, jint* to, size_t count) {
    //判断源数组的指针地址是否大于目标指针的地址
    if (from > to) {
      jint *end = from + count;    //源数组的结束指针=源数组起始地址+数组长度
      while (from < end)    //循环从首地址开始到结束地址结束
        *(to++) = *(from++);    //将源数组的每个指针指向的地址赋值给目标数组的每个指针
    }
    else if (from < to) {
      jint *end = from;
      from += count - 1;
      to   += count - 1;
      while (from >= end)
        *(to--) = *(from--);
    }
  }

相关文章

微信公众号

最新文章

更多