泛型

1、泛型概述:

  1. 泛型是一種把類型明確的工做推遲到建立對象或者調用方法的時候纔去明確的特殊的類型。也叫作 參數化類型,把類型看成參數同樣的傳遞。
  2. 格式:
    <數據類型> 注意:此處的數據類型只能是引用類型。設計模式

    (八大引用類型:Short、Integer、Long、Character、Boolean、Float、Double、Byte)+String
    
        (還包括一些自定義類型Student等)
  3. 使用泛型的好處:
    A: 把運行時期的問題提早到了編譯期間。
    B: 避免了強制類型轉換。
    C: 優化了程序設計,解決了黃色警告線。

2、簡單泛型:

有許多緣由促成了泛型的出現,而最引人注目的一個緣由,就是爲了創造容器類。有些狀況下,咱們確實但願容器可以同時持有多種類型的對象。可是,一般而言咱們只會使用容器來存儲一種類型的對象。泛型的主要目的之一就是用來指定容器要持有什麼類型的,所以與其使用Object,咱們更喜歡暫時不指定類型,而是稍後再決定具體使用什麼類型。要達到這個目的,須要使用類型參數,用尖括號括住,放在類名後面。而後在使用這個類的時候,再用實際的類型替換此類型參數。在下面的例子中,T就是類型參數:安全

class Tt{}
public class Holder<T> {
    private T a;
    public Holder(T a){this.a = a;}
    public T get(){return a;}
    public static void main(String[] args) {
        Tt t = new Tt();
        Holder<Tt> h = new Holder<Tt>(t);
        h.get();
    }
}

如今,當你建立Holder對象時,必須指明想持有什麼類型的對象,將其置於尖括號內。而後,你就只能在Holder中存入該類型(或其子類,由於多態與泛型不衝突)的對象了。而且,在你從Holder中取出它持有的對象時,自動地就是正確的類型。ide

這就是Java泛型的核心概念:告訴編譯器想使用什麼類型,而後編譯器幫你處理一切細節。函數

一個元組類庫

僅一次方法調用就能返回多個對象,你應該常常須要這樣的功能吧。但是return語句只容許返回單個對象,所以解決辦法就是建立一個對象,用它來持有想要返回的多個對象。如今咱們有了泛型,就可以一次性地解決該問題,之後不再用在這個問題上浪費時間了。同時,咱們在編譯器就能確保類型安全。優化

這個概念稱爲元組(tuple),它是將一組對象直接打包存儲於其中的一個單一對象。這個容器對象容許讀取其中元素,可是不容許向其中存放新的對象。(這個概念也稱爲數據傳道對象,或信使。)this

一般,元組能夠具備任意長度。咱們也能夠利用繼承機制實現長度更長的元組。從下面的例子中能夠看到,增長類型參數是件很簡單的事情:設計

class TwoTuple<A,B>{
    public A a;
    public B b;
    public TwoTuple(A a,B b) {
        this.a = a;
        this.b = b;
    }
}
public class ThreeTuple<A,B,C> extends TwoTuple<A,B> {
    public C c;
    public ThreeTuple(A a,B b,C c) {
        super(a,b);
        this.c = c;
    }
}

三:泛型接口

泛型也能夠應用於接口。例如生成器(generator),這是一種專門負責建立對象的類。實際上這是工廠方法設計模式的一種應用。不過,當使用生成器建立新的對象時,它不須要任何參數數,而工廠方法通常須要參數。也就是說生成器無需額外的信息就知道如何建立新對象。通常而言,一個生成器只定義一個方法,該方法用以產生新的對象。code

public interface Generator<T>{
    T next();
}

方法next()的返回類型是參數化的T。正如你所見到的,接口使用泛型與類使用泛型沒什麼區別。對象

四:泛型方法

到目前爲止,咱們看到的泛型都是應用於整個類上。但一樣能夠在類中包含參數化方法,而這個方法所在的類能夠是泛型類,也能夠不是泛型類。也就是說,是否擁有泛型方法與其所在的類是不是泛型沒有關係。繼承

泛型方法使得該方法可以獨立於類而產生變化。如下是一個基本的指導原則:不管什麼時候,只要你能作到,你就應該儘可能使用泛型方法。也就是說,若是使用泛型方法能夠取代將整個類泛型化,那麼就應該只使用泛型方法,由於它可使事情更清楚明白。另外,對於一個static的方法而言,沒法訪問泛型類的類型參數。因此,若是static方法須要使用泛型能力,就必須使其成爲泛型方法。

要定義泛型方法只需將泛型參數列表置於返回值以前,就像下面這樣:

public class Test {
    public <T> void f(T x){
        System.out.println(x.getClass().getName());
    }
}

注意,當使用泛型類時,必須在建立對象的時候指定類型參數的值,而使用泛型方法的時候,一般沒必要指明參數類型,由於編譯器會爲咱們找出具體的類型。這稱爲類型參數推斷(type argument inference)。所以,咱們能夠像調用普通方法同樣調用f(),並且就好像是f()被無限次地
重載過。若是調用f()時傳入基本類型,自動打包機制就會介入其中,將基本類型的使包裝爲對應的對象。事實上,泛型方法與自動打包避免了許多之前咱們不得不本身編寫出來的代碼。

槓桿利用類型參數推斷

類型推斷只對賦值操做有效,其餘時候並不起做用。若是你將一個泛型方法調用的結果做爲參數傳遞給另外一個方法,這時編譯器並不會執行類型推斷。在這種狀況下,編譯器認爲:調用泛型方法後其返回値被賦給一個Objeet類型的變量。

顯式的類型說明

在泛型方法中,能夠顯式地指明類型,不過這種語法不多使用。要顯式地指明類型,必須在點操做符與方法名之間插入尖括號,而後把類型置於尖括號內。若是是在定義該方法的類的內部,必須在點操做符以前使用this關鍵字,若是是使用static的方法,必須在點操做符以前加上類名。例:

public class Test {
    public <T> void f(T x){
        System.out.println(x.getClass().getName());
    }
    static <T> Test g(){
        return new Test();
    }
    static  void h(Test t){
    }
    public static void main(String[] args) {
        h(Test.<Test>g());
    }
}

可變參數類型與泛型
泛型方法與可變參數列表可以很好地共存。

public class Test {
    public static <T> List<T> makeList(T... ts){
        List<T> list = new LinkedList<T>();
        for(T t:ts){
            list.add(t);
        }
        return list;
    }
    public static void main(String[] args) {
        List<String> list = makeList("1","2","3");
        for(String str :list){
            System.out.print(str+",");
        }
    }
    //輸出:1,2,3,
}

5、泛型通配符:

1、基本概念:

  1. 無邊界的通配符(Unbounded Wildcards), 就是<?>, 好比List<?>.
      無邊界的通配符的主要做用就是讓泛型可以接受未知類型的數據.
  2. 固定上邊界的通配符(Upper Bounded Wildcards):
      使用固定上邊界的通配符的泛型, 就可以接受指定類及其子類類型的數據. 要聲明使用該類通配符, 採用<? extends E>的形式, 這裏的E就是該泛型的上邊界. 注意: 這裏雖然用的是extends關鍵字, 卻不只限於繼承了父類E的子類, 也能夠代指顯現了接口E的類.
  3. 固定下邊界的通配符(Lower Bounded Wildcards):
      使用固定下邊界的通配符的泛型, 就可以接受指定類及其父類類型的數據. 要聲明使用該類通配符, 採用<? super E>的形式, 這裏的E就是該泛型的下邊界. 注意: 你能夠爲一個泛型指定上邊界或下邊界, 可是不能同時指定上下邊界.
    2、基本使用方法:

    1.無邊界的通配符的使用

    咱們以在集合List中使用<?>爲例. 如:

public static void printList(List<?> list) {
    for (Object o : list) {
        System.out.println(o);
    }
}
public static void main(String[] args) {
    List<String> l1 = new ArrayList<>();
    l1.add("aa");
    l1.add("bb");
    l1.add("cc");
    printList(l1);
    List<Integer> l2 = new ArrayList<>();
    l2.add(11);
    l2.add(22);
    l2.add(33);
    printList(l2);

}

這種使用List<?>的方式就是父類引用指向子類對象. 注意, 這裏的printList方法不能寫成public static void printList(List<Object> list)的形式,雖然Object類是全部類的父類, 可是List<Object>跟其餘泛型的List如List<String>, List<Integer>不存在繼承關係, 所以會報錯.
有一點咱們必須明確, 咱們不能對List<?>使用add方法, 僅有一個例外, 就是add(null). 爲何呢? 由於咱們不肯定該List的類型, 不知道add什麼類型的數據纔對, 只有null是全部引用數據類型都具備的元素. 請看下面代碼:

public static void addTest(List<?> list) {
    Object o = new Object();
    // list.add(o); // 編譯報錯
    // list.add(1); // 編譯報錯
    // list.add("ABC"); // 編譯報錯
    list.add(null);
}

因爲咱們根本不知道list會接受到具備什麼樣的泛型List, 因此除了null以外什麼也不能add.
還有, List<?>也不能使用get方法, 只有Object類型是個例外. 緣由也很簡單, 由於咱們不知道傳入的List是什麼泛型的, 因此沒法接受獲得的get, 可是Object是全部數據類型的父類, 因此只有接受他能夠, 請看下面代碼:

public static void getTest(List<?> list) {
    // String s = list.get(0); // 編譯報錯
    // Integer i = list.get(1); // 編譯報錯
    Object o = list.get(2);
}

那位說了, 不是有強制類型轉換麼? 是有, 可是咱們不知道會傳入什麼類型, 好比咱們將其強轉爲String, 編譯是經過了, 可是若是傳入個Integer泛型的List, 一運行還會出錯. 那位又說了, 那麼保證傳入的String類型的數據不就行了麼? 那樣是沒問題了, 可是那還用<?>幹嗎呀? 直接List<String>不就好了.

2. 固定上邊界的通配符的使用

我仍舊以List爲例來講明:

public static double sumOfList(List<? extends Number> list) {
    double s = 0.0;
    for (Number n : list) {
        // 注意這裏獲得的n是其上邊界類型的, 也就是Number, 須要將其轉換爲double.
        s += n.doubleValue();
    }
    return s;
}

public static void main(String[] args) {
    List<Integer> list1 = Arrays.asList(1, 2, 3, 4);
    System.out.println(sumOfList(list1));
    List<Double> list2 = Arrays.asList(1.1, 2.2, 3.3, 4.4);
    System.out.println(sumOfList(list2));
}

有一點咱們須要記住的是, List<? extends E>不能使用add方法, 請看以下代碼:

public static void addTest2(List<? extends Number> l) {
    // l.add(1); // 編譯報錯
    // l.add(1.1); //編譯報錯
    l.add(null);
}

緣由很簡單, 泛型<? extends E>指的是E及其子類, 這裏傳入的多是Integer, 也多是Double, 咱們在寫這個方法時不能肯定傳入的什麼類型的數據, 若是咱們調用:

1 List<Integer> list = new ArrayList<>();
2 addTest(list);

那麼咱們以前寫的add(1.1)就會出錯, 反之亦然, 因此除了null以外什麼也不能add. 可是get的時候是能夠獲得一個Number, 也就是上邊界類型的數據的, 由於無論存入什麼數據類型都是Number的子類型, 獲得這些就是一個父類引用指向子類對象.

3. 固定下邊界通配符的使用.

這個較前面的兩個有點難理解, 首先仍以List爲例:

public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}

public static void main(String[] args) {
    List<Object> list1 = new ArrayList<>();
    addNumbers(list1);
    System.out.println(list1);
    List<Number> list2 = new ArrayList<>();
    addNumbers(list2);
    System.out.println(list2);
    List<Double> list3 = new ArrayList<>();
    // addNumbers(list3); // 編譯報錯
}

咱們看到, List<? super E>是可以調用add方法的, 由於咱們在addNumbers所add的元素就是Integer類型的, 而傳入的list不論是什麼, 都必定是Integer或其父類泛型的List, 這時add一個Integer元素是沒有任何疑問的. 可是, 咱們不能使用get方法, 請看以下代碼:

public static void getTest2(List<? super Integer> list) {
    // Integer i = list.get(0); //編譯報錯
    Object o = list.get(1);
}

這個緣由也是很簡單的, 由於咱們所傳入的類都是Integer的類或其父類, 所傳入的數據類型多是Integer到Object之間的任何類型, 這是沒法預料的, 也就沒法接收. 惟一能肯定的就是Object, 由於全部類型都是其子類型.
使用? super E還有個常見的場景就是Comparator. TreeSet有這麼一個構造方法:

1 TreeSet(Comparator<? super E> comparator)

就是使用Comparator來建立TreeSet, 你們應該都清楚, 那麼請看下面的代碼:

public class Person {
    private String name;
    private int age;
    /*
     * 構造函數與getter, setter省略
     */
}

public class Student extends Person {
    public Student() {}

    public Student(String name, int age) {
        super(name, age);
    }
}

class comparatorTest implements Comparator<Person>{
    @Override
    public int compare(Student s1, Student s2) {
        int num = s1.getAge() - s2.getAge();
        return num == 0 ? s1.getName().compareTo(s2.getName()) :  num;
    }
}

public class GenericTest {
    public static void main(String[] args) {
        TreeSet<Person> ts1 = new TreeSet<>(new comparatorTest());
        ts1.add(new Person("Tom", 20));
        ts1.add(new Person("Jack", 25));
        ts1.add(new Person("John", 22));
        System.out.println(ts1);

        TreeSet<Student> ts2 = new TreeSet<>(new comparatorTest());
        ts2.add(new Student("Susan", 23));
        ts2.add(new Student("Rose", 27));
        ts2.add(new Student("Jane", 19));
        System.out.println(ts2);
    }
}

不知你們有想過沒有, 爲何Comparator<Person>這裏用的是父類Person, 而不是子類Student. 初學時很容易困惑, ? super E不該該E是子類纔對麼? 其實, 實現接口時咱們所設定的類型參數不是E, 而是?; E是在建立TreeSet時設定的. 如:

1 TreeSet<Person> ts1 = new TreeSet<>(new comparatorTest());
2 TreeSet<Student> ts2 = new TreeSet(new comparatorTest());

這裏實例化的comparatorTest的泛型就是<Student super Student>和<Person super Student>(我這麼寫只是爲了說明白). 在實現接口時使用:

1 // 這是錯誤的
2 class comparatorTest implements Comparator<Student> {...}

那麼上面的結果就成了: <Student super Person>和<Person super Person>, <Student super Person>顯然是錯誤的.

相關文章
相關標籤/搜索