LeetCode刷題時引發的思考:Java中ArrayList存放的是值還是引用?
好好學習,天天向上
本文已收錄至我的Github倉庫DayDayUP:github.com/RobodLee/DayDayUP,歡迎Star,更多文章請前往:目錄導航
前言
今天我在刷LeetCode的時候遇到了一個問題,就是ArrayList新增不進去資料,其實不是沒有新增進去,而是新增進去的資料被改變了,為什麼會改變了呢?其實涉及到ArrayList存放的是值還是引用的問題,網上有很多回答是:如果是基本資料型別則存放的是值,如果是物件存放的就是引用。那麼到底是什麼呢,讓我們來一探究竟吧!
正文
原題在此:
39. 組合總和
給定一個無重複元素的陣列
candidates
和一個目標數target
,找出candidates
中所有可以使數字和為target
的組合。
candidates
中的數字可以無限制重複被選取。
我一開始寫的程式碼是這樣的:
class Solution { private List<List<Integer>> result = new ArrayList(); private ArrayList<Integer> solution = new ArrayList<>(); public List<List<Integer>> combinationSum(int[] candidates, int target) { backtracking(candidates,target,0); return result; } public void backtracking(int[] candidates, int residual, int start) { if (residual < 0) { return; } if (residual == 0) { result.add(solution); return; } for(int i = start;i<candidates.length;i++) { solution.add(candidates[i]); backtracking(candidates,residual-candidates[i],i); solution.remove(solution.size()-1); } } }
看了一遍,沒什麼問題,非常自信的點了執行,結果懵逼了,List裡面什麼也沒有
遇Bug沒關係,首先來分析一下:既然輸出是兩個 [],那麼List中肯定新增過兩次資料,也就是進入了if語句中,那麼可能的原因就是程式碼寫的有問題導致 solution中沒有資料,那就列印一下看看唄。
修改程式碼:
…… if (residual == 0) { System.out.println(solution.toString()); //列印solution result.add(solution); return; } ……
看一下控制檯,solution正常列印了,而且結果是正確的
既然solution有正確的資料,那麼問題就應該出現在List的新增上,那就再列印一下看看。
修改程式碼:
……
if (residual == 0) {
result.add(solution);
System.out.println("----------");
for (List<Integer> integers : result) {
System.out.println(integers.toString()); //列印List中的內容
}
System.out.println("==========");
return;
}
……
從列印的內容上看,第一次新增的是 [2,2,3],沒有問題。但是第二次的列印結果就很奇怪,是兩個 [7],也就是我第一次新增的內容被修改了。那麼可能的原因只有一個,List中新增進去的是一個引用,而並非實際的值,不然結果怎麼會改變呢。但是List中存放的真的是引用嗎?我們來進一步的驗證。
首先我們將ArrayList中的泛型指定為基本資料型別的包裝類和我們自定義的一個User類:
public class ArrayListTest {
static class User {
private String name;
private int age;
public User(String name , int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public static void main(String[] args) {
ArrayList<Integer> integerArrayList = new ArrayList<>();
int testInt = 10;
integerArrayList.add(testInt);
testInt = 20;
integerArrayList.add(testInt);
System.out.println(integerArrayList.toString());
ArrayList<Double> doubleArrayList = new ArrayList<>();
double testDouble = 100.0;
doubleArrayList.add(testDouble);
testDouble = 200.0;
doubleArrayList.add(testDouble);
System.out.println(doubleArrayList.toString());
ArrayList<User> userArrayList = new ArrayList<>();
User testUser = new User("張三",20);
userArrayList.add(testUser);
testUser.age = 22;
userArrayList.add(testUser);
for (User user : userArrayList) {
System.out.println(user.toString());
}
}
這裡我們試了Integer和Double兩種,看一下結果:
由此可見,修改int和double不會對之前的內容造成影響,但是修改User會對之前的內容造成影響。所以,ArrayList中如果是基本資料型別,那麼存放的就是值,如果是物件,那麼存放的就是物件的引用而不是物件的拷貝。看樣子這個結論是正確的,但是需要注意的一個問題就是ArrayList可以存放基本資料型別嗎?如果將泛型指定為基本資料型別就會報錯:
所以說,ArrayList不存在存放的是基本資料型別的問題,只能存放基本資料型別的包裝類,也就是說存放基本資料型別的時候會自動裝箱成一個物件,然後把引用存放進去。那好,就算基本資料型別存不了,存的是包裝類,那麼我修改了裡面的內容之後,Integer和Double兩次卻存放了不同的值,而User修改後存放了兩個相同的內容。因為如果泛型是Integer或Double的話,兩次存放的是不同的物件的引用而不是一個,如果是一個的話,那當然會導致之前的內容改變咯。《Java程式設計思想》裡面說過:無論何時,對同一個物件呼叫hashCode()都應該生成同樣的值。所以我們來列印一下他們的hash值就可以判斷是不是同一個物件。我們使用System.identityHashCode()來獲取hash值:
……
public static void main(String[] args) {
ArrayList<Integer> integerArrayList = new ArrayList<>();
int testInt = 10;
integerArrayList.add(testInt);
testInt = 20;
integerArrayList.add(testInt);
for (Integer integer : integerArrayList) {
System.out.println(integer + " : "+ System.identityHashCode(integer));
}
System.out.println();
ArrayList<Double> doubleArrayList = new ArrayList<>();
double testDouble = 100.0;
doubleArrayList.add(testDouble);
testDouble = 200.0;
doubleArrayList.add(testDouble);
for (Double aDouble : doubleArrayList) {
System.out.println(aDouble + " : "+ System.identityHashCode(aDouble));
}
ArrayList<User> userArrayList = new ArrayList<>();
User testUser = new User("張三",20);
System.out.println("\nUser修改前:");
System.out.println(testUser.toString());
System.out.println(System.identityHashCode(testUser));
userArrayList.add(testUser);
testUser.age = 22;
System.out.println("User修改後:");
System.out.println(testUser.toString());
System.out.println(System.identityHashCode(testUser));
userArrayList.add(testUser);
System.out.println("\n遍歷ArrayList<User>:");
for (User user : userArrayList) {
System.out.println(user.toString());
System.out.println(System.identityHashCode(user));
}
}
……
列印的結果如下:
從列印的結果可以看出,Integer和Double並不是修改內容,而是存了一個新的物件的引用進去,所以存放基本資料型別的包裝類也是引用並非是值,而User物件修改後卻可以影響到之前已經儲存的內容,兩個User是同一個。
為什麼Integer和Double修改不了呢?因為他們都屬於不可變數,都是final修飾的,也就是說第二次賦值的時候指向的是一個新的物件,而不是修改之前的內容。從原始碼中我們可以看到:
可以看到,基本資料型別的包裝類和String一樣都是final修飾的,而且Double和Integer等基本資料型別包裝類中也沒有提供修改值的方法,也就是說之前看樣子是在修改資料,其實是指向了一個新的記憶體地址,ArrayList中第二次存放資料的時候,並沒有改變第一次存放的引用中的記憶體地址中的值,而是存了一個新的引用。
結論
那麼到現在為止,就可以得出一個結論了: ArrayList中存放的是引用而不是值
。
網上有的人說“存基本資料型別存的是值,存物件存的是引用”這個結論是錯誤的。回到最開始的問題,我這題應該怎麼寫?其實只要克隆一份就好了:
……
if (residual == 0) {
result.add((List<Integer>) solution.clone());
return;
}
……