【集合类】 4 System:System.arraycopy方法解析

引言

看 JDK 源码的时候,Java 开发设计者在对数组的复制时,通常都会使用 System.arraycopy() 方法。

其实对数组的复制,有四种方法:

  • for
  • clone
  • System.arraycopy
  • arrays.copyof

本文章主要分析 System.arraycopy() ,带着几个问题去看这个方法:

  • 深复制,还是浅复制
  • String 的一维数组和二维数组复制是否有区别
  • 线程安全,还是不安全
  • 高效还是低效

System.arraycopy() 的 API :

public static void arraycopy(
                             Object src,  //源数组
                             int srcPos,  //源数组的起始位置
                             Object dest, //目标数组
                             int destPos, //目标数组的起始位置
                             int length   //复制长度
                             )

1.System.arraycopy

1.1 深复制还是浅复制

我们构建一个User类型源数组,复制后至target数组,比较第一个元素的内存地址,判断结果是相同的,证明为浅复制;后修改target数组数组的第一个元素,发现源数组也变了。

public class SystemArrayCopyTestCase {

    public static void main(String[] args) {
        User[] users = new User[] { 
                new User(1, "seven", "seven@qq.com"), 
                new User(2, "six", "six@qq.com"),
                new User(3, "ben", "ben@qq.com") };// 初始化对象数组
        
        User[] target = new User[users.length];// 新建一个目标对象数组
        
        System.arraycopy(users, 0, target, 0, users.length);// 实现复制
        
        System.out.println("源对象与目标对象的物理地址是否一样:" + (users[0] == target[0] ? "浅复制" : "深复制"));  //浅复制
        
        target[0].setEmail("admin@sina.com");
        
        System.out.println("修改目标对象的属性值后源对象users:");
        for (User user : users) {
            System.out.println(user);
        }
       
    }
}

class User {
    private Integer id;
    private String username;
    private String email;

    // 无参构造函数
    public User() {
    }

    // 有参的构造函数
    public User(Integer id, String username, String email) {
        super();
        this.id = id;
        this.username = username;
        this.email = email;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

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

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @Override
    public String toString() {
        return "User [id=" + id + ", username=" + username + ", email=" + email + "]";
    }
}

图示:对象复制的图示
在这里插入图片描述
所以,得出的结论是,System.arraycopy() 在拷贝数组的时候,采用的使用潜复制,复制结果是一维的引用变量传递给副本的一维数组,修改副本时,会影响原来的数组。

1.2 一维数组和多维数组的复制的区别

1.2.1 一维数组

构建一维数组作源数组,进行拷贝,产生target数组;然后修改target数组中的后2个元素,发现源数组没变,证明是值拷贝,修改副本不会影响原来的值

  String[] st  = {"A","B","C","D","E"};
        String[] dt  = new String[5];
        System.arraycopy(st, 0, dt, 0, 5);
        
        //改变dt的值
        dt[3] = "M";
        dt[4] = "V";
        
        System.out.println("两个数组地址是否相同:" + (st == dt)); //false
        System.out.println("两个元素地址是否相同:" + (st[0] == dt[0])); // true
        
        for(String str : st){
            System.out.print(" " + str +" ");   // A  B  C  D  E 
            
        }
        System.out.println(); 
        for(String str : dt){
            System.out.print(" " + str +" ");   // A  B  C  M  V 
        }

注意:JAVA1.6 执行结果,我们发现2个数组不是相同的,但是首个元素相同,原因是在System.arraycopy()进行复制的时候,首先检查了字符串常量池是否存在该字面量,一旦存在,则直接返回对应的内存地址,如不存在,则在内存中开辟空间保存对应的对象。

1.2.2 多维数组

构建二维数组作源数组,进行拷贝至目标数组,修改至目标数组的一个元素(这个元素此时是个数组),发现源数组也变了,说明是二者是相同的引用,而这时改变其中任何一个数组的元素的值,其实都修改了“那些数组”的元素的值,所以原数组和新数组的元素值都一样了。

  String[][] s1 = {
                    {"A1","B1","C1","D1","E1"},
                    {"A2","B2","C2","D2","E2"},
                    {"A3","B3","C3","D3","E3"}
                        };
        String[][] s2 = new String[s1.length][s1[0].length];  
        
        System.arraycopy(s1, 0, s2, 0, s2.length);  
        
        for(int i = 0;i < s1.length ;i++){ 
         
           for(int j = 0; j< s1[0].length ;j++){  
              System.out.print(" " + s1[i][j] + " ");
           }  
           System.out.println();  
        }  
        
        //  A1  B1  C1  D1  E1 
        //  A2  B2  C2  D2  E2 
        //  A3  B3  C3  D3  E3 
        
        
        s2[0][0] = "V";
        s2[0][1] = "X";
        s2[0][2] = "Y";
        s2[0][3] = "Z";
        s2[0][4] = "U";
        
        System.out.println("----修改值后----");  
        
        
        for(int i = 0;i < s1.length ;i++){  
               for(int j = 0; j< s1[0].length ;j++){  
                  System.out.print(" " + s1[i][j] + " ");
               }  
               System.out.println();  
         }  

        //  Z   Y   X   Z   U 
        //  A2  B2  C2  D2  E2 
        //  A3  B3  C3  D3  E3 

1.3 线程安全,还是不安全

结论:System.ayyaycopy是不安全的。

代码:多线程对数组进行复制 (java中System.arraycopy是线程安全的吗? )

public class ArrayCopyThreadSafe {
    private static int[] arrayOriginal = new int[1024 * 1024 * 10];
    private static int[] arraySrc = new int[1024 * 1024 * 10];
    private static int[] arrayDist = new int[1024 * 1024 * 10];
    private static ReentrantLock lock = new ReentrantLock();

    private static void modify() {
        for (int i = 0; i < arraySrc.length; i++) {
            arraySrc[i] = i + 1;
        }
    }

    private static void copy() {
        System.arraycopy(arraySrc, 0, arrayDist, 0, arraySrc.length);
    }

    private static void init() {
        for (int i = 0; i < arraySrc.length; i++) {
            arrayOriginal[i] = i;
            arraySrc[i] = i;
            arrayDist[i] = 0;
        }
    }

    private static void doThreadSafeCheck() throws Exception {
        for (int i = 0; i < 100; i++) {
            System.out.println("run count: " + (i + 1));
            init();
            Condition condition = lock.newCondition();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    lock.lock();
                    condition.signalAll();
                    lock.unlock();
                    copy();
                }
            }).start();


            lock.lock();
            // 这里使用 Condition 来保证拷贝线程先已经运行了.
            condition.await();
            lock.unlock();

            Thread.sleep(2); // 休眠2毫秒, 确保拷贝操作已经执行了, 才执行修改操作.
            modify();

            if (!Arrays.equals(arrayOriginal, arrayDist)) {
                throw new RuntimeException("System.arraycopy is not thread safe");
            }
        }
    }

    public static void main(String[] args) throws Exception {
        doThreadSafeCheck();
    }
}

这个例子的具体操作是:

  • arrayOriginal 和 arraySrc 初始化时是相同的, 而 arrayDist 是全为零的.

  • 启动一个线程运行 copy() 方法来拷贝 arraySrc 到 arrayDist 中.

  • 在主线程执行 modify() 操作, 修改 arraySrc 的内容. 为了确保 copy() 操作先于 modify() 操作, 我使用 Condition, 并且延时了两毫秒, 以此来保证执行拷贝操作(即System.arraycopy) 先于修改操作.

  • 根据第三点, 如果 System.arraycopy 是线程安全的, 那么先执行拷贝操作, 再执行修改操作时, 不会影响复制结果, 因此 arrayOriginal 必然等于 arrayDist; 而如果 System.arraycopy 是线程不安全的, 那么 arrayOriginal 不等于 arrayDist.

运行程序:

run count: 1
Exception in thread "main" java.lang.RuntimeException: System.arraycopy is not thread safe
	at ArrayCopyThreadSafe.doThreadSafeCheck(ArrayCopyThreadSafe.java:54)
	at ArrayCopyThreadSafe.main(ArrayCopyThreadSafe.java:60)

所以,System.ayyaycopy是不安全的。

1.4 高效还是低效

for 和System.arraycopy 对比复制数组

当测试数组的范围比较小的时候,两者相差的时间无几,当测试数组的长度达到百万级别,System.arraycopy的速度优势就开始体现了,根据对底层的理解,System.arraycopy是对内存直接进行复制,减少了for循环过程中的寻址时间,从而提高了效能。

2.Arrays.copyOf

java 1.8 复用System.arraycopy,区别在于直接返回一个target数组,不需要指定一个target数组。并且等价于指定0开始,长度为newLength的数组片段

    public static <T> T[] copyOf(T[] original, int newLength) {
        return (T[]) copyOf(original, newLength, original.getClass());
    }

3.Arrays.copyOfRange

java 1.8 复用System.arraycopy,区别在于直接返回一个target数组,不需要指定一个target数组。

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

总结:Arrays中的静态方法类底层几乎复用相同的逻辑

4.Collections.copy

把源数组拷贝至目标数组,底层用的for循环,效率低。

    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");

        if (srcSize < COPY_THRESHOLD ||
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; i<srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(si.next());
            }
        }
    }

注意:
1.进行集合复制时,一要声明目的集合的元素的个数,并且要等于或者大于源集合的元素的个数。
2.如果不声明或者小于源集合的元素个数,这样就会报错,报下标界的异常(java.lang.IndexOutOfBoundsException)。

原因是for循环外层是源数组,目标数组过小,set方法导致下标越界,因为没有像add方法那样产生自动扩容。

5.ArrayList.clone

底层也是调用Arrays.copyOf(),而Arrays.copyOf底层复用System.arraycopy

 public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值