在Arrays.asList()引起的問題中進一步學習集合與泛型等內容

  前言

  最近在網上看到一個問題,狀況相似以下(記爲問題1):java

    public class Demo {

        public static void main(String[] args) {
            System.out.println(testInteger(1));
            System.out.println(testInt(1));
        }
        
        public static boolean testInteger (Integer num) {
            Integer[] nums = new Integer[]{1, 2, 3, 4, 5, 6};
            boolean flag = Arrays.asList(nums).contains(num);
            return flag;
        }
        
        public static boolean testInt (int num) {
            int[] nums = new int[]{1, 2, 3, 4, 5, 6};
            boolean flag = Arrays.asList(nums).contains(num);
            return flag;
        }
    }

    結果第一個輸出爲true,提問者以爲很正常,第二個輸出卻爲false了,很奇怪。若是沒有深刻使用過該集合,或是理解泛型,在個人第一眼看來,也是有疑惑。按理來講,Java中自己針對基本類型與對應包裝類型,就有自動裝箱拆箱功能,你Integer能行,我int按理來講應該也能行。因而我大體在網上搜尋了相似的問題,發現除了上面的問題,還有相似以下的狀況(記爲問題2)數組

    public class Demo2 {

        public static void main(String[] args) {
            Integer[] nums1 = new Integer[]{1, 2, 3, 4, 5, 6};
            List list1 = Arrays.asList(nums1);
            list1.set(0, 888);
            System.out.println(list1);
            
            int[] nums2 = new int[]{1, 2, 3, 4, 5, 6};
            List list2 = Arrays.asList(nums2);
            list2.set(0, 888);
            System.out.println(list2);
        }

    }

  第一個輸出正常,集合list1中第一個元素修改成888,可是第二輸出還沒到就已經報錯,完整運行結果以下:dom

  java.lang.ArrayStoreException:數組存儲異常。下面具體就集合方法與泛型探究上面的問題。ide

  過程

  Integer[] nums = new Integer[]{1, 2, 3, 4, 5, 6};
  Arrays.asList(nums);進入這個方法,源代碼以下:工具

    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }

    短短的兩行代碼,內容卻並不簡單
    1.方法的參數比較特殊:參數是泛型類的,而且是可變參數。一個泛型,一個可變參數,說實話,初學者或者開發中不敢說都沒用過,但要是說有多經常使用,顯然也不符合。因此其實上面的內容在這一步就已經出問題了,下面會具體分析。    
    2.方法的返回值是一個ArrayList,這裏又是一個大坑,這個ArrayList並非咱們之前學習或者平時經常使用到的有序集合ArrayList,而是數組工具類Arrays類的一個靜態內部類,繼承了AbstractList,以下所示:學習

    private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable
    {
        private static final long serialVersionUID = -2764017481108945198L;
        private final E[] a;

        ArrayList(E[] array) {
            //上面調用的構造方法內容即爲這裏,調用requireNonNull方法,對象array爲空的話會報空指針異常,不爲空則將其賦值給成員a。
            a = Objects.requireNonNull(array);
        }
        
        //賦值後此時a就表明了array數組的內容,因此後面的方法基本上都圍繞a展開。
        
        @Override
        public int size() {
            return a.length;
        }

        @Override
        public Object[] toArray() {
            return a.clone();
        }
        ......
        
    }

  requireNonNull方法內容:優化

    public static <T> T requireNonNull(T obj) {
        if (obj == null)
            throw new NullPointerException();
        return obj;
    }

  完整的過完了這一遍流程後,咱們要思考的是,經過Arrays.asList(nums)方法,咱們獲得的究竟是一個什麼。從上面的內容,咱們首先能夠肯定是一個集合。在問題1中,包裝類Integer狀況下,調用Arrays.asList(nums),基於可變參數的定義,這裏咱們至關於傳入可變參數的長度爲5。
  在public static <T> List<T> asList(T... a)方法中,此時至關於泛型T指定爲Integer類型。而後private final E[] a; 此時的數組a的泛型E也相應爲Integer。此時a的內容至關於 new Integer[]{1,2,3,4,5,6}; 即一個Integer類型的一維數組,集合也隨之爲List<Integer>ui

最後獲取的是一個Arrays的一個靜態內部類ArrayList對象,在內部類中重寫了contains方法:spa

    @Override
    public boolean contains(Object o) {
        return indexOf(o) != -1;
    }

  因此在問題1中的第一種狀況傳入1時會自動轉爲對象Object類型,即上面的o,此時顯然成立,因此返回true。
  那麼第二種狀況輸出false,問題出在哪裏?
  在基本類型int狀況時,一樣基於可變參數的定義,同時基於java的自動轉換類型,跟上面同樣傳入可變參數的長度仍是至關於5嗎?其實不是,根本緣由在於這裏除了可變參數的定義,還有泛型的定義,可是別忘了泛型也有一些限制,首先第一點就是:
  泛型的類型參數只能是類類型(包括自定義類),不能是簡單類型!
  因此這裏其實就已經有分歧了,因此這裏咱們至關於傳入了一個對象,對象類型爲數組類型,可變參數的長度是1,而不是5。此時a至關於一個二維數組,在上面的狀況中,即a數組的元素類型爲數組,元素個數爲1,而不是上面的Integer,此時集合爲List<int[]>。
  最後獲得的集合是這樣子:List<int[]>形式,集合長度爲1,包含了一個數組對象,而不是誤覺得的List<Intger>。
  接着調用contains方法,集合中就一個數組類型對象,顯然不包含1。因此問題1中int狀況下輸出false
  一樣的,在問題2的int狀況下,list2.set(0,888);即至關於將集合索引爲0的元素(即第1個,這裏集合中就一個數組元素)賦值爲一個888自動轉型的Integer類,給1個數組Array類型對象賦值Intger對象,因此才報錯了上面的數組存儲異常。指針

  梳理與驗證

  爲了更好的理清上面的內容,作以下擴展。

  首先是Integer對象類型。

Integer[] nums = new Integer[]{1, 2, 3, 4, 5, 888};
List<Integer> list2 = Arrays.asList(nums);
System.out.println(list2.get(5));//輸出888

  這裏咱們順利的經過一次索引拿到在這個888。

  接着是int基本類型,前面提到了既然是可變參數,咱們傳了一個數組,一個數組也是一個對象,那咱們能夠傳入多個數組看看,以下所示。

int[] nums1 = new int[]{1, 2, 3, 4, 5, 666};
int[] nums2 = new int[]{1, 2, 3, 4, 5, 777};
int[] nums3 = new int[]{1, 2, 3, 4, 5, 888};
List<int[]> list1 = Arrays.asList(nums1,nums2,nums3);
System.out.println(list1.get(2)[5]);//輸出888

  這裏的get方法咱們點進去查看源代碼:

        @Override
        public E get(int index) {
            return a[index];
        }    

  因此這裏咱們輸出其實就是a[2][5],拿到888,這也印證了前面爲何說本質上就是一個二維數組的緣由,同時加深了咱們對集合與數組關係的理解。

   更多的陷阱

  前面重點提到這裏咱們經過Arrays.asList()方法獲得的"ArrayList",並非咱們平時經常使用的那個ArrayList,既然強調了,固然是爲了要區分,那麼不區分會有什麼問題呢,下面以簡單的Integer狀況爲例:

Integer[] nums = new Integer[]{1, 2, 3, 4, 5};
List<Integer> list = Arrays.asList(nums);
list.add(888);
System.out.println(list);

  集合後面加個888,以爲會打印出來什麼?【1, 2, 3, 4, 5,888】?

  而後事實是還沒到打印就已經拋出異常了,以下所示:

  java.lang.UnsupportedOperationException:不支持的操做異常。爲何會不支持,咱們之前一直add,remove等等都沒問題。深究到底查看源代碼。

  首先java.util包下的ArrayList即咱們熟知的,它的add方法實現以下:

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

  這也是咱們一直以來操做沒毛病的緣由。

  再來看這個"ArrayLitst",它在繼承抽象類AbstractList的時候,並未實現(或者準確來講叫作重寫)add方法,因此這裏在調用add方法的時候,其實是調用的抽象類AbstractList中已經實現的add方法,咱們來看其方法內容:

    public boolean add(E e) {
        add(size(), e);
        return true;
    }

  經過add(size(), e);這個傳入2個參數的方法咱們繼續查看內容:

    public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }

  真相大白!throw new UnsupportedOperationException();這個異常從哪來的一目瞭然。這個"ArrayLitst"根本沒有實現add方法,因此纔會報錯。回到初始,還能想起集合與數組的種種關聯,數組自己長度就是不可變的,而這裏本質上咱們就是在操做數組,因此沒有add方法不是很正常嗎。仔細查看其類的源碼,會發現例如remove刪除等方法,也是沒有實現的,因此一樣也會報錯。

  繼續探討,那麼這裏增也不行,刪也不行,那我改總行吧,就數組而言,按理來講應該是支持的,而實際狀況也的確如此。在"ArrayLitst"內部類中,其重寫了set方法,方法能將指定索引處的元素修改成指定值,同時將舊元素的值做爲返回值返回。

        @Override
        public E set(int index, E element) {
            E oldValue = a[index];
            a[index] = element;
            return oldValue;
        }

  可是這裏還要注意一點,若是咱們在這裏針對集合修改了某處元素值,那麼原來數組的內容也會相應改變!即經過Arrays.asList()方法,獲得的集合與原數組就已經關聯起來,反之,若是咱們修改了數組內容,那麼集合獲取到的內容也會隨之改變。實踐檢驗一下:

        Integer[] nums = new Integer[]{1, 2, 3, 4, 5};
        List<Integer> list = Arrays.asList(nums);
        list.set(0, 888);//修改集合內容
        nums[1] = 999;//修改數組內容
        for(Integer i:nums) {
            System.out.println(i);
        }
        System.out.println(list);

  運行後,控制檯輸出以下:

  咱們發現,不管是修改數組,仍是修改集合,另外一方都會相應改變。

   小結

  一開始覺得是一個小問題,漸漸的發現,其中內容很多,集合是咱們開發中算是很經常使用類庫了,良好的熟悉程度能對咱們的開發優化很多。而泛型關聯到反射等等核心內容,若是想深刻學習,也須要認真下功夫,在問題的探究中每每能有更深入的印象。

相關文章
相關標籤/搜索