Java泛型的那些事

淵渟嶽發表於2022-02-28

1.泛型概述

1.1.為什麼使用泛型

沒有泛型,在編寫程式碼時只能使用具體型別或Object型別,無法做到使用者想要使用什麼型別就是型別。比如:建立一個方法,形參需要指定需要使用的資料型別,在建立方法之初就已經決定了該方法可以處理的資料型別,這大大限制了程式設計的靈活性。正因如此,才出現了在使用時才決定具體型別是什麼的泛型程式設計。

1.2.泛型是什麼

泛:廣泛、普遍,非具體的東西,泛型就是定義之初用符號表示不具體的型別,在使用的時候才動態地指定具體的型別。更應該明白這種泛型程式設計設計思想,使用泛型帶來的好處是程式碼更加簡潔、更加靈活、使程式更加健壯(編譯期沒警告,執行期不會出現類強轉異常--ClassCastException)。

2.泛型介面、類、方法

泛型允許在定義介面、類、方法時使用,將在宣告變數、建立物件、呼叫方法時動態地指定。

2.1.泛型介面

定義泛型介面:比如集合中的List介面

// 定義介面時指定泛型:E,E型別在介面中就可以作為型別使用
public interface List<E> extends Collection<E>{
    ……
    boolean add(E e);
    Iterator<E> iterator();
    ……
}
// 定義介面時指定泛型:K 和 V,K和V型別在介面中就可以作為型別使用
public interface Map<K,V>{
    ……
    Set<K> keySet();
    Collection<V> values();
    Set<Map.Entry<K, V>> entrySet();
    ……
}

使用泛型介面:List介面的泛型型別E,在使用時指定為具體型別String

public static void main(String[] args) {
    List<String> list = new ArrayList<>();// 指定泛型型別E=String
    list.add("我只認識字串");//boolean add(E e); 等價於boolean add(String e);
    Iterator<String> iterator = list.iterator();//Iterator<E> iterator();

    while (iterator.hasNext()){
        String next = iterator.next();//不需要強轉為String
        System.out.println(next);
    }
}

關於泛型介面Map<K,V> 集合怎麼用,就自行編寫感受下。

2.2.泛型類

普通泛型類

定義泛型類

public class DemoFx<D> {
    private D dd;
    public D getDd(){
        return this.dd;
    }
    public void setDd(D dd){
        this.dd = dd;
    }
}

使用泛型類

public static void main(String[] args) {
    DemoFx<String> stringDemoFx = new DemoFx<>();
    stringDemoFx.setDd("我是字串型別");
    System.out.println(stringDemoFx.getDd());
}

泛型類的繼承與實現

定義泛型類:以ArrayList 類為例,繼承泛型抽象類和實現泛型介面:

public class ArrayList<E> extends AbstractList<E> 
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
    ……
    public E get(int index) {
        rangeCheck(index);
        return elementData(index);
    }
    ……
}

使用泛型類

public static void main(String[] args) {
    List<String> list = new ArrayList<String>();// 指定泛型型別E=String
    list.add("我只認識字串");
    String s = list.get(0);// 返回值為String
}

2.3.泛型方法

定義泛型方法:還是ArrayList案例

public class ArrayList<E> extends AbstractList<E> 
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
    ……
    public <T> T[] toArray(T[] a) {
        if (a.length < size)
            // Make a new array of as runtime type, but my contents:
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }
    ……
}

使用泛型方法:public <T> T[] toArray(T[] a)

public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    list.add("s1");
    list.add("s2");
    list.add("sn");
    // public <T> T[] toArray(T[] a) 
    String[] strings = list.toArray(new String[list.size()]);
    System.out.println(Arrays.asList(strings));
}

3.型別萬用字元

3.1.使用型別萬用字元

萬用字元表示符號是問號<?>,它是未知型別,可以匹配任何型別,也稱為無界萬用字元

對比”萬用字元“和”泛型“建立的方法

// 萬用字元定義
public void foreach(List<?> list){
    for (int i =0 ;i<list.size();i++) {
        Object o = list.get(i);
        System.out.println(o.toString());
    }
}
// 泛型定義
public <T> void foreach2(List<T> list){
    for(T t : list){
        System.out.println(t.toString());
    }
}
// 使用
public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    list.add("s1");
    list.add("s2");
    list.add("sn");
    Demo demo = new Demo();
    demo.foreach(list); // 萬用字元
    demo.foreach2(list); // 泛型
}

萬用字元和泛型都可以實現相同的效果,並且泛型方法還可以使用本身定義的泛型型別,而萬用字元的”?“不可以當作資料型別來使用,所以萬用字元方法案例中只能用Object來接收list的元素,這也是萬用字元的缺點:無法確定未知型別是什麼型別。

所以萬用字元的出現到底有什麼用呢?

萬用字元為泛型的一種特例,無需定義既可在形參中使用的未知型別。

泛型和萬用字元的區別

  • Java編譯器把泛型【T】推斷成T型別,在程式碼塊中允許出現 T型別變數;而把萬用字元【?】推斷成未知型別,不存在 ?型別變數;
  • Class<T>需要依賴於T,需要在方法宣告時指定<T>,而Class<?>則不需要;

這樣可能更好理解泛型和萬用字元:泛型 強調的是型別,萬用字元 強調的是符號

Class<?> 表示任意型別,但又不等同於Class<Object>,前者在型別不匹配的情況下只能夠插入null,但是後者可以插入Object或任何Object物件的子類。

例如:不能往List<?> list裡新增任意型別的物件,除了null

image

3.2.型別上限

萬用字元上限:<? extends Demo> ,萬用字元【?】的上限是Demo型別,既是<? extends Demo> 的範圍是Demo或其子類型別。

泛型上限:<T extends Demo> ,和萬用字元理解一樣。型別上限如圖

image

案例

建立三個類DemoFather、Demo、DemoChildren,關係如上圖

public class DemoTest {

    public static void main(String[] args) {
        List<DemoChildren> demoChildrens = new ArrayList<>();
        demoChildrens.add(new DemoChildren());
        demoChildrens.add(new DemoChildren());

        DemoTest test = new DemoTest();
        test.testDemo(demoChildrens); // 萬用字元
        test.testDemo2(demoChildrens);// 泛型

    }

    // 萬用字元上限:控制list 集合允許的型別範圍為Demo或其子類
    public void testDemo(List<? extends Demo> list){
        // 若無上限,這裡只能用Object型別代替Demo型別
        for (Demo demo : list){
            System.out.println(demo.toString());
        }
    }

    // 泛型上限:控制list 集合允許的型別範圍為Demo或其子類
    public <T extends Demo> void testDemo2(List<T> list){
        for (T t : list){
            System.out.println(t.toString());
        }
        // or
        for(Demo demo:list){
            System.out.println(demo.toString());
        }
    }

}

泛型的上限是在定義時確定上限<T extends Demo>;萬用字元直接在形參上確定上限<? extends Demo>。其實都很好理解,型別上限就是在一般寫法的基礎上加入範圍“上限”,既是 extends xxx。

原始碼的一些例子

// 介面泛型上限
public interface ObservableArray<T extends ObservableArray<T>> extends Observable {……}
// 抽象類泛型上限
public abstract class ArrayListenerHelper<T extends ObservableArray<T>> extends ExpressionHelperBase {……}
public abstract class CellBehaviorBase<T extends Cell> extends BehaviorBase<T> {……}
// 方法泛型上限
public static <T extends Number> ReadOnlyLongProperty readOnlyLongProperty(final ReadOnlyProperty<T> property) {……}
// 萬用字元上限
void putAll(Map<? extends K, ? extends V> m);

3.3.型別下限

萬用字元下限:<? super Demo> ,萬用字元【?】的下限是Demo型別,既是<? super Demo> 的範圍是Demo的父類型別。

泛型下限:。主要是因為型別下限會令人困惑並且不是特別有用。為什麼型別引數沒有下限的一些解釋:http://www.angelikalanger.com/GenericsFAQ/FAQSections/TypeParameters.html#FAQ107

image

public static void main(String[] args) {
    Demo demo = new Demo();
    List<Demo> demos = new ArrayList<>();
    demos.add(demo);
    DemoTest test = new DemoTest();
    test.testSuper(demos);

    DemoFather demoFather = new DemoFather();
    List<DemoFather> demoFathers = new ArrayList<>();
    demoFathers.add(demoFather);
    DemoTest test2 = new DemoTest();
    test2.testSuper(demoFathers);
}

public void testSuper(List<? super Demo> list){
    // 雖然有下限,但無法直接使用Demo型別接收引數
    for (Object obj : list){
        System.out.println(obj.toString());
    }
}

雖然有下限,但無法直接使用Demo型別接收引數。這就像“向上轉型”和“向下轉型”,向上轉型是自動的,向下轉型需要強轉;型別上限可以使用最大型別(父類)接收比它小的型別(子類),型別下限不可以使用最小型別(子類)接受可能比它大的型別(父類)。

原始碼的一些例子

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    ……
    public void sort(Comparator<? super E> c) {
        final int expectedModCount = modCount;
        Arrays.sort((E[]) elementData, 0, size, c);
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
        modCount++;
    }
}

public class Arrays {
    public static <T> void sort(T[] a, int fromIndex, int toIndex,
                                Comparator<? super T> c) {
        if (c == null) {
            sort(a, fromIndex, toIndex);
        } else {
            rangeCheck(a.length, fromIndex, toIndex);
            if (LegacyMergeSort.userRequested)
                legacyMergeSort(a, fromIndex, toIndex, c);
            else
                TimSort.sort(a, fromIndex, toIndex, c, null, 0, 0);
        }
    }
}

關於泛型字母E、K、V、T是什麼?

  • E表示Element,
  • K表示Key,
  • V表示Value,
  • T表示Type,
  • N表示Number,
  • ? 表示 未知型別

除了【?】,其他泛型符號你寫成字串都可以,但要注意可讀性,一般都是使用單個大寫字母表示,不然程式碼反而不簡潔、不易閱讀。

4.泛型實現原理--型別擦除

把一個具有泛型資訊的物件 賦給 另一個沒有泛型資訊的變數引用,型別資訊都將被擦除

案例一:不指定泛型上限,型別擦除後為Object

public static void main(String[] args) {
    // 定義集合泛型E = String
    List<String> stringArrayList = new ArrayList<>();
    stringArrayList.add("test1");
    // 獲取到的型別為:String
    String s = stringArrayList.get(0);
    // 把帶有泛型資訊的stringArrayList 物件賦給不確定泛型的List
    List listObject = stringArrayList;
    // listObject 只知道get的型別為Object,而不是String
    Object obj = listObject.get(0);
}

案例二:指定泛型上限,型別擦除後為上限的型別

public class DemoFather {}
public class Demo extends DemoFather{}
public class DemoChildren<T extends DemoFather> {
    private T t;
    public T getT(){
        return this.t;
    }
    public void setT(T t){
        this.t= t;
    }
}
// 測試public class DemoTest {
 public static void main(String[] args) {
        //class DemoChildren<T extends DemoFather>,指定泛型T=Demo型別
        DemoChildren<Demo> demoChildren = new DemoChildren<Demo>();
        // 拿到的方法型別確實是T=Demo型別
        Demo demo = demoChildren.getT();
        // 把帶有泛型資訊的 demoChildren 物件賦給不確定泛型的demoChildren2
        DemoChildren demoChildren2 =demoChildren;
        // 再來獲取方法的型別時,變為了上限的DemoFather型別
        DemoFather demoFather = demoChildren2.getT();
    }
}

結論:

指定泛型上限時,型別擦除後為上限的型別;反之是Object型別,因為Java中所有類都預設繼承了Object類。

所以案例二的泛型類在編譯階段是長這樣的

public class DemoChildren {
    private DemoFather t;
    public DemoFather getT(){
        return this.t;
    }
    public void setT(DemoFather t){
        this.t= t;
    }
}
// 原來的泛型類,對比一下
public class DemoChildren<T extends DemoFather> {
    private T t;
    public T getT(){
        return this.t;
    }
    public void setT(T t){
        this.t= t;
    }
}

image

Java往期文章
Java全棧學習路線、學習資源和麵試題一條龍
我心裡優秀架構師是怎樣的?
免費下載經典程式設計書籍

image

相關文章