《java編程思想》—— 泛型

爲何要使用泛型程序設計?

通常的類和方法,只能使用具體的類型:要麼是基本類型,要麼是自定義類的對應類型;若是要編寫能夠應用於多種類型的代碼,這種刻板的限制對代碼的束縛就會很大。
----摘自原書

Ordinary classes and methods work with specific types: either
primitives or class types. If you are writing code that might be used
across more types, this rigidity can be overconstraining.
----摘自英文版html

從原書的第一段話引出,其實泛型的出現是爲了讓編寫的代碼能夠應用於多種類型,解除只能使用具體類型的限制,這也就是參數化類型的概念。java

泛型出現的契機

泛型是在Java SE5出現的,也就是說java5版本以前的java是不存在泛型的概念的。而Java5這個版本增長了泛型設計其中重要的一個緣由就是:優雅的安全的讓容器類解除只能使用具體類型的束縛,從而適用於多種類型數組

下面以ArrayList爲例比較先後差別,證實泛型的優雅和安全安全

  • java1.4版本
public class ArrayList // 省略繼承和實現
{
    transient Object[] elementData;         // 用於存儲ArrayList對象的數組
    public Object get(int index) { . . . }  // 獲取數組對象
    public void add(Object o) { . . . }     // 添加數組對象
}
  • java5版本
public class ArrayList<E>
{
    transient Object[] elementData;
    public E get(int index) {...}
    public boolean add(E e) {...}

從二者對比能夠看出,java1.4版本是使用Object類做爲對象存取的的出參入參,這樣的好處天然是可讓ArrayList類知足編寫一次適用於多種類型的代碼設計,但是這樣就暴露如下幾個問題了:oracle

    1. 當獲取一個值時必須進行強制類型轉換
ArrayList strs = new ArrayList();
strs.add("hello");
String str = (String) strs.get(0);

這裏若是不加(String)強制轉換,那麼代碼在編譯期就會報錯:Incompatible types,並提示files.get(0)返回的是一個Object對象但是接收的是String類型對象,須要作類型強制轉換。編輯器

    1. 當添加一個值時沒有在編譯器作類型錯誤檢査
ArrayList files = new ArrayList();
files.add(new File("./hello.text"));
File file = (File)files.get(0);     // 正常
String file = (String)files.get(0); // 編譯器正常,運行期報錯

代碼在編譯期不會出錯,但是在運行期的時候就會報強轉錯誤:java.lang.ClassCastException,這個問題其實也就是使用Object類的弊端了。工具

既然java1.4出現了上述問題,那麼如今就是用java5版本的泛型來解決上述的問題,以下:測試

    1. 當獲取一個值時必須進行強制類型轉換(解決
// 這裏第二個<>省略了String,是由於類型推導
ArrayList<String> strs = new ArrayList<>();
strs.add("hello");
String str = strs.get(0);

能夠看出java5以後以後就不須要作類型強轉了,這是由於get方法的返回類型已經在實例化的時候被<String>給參數化了。this

    1. 當添加一個值時沒有在編譯器作類型錯誤檢査(解決
ArrayList<File> files = new ArrayList<>();
files.add(new File("./../Fibonacci.java"));
File file = (File)files.get(0);             // 正常
String file = (String)files.get(0);         // 編譯期就報錯了

能夠看出1.5以後以後就不須要可能到線上纔會發現的bug,在編寫代碼的時候編輯器就給提早給提示:Inconvertible types; cannot cast 'java.io.File' to 'java.lang.String'(禁止File類型轉換成String類型了)設計

雖然泛型解決的問題還有不少,可是總的來講都是爲了:更優雅的更安全的讓容器類解除只能使用具體類型的束縛,從而適用於多種類型

泛型的語法&使用範圍

下面咱們就來正式講一下泛型的語法,以及使用範圍:

泛型接口

  • 語法定義:

    • 定義泛型:接口名以後定義該類會使用到的全部泛型。
    • 引用泛型:除了static方法因不能使用外部實例參數外,其餘繼承、實現、成員變量,成員方法等均可使用。
    • 泛型實參:經過繼承類的實例化時傳入,不傳默認是Object
  • 語法結構:
interface  接口名 <定義泛型標識> extends 父接口名 <引用泛型標識> {
  public 引用泛型標識 var; 
  ...
}
  • 案例 —— 生成器接口:
/**
 * 生成器是一種專門負責建立對象的類,是相似工廠模式,不一樣的是工廠模式通常須要入參,而生成器不須要。
 * 即生成器是:無需額外的信息就能夠知道如何建立對象,通常來講生成器只會定義一個建立對象的方法。
 * 本例子中的建立對象方法就是next
 * @param <T>
 */
public interface Generator<T> {
  T next();
}

泛型類

  • 語法定義:

    • 定義泛型:類名以後定義該類會使用到的全部泛型。
    • 引用泛型:除了static方法因不能使用外部實例參數外,其餘繼承、實現、成員變量,成員方法,方法返回值等均可使用。
    • 泛型實參:類的實例化時鑽石符放在類名以後,如:new ArrayList<String>()
  • 語法結構:
class  類名 <定義泛型標識> 
    extends 父類名 <引用泛型標識>, implements 接口名 <引用泛型標識> {
  private 引用泛型標識 var; 
  ...
}
  • 案例 —— 生成器具體實現類:
public class TestBasicGenerator<T> implements Generator<T> {
  private Class type = null;
  // 原本下面寫法會更優雅一點,可是由於泛型的類型擦除致使這種寫法是會報錯的
  // private Class type = T.class;
  public TestBasicGenerator(Class<T> clazz) {
    type = clazz;
  }
  public static <T> Generator<T> gen(Class<T> clazz) {
    return new TestBasicGenerator<T>(clazz);
  }
  public T next() {
    try {
      return (T) type.newInstance();
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
  public static void main(String[] args) {
    // 書上的方法,經過靜態泛型方法:簡單
    Generator<Coffee> gen = TestBasicGenerator.gen(Coffee.class);
    Coffee c =  gen.next();

    // 課後習題,經過實例化方法:複雜
    Generator<Coffee> gen1 = new TestBasicGenerator<Coffee>(Coffee.class);
    Coffee c1 =  gen1.next();
  }
}

泛型方法

  • 語法定義:

    • 定義泛型:該方法修飾符以後定義該方法會使用到的全部泛型。
    • 引用泛型:包括返回值、參數、內部變量,內部方法等,除非該方法是static方法,不然也可使用類上定義的泛型,二者不衝突。
    • 泛型實參:

      • 顯示傳遞:方法調用時鑽石符放在方法名以前,'.'號以後,如:New.<String>show("hello"),通常建議用顯示傳遞,方便閱讀。
      • 隱式傳遞:無需傳遞,編譯器會根據上下文(入參或者返回值接收變量)推導出具體的類型,如:New.show("hello")Map<String, List<String>> sls = New.map()(前者根據是非賦值語句的入參推導,後者根據賦值語句的接收變量推導)。
    • 語法結構:
public class 類名 {
    public <定義泛型標識> 返回類型 方法名(引用泛型標識 形參名) {
        ...
    }
}
  • 案例 —— 建立經常使用容器對象的工具類(簡略版):
public class New {
  public static <K,V> Map<K,V> map() {
          return new HashMap<K,V>();
  }
  public static <T> void show(T title) {
          System.out.println(title);
  }
    // Examples:
  public static void main(String[] args) {
    Map<String, List<String>> sls = New.map();
    // 顯示傳遞
    New.<String>show("hello");
    // 隱式傳遞
    New.show("hello");
    // 編譯器會報show方法不能傳遞String類型,證實顯示傳遞爲主,隱式傳遞爲輔
    New.<Integer>show("hello");
  }
}

可變參數與泛型方法

可變參數方法能夠與泛型無縫結合:

  • 案例 —— 入參聚合:
public class GenericVarargs {
  public static <T> List<T> makeList(T... args) {
    List<T> result = new ArrayList<T>();
    for(T item : args)
      result.add(item);
    return result;
  }
  public static void main(String[] args) {
    ls = makeList("A", "B", "C");
    System.out.println(ls);    // 打印出:[A, B, C]
  }
}

泛型邊界

有時您可能但願在泛型類、泛型方法、泛型接口中限制一下傳入的泛型實參的類型。例如,對數字進行操做的方法可能只想接受Number子類或者父類的實例,那這個時候就須要用到邊界通配符了:

泛型上界類型通配符

使用上界限制類型參數,須要藉助extends關鍵之,先在<>中寫類型參數的標誌號,後跟extends,最後纔是上界類型(注意:上界類型能夠多個,可是最多隻容許一個類搭配多個接口,類還必須寫在第一位,由於java是單繼承多實現),用法通常只用於泛型方法泛型接口泛型類這三處語法中的定義泛型的地方。

  • 複雜案例以下(僅作案例):
// 表示類型實參只能是Number的子類,而且該子類還要實現List接口,不然編譯報錯(上界不包含上)
public class TestTypeErasure<T extends Number & List<T>> {
    ...
}
  • 上界通配符

泛型下屆類型通配符

使用下屆限制類型參數,須要藉助super關鍵之,先在<>中寫無界通配符?,後跟super,最後纔是下屆具體類型或泛型標識符(注意:這裏就只能一個了),用法通常只用於限制容器類值

  • 複雜案例以下(僅作案例):
// 表示類型實參能夠是Integer類型,或者Integer的父類(下屆包含下)
public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}

泛型無界類型通配符

上面講下屆通配符時須要藉助無界通配符,可是它不止有哪種寫法,還能夠直接在<>中寫?,表示不限定類型參數,相似<Object>。但不一樣的是使用了無界通配符<>,就限定使用add/addAll這樣的插入方法插入非null值,只能經過賦值來實現,因此通常<?>只用在方法的形參中。

  • 案例以下:
// 表示類型實參能夠是Integer類型,或者Integer的父類(下屆包含下)
public class TestBounds{
    public static void printList(List<?> list) {
    list.add(null);    // 正常
    list1.add(2);        // 編譯報錯
  }
  public static void main(String[] args) {
    List<Object> list = new ArrayList<>(); 
    list.add(2);

    List<?> list1 = new ArrayList<>();
    list.add(null);    // 正常
    list1.add(2);        // 編譯報錯
  }
}

泛型的實現原理——類型擦除(type erasure)

什麼是類型擦除?

一、Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods.
二、Insert type casts if necessary to preserve type safety.
三、Generate bridge methods to preserve polymorphism in extended generic types.
---摘自oracle官網java8文檔

一、替換全部泛型類型中的類型參數爲其邊界,若是無邊界,將替換爲Object。所以,生成的字節碼僅包含普通的類,接口和方法。
二、若有必要,插入類型強轉以保持類型安全。
三、生成橋接方法以保留繼承泛型類型中的多態性。

從官網描述上看程序在解析成字節碼以後,會把:定義泛型的地方擦除;使用泛型的地方用邊界(上界)替換;傳入泛型實參的地方也擦除,若是可能形成類型安全問題,就加類型強轉;若是父類在類型擦除以後不合符子類的調用了,子類會增長橋接方法來保留多態。

  • 案例證實——TestTypeErasure類:
public class TestTypeErasure<T extends Number> {
  public T num;
  public <E> void test(E arg) {
    System.out.println(arg);
  }
  public static void main(String[] args) {
    new ArrayList<Byte>();
  }
}
  • java6對TestTypeErasure的字節碼反編譯以後:
public class generics.yjm.TestTypeErasure extends java.lang.Object{
    public java.lang.Number num;
    public generics.yjm.TestTypeErasure();
      Code:
       0:   aload_0
       1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
       4:   return
    public void test(java.lang.Object);
      Code:
       0:   getstatic       #2; //Field java/lang/System.out:Ljava/io/PrintStream;
       3:   aload_1
       4:   invokevirtual   #3; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
       7:   return
    public static void main(java.lang.String[]);
      Code:
       0:   new     #4; //class java/util/ArrayList
       3:   dup
       4:   invokespecial   #5; //Method java/util/ArrayList."<init>":()V
       7:   pop
       8:   return
}

從java6的反編譯的代碼中能夠分析出,原來的T被編譯成了java.lang.NumberE被替換成了java.lang.Object<Byte>被擦除不存了,符合上述原則

  • java8反編譯以後:
public class generics.yjm.TestTypeErasure<T extends java.lang.Number> {
  public T num;
  public generics.yjm.TestTypeErasure();
    Code:
       0: aload_0
       1: invokespecial #1  // Method java/lang/Object."<init>":()V
       4: return
  public <E> void test(E);
    Code:
       0: getstatic     #2 // Field java/lang/System.out:Ljava/io/PrintStream;
       3: aload_1
       4: invokevirtual #3 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
       7: return
  public static void main(java.lang.String[]);
    Code:
       0: new           #4  // class java/util/ArrayList
       3: dup
       4: invokespecial #5  // Method java/util/ArrayList."<init>":()V
       7: pop
       8: return
}

從java8的反編譯的代碼中能夠分析出,原來的T仍是TE仍是E<Byte>被擦除了。但仔細分析代碼,其實和java6的反編譯的效果仍然是同樣的,只是java8保留了標識號來作佔位符而已(此說法只爲了說服本身,若是有更好的解釋歡迎告知,不勝感激)。

爲何使用類型擦除?

  • 咱們已經知道泛型是在java SE5纔出現的產物,出現的主要緣由就是爲了解決容器類的類型安全問題,但是由於要遵循java的版本迭代原則:二進制兼容(Binary Compatibility)原則,從而折中的採用了類型擦除這樣的方法來實現java版的泛型。

泛型限制

那既然java的泛型是一個折中版,老是有一些限制是要注意的,以下:

  • 泛型不支持基本類型
  • 沒法建立類型參數(泛型)的實例
  • 靜態字段static不能修飾類型參數
  • 沒法使用類型參數進行強制轉換或者instanceof
  • 沒法建立參數化類型的數組
  • 沒法建立,捕獲或拋出參數化類型的對象
  • 兩個方法,在其餘條件相同的狀況下,只是泛型不一樣不能當作是方法的重載,會編譯出錯

泛型標誌號的通用定義

泛型的標誌號的範圍是<a~Z>這26個字母的大小寫,由於標誌號太多,爲了增長代碼可讀性,讓每一個標誌號有本身的含義,就默認有了下面一套規範(非強制規範,只是爲了方便理解和閱讀):

  • 集合泛型類型:E或者T,如:ArrayList<E>
  • 映射泛型類型:K,V,如:Map <K,V>
  • 數值泛型類型:N
  • 字符泛型類型:S
  • 布爾值泛型類型:B

總的來講,命名規則就是:方便理解

文章涉及的小知識點

  1. 類型推導(type inference):

    • 類型推導與泛型類:是指,編譯器會在編譯期根據變量聲明時的泛型類型自動推斷出實例化的泛化類型,固然要求就是java6版本中不能夠省略<>(術語:diamond,我喜歡稱之爲鑽石符)即,ArrayList<String> strs = new ArrayList<String>(),只能夠簡寫成ArrayList<String> strs = new ArrayList<>(),java7及以上能夠省略。
    • 類型推導與泛型方法:如上述的方法隱式傳遞就是方法的類型推導,不同的是鑽石符能夠省略。
  2. 二進制兼容原則:指在相同系統環境中,高版本的Java編譯低版本的java文件產生的二進制要與低版本編譯出來的二進制兼容(如:java8版本編譯java7語法寫的java類生成的二進制要和在java7時編譯出來的二進制兼容),也就是所謂的向後兼容

    • Java 8(徹底二進制與Java 7兼容)
    • Java 7(大多數二進制與Java 6兼容)
    • Java 6(主要是與Java 5兼容的二進制文件,加上一些模糊處理程序在規範以外生成類文件的註釋,所以這些類文件可能沒法運行)
    • Java 5(大多數二進制與Java 1.4.2兼容,加上與混淆器相同的註釋)
    • JAVA 1.0-1.4.2(大多數二進制版本與之前的版本兼容,一些前向兼容性的註釋甚至能夠工做,但沒有通過測試)
  3. getTypeParameters方法做用:返回在類上申明的泛型的標識符,併合併成數組放回,若是類上未申明未泛型標識符,那就返回空數組。
  4. 橋接方法生成案例:
// 編譯前:
public class Node<T> {
    public T data;
    public Node(T data) { this.data = data; }
    public void setData(T data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}
public class MyNode extends Node<Integer> {
    public MyNode(Integer data) { super(data); }
    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}
// 編譯後:
public class Node {
    public Object data;
    public Node(Object data) { this.data = data; }
    public void setData(Object data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}
public class MyNode extends Node {
    public MyNode(Integer data) { super(data); }
    // 由於super(data)緣故,編譯器會生成橋接方法,委託給原始的setData方法
    public void setData(Object data) {
        setData((Integer) data);
    }
    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}
  1. List<?>、List、List<Object>、List<? extends Object>區別?

    • List<?>:限制除了能使用add/addAll等方法插入null值,其餘類型都不能夠。也包含泛型的特性,能夠是任意一種實參類型。
    • List:無限制,能夠是任意一種或多種具體類型,但缺乏泛型給予的編譯期類型安全保障。
    • List<Object>:等同List。
    • List<? extends Object>:和List<?>基本相同,只是多加了一個限制,只能是Object類型的子類。
  2. 上下邊界通配符不能同時使用(廢話)。

文章引用

oracle官網——類型擦除

相關文章
相關標籤/搜索