前言
最近在網上看到一個問題,情況類似如下(記為問題1):
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,但是第二輸出還沒到就已經報錯,完整運行結果如下:
java.lang.ArrayStoreException:數組存儲異常。下面具體就集合方法與泛型探究上面的問題。
過程
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>
最后獲取的是一個Arrays的一個靜態內部類ArrayList對象,在內部類中重寫了contains方法:
@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);
運行后,控制台輸出如下:
我們發現,不論是修改數組,還是修改集合,另一方都會相應改變。
小結
一開始以為是一個小問題,漸漸的發現,其中內容不少,集合是我們開發中算是很常用類庫了,良好的熟悉程度能對我們的開發優化不少。而泛型關聯到反射等等核心內容,如果想深入學習,也需要認真下功夫,在問題的探究中往往能有更深刻的印象。