8. 泛型

寫在之前

以下是《瘋狂Java講義》中的一些知識,如有錯誤,煩請指正。


泛型初衷

  • 集合對元素類型沒有任何限制,這樣可能引發一些問題:例如想創建一個只能保存Dog對象的集合,但程序也可以輕易地將Cat對象“丟”進去,所以可能引發異常。
  • 由于把對象“丟進”集合時,集合丟失了對象的狀態信息,集合只知道它盛裝的是Object,因此取出集合元素后通常還需要進行強制類型轉換。這種強制類型轉換既會增加編程的復雜度、也可能引發ClassCastException

泛型就是允許在定義類、接口指定類型形參,這個類型形參在將在聲明變量、創建對象時確定(即傳入實際的類型參數,也可稱為類型實參)。

import java.util.*;
public class GenericList
{
    public static void main(String[] args)
    {
        // 創建一個只想保存字符串的List集合
        List<String> strList = new ArrayList<String>();  // 構造器后不需要帶完整的泛型信息
        strList.add("瘋狂Java講義");
        strList.add("瘋狂Android講義");
        // 下面代碼將引起編譯錯誤
        strList.add(5);    // ②
        strList.forEach(str -> System.out.println(str.length())); // ③
    }
}

可以為任何類、接口添加泛型聲明。

public class Apple<T>
{
    // 使用T類型形參定義實例變量
    private T info;
    public Apple(){}
    // 下面方法中使用T類型形參來定義構造器
    public Apple(T info)
    {
        this.info = info;
    }
    public void setInfo(T info)
    {
        this.info = info;
    }
    public T getInfo()
    {
        return this.info;
    }
    public static void main(String[] args)
    {
        // 由于傳給T形參的是String,所以構造器參數只能是String
        Apple<String> a1 = new Apple<>("蘋果");
        System.out.println(a1.getInfo());
        // 由于傳給T形參的是Double,所以構造器參數只能是Double或double
        Apple<Double> a2 = new Apple<>(5.67);
        System.out.println(a2.getInfo());
    }
}

泛型類派生子類

當創建帶泛型聲明的接口、父類之后,可以為該接口創建實現類或派生子類。注意:使用這些接口、父類時,不能再包含類型形參。應該為類型形參傳入實際的類型。使用類、接口時也可以不為類型形參傳入實際的類型參數。

public class A2 extends Apple
{
    // 重寫父類的方法
    public String getInfo()
    {
        // super.getInfo()方法返回值是Object類型,
        // 所以加toString()才返回String類型
        return super.getInfo().toString();
    }
}

并不存在泛型類
雖然可以把ArrayList<String>類當成ArrayList的子類,事實上ArrayList<String>類也確實是一種特殊的ArrayList類,這個ArrayList<String>對象只能添加String對象作為集合元素。但實際上,系統并沒有為ArrayList<String>生成新的class文件,而且也不會把ArrayList<String>當成新類來處理。

系統中不會真正生成泛型類,所以instanceof運算符后不能使用泛型類,也不能在靜態變量聲明中使用類型形參

類型通配符

如果FOO是Bar的一個子類型(子類或者子接口),而G是具有泛型聲明的類或者接口,G<Foo>并不是G<Bar>的子類型。數組和泛型不同,假設Foo是Bar的一個子類型,那么Foo[]依然是Bar[]的子類型。
List<String>對象不能被當成List<Object>對象使用,也就是說:List<String>類并不是List<Object>類的子類。

使用類型通配符
為了表示各種泛型List的父類,我們需要使用類型通配符,類型通配符是一個問號(?),將一個問號作為類型實參傳給List集合,寫作:List<?>(意思是未知類型元素的List)。這個問號(?)被稱為通配符,它的元素類型可以匹配任何類型

類型通配符的上限
使用List<?>這種形式是,即表明這個List集合可以是任何泛型List的父類。但還有一種特殊的情形,我們不想這個List<?>是任何泛型List的父類,只想表示它是某一類泛型List的父類。

//Shape類型的子類型,Shape稱為通配符的上限
List<? extends Shape>

import java.util.*;
public class Canvas
{
//  // 同時在畫布上繪制多個形狀
//  public void drawAll(List<Shape> shapes)
//  {
//      for (Shape s : shapes)
//      {
//          s.draw(this);
//      }
//  }
// 比較臃腫
//  public void drawAll(List<?> shapes)
//  {
//      for (Object obj : shapes)
//      {
//          Shape s = (Shape)obj;
//          s.draw(this);
//      }
//  }
    // 同時在畫布上繪制多個形狀,使用被限制的泛型通配符
    public void drawAll(List<? extends Shape> shapes)
    {
        for (Shape s : shapes)
        {
            s.draw(this);
        }
    }

    public static void main(String[] args)
    {
        List<Circle> circleList = new ArrayList<Circle>();
        Canvas c = new Canvas();
        // 由于List<Circle>并不是List<Shape>的子類型,
        // 所以下面代碼引發編譯錯誤
        c.drawAll(circleList);
    }
}

類型形參的上限
Java泛型不僅允許在使用通配符形參時設定類型上限,也可以在定義類型形參時設定上限,用于表示創給該類型形參的實際類型必須是該上限類型,或是該上限類型的子類。例如: Apple<T extends Number>

public class Apple<T extends Number>
{
    T col;
    public static void main(String[] args)
    {
        Apple<Integer> ai = new Apple<>();
        Apple<Double> ad = new Apple<>();
        // 下面代碼將引起編譯異常,下面代碼試圖把String類型傳給T形參
        // 但String不是Number的子類型,所以引發編譯錯誤
        Apple<String> as = new Apple<>();
    }
}

泛型方法

如果定義類、接口是沒有使用類型形參,但定義方法時想自己定義類型形參,這也是可以的。泛型方法的方法簽名比普通方法的方法簽名多了類型形參聲明。

修飾符 <T, S> 返回值類型 方法名(形參列表)
{
}

下面看一個只定義了一個T類型形參的泛型方法

import java.util.*;
public class GenericMethodTest
{
    // 聲明一個泛型方法,該泛型方法中帶一個T類型形參,
    static <T> void fromArrayToCollection(T[] a, Collection<T> c)
    {
        for (T o : a)
        {
            c.add(o);
        }
    }
    public static void main(String[] args)
    {
        Object[] oa = new Object[100];
        Collection<Object> co = new ArrayList<>();
        // 下面代碼中T代表Object類型
        fromArrayToCollection(oa, co);
        String[] sa = new String[100];
        Collection<String> cs = new ArrayList<>();
        // 下面代碼中T代表String類型
        fromArrayToCollection(sa, cs);
        // 下面代碼中T代表Object類型
        fromArrayToCollection(sa, co);
        Integer[] ia = new Integer[100];
        Float[] fa = new Float[100];
        Number[] na = new Number[100];
        Collection<Number> cn = new ArrayList<>();
        // 下面代碼中T代表Number類型,注意只比較泛型參數!!
        fromArrayToCollection(ia, cn);
        // 下面代碼中T代表Number類型
        fromArrayToCollection(fa, cn);
        // 下面代碼中T代表Number類型
        fromArrayToCollection(na, cn);
        // 下面代碼中T代表Object類型
        fromArrayToCollection(na, co);
        // 下面代碼中T代表String類型,但na是一個Number數組,
        // 因為Number既不是String類型,
        // 也不是它的子類,所以出現編譯錯誤
//      fromArrayToCollection(na, cs);
    }
}

為了讓編譯器能準確判斷,不要制造迷惑。來看一個定義了兩個類型形參的錯誤例子

import java.util.*;
public class ErrorTest
{
    // 聲明一個泛型方法,該泛型方法中帶一個T類型形參
    static <T> void test(Collection<T> from, Collection<T> to)
    {
        for (T ele : from)
        {
            to.add(ele);
        }
    }
    public static void main(String[] args)
    {
        List<Object> as = new ArrayList<>();
        List<String> ao = new ArrayList<>();
        // 下面代碼將產生編譯錯誤
        test(as , ao);
    }
}

上面的程序面臨選擇時就無法識別T所代表的實際類型。

泛型方法和通配符的區別
大多數時候都可以使用泛型方法代替類型通配符。
使用通配符:在不同的調用點傳入不同的實際類型;
泛型方法:允許類型形參被用來表示方法的一個或多個參數之間的類型依賴關系。
類型通配符既可以在方法簽名中定義形參的類型,也可以用于定義變量的類型;但泛型方法中的類型形參必須在對應方法中顯示聲明。

菱形語法與泛型構造器

class Foo
{
    public <T> Foo(T t)
    {
        System.out.println(t);
    }
}
public class GenericConstructor
{
    public static void main(String[] args)
    {
        // 泛型構造器中的T參數為String。
        new Foo("瘋狂Java講義");
        // 泛型構造器中的T參數為Integer。
        new Foo(200);
        // 顯式指定泛型構造器中的T參數為String,
        // 傳給Foo構造器的實參也是String對象,完全正確。
        new <String> Foo("瘋狂Android講義");
        // 顯式指定泛型構造器中的T參數為String,
        // 但傳給Foo構造器的實參是Double對象,下面代碼出錯
        new <String> Foo(12.3);
    }
}

菱形語法允許調用構造器時在構造器后使用一對尖括號來代表泛型信息。但如果程序顯示制定了泛型構造器中聲明的類型形參的實際類型,則不可使用菱形語法(只使用尖括號)。

class MyClass<E>
{
    public <T> MyClass(T t)
    {
        System.out.println("t參數的值為:" + t);
    }
}
public class GenericDiamondTest
{
    public static void main(String[] args)
    {
        // MyClass類聲明中的E形參是String類型。
        // 泛型構造器中聲明的T形參是Integer類型
        MyClass<String> mc1 = new MyClass<>(5);
        // 顯式指定泛型構造器中聲明的T形參是Integer類型,
        MyClass<String> mc2 = new <Integer> MyClass<String>(5);
        // MyClass類聲明中的E形參是String類型。
        // 如果顯式指定泛型構造器中聲明的T形參是Integer類型
        // 此時就不能使用"菱形"語法,下面代碼是錯的。
//      MyClass<String> mc3 = new <Integer> MyClass<>(5);
    }
}

設定通配符下限
Java集合框架中的TreeSet<E>有一個構造器也用到了這種設定通配符下限的語法,如下所示:
TreeSet(Comparator<? super E> c)

<? super Type>表示它必須是Type本身或者其父類。

import java.util.*;
public class MyUtils
{
    // 下面dest集合元素類型必須與src集合元素類型相同,或是其父類
    public static <T> T copy(Collection<? super T> dest
        , Collection<T> src)
    {
        T last = null;
        for (T ele  : src)
        {
            last = ele;
            dest.add(ele);
        }
        return last;
    }
    public static void main(String[] args)
    {
        List<Number> ln = new ArrayList<>();
        List<Integer> li = new ArrayList<>();
        li.add(5);
        // 此處可準確的知道最后一個被復制的元素是Integer類型
        // 與src集合元素的類型相同
        Integer last = copy(ln , li);    // ①
        System.out.println(ln);
    }
}
//TreeSet(Comparator<? super E> c)
import java.util.*;
public class TreeSetTest
{
    public static void main(String[] args)
    {
        // Comparator的實際類型是TreeSet的元素類型的父類,滿足要求
        TreeSet<String> ts1 = new TreeSet<>(
            new Comparator<Object>()
        {
            public int compare(Object fst, Object snd)
            {
                return hashCode() > snd.hashCode() ? 1
                    : hashCode() < snd.hashCode() ? -1 : 0;
            }
        });
        ts1.add("hello");
        ts1.add("wa");
        // Comparator的實際類型是TreeSet元素的類型,滿足要求
        TreeSet<String> ts2 = new TreeSet<>(
            new Comparator<String>()
        {
            public int compare(String first, String second)
            {
                return first.length() > second.length() ? -1
                    : first.length() < second.length() ? 1 : 0;
            }
        });
        ts2.add("hello");
        ts2.add("wa");
        System.out.println(ts1);
        System.out.println(ts2);
    }
}

Java8改進的類型推斷

  • 可調用方法的上下文推斷類型參數的目標類型
  • 可在方法的調用鏈中,將推斷得到的類型參數傳遞到最后一個方法。
class MyUtil<E>
{
    public static <Z> MyUtil<Z> nil()
    {
        return null;
    }
    public static <Z> MyUtil<Z> cons(Z head, MyUtil<Z> tail)
    {
        return null;
    }
    E head()
    {
        return null;
    }
}
public class InferenceTest
{
    public static void main(String[] args)
    {
        // 可以通過方法賦值的目標參數來推斷類型參數為String
        MyUtil<String> ls = MyUtil.nil();
        // 無需使用下面語句在調用nil()方法時指定類型參數的類型
        MyUtil<String> mu = MyUtil.<String>nil();
        // 可調用cons方法所需的參數類型來推斷類型參數為Integer
        MyUtil.cons(42, MyUtil.nil());
        // 無需使用下面語句在調用nil()方法時指定類型參數的類型
        MyUtil.cons(42, MyUtil.<Integer>nil());

        // 希望系統能推斷出調用nil()方法類型參數為String類型,
        // 但實際上Java 8依然推斷不出來,所以下面代碼報錯
//      String s = MyUtil.nil().head();
        String s = MyUtil.<String>nil().head();
    }
}

擦除和轉換

在嚴格的泛型代碼里,帶泛型聲明的類總應該帶著類型參數。但為了與老的Java代碼保持一致,也允許在使用帶泛型聲明的類時不指定類型參數。如果沒有為這個泛型類指定類型參數,則該類型參數被稱作一個raw type(原始類型),默認是該聲明該參數時指定的第一個上限類型。

當把一個具有泛型信息的對象賦給另一個沒有泛型信息的變量時,則所有在尖括號之間的類型信息都被扔掉了。比如說一個List<String>類型被轉換為List,則該List對集合元素的類型檢查變成了成類型變量的上限(即Object),這種情況被為擦除。

class Apple<T extends Number>
{
    T size;
    public Apple()
    {
    }
    public Apple(T size)
    {
        this.size = size;
    }
    public void setSize(T size)
    {
        this.size = size;
    }
    public T getSize()
    {
        return this.size;
    }
}
public class ErasureTest
{
    public static void main(String[] args)
    {
        Apple<Integer> a = new Apple<>(6);
        // a的getSize方法返回Integer對象
        Integer as = a.getSize();
        // 把a對象賦給Apple變量,丟失尖括號里的類型信息
        Apple b = a; 
        // b只知道size的類型是Number
        Number size1 = b.getSize();
        // 下面代碼引起編譯錯誤
        Integer size2 = b.getSize();  
    }
}
import java.util.*;
public class ErasureTest2
{
    public static void main(String[] args)
    {
        List<Integer> li = new ArrayList<>();
        li.add(6);
        li.add(9);
        List list = li;
        // 下面代碼引起“未經檢查的轉換”的警告,編譯、運行時完全正常
        List<String> ls = list;     // ①
        // 但只要訪問ls里的元素,如下面代碼將引起運行時異常。
        System.out.println(ls.get(0));
    }
}

泛型與數組

數組元素的類型不能包含類型變量或者類型形參。

List<?> lsa = new ArrayList<?>[10];
Object[] oa = lsa;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li;
Object target = lsa[1].get(0);
if (target instanceof String){
String s = (String) target;
}

類似的,創建元素類型是類型變量的數組對象也將導致編譯錯誤。由于類型變量在運行時并不存在,而編譯器無法確定實際類型是什么。

<T> T[] makeArray(Collection<T> coll){
    //下面代碼將導致編譯錯誤
    return new T[coll.size()];
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 開發人員在使用泛型的時候,很容易根據自己的直覺而犯一些錯誤。比如一個方法如果接收List作為形式參數,那么如果嘗試...
    時待吾閱讀 1,072評論 0 3
  • object 變量可指向任何類的實例,這讓你能夠創建可對任何數據類型進程處理的類。然而,這種方法存在幾個嚴重的問題...
    CarlDonitz閱讀 934評論 0 5
  • 一、泛型簡介1.引入泛型的目的 了解引入泛型的動機,就先從語法糖開始了解。 語法糖 語法糖(Syntactic S...
    Android進階與總結閱讀 1,033評論 0 9
  • 第8章 泛型 通常情況的類和函數,我們只需要使用具體的類型即可:要么是基本類型,要么是自定義的類。但是在集合類的場...
    光劍書架上的書閱讀 2,158評論 6 10
  • 突然有點失去寫作的靈感,想來點靈感寫點什么好? 春節過得匆匆的,時間是一去不復返的。 我依然一早去吃早餐,即使上的...
    畫心心語閱讀 401評論 5 1