java基礎知識梳理&泛型初探

目錄

概述

範型的使用

類型參數

類型通配符

泛型方法

泛型類

限定類型參數上限

上界通配符(Upper Bounds Wildcards),用來限定泛型的上界。

下界通配符(Lower Bounds Wildcards),用來限定泛型的下界。

範型的特點

範型是類型擦除的

不能創建一個範型類型實例

不能初始化範型數組

基本類型不能做類型參數

static 的語境不能引用類型變量


概述

所謂範型,就是允許在定義類、接口、方法時使用類型形參,這個類型形參將在聲明變量、創建對象、調用方法時動態地指定(即傳入實際的類型參數,也可稱爲類型實參)。
範型可以減少強制類型的轉換,可以規範集合的元素類型,還可以提高代碼的安全性和可讀性,正是因爲有這些優點,自從 Java 引入範型後,項目的編碼規則上便多了一條:優先使用範型。

範型的使用

類型參數

類型參數就是我們在定義泛型類或者方法是動態指定的參數。

類型參數的命名規則

類型參數名稱命名爲單個大寫字母,比如 Collection<E>
但是這個命名規則我們一般會遵循一般的約定,以便可以在使用普通類或接口名稱時能夠容易地區分類型參數,增加代碼的可讀性。
以下是常用的類型參數名稱列表:

  • E:元素 Element,主要由Java集合(Collections)框架使用。
  • K:鍵 Key,主要用於表示映射中的鍵的參數類型。
  • V:值 Value,主要用於表示映射中的值的參數類型。
  • N:數字 Number,主要用於表示數字。
  • T:類型,主要用於表示第一類通用型參數。
  • S:類型,主要用於表示第二類通用類型參數。
  • U:類型,主要用於表示第三類通用類型參數。
  • V:類型,主要用於表示第四個通用類型參數。

類型通配符

類型通配符一般是使用 ? 代替具體的類型參數,表示未知類型。例如 List<?> 在邏輯上是 List<String>List<Integer> 等所有 List<具體類型實參> 的父類。
類型通配符的形式有 <?>、 <? extends Class> 和 <? super Class>

基本使用

爲了表示各種範型 List 的父類,我們需要使用類型通配符,類型的通配符就是一個問號 ?,將它作爲類型實參傳給 List集合:List<?>,就標示未知類型元素的 List,它的元素類型可以匹配任何類型。 

public void testGeneric() {
    List<String> name = new ArrayList<String>();
    List<Integer> age = new ArrayList<Integer>();
    List<Shape> shape = new ArrayList<Shape>();
    name.add("icon");
    age.add(18);
    shape.add(new Shape());
    getData(name);
    getData(age);
    getData(shape);
}
public static void getData(List<?> data) {
    Log.e("Test","data :" + data.get(0));
}
public static class Shape {
    @Override
    public String toString() {
        return "Shape";
    }
}

編譯沒問題,運行結果:

E/Test: data :icon
E/Test: data :18
E/Test: data :Shape

因爲 getData() 方法的參數是 List 類型的,所以 name,age,shape 都可以作爲這個方法的實參,這就是通配符的作用。

上面程序中使用的 List<?>,其實這種寫法可以適用於任何支持範型聲明的接口和類,比如 Set<?>Map<?,?>等。

設定類型通配符的上限

假設有下面的使用場景,我們不想使 List<?> 使任何範型 List 的父類,只想表示它是某一類範型List的父類,這時候我們就要限定通配符的上限了。
<? extends Class> 表示該通配符所代表的類型是 Class 類型本身或者它的子類。或者 <? extends T>
把上面的 getData 方法修改一下:

public void testGeneric() {
    List<String> name = new ArrayList<String>();
    List<Integer> age = new ArrayList<Integer>();
    List<Shape> shape = new ArrayList<Shape>();
    List<Circle> circle = new ArrayList<Circle>();
    name.add("icon");
    age.add(18);
    shape.add(new Shape());
    getData(name);  // 編譯報錯
    getData(age);   // 編譯報錯
    getData(shape); // 編譯通過
    getData(circle);// 編譯通過
}
public static void getData(List<? extends Shape> data) {
    Log.e("Test","data :" + data.get(0));
}
public static class Shape {
    @Override
    public String toString() {
        return "Shape";
    }
}
public static class Circle extends Shape {
    @Override
    public String toString() {
        return "Circle";
    }
}

前面兩個用法就會報錯:

Error:(74, 17) 錯誤: 不兼容的類型: List<String>無法轉換爲List<? extends Shape>
Error:(75, 17) 錯誤: 不兼容的類型: List<Integer>無法轉換爲List<? extends Shape>

設定類型通配符的下限

public static void getData(List<? super Shape> data) {
    Log.e("Test","data :" + data.get(0));
}

getData(name);  // 編譯報錯
getData(age);   // 編譯報錯
getData(shape); // 編譯通過
getData(circle);// 編譯報錯

泛型方法

範型方法就是在聲明方法時定義一個或多個類型形參,該方法在調用時可以接收不同類型的參數。根據傳遞給泛型方法的參數類型,編譯器適當地處理每一個方法調用。
範型方法的類型作用域是整個方法。
範型方法的用法格式如下:

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

下面是定義泛型方法的規則:

  • 所有泛型方法聲明都有一個類型參數聲明部分(由尖括號分隔),該類型參數聲明部分在方法返回類型之前(在上面例子中的<T, S>)。
  • 每一個類型參數聲明部分包含一個或多個類型參數,參數間用逗號隔開。一個泛型參數,也被稱爲一個類型變量,是用於指定一個泛型類型名稱的標識符。
  • 類型參數可以用來聲明方法參數。
  • 類型參數能被用來聲明返回值類型。

先來看一個範型參數來聲明方法參數的例子:

public <T> String getData(T t) {
    return String.valueOf(t);
}

再來看一個範型參數來聲明返回值類型的例子:

private Map<String, Object> mDatas = new ArrayMap<>();
public <T> T getData(String name) {
    return (T) mDatas.get(name);
}

泛型類

泛型類的聲明和非泛型類的聲明類似,除了在類名後面添加了類型參數聲明部分。
和泛型方法一樣,泛型類的類型參數聲明部分也包含一個或多個類型參數,參數間用逗號隔開。
一個泛型參數,也被稱爲一個類型變量,是用於指定一個泛型類型名稱的標識符。因爲他們接受一個或多個參數,這些類被稱爲參數化的類或參數化的類型。

public class Paradigm<T> {
    private T t;
    public Paradigm() {
    }
    public Paradigm(T t) {
        this.t = t;
    }
    public T getT() {
        return t;
    }
    public Paradigm<T> setT(T t) {
        this.t = t;
        return this;
    }
}

限定類型參數上限

Java 範型不僅允許在使用通配符形參時設定上限,而且也可以在定義類型參數時設定上限,用於表示傳給該類型形參的實際類型要麼是該類型上限,要麼使該類型的子類。
這種做法可以用在範型方法和範型類中。
用法:<U, T extends Class1>

public <T extends Shape & Serializable> String getData(T t) {
    return String.valueOf(t);
}
public static class Shape {
    @Override
    public String toString() {
        return "Shape";
    }
}

形如 <U, T extends Class1 & Interface1> 表示 T 是繼承了 Class1 的類以及實現了 Interface1,後面的接口可以有多個,因爲 Java 是單繼承,因此父類只能有1個。類要寫在接口的前面。

<? extends T> 是指上界通配符(Upper Bounds Wildcards),用來限定泛型的上界。

        Paradigm<? extends Number> numberParadigm = new Paradigm<Integer>(Integer.valueOf(123));

        Number t2 = numberParadigm.getT();
        //不能存入任何元素
        numberParadigm.setT(Integer.valueOf(123));    //Error  編譯錯誤
        numberParadigm.setT(Float.valueOf(0.5F));    //Error  編譯錯誤
        numberParadigm.setT(Double.valueOf(0.5F));    //Error  編譯錯誤

<? extends Number> 這裏的問號「?」即爲泛型持有的類型,它的範圍必須是Number類的子類型或者本身,但不可以是Number的超類或者其它不相關的類型。

<? extends Number>會使Paradigm的setT()方法失效。但getT()方法還有效。

原因是編譯器只知道容器內是Nubber或者它的派生類,但具體是什麼類型不知道。可能是Nubber?可能是Integer?也可能是Float,Double?編譯器在看到後面用Paradigm<Integer>賦值以後,容器裏沒有被標上有“Integer”。而是標上一個佔位符:CAP#1,來表示捕獲一個Nubber或Nubber的子類,具體是什麼類不知道,代號CAP#1。然後無論是想往裏插入Integer或者Float或者Nubber編譯器都不知道能不能和這個CAP#1匹配,所以就都不允許。

<? super T> 是指下界通配符(Lower Bounds Wildcards),用來限定泛型的下界。

        Paradigm<? super Number> numberParadigm = new Paradigm<Number>(Integer.valueOf(123));

        //存入元素正常
        numberParadigm.setT(Integer.valueOf(123));    
        numberParadigm.setT(Float.valueOf(0.5F));    
        numberParadigm.setT(Double.valueOf(0.5F));    

        //讀取出來的東西只能存放在Object類裏
        Object o1 = numberParadigm.getT();
        Integer o2 = numberParadigm.getT();  //Error
        Float o3 = numberParadigm.getT();  //Error

因爲下界規定了元素的最小粒度的下限,實際上是放鬆了容器元素的類型控制。既然元素是Nubber的基類,那往裏存粒度比Nubber小的都可以。但往外讀取元素就費勁了,只有所有類的基類Object對象才能裝下。但這樣的話,元素的類型信息就全部丟失。 

自定義泛型T和類型通配符?的區別

首先他們都表示不確定的類型。
自定義泛型 T 可以在方法體內進行各種操作,比如:

T t = it.next();
System.out.println(t);

也可以方法返回值:

public  static <T> T getData(List<T> data){
    Log.e("Test","data :" + data.get(0));
    return data.get(0);
}

也就是說,當你僅僅想表達一種不確定類型時可以用類型通配符?,但你如果相對類型參數進行操作或者是想表達兩個類型參數之間或者參數與返回值之間關係時,這時就要用自定義泛型 T。

範型的特點

範型是類型擦除的

Java 的範型在編譯器有效,在運行期被刪除,也就是說所有的反省參數類型在編譯期後都會被清除掉。
下面看一段代碼:

public void listMethod(List<String> strings) {
}
public void listMethod(List<Integer> strings) {
}

這段代碼是否能編譯呢?
事實上,這段代碼時無法編譯的,編譯時報錯信息如下:

`listMethod(List<String>)` clashes with `listMethod(List<Integer>)`; both methods have same erasure

此錯誤信息是說 listMethod(List<String>) 方法在編譯時擦除類型後的方法是 listMethod(List<E>),它與另一個方法相沖突。這就是 Java 範型擦除引起的問題:在編譯後所有的範型類型都會做相應的轉化:
轉化規則如下:

  • List<String>List<Integer>List<T>擦除後的類型爲List
  • List<String>[]擦除後的類型爲List[]
  • List<? extends E>、List<? super E> 擦除後的類型爲 List<E>
  • List<T extends Serializable & Cloneable> 擦除後爲 List<Serializable>

範型的擦除還表現在當把一個具有範型信息的對象賦給另一個沒有範型信息的變量時,所有再尖括號之間的類型信息都將被扔掉。
比如:一個 List<String> 類型被轉換爲 List,則該 List 對集合元素的類型檢查變成了類型變量的上限(即 Object)。
下面用一個例子來示範這種擦除:

        public void testGenericErasure() {
        // 傳入 Integer 作爲類型形參的值
        Paradigm<Integer> paradigm = new Paradigm<>(8);
        // paradigm 的 get 方法返回 Integer 對象
        Integer size = paradigm.get();
        // 把 paradigm 對象賦值給b變量,此時會丟失<>裏的類型信息
        Paradigm b = paradigm;
        //這個代碼會引起編譯錯誤
//        Integer size1 = b.get();
        // b只知道size的類型是Number,但具體是Number的哪個子類就不清楚了。
        //下面的用法是正確的
        Number size2 = b.get();
    }

    public class Paradigm<T extends Number> {
        private T size;
        public Paradigm(T size) {
            this.size = size;
        }
        public void add(T size) {
            this.size = size;
        }
        public T get() {
            return size;
        }
    }

類型轉換:

public void testGenericConvert() {
    List<Integer> li = new ArrayList<>();
    li.add(8);
    // 類型擦除
    List list = li;
    // 類型轉換
    List<String> ls = list;
    // 下面的代碼會引起運行時異常
    // Caused by: java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
    Log.e("Test",ls.get(0));
}

明白了這些,對下面的代碼就容易理解了:

List<String> stringList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
Log.e("Test",""+stringList.getClass().equals(integerList.getClass()));

返回結果爲 true。 List<String> 和 List<Integer> 擦除後的類型都是 List,沒有任何區別。
之所以設計成可擦除的,有下面兩個原因:

  • 避免JVM大換血。由於範型是Java5以後才支持的,如果JVM也把範型類型延續到運行期,那麼JVM就需要進行大量的重構工作了。也就是說,Java 中的泛型機制其實就是一顆語法糖,並不涉及JVM的改動。
  • 版本兼容問題。在編譯器擦除可以更好地支持原生類型,在Java5或者Java6平臺上,即使聲明一個List這樣的原生類型也是支

不能創建一個範型類型實例

如果 T 是一個類型變量,那麼下面的語句是非法的:

T obj = new T();

T 由它的限界代替,這可能是 Object,或者是抽象類,因此對 new T() 的調用沒有意義。

不能初始化範型數組

數組元素的類型不能包含類型變量或者類型形參,除非是無上限的類型通配符。但是可以聲明元素類型包含類型變量或類型形參的數據。
也就是說,下面的代碼是OK的:

List<String>[] stringList ;

public class Paradigm<T extends Number> {
    private List<T> list = new ArrayList<T>();
}

下面的代碼,等號後面的代碼是非法的:

List<String>[] stringList = new ArrayList<String>[10];

基本類型不能做類型參數

因此,List<int> 是非法的,我們必須使用包裝類。

static 的語境不能引用類型變量

在一個範型類中,static 方法和 static 域均不可以引用類的類型變量,因爲類型擦除後類型變量就不存在了。而且,static 域在該類的諸範型實例之間是共享的。因此,static 的語境不能引用類型變量。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章