看 JDK 源碼的時候,Java 開發設計者在對數組的複製時,一般都會使用 System.arraycopy() 方法。java
其實對數組的複製,有四種方法:數組
for安全
clone多線程
System.arraycopyide
arrays.copyof函數
本文章主要分析 System.arraycopy() ,帶着幾個問題去看這個方法:測試
深複製,仍是淺複製this
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", "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() 在拷貝數組的時候,採用的使用潛複製,複製結果是一維的引用變量傳遞給副本的一維數組,修改副本時,會影響原來的數組。
代碼:一維數組的複製
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循環過程當中的尋址時間,從而提升了效能。