Java集合與泛型中的幾個陷阱,你掉進了幾個?

帥地發表於2019-05-13

下面我總結了集合、泛型、陣列轉集合等一些常見的陷進,認真看完,相信你絕對有所收穫。

1、List ,List<?> 與 List<Object> 有區別嗎?

說實話,我敢保證很多人是不知道 List, List<?> 與 List<Object> 之間的區別的。

1、我們先來看看 List 與 List<Object>

很多可能覺得 List<Object>的用法與 List 是一樣的,例如很多人認為

List<Object> list;

List list;

這兩種定義方法是一模一樣的,然而他們是不一樣的。看下面一段程式碼

    List<Integer> t1 = new ArrayList<>();
    // 編譯通過
    List t2 = t1;
    //編譯失敗
    List<Object> t3 = t1;

t1 可以賦給 t2, 但是 t1 不能賦給 t3,會丟擲如下異常

Java集合與泛型中的幾個陷阱,你掉進了幾個?

從這裡可以看出

List list;

List<Object> list;

是有區別的,List 變數可以接受任何泛型的變數,而 List 則不可以。

2、我們在看看 Lis<?> 有什麼需要注意的地方:

看下面一段程式碼:

    List<Object> t1 = new ArrayList<>();
    List<?> t2 = t1;
    // 編譯通過
    t2.remove(0);
    t2.clear();
    // 編譯不通過
    t2.add(new Object());

List<?> 是一個泛型,在沒有賦值之前,是可以接受任何集合的賦值的,我想這點大家都知道,但是請注意,賦值之後就不能往裡面新增元素了,提示如下錯誤:

Java集合與泛型中的幾個陷阱,你掉進了幾個?

所以 List<?> 一般用來作為引數來接受外部的集合,或者返回一個不知道具體元素的集合。

List 與 List<?>, List<Object> 的細微區別知道了吧?

2、<? extends T> 與 <? super T>你真的懂嗎?

我們知道泛型 List<T> 只能放置一種型別,如果你採用 List<Object> 來放置多種型別,然後再進行型別強制轉換的話,那會失去了泛型的初衷。

為了能夠放置多種型別,於是有了 <? extend T> 與 <? super T>,下面先說一些你可能原本就知道的知識:

1、對於 <? extends T> a,a 這個變數可以接受 T 及其 T 子類的集合,上界為 T,並且從 a 取出來的型別都會被強制轉換為 T。重點看下面一個例子:

注意:我們先約定 Cat(貓) 繼承自 Animal(動物),RedCat(黑貓) 繼承自 Cat

    List<Animal> animals = new ArrayList<>();
    List<Cat> cats = new ArrayList<>();
    List<RedCat> redCats = new ArrayList<>();
    // 可以通過編譯
    List<? extends  Cat> extendsCat = redCats;
    // 不能通過編譯,因為只能接受 Cat 及其子類的集合
    extendsCat = animals;
        
    // 重點注意:下面三行都不能通過編譯
    extendsCat.add(new Animal());
    extendsCat.add(new Cat());
    extendsCat.add(new RedCat());
    // 重點注意:可以通過編譯
    extendsCat.add(null);

注意,<? extends T>最需要注意的是,就是不能向裡面新增除null之外的其他所有元素,這個和 List<?> 有點類似。

2、現在說說 <? super T>,它和 <? extends T> 有點相反。對於 <? super T> a,a 這個變數可以接受 T 及其 T 父類的集合,下界為 T,並且從 a 取出來的型別都會被強制轉換為 Object。重點看下面一個例子:

    List<Animal> animals = new ArrayList<>();
    List<Cat> cats = new ArrayList<>();
    List<RedCat> redCats = new ArrayList<>();
    // 可以通過編譯
    List<? super  Cat> superCat = animals;
    // 不能通過編譯,因為只能接受 Cat 及其父類的集合
    superCat = redCats;

    // 重點注意:不能通過編譯,只能新增 Cat 及其 Cat 的子類
    superCat.add(new Animal());
    // 重點注意,可以通過編譯
    superCat.add(new Cat());
    superCat.add(new RedCat());
    superCat.add(null);

注意,<? super T>最需要注意的是,在雖然可以接受 T 及其父類的賦值,但是隻能向裡面新增 T 及其 T 的子類

總結

1、List<? extends T> a ,可以把 a 及其 a 的子類賦給 a,從 a 裡取的元素都會被強制轉換為 T 型別,不過需要注意的是,不能向 a 新增任何除 null 外是元素

2、List<? super T> a ,可以把 a 及其 a 的父類賦給 a,從 a 裡取的元素都會被強制轉換為 Object 型別,不過需要注意的是,可以向 a 新增元素,但新增的只能是 T 及其子類元素

3、泛型與過載

我們先來看一道題,你覺得下面這道題能夠編譯通過嗎?

 public class GernerTypes {
    public static void  method(List<Integer> list) {
        System.out.println("List<Integer> list");
    }
    public static void method(List<String> list) {
        System.out.println("List<String> list");
    }
}

答是編譯不通過

兩個方法的引數不同,為什麼會過載不通過呢?

實際上在 Java 的泛型中,泛型只存在於原始碼中,在編譯後的位元組碼中,泛型已經被替換為原生型別了,並且在相應的地方插入了強制轉換的程式碼。為了方便理解,可以看下面的一段程式碼例子:

 // 原始碼
     public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        System.out.println(list.get(0));
    }

編譯之後泛型就不存在了,並且在相應的地方插入了強制轉換的程式碼,編譯之後,我們反編譯的程式碼如下:

     // 反編譯之後的程式碼
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add(1);
        System.out.println((Integer)list.get(0));
    }

這種 編譯之後泛型就不存在了,並且在相應的地方插入了強制轉換程式碼的機制我們也稱之為擦除

所以上面的兩個方法,看似引數不一樣,但是經過編譯擦出之後,他們的引數就是一樣的了,所以編譯不通過。

4、陣列與集合相互轉換時需要注意的點

1、陣列轉集合

大家先看一個例子吧,

    public static void main(String[] args) {
        String[] arr = {"one", "two", "three"};
        // 陣列轉換成集合
        List<String> list = Arrays.asList(arr);
        // 向集合新增元素:編譯正常,但執行時丟擲了異常
        list.add("four");
    }

向集合新增元素丟擲瞭如下異常:

Java集合與泛型中的幾個陷阱,你掉進了幾個?

問題來了,向集合新增元素為啥會丟擲異常呢??

我們先來看一下 Arrays.asList(arr) 方法究竟返回了什麼?

原始碼如下:

Java集合與泛型中的幾個陷阱,你掉進了幾個?

返回的明明是 ArrayList 啊,為啥就不能新增元素呢??

實際上,此 ArrayList 非彼 ArrayList,這個返回的 ArrayList 實際上是 Arrays 的一個內部類。該內部類也是十分簡單,和真實的那個 ArrayList 沒得比,部分原始碼如下:

Java集合與泛型中的幾個陷阱,你掉進了幾個?

而且這個假的 ArrayList 是直接 引用原陣列的,不然你看它的構造器(第二條畫線)

Java集合與泛型中的幾個陷阱,你掉進了幾個?

也就是說,ArrayList 內部是直接引用 arr 陣列,你對 arr 陣列進行改變,也會同時改變到 list 集合。

下面的程式碼證明這一點

    public static void main(String[] args) {
        String[] arr = {"one", "two", "three"};
        // 陣列轉換成集合
        List<String> list = Arrays.asList(arr);
        // 修改 arr
        arr[0] = "0";
        //列印看看
        System.out.println(list.get(0));
    }

列印結果是 “0”。

所以,我們向 list 新增元素肯定失敗,因為 arr 陣列的長度了 3 ,本來就有 3 個元素了,你在向裡面新增第四個元素,肯定是不行的。

所以,在把陣列轉換為集合的過程中,需要特別注意。

建議大家這樣轉換比較安全

List<String> list = new ArrayList<>(Arrays.asList(arr));

2、集合轉陣列

集合轉換為陣列相對比較不苛刻,我就不拉很多原始碼來進行分析了,我只簡單說下幾個需要注意的地方。例如對於下面這個轉換:

    // 集合大小為 size
    List<String> list = new ArrayList<>();
    // 長度為 n 的陣列
    String[] arr = new String[n];
    // 進行轉換
    list.toArray(arr);

1、如果陣列長度比集合小:由於 arr 的長度不夠,所以集合裡的元素不會賦給 arr,而且自己再重新建立一個新陣列反回去。

2、如果陣列長度不小於集合:此時 arr 的長度夠了,所以集合裡的元素直接複製給 arr 陣列,不會重新建立一個新的元素。

一覽原始碼:

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // 重新建立一個陣列來返回去
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    // 長度夠的話直接複製給 a
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
    }

以上這些陷進相信有不少人是不知道了,我把它總結整理了出來,希望大家看完能夠有所收穫。

下面我總結了集合、泛型、陣列轉集合等一些常見的陷進,認真看完,相信你絕對有所收穫。

1、List ,List<?> 與 List<Object> 有區別嗎?

說實話,我敢保證很多人是不知道 List, List<?> 與 List<Object> 之間的區別的。

1、我們先來看看 List 與 List<Object>

很多可能覺得 List<Object>的用法與 List 是一樣的,例如很多人認為

List<Object> list;

List list;

這兩種定義方法是一模一樣的,然而他們是不一樣的。看下面一段程式碼

    List<Integer> t1 = new ArrayList<>();
    // 編譯通過
    List t2 = t1;
    //編譯失敗
    List<Object> t3 = t1;

t1 可以賦給 t2, 但是 t1 不能賦給 t3,會丟擲如下異常

Java集合與泛型中的幾個陷阱,你掉進了幾個?

從這裡可以看出

List list;

List<Object> list;

是有區別的,List 變數可以接受任何泛型的變數,而 List 則不可以。

2、我們在看看 Lis<?> 有什麼需要注意的地方:

看下面一段程式碼:

    List<Object> t1 = new ArrayList<>();
    List<?> t2 = t1;
    // 編譯通過
    t2.remove(0);
    t2.clear();
    // 編譯不通過
    t2.add(new Object());

List<?> 是一個泛型,在沒有賦值之前,是可以接受任何集合的賦值的,我想這點大家都知道,但是請注意,賦值之後就不能往裡面新增元素了,提示如下錯誤:

Java集合與泛型中的幾個陷阱,你掉進了幾個?

所以 List<?> 一般用來作為引數來接受外部的集合,或者返回一個不知道具體元素的集合。

List 與 List<?>, List<Object> 的細微區別知道了吧?

2、<? extends T> 與 <? super T>你真的懂嗎?

我們知道泛型 List<T> 只能放置一種型別,如果你採用 List<Object> 來放置多種型別,然後再進行型別強制轉換的話,那會失去了泛型的初衷。

為了能夠放置多種型別,於是有了 <? extend T> 與 <? super T>,下面先說一些你可能原本就知道的知識:

1、對於 <? extends T> a,a 這個變數可以接受 T 及其 T 子類的集合,上界為 T,並且從 a 取出來的型別都會被強制轉換為 T。重點看下面一個例子:

注意:我們先約定 Cat(貓) 繼承自 Animal(動物),RedCat(黑貓) 繼承自 Cat

    List<Animal> animals = new ArrayList<>();
    List<Cat> cats = new ArrayList<>();
    List<RedCat> redCats = new ArrayList<>();
    // 可以通過編譯
    List<? extends  Cat> extendsCat = redCats;
    // 不能通過編譯,因為只能接受 Cat 及其子類的集合
    extendsCat = animals;
        
    // 重點注意:下面三行都不能通過編譯
    extendsCat.add(new Animal());
    extendsCat.add(new Cat());
    extendsCat.add(new RedCat());
    // 重點注意:可以通過編譯
    extendsCat.add(null);

注意,<? extends T>最需要注意的是,就是不能向裡面新增除null之外的其他所有元素,這個和 List<?> 有點類似。

2、現在說說 <? super T>,它和 <? extends T> 有點相反。對於 <? super T> a,a 這個變數可以接受 T 及其 T 父類的集合,下界為 T,並且從 a 取出來的型別都會被強制轉換為 Object。重點看下面一個例子:

    List<Animal> animals = new ArrayList<>();
    List<Cat> cats = new ArrayList<>();
    List<RedCat> redCats = new ArrayList<>();
    // 可以通過編譯
    List<? super  Cat> superCat = animals;
    // 不能通過編譯,因為只能接受 Cat 及其父類的集合
    superCat = redCats;

    // 重點注意:不能通過編譯,只能新增 Cat 及其 Cat 的子類
    superCat.add(new Animal());
    // 重點注意,可以通過編譯
    superCat.add(new Cat());
    superCat.add(new RedCat());
    superCat.add(null);

注意,<? super T>最需要注意的是,在雖然可以接受 T 及其父類的賦值,但是隻能向裡面新增 T 及其 T 的子類

總結

1、List<? extends T> a ,可以把 a 及其 a 的子類賦給 a,從 a 裡取的元素都會被強制轉換為 T 型別,不過需要注意的是,不能向 a 新增任何除 null 外是元素

2、List<? super T> a ,可以把 a 及其 a 的父類賦給 a,從 a 裡取的元素都會被強制轉換為 Object 型別,不過需要注意的是,可以向 a 新增元素,但新增的只能是 T 及其子類元素

3、泛型與過載

我們先來看一道題,你覺得下面這道題能夠編譯通過嗎?

 public class GernerTypes {
    public static void  method(List<Integer> list) {
        System.out.println("List<Integer> list");
    }
    public static void method(List<String> list) {
        System.out.println("List<String> list");
    }
}

答是編譯不通過

兩個方法的引數不同,為什麼會過載不通過呢?

實際上在 Java 的泛型中,泛型只存在於原始碼中,在編譯後的位元組碼中,泛型已經被替換為原生型別了,並且在相應的地方插入了強制轉換的程式碼。為了方便理解,可以看下面的一段程式碼例子:

 // 原始碼
     public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        System.out.println(list.get(0));
    }

編譯之後泛型就不存在了,並且在相應的地方插入了強制轉換的程式碼,編譯之後,我們反編譯的程式碼如下:

     // 反編譯之後的程式碼
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add(1);
        System.out.println((Integer)list.get(0));
    }

這種 編譯之後泛型就不存在了,並且在相應的地方插入了強制轉換程式碼的機制我們也稱之為擦除

所以上面的兩個方法,看似引數不一樣,但是經過編譯擦出之後,他們的引數就是一樣的了,所以編譯不通過。

4、陣列與集合相互轉換時需要注意的點

1、陣列轉集合

大家先看一個例子吧,

    public static void main(String[] args) {
        String[] arr = {"one", "two", "three"};
        // 陣列轉換成集合
        List<String> list = Arrays.asList(arr);
        // 向集合新增元素:編譯正常,但執行時丟擲了異常
        list.add("four");
    }

向集合新增元素丟擲瞭如下異常:

Java集合與泛型中的幾個陷阱,你掉進了幾個?

問題來了,向集合新增元素為啥會丟擲異常呢??

我們先來看一下 Arrays.asList(arr) 方法究竟返回了什麼?

原始碼如下:

Java集合與泛型中的幾個陷阱,你掉進了幾個?

返回的明明是 ArrayList 啊,為啥就不能新增元素呢??

實際上,此 ArrayList 非彼 ArrayList,這個返回的 ArrayList 實際上是 Arrays 的一個內部類。該內部類也是十分簡單,和真實的那個 ArrayList 沒得比,部分原始碼如下:

Java集合與泛型中的幾個陷阱,你掉進了幾個?

而且這個假的 ArrayList 是直接 引用原陣列的,不然你看它的構造器(第二條畫線)

Java集合與泛型中的幾個陷阱,你掉進了幾個?

也就是說,ArrayList 內部是直接引用 arr 陣列,你對 arr 陣列進行改變,也會同時改變到 list 集合。

下面的程式碼證明這一點

    public static void main(String[] args) {
        String[] arr = {"one", "two", "three"};
        // 陣列轉換成集合
        List<String> list = Arrays.asList(arr);
        // 修改 arr
        arr[0] = "0";
        //列印看看
        System.out.println(list.get(0));
    }

列印結果是 “0”。

所以,我們向 list 新增元素肯定失敗,因為 arr 陣列的長度了 3 ,本來就有 3 個元素了,你在向裡面新增第四個元素,肯定是不行的。

所以,在把陣列轉換為集合的過程中,需要特別注意。

建議大家這樣轉換比較安全

List<String> list = new ArrayList<>(Arrays.asList(arr));

2、集合轉陣列

集合轉換為陣列相對比較不苛刻,我就不拉很多原始碼來進行分析了,我只簡單說下幾個需要注意的地方。例如對於下面這個轉換:

    // 集合大小為 size
    List<String> list = new ArrayList<>();
    // 長度為 n 的陣列
    String[] arr = new String[n];
    // 進行轉換
    list.toArray(arr);

1、如果陣列長度比集合小:由於 arr 的長度不夠,所以集合裡的元素不會賦給 arr,而且自己再重新建立一個新陣列反回去。

2、如果陣列長度不小於集合:此時 arr 的長度夠了,所以集合裡的元素直接複製給 arr 陣列,不會重新建立一個新的元素。

一覽原始碼:

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // 重新建立一個陣列來返回去
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    // 長度夠的話直接複製給 a
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
    }

以上這些陷進相信有不少人是不知道了,我把它總結整理了出來,如果大家看完覺得有收穫,不妨點個底部小卡片 + 點贊鼓勵我一下?

相關文章