1. 程式人生 > >System.arraycopy方法詳解 深淺拷貝

System.arraycopy方法詳解 深淺拷貝

System:System.arraycopy方法詳解
java 4.5k 次閱讀 · 讀完需要 29 分鐘
看 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 //複製長度
)
深複製還是淺複製
程式碼:物件陣列的複製:

public class SystemArrayCopyTestCase {

public static void main(String[] args) {
    User[] users = new User[] { 
            new User(1, "seven", "
[email protected]
"), new User(2, "six", "[email protected]"), new User(3, "ben", "[email protected]") };// 初始化物件陣列 User[] target = new User[users.length];// 新建一個目標物件陣列 System.arraycopy(users, 0, target, 0, users.length);// 實現複製 System.out.println("源物件與目標物件的實體地址是否一樣:" + (users[0] == target[0] ? "淺複製" : "深複製")); //淺複製 target[0].setEmail("
[email protected]
"); 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 + "]";
}

}
圖示:物件複製的圖示

clipboard.png

所以,得出的結論是,System.arraycopy() 在拷貝陣列的時候,採用的使用潛複製,複製結果是一維的引用變數傳遞給副本的一維陣列,修改副本時,會影響原來的陣列。

一維陣列和多維陣列的複製的區別
程式碼:一維陣列的複製

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

使用該方法對一維陣列在進行復制之後,目標陣列修改不會影響原資料,這種複製屬性值傳遞,修改副本不會影響原來的值。

但是,請重點看以下程式碼:

    String[] st  = {"A","B","C","D","E"};
    String[] dt  = new String[5];
    System.arraycopy(st, 0, dt, 0, 5);

   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  D  E 
    }

    System.out.println("陣列內對應位置的String地址是否相同:" + st[0] == dt[0]); // true

既然是屬性值傳遞,為什麼 st[0] == dt[0] 會相等呢? 我們再深入驗證一下:

    String[] st  = {"A","B","C","D","E"};
    String[] dt  = new String[5];
    System.arraycopy(st, 0, dt, 0, 5);
    dt[0] = "F" ;
    
    for(String str : st){
        System.out.print(" " + str +" ");   // A  B  C  D  E 
        
    }
    System.out.println(); 
    for(String str : dt){
        System.out.print(" " + str +" ");   // F  B  C  D  E 
    }


    System.out.println("陣列內對應位置的String地址是否相同:" + st[0] == dt[0]); // false

為什麼會出現以上的情況呢?

通過以上兩段程式碼可以推斷,在System.arraycopy()進行復制的時候,首先檢查了字串常量池是否存在該字面量,一旦存在,則直接返回對應的記憶體地址,如不存在,則在記憶體中開闢空間儲存對應的物件。

程式碼:二維陣列的複製

    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 

上述程式碼是對二維陣列進行復制,陣列的第一維裝的是一個一維陣列的引用,第二維裡是元素數值。對二維陣列進行復制後後,第一維的引用被複制給新陣列的第一維,也就是兩個陣列的第一維都指向相同的“那些陣列”。而這時改變其中任何一個數組的元素的值,其實都修改了“那些陣列”的元素的值,所以原陣列和新陣列的元素值都一樣了。

執行緒安全,還是不安全
程式碼:多執行緒對陣列進行復制 (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
run count: 2
Exception in thread “main” java.lang.RuntimeException: System.arraycopy is not thread safe
at com.test.ArrayCopyThreadSafe.doThreadSafeCheck(ArrayCopyThreadSafe.java:62)
at com.test.ArrayCopyThreadSafe.main(ArrayCopyThreadSafe.java:68)
所以,System.arraycopy是不安全的。

高效還是低效
程式碼:for vs System.arraycopy 複製陣列

    String[] srcArray = new String[1000000];
    String[] forArray = new String[srcArray.length];
    String[] arrayCopyArray  = new String[srcArray.length];
    
    //初始化陣列
    for(int index  = 0 ; index  < srcArray.length ; index ++){
        srcArray[index] = String.valueOf(index);
    }
    
    long forStartTime = System.currentTimeMillis();
    for(int index  = 0 ; index  < srcArray.length ; index ++){
        forArray[index] = srcArray[index];
    }
    long forEndTime = System.currentTimeMillis();
    System.out.println("for方式複製陣列:"  + (forEndTime - forStartTime));

    long arrayCopyStartTime = System.currentTimeMillis();
    System.arraycopy(srcArray,0,arrayCopyArray,0,srcArray.length);
    long arrayCopyEndTime = System.currentTimeMillis();
    System.out.println("System.arraycopy複製陣列:"  + (arrayCopyEndTime - arrayCopyStartTime));

通過以上程式碼,當測試陣列的範圍比較小的時候,兩者相差的時間無幾,當測試陣列的長度達到百萬級別,System.arraycopy的速度優勢就開始體現了,根據對底層的理解,System.arraycopy是對記憶體直接進行復制,減少了for迴圈過程中的定址時間,從而提高了效能。

源自:https://segmentfault.com/a/1190000009922279