1. 程式人生 > 實用技巧 >LeetCode刷題時引發的思考:Java中ArrayList存放的是值還是引用?

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