Java的泛型詳解(一)

Java的泛型詳解

泛型的好處

  • 編寫的代碼能夠被不一樣類型的對象所重用。
  • 由於上面的一個優勢,泛型也能夠減小代碼的編寫。

泛型的使用

簡單泛型類

public class Pair<T> {

   private  T first;

   private T second;

   public Pair() {
       first = null;
       second = null;
   }

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

   public T getFirst(){
      return  first;
   }

   public T getSecond(){
       return second;
   }

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

   public void setSecond(T second) {
       this.second = second;
   }
}
  • 上面例子能夠看出泛型變量爲T;
  • 用尖括號(<>)括起來,並放在類名後面;
  • 泛型還能夠定義多個類型變量好比上面的例子 first和second不一樣的類型:
    public class Pair<T, U> {....}

注: 類型變量的定義須要必定的規範:
(1) 類型變量使用大寫形式,而且要比較短;
(2)常見的類型變量特別表明一些意義:變量E 表示集合類型,K和V表示關鍵字和值的類型;T、U、S表示任意類型;java

  • 類定義的類型變量能夠做爲方法的返回類型或者局部變量的類型;

例如: private T first;數組

  • 用具體的類型替換類型變量就能夠實例化泛型類型;
    例如: Pair<String> 表明將上述全部的T 都替換成了String
  • 因而可知泛型類是能夠看做普通類的工廠

泛型方法

  • 咱們應該如何定義一個泛型方法呢?
  • 泛型的方法能夠定義在泛型類,也能夠定義在普通類,那若是定義在普通類須要有一個尖括號加類型來指定這個泛型方法具體的類型;
public class TestUtils {
    public static <T> T getMiddle(T... a){
        return  a[a.length / 2];
    }
}
  • 類型變量放在修飾符(static)和返回類型的中間;
  • 當你調用上面的方法的時候只須要在方法名前面的尖括號放入具體的類型便可;
String middle = TestUtils.<String>getMiddle("a", "b", "c");

若是上圖這種狀況其實能夠省略 ,由於編譯器可以推斷出調用的方法必定是String,因此下面這種調用也是能夠的; 安全

String middle = TestUtils.getMiddle("a", "b", "c");

可是若是是如下調用可能會有問題:

如圖:能夠看到變意思沒有辦法肯定這裏的類型,由於此時咱們入參傳遞了一個Double3.14 兩個Integer17290 編譯器認爲這三個不屬於同一個類型;
此時有一種解決辦法就是把整型寫成Double類型
函數

類型變量的限定

  • 有時候咱們不能無限制的讓使用者傳遞任意的類型,咱們須要對咱們泛型的方法進行限定傳遞變量,好比以下例子

計算數組中最下的元素
this

  • 這個時候是沒法編譯經過的,且編譯器會報錯
  • 由於咱們的編譯器不能肯定你這個T 類型是否有compareTo這個函數,因此這麼能讓編譯器相信咱們這個T是必定會有compareTo呢?
  • 咱們能夠這麼寫<T extends Comparable> 這裏的意思是T必定是繼承Comparable的類
  • 由於Comparable是必定有compareTo這個方法,因此T必定有compareTo方法,因而編譯器就不會報錯了
  • 由於加了限定那麼min這個方法也只有繼承了Comparable的類才能夠調用;
  • 若是要限定方法的泛型繼承多個類能夠加extends 關鍵字並用&分割如:T extends Comparable & Serializable
  • 限定類型是用&分割的,逗號來分割多個類型變量<T extends Comparable & Serializable , U extends Comparable>

類型擦除

不論何時定義一個泛型類型,虛擬機都會提供一個相應的原始類型(raw type)。原始類型的名字就是刪掉類型參數後的泛型類型。擦除類型變量,並替換限定類型(沒有限定類型的變量用Object)翻譯

列如: Pair 的原始類型以下所示 code

public class Pair {

    private  Object first;

    private Object second;

    public Pair() {
        first = null;
        second = null;
    }

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

    public Object getFirst(){
       return  first;
    }

    public Object getSecond(){
        return second;
    }

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

    public void setSecond(Object second) {
        this.second = second;
    }
}
  • 由於上面的T是沒有限定變量,因而用Object代替了;
  • 若是有限定變量則會以第一個限定變量替換爲原始類型如:
public class Interval<T extends Comparable & Serializable> implements Serializable{
   private T lower;
   private T upper;
}
  • 原始類型以下所示:
public class Interval  implements Serializable{
   private Comparable lower;
   private Comparable upper;
}

翻譯泛型表達式

  • 上面說到泛型擦除類型變量後對於無限定變量後會以Object來替換泛型類型變量;
  • 可是咱們使用的時候並不須要進行強制類型轉換;
  • 緣由是編譯器已經強制插入類型轉換;

例如:對象

Pair<Employee> buddies = ...;
 Employee buddy = buddies.getFirst();
  • 擦除getFirst的返回類型後將返回Object類型,可是編譯器自動插入Employee的強制類型轉換,編譯器會把這個方法調用翻譯爲兩條虛擬機指令;
    • 對原始方法Pair.getFirst的調用
    • 將返回的Object類型強制轉換爲Employee類型;

咱們能夠反編譯驗證一下
blog

關鍵的字節碼有如下兩條
9: invokevirtual #4 // Method com/canglang/Pair.getFirst:()Ljava/lang/Object;
12: checkcast #5 // class com/canglang/model/Employee繼承

虛擬機指令含義以下:

  • invokevirtual:虛函數調用,調用對象的實例方法,根據對象的實際類型進行派發,支持多態;
  • checkcast:用於檢查類型強制轉換是否能夠進行。若是能夠進行,checkcast指令不會改變操做數棧,不然它會拋出ClassCastException異常;

由此咱們能夠驗證了上述的結論,在反編譯後的字節碼中看到,當對泛型表達式調用時,虛擬機操做以下:

  • 對於對象的實際類型進行替換泛型;
  • 檢查類型是否能夠強制轉換,若是能夠將對返回的類型進行強制轉換;

翻譯泛型方法

類型擦除也會出如今泛型方法裏面

public static <T extends Comparable> T min(T[] a)

類型擦除後

public static Comparable  min(Comparable[] a)

此時能夠看到類型參數T已經被擦除了,只剩下限定類型Comparable;
方法的類型擦除帶來了兩個複雜的問題,看下面的示例:

public class DateInterval extends Pair<LocalDate> {
    public void setSecond(LocalDate second){
        System.out.println("DateInterval: 進來這裏了!");
    }
}

此時有個問題,從Pair繼承的setSecond方法類型擦除後爲

public void setSecond(Object second)

這個和DateInterval的setSecond明顯是兩個不一樣的方法,由於他們有不一樣的類型的參數,一個是Object,一個LocalDate;
那麼看下面一個列子

public class Test {
    public static void main(String[] args) {
        DateInterval interval = new DateInterval();
        Pair<LocalDate> pair = interval;
        pair.setSecond(LocalDate.of(2020, 5, 20));
    }
}

Pair引用了DateInterval對象,因此應該調用DateInterval.setSecond。
咱們看一下運行結果

可是看了反編譯的字節碼可能發現一個問題:
17: invokestatic #4 // Method java/time/LocalDate.of:(III)Ljava/time/LocalDate;
20: invokevirtual #5 // Method com/canglang/Pair.setSecond:(Ljava/lang/Object;)V
這裏能夠看到此處字節碼調用的是Pair.setSecond

這裏有個重要的概念就是橋方法

引用Oracle中對於這個現象的解釋
爲了解決此問題並在類型擦除後保留通用類型的 多態性,
Java編譯器生成了一個橋接方法,以確保子類型可以按預期工做。
對於DateInterval類,編譯器爲setSecond生成如下橋接方法:

public class DateInterval extends Pair {
    // Bridge method generated by the compiler
    //
    public void setSecond(Object second) {
        setSecond((LocalDate)second);
    }
    public void setSecond(LocalDate second){
        System.out.println("DateInterval: 進來這裏了!");
    }
}

那麼咱們如何驗證是否生成這個橋方法呢?咱們能夠反編譯一下DateInterval.java看一下字節碼;

public void setSecond(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: checkcast #5 // class java/time/LocalDate
5: invokevirtual #6 // Method setSecond:(Ljava/time/LocalDate;)V
8: return
我截取了部分發如今 DateInterval的字節碼中的確會有一個橋方法,同時驗證了上面的問題;

總結:

  • 虛擬機中沒有泛型,只有普通的類和方法
  • 全部的類型參數都用他們的限定類型替換
  • 橋方法被合成來保持多態
  • 爲保持類型安全性,必要時插入強制類型轉換
相關文章
相關標籤/搜索