Java程序員必備基礎:泛型解析

前言

整理了Java泛型的相關知識,算是比較基礎的,但願你們一塊兒學習進步。 java

1、什麼是Java泛型

Java 泛型(generics)是 JDK 5 中引入的一個新特性,其本質是參數化類型,解決不肯定具體對象類型的問題。其所操做的數據類型被指定爲一個參數(type parameter)這種參數類型能夠用在類、接口和方法的建立中,分別稱爲泛型類、泛型接口、泛型方法。面試

泛型類

泛型類(generic class) 就是具備一個或多個類型變量的類。一個泛型類的簡單例子以下:編程

//常見的如T、E、K、V等形式的參數經常使用於表示泛型,編譯時沒法知道它們類型,實例化時須要指定。
public class Pair <K,V>{
    private K first;
    private  V second;

    public Pair(K first, V second) {
        this.first = first;
        this.second = second;
    }

    public K getFirst() {
        return first;
    }

    public void setFirst(K first) {
        this.first = first;
    }

    public V getSecond() {
        return second;
    }

    public void setSecond(V second) {
        this.second = second;
    }

    public static void main(String[] args) {
    // 此處K傳入了Integer,V傳入String類型
        Pair<Integer,String> pairInteger = new Pair<>(1, "第二");
        System.out.println("泛型測試,first is " + pairInteger.getFirst()
                + " ,second is " + pairInteger.getSecond());
    }
}


複製代碼

運行結果以下:數組

泛型測試,first is 1 ,second is 第二
複製代碼

泛型接口

泛型也能夠應用於接口。安全

public interface Generator<T> {
    T next();
}
複製代碼

實現類去實現這個接口的時候,能夠指定泛型T的具體類型。bash

指定具體類型爲Integer的實現類:app

public class NumberGenerator implements Generator<Integer> {
    
    @Override
    public Integer next() {
        return new Random().nextInt();
    }
}
複製代碼

指定具體類型爲String的實現類:dom

public class StringGenerator implements Generator<String> {
    
    @Override
    public String next() {
        return "測試泛型接口";
    }
}
複製代碼

泛型方法

具備一個或多個類型變量的方法,稱之爲泛型方法。ide

public class GenericMethods {

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

    public static void main(String[] args) {
        GenericMethods gm = new GenericMethods();
        gm.f("字符串");
        gm.f(666);
    }
}
複製代碼

運行結果:post

java.lang.String
java.lang.Integer
複製代碼

2、泛型的好處

Java語言引入泛型的好處是安全簡單。泛型的好處是在編譯的時候檢查類型安全,而且全部的強制轉換都是自動和隱式的,提升代碼的重用率。

咱們先來看看一個只能持有單個對象的類。

public class Holder1 {
    private Automobile a;

    public Holder1(Automobile a) {
        this.a = a;
    }

    public Automobile getA() {
        return a;
    }
}
複製代碼

咱們能夠發現,這個類的重用性不怎樣。要使它持有其餘類型的任何對象,在jdk1.5泛型以前,能夠把類型設置爲Object,以下:

public class Holder2 {
    private Object a;

    public Holder2(Object a) {
        this.a = a;
    }

    public Object getA() {
        return a;
    }

    public void setA(Object a) {
        this.a = a;
    }

    public static void main(String[] args) {
        Holder2 holder2 = new Holder2(new Automobile());
        //強制轉換
        Automobile automobile = (Automobile) holder2.getA();
        holder2.setA("測試泛型");
        String s = (String) holder2.getA();
    }
}

複製代碼

咱們引入泛型,實現功能那個跟Holder2類一致的Holder3,以下:

public class Holder3<T> {

    private T a;

    public T getA() {
        return a;
    }

    public void setA(T a) {
        this.a = a;
    }

    public Holder3(T a) {
        this.a = a;
    }

    public static void main(String[] args) {
        Holder3<Automobile> holder3 = new Holder3<>(new Automobile());
        Automobile automobile = holder3.getA();
    }
}

複製代碼

所以,泛型的好處很明顯了:

  • 不用強制轉換,所以代碼比較簡潔;(簡潔性)
  • 代替Object來表示其餘類型對象,與ClassCastException異常劃清界限。(安全性)
  • 泛型使代碼可讀性加強。(可讀性)

3、泛型通配符

咱們定義泛型時,常常遇見T,E,K,V,?等通配符。本質上這些都是通配符,是編碼時一種約定俗成的東西。固然,你換個A-Z中另外一個字母表示沒有關係,可是爲了可讀性,通常有如下定義:

  • ? 表示不肯定的 java 類型
  • T (type) 表示具體的一個java類型
  • K V (key value) 分別表明java鍵值中的Key Value
  • E (element) 表明Element

爲何須要引入通配符呢,咱們先來看一個例子:

class Fruit{
    public int getWeigth(){
        return 0;
    }
}
//Apple是水果Fruit類的子類
class Apple extends Fruit {
    public int getWeigth(){
        return 5;
    }
}

public class GenericTest {
    //數組的傳參
    static int sumWeigth(Fruit[] fruits) {
        int weight = 0;
        for (Fruit fruit : fruits) {
            weight += fruit.getWeigth();
        }
        return weight;
    }
    
    static int sumWeight1(List<? extends Fruit> fruits) {
        int weight = 0;
        for (Fruit fruit : fruits) {
            weight += fruit.getWeigth();
        }
        return weight;
    }
    static  int sumWeigth2(List<Fruit> fruits){
        int weight = 0;
        for (Fruit fruit : fruits) {
            weight += fruit.getWeigth();
        }
        return weight;
    }

    public static void main(String[] args) {
        Fruit[] fruits = new Apple[10];
        sumWeigth(fruits);
        List<Apple> apples = new ArrayList<>();
        sumWeight1(apples);
        //報錯
        sumWeigth2(apples);
    }
}
複製代碼

咱們能夠發現,Fruit[]與Apple[]是兼容的。List<Fruit>List<Apple>不兼容的,集合List是不能協變的,會報錯,而List與List<? extends Fruits> 是OK的,這就是通配符的魅力所在。通配符一般分三類:

  • 無邊界通配符,如List<?>
  • 上邊界限定通配符,如<? extends E>;
  • 下邊界通配符,如<? super E>;

?無邊界通配符

無邊界通配符,它的使用形式是一個單獨的問號:List<?>,也就是沒有任何限定。

看個例子:

public class GenericTest {

    public static void printList(List<?> list) {
        for (Object object : list) {
            System.out.println(object);
        }
    }

    public static void main(String[] args) {
        List<String> list1 = new ArrayList<>();
        list1.add("A");
        list1.add("B");
        List<Integer> list2 = new ArrayList<>();
        list2.add(100);
        list2.add(666);
        //報錯,List<?>不能添加任何類型
        List<?> list3 = new ArrayList<>();
        list3.add(666);
    }
}
複製代碼

無界通配符(<?>)能夠適配任何引用類型,看起來與原生類型等價,但與原生類型仍是有區別,使用 無界通配符則代表在使用泛型 。同時,List<?> list不能夠添加任何類型,由於並不知道實際是哪一種類型。可是List list由於持有的是Object類型對象,因此能夠add任何類型的對象。

上邊界限定通配符 < ? extends E>

使用 <? extends Fruit> 形式的通配符,就是上邊界限定通配符。 extends關鍵字表示這個泛型中的參數必須是 E 或者 E 的子類,請看demo:

class apple extends Fruit{}
static int sumWeight1(List<? extends Fruit> fruits) {
    int weight = 0;
    for (Fruit fruit : fruits) {
        weight += fruit.getWeigth();
    }
    return weight;
}
public static void main(String[] args) {
    List<Apple> apples = new ArrayList<>();
    sumWeight1(apples);
}
複製代碼

可是,如下這段代碼是不可行的:

static int sumWeight1(List<? extends Fruit> fruits){
   //報錯
   fruits.add(new Fruit());
   //報錯
   fruits.add(new Apple());
}
複製代碼
  • List<Fruit>裏只能添加Fruit類對象及其子類對象(如Apple對象,Oragne對象),在List<Apple>裏只能添加Apple類和其子類對象。
  • 咱們知道List<Fruit>、List<Apple>等都是List<? extends Fruit>的子類型。假設一開始傳參是List<Fruit> list,兩個添加沒問題,那若是傳來List<Apple> list,添加就失敗了,編譯器爲了保護本身,直接禁用添加功能了。
  • 實際上,不能往List<? extends E> 添加任意對象,除了null。

下邊界限定通配符 < ? super E>

使用 <? super E> 形式的通配符,就是下邊界限定通配符。 super關鍵字表示這個泛型中的參數必須是所指定的類型E,或者是此類型的父類型,直至 Object。

public class GenericTest {

    private static <T> void test(List<? super T> dst, List<T> src){
        for (T t : src) {
            dst.add(t);
        }
    }

    public static void main(String[] args) {
        List<Apple> apples = new ArrayList<>();
        List<Fruit> fruits = new ArrayList<>();
        test(fruits, apples);
    }
}

複製代碼

能夠發現,List<? super E>添加是沒有問題的,由於子類是能夠指向父類的,它添加並不像List<? extends E>會出現安全性問題,因此可行。

4、泛型擦除

什麼是類型擦除

什麼是Java泛型擦除呢? 先來看demo:

Class c1 = new ArrayList<Integer>().getClass();
Class c2 = new ArrayList<String>().getClass();
System.out.println(c1 == c2);
/* Output
true
*/
複製代碼

ArrayList <Integer>ArrayList <String> 很容易被認爲是不一樣的類型。可是這裏輸出結果是true,這是由於Java泛型是使用擦除實現的,不論是ArrayList<Integer>() 仍是new ArrayList<String>(),在編譯生成的字節碼中都不包含泛型中的類型參數,即都擦除成了ArrayList,也就是被擦除成「原生類型」,這就是泛型擦除。

類型擦除底層

Java泛型在編譯期完成,它是依賴編譯器實現的。其實,編譯器主要作了這些工做:

  • set()方法的類型檢驗
  • get()處的類型轉換,編譯器插入了一個checkcast語句,

再看個例子:

public class GenericTest<T> {

    private T t;

    public T get() {
        return t;
    }

    public void set(T t) {
        this.t = t;
    }

    public static void main(String[] args) {
        GenericTest<String> test = new GenericTest<String>();
        test.set("jay@huaxiao");
        String s = test.get();
        System.out.println(s);
    }
}
/* Output
jay@huaxiao
*/

複製代碼

javap -c GenericTest.class反編譯GenericTest類可得

public class generic.GenericTest<T> {
  public generic.GenericTest();
    Code:
       0: aload_0
       1: invokespecial #1 // Method java/lang/Object."<init>":()V
       4: return

  public T get();
    Code:
       0: aload_0
       1: getfield      #2 // Field t:Ljava/lang/Object;
       4: areturn

  public void set(T);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2 // Field t:Ljava/lang/Object;
       5: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #3 // class generic/GenericTest
       3: dup
       4: invokespecial #4 // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #5 // String jay@huaxiao
      11: invokevirtual #6 // Method set:(Ljava/lang/Object;)V
      14: aload_1
      15: invokevirtual #7 // Method get:()Ljava/lang/Object;
      18: checkcast     #8 // class java/lang/String
      21: astore_2
      22: getstatic     #9 // Field java/lang/System.out:Ljava/io/PrintStream;
      25: aload_2
      26: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      29: return
}
複製代碼
  • 看第11,set進去的是原始類型Object(#6);
  • 看第15,get方法得到也是Object類型(#7),說明類型被擦出了。
  • 再看第18,它作了一個checkcast操做,是一個String類型,強轉。

5、泛型的限制與侷限

使用Java泛型須要考慮如下一些約束與限制,其實幾乎都跟泛型擦除有關。

不能用基本類型實例化類型化參數

不能用類型參數代替基本類型。所以, 沒有 Pair<double>, 只 有Pair<Double>。 固然, 其緣由是類型擦除。擦除以後, Pair 類含有 Object 類型的域, 而 Object 不能存儲 double值。

運行時類型查詢只適用於原始類型

如,getClass()方法等只返回原始類型,由於JVM根本就不知道泛型這回事,它只知道原始類型。

if(a instanceof Pair<String>) //ERROR,僅測試了a是不是任意類型的一個Pair,會看到編譯器ERROR警告

if(a instanceof Pair<T>) //ERROR

Pair<String> p = (Pair<String>) a;//WARNING,僅測試a是不是一個Pair

Pair<String> stringPair = ...;
Pair<Employee> employeePair = ...;
if(stringPair.getClass() == employeePair.getClass())  //會獲得true,由於兩次調用getClass都將返回Pair.class
複製代碼

不能建立參數化類型的數組

不能實例化參數化類型的數組, 例如:

Pair<String>[] table = new Pair<String>[10]; // Error
複製代碼

不能實例化類型變量

不能使用像 new T(...),newT[...] 或 T.class 這樣的表達式中的類型變量。例如, 下面的Pair<T> 構造器就是非法的:

public Pair() { first = new T(); second = new T(); } // Error 
複製代碼

使用泛型接口時,須要避免重複實現同一個接口

interface Swim<T> {}

class Duck implements Swim<Duck> {}

class UglyDuck extends Duck implements Swim<UglyDuck> {}
複製代碼

能夠消除對受查異常的檢查

@SuppressWamings("unchecked") 
public static <T extends Throwable〉void throwAs(Throwable e) throws T { throw (T) e; }
複製代碼

定義API返回報文時,儘可能使用泛型;

public class Response<T> extends BaseResponse {
    private static final long serialVersionUID = -xxx;

    private T data;
    
    private String code;

    public Response() {
    }

    public T getData() {
        return this.data;
    }

    public void setData(T data,String code ) {
        this.data = data;
        this.code = code;
    }
}
複製代碼

6、Java泛型常見面試題

Java泛型常見幾道面試題

  • Java中的泛型是什麼 ? 使用泛型的好處是什麼?(第一,第二小節可答)
  • Java的泛型是如何工做的 ? 什麼是類型擦除 ? (第四小節可答)
  • 什麼是泛型中的限定通配符和非限定通配符 ? (第三小節可答)
  • List<? extends T>和List <? super T>之間有什麼區別 ?(第三小節可答)
  • 你瞭解泛型通配符與上下界嗎?(第三小節可答)

參考與感謝

我的公衆號

  • 若是你是個愛學習的好孩子,能夠關注我公衆號,一塊兒學習討論。
  • 若是你以爲本文有哪些不正確的地方,能夠評論,也能夠關注我公衆號,私聊我,你們一塊兒學習進步哈。
相關文章
相關標籤/搜索