Java泛型基礎知識總結(超級詳細)

一、基本介紹

Java泛型是J2 SE1.5中引入的一個新特性,其本質是參數化類型,也就是說所操作的數據類型被指定爲一個參數(type parameter)這種參數類型可以用在類、接口和方法的創建中,分別稱爲泛型類、泛型接口、泛型方法。 

二、提出背景

Java集合(Collection)中元素的類型是多種多樣的。例如,有些集合中的元素是Byte類型的,而有些則可能是String類型的,等等。Java允許程序員構建一個元素類型爲Object的Collection,其中的元素可以是任何類型在Java SE 1.5之前,沒有泛型(Generics)的情況下,通過對類型Object的引用來實現參數的“任意化”,“任意化”帶來的缺點是要作顯式的強制類型轉換,而這種轉換是要求開發者對實際參數類型可以在預知的情況下進行的。對於強制類型轉換錯誤的情況,編譯器可能不提示錯誤,在運行的時候纔出現異常,這是一個安全隱患。因此,爲了解決這一問題,J2SE 1.5引入泛型也是自然而然的了。

1、代碼實例

package javase.genericity;

import java.util.ArrayList;
import java.util.List;

public class Test {
    public static void main(String[] args) {
        List list = new ArrayList<>();
        list.add("CSDN");
        list.add("素小暖");
        list.add(29);
        for (int i = 0; i < list.size(); i++) {
            String str = (String) list.get(i);
            System.out.println("泛型測試,str = " + str);
        }
    }
}

2、控制檯輸出 

崩潰了。

然而爲什麼呢?

ArrayList可以存放任意類型,例子中添加了一個String類型,添加了一個Integer類型,再使用時都以String的方式使用,因此程序崩潰了。爲了解決類似這樣的問題(在編譯階段就可以解決),泛型應運而生。

我們將第一行聲明初始化list的代碼更改一下,編譯器會在編譯階段就能夠幫我們發現類似這樣的問題。

定義泛型之後,編譯都通不過了,要的就是這個效果!

三、泛型的優缺點

1、優點

(1)類型安全

泛型的主要目的是提高Java程序的類型安全。通過知道使用泛型定義的變量的類型限制,編譯器可以在非常高的層次上驗證類型假設。沒有泛型,這些假設就只能存在於系統開發人員的頭腦中。

通過在變量聲明中捕獲這一附加的類型信息,泛型允許編譯器實施這些附加的類型約束。類型錯誤就可以在編譯時被捕獲了,而不是在運行時當作ClassCastException展示出來。將類型檢查從運行時挪到編譯時有助於Java開發人員更早、更容易地找到錯誤,並可提高程序的可靠性。

(2)消除強制類型轉換

泛型的一個附帶好處是,消除源代碼中的許多強制類型轉換。這使得代碼更加可讀,並且減少了出錯機會。儘管減少強制類型轉換可以提高使用泛型類的代碼的累贊程度,但是聲明泛型變量時卻會帶來相應的累贊程度。在簡單的程序中使用一次泛型變量不會降低代碼累贊程度。但是對於多次使用泛型變量的大型程序來說,則可以累積起來降低累贊程度。所以泛型消除了強制類型轉換之後,會使得代碼加清晰和筒潔。

(3)更高的效率

在非泛型編程中,將筒單類型作爲Object傳遞時會引起Boxing(裝箱)和Unboxing(拆箱)操作,這兩個過程都是具有很大開銷的。引入泛型後,就不必進行Boxing和Unboxing操作了,所以運行效率相對較高,特別在對集合操作非常頻繁的系統中,這個特點帶來的性能提升更加明顯。

(4)潛在的性能收益

泛型爲較大的優化帶來可能。在泛型的初始實現中,編譯器將強制類型轉換(沒有泛型的話,Java系統開發人員會指定這些強制類型轉換)插入生成的字節碼中。但是更多類型信息可用於編譯器這一事實,爲未來版本的JVM的優化帶來可能。

四、使用泛型時的注意事項

1、在定義一個泛型類時,在“<>”之間定義形式類型參數,例如:“class TestGen<K,V>”,其中“K”,“V”不代表值,而是表示類型。

2、實例化泛型對象時,一定要在類名後面指定類型參數的值(類型),一共要有兩次書寫。

3、使用泛型時,泛型類型必須爲引用數據類型,不能爲基本數據類型,Java中的普通方法,構造方法,靜態方法中都可以使用泛型,方法使用泛型之前必須先對泛型進行聲明,可以使用任意字母,一般都要大寫。

4、不可以定義泛型數組。

5、在static方法中不可以使用泛型,泛型變量也不可以用static關鍵字來修飾。

6、根據同一個泛型類衍生出來的多個類之間沒有任何關係,不可以互相賦值。

7、泛型只在編譯器有效

8、instanceof不允許存在泛型參數

以下代碼不能通過編譯,原因一樣,泛型類型被擦除了

五、泛型的使用

泛型有三種使用方式,分別爲:泛型類、泛型接口、泛型方法

 1、泛型類

package javase.genericity;
 
//此處T可以隨便寫爲任意標識,常見的如T、E、K、V等形式的參數常用於表示泛型
//在實例化泛型類時,必須指定T的具體類型
public class Generic<T> {
    //key這個成員變量的類型爲T,T的類型由外部指定
    private T key;
    //泛型構造方法形參key的類型也爲T,T的類型由外部指定
    public Generic(T key){
        this.key = key;
    }
    //泛型方法getKey的返回值類型爲T,T的類型由外部指定
    public T getKey(){
        return key;
    }
 
    public static void main(String[] args) {
        //泛型的類型參數只能是類類型(包括自定義類),不能是簡單類型
        //傳入的實參類型需與泛型的類型參數類型相同,即爲Integer.
        Generic<Integer> genericInteger = new Generic<Integer>(123456);
        //傳入的實參類型需與泛型的類型參數類型相同,即爲String.
        Generic<String> genericString = new Generic<String>("江疏影");
        System.out.println("泛型測試,key is "+genericInteger.getKey());
        System.out.println("泛型測試,key is "+genericString.getKey());
    }
}

泛型參數就是隨便傳的意思!


Generic generic = new Generic("111111");
Generic generic1 = new Generic(4444);
Generic generic2 = new Generic(55.55);
Generic generic3 = new Generic(false);

2、泛型接口

泛型接口與泛型類的定義及使用基本相同。泛型接口常被用在各種類的生產器中,可以看一個例子:

//定義一個泛型接口
public interface Generator<T> {
    public T next();
}

當實現泛型接口的類,未傳入泛型實參時:

/**
 * 未傳入泛型實參時,與泛型類的定義相同,在聲明類的時候,需將泛型的聲明也一起加到類中
 * 即:class FruitGenerator<T> implements Generator<T>{
 * 如果不聲明泛型,如:class FruitGenerator implements Generator<T>,編譯器會報錯:"Unknown class"
 */
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}

當實現泛型接口的類,傳入泛型實參時:

package javase.genericity;
 
import java.util.Random;
 
public class FruitGenerator implements  Generator<String>{
    String[] fruits = new String[]{"apple","banana","Pear"};
    @Override
    public String next() {
        Random random = new Random();
        System.out.println(fruits[random.nextInt(3)]);
        return fruits[random.nextInt(3)];
    }
 
    public static void main(String[] args) {
        FruitGenerator ff = new FruitGenerator();
        ff.next();
    }
}

3、泛型通配符

我們知道integer是number的一個子類,同時Generic<Integer>和Generic<Number>實際上是相同的一種基本類型。那麼問題來了,在使用Generic<Number>作爲形參的方法中,能否使用Generic<Integer>的實例傳入呢?在邏輯上類似於Generic<Number>和Generic<Integer>是否可以看成具有父子關係的泛型類型呢?

爲了弄清楚這個問題,我們使用Generator<T>這個泛型類繼續看下面的例子:

回到上面的例子,如何解決上面的問題?總不能爲了定義一個新的方法來處理Generic<Integer>類型的類,這顯然與java中的多臺理念相違背。因此我們需要一個在邏輯上可以表示同時是Generic<Integer>和Generic<Number>父類的引用類型。由此類型通配符應運而生。

我們可以將上面的方法改一下:

類型通配符一般是使用?代替具體的類型參數,注意了,此處?是類型實參,而不是類型形參。此處的?和Number、String、Integer一樣都是一種實際的類型,可以把?看成所有類型的父類。是一種真實的類型。

可以解決當具體類型不確定的時候,這個通配符就是 ?  ;當操作類型時,不需要使用類型的具體功能時,只使用Object類中的功能。那麼可以用 ? 通配符來表未知類型。

4、泛型方法

泛型類,是在實例化類的時候指明泛型的具體類型;

泛型方法,是在調用方法的時候指明泛型的具體類型。

package javase.genericity;

public class Test {
    public static void main(String[] args) {
        try {
            Object CSDN = genericMethod(Class.forName("javase.genericity.CSDN"));
            System.out.println(CSDN);
            Object OSCHINA = genericMethod(Class.forName("javase.genericity.Oschina"));
            System.out.println(OSCHINA);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    /**
     * 泛型方法的基本介紹
     * @param tClass 傳入的泛型實參
     * @return T 返回值爲T類型
     * 說明:
     *     1)public 與 返回值中間<T>非常重要,可以理解爲聲明此方法爲泛型方法。
     *     2)只有聲明瞭<T>的方法纔是泛型方法,泛型類中的使用了泛型的成員方法並不是泛型方法。
     *     3)<T>表明該方法將使用泛型類型T,此時纔可以在方法中使用泛型類型T。
     *     4)與泛型類的定義一樣,此處T可以隨便寫爲任意標識,常見的如T、E、K、V等形式的參數常用於表示泛型。
     */
    public static <T> T genericMethod(Class<T> tClass)throws InstantiationException,IllegalAccessException{
        T instance = tClass.newInstance();
        return instance;
    }
}

  • 泛型方法與可變參數

如果靜態方法要使用泛型的話,必須將靜態方法也定義成泛型方法 。

package javase.genericity;
 
public class GenericFruit {
    //靜態方法中使用泛型,必須要將泛型定義在方法上。
    public static <T> void printMsg(T...args){
        for(T t:args){
            System.out.println("泛型測試,it is "+t);
        }
    }
 
    public static void main(String[] args) {
        printMsg("1111",2222,"江疏影","0.00",55.55);
    }
}

儘量使用泛型方法!

六、泛型上下邊界

1、設定通配符上限

首先,我們來看一下設定通配符上限用在哪裏....

現在,我想接收一個List集合,它只能操作數字類型的元素【Float、Integer、Double、Byte等數字類型都行】,怎麼做???

我們學習了通配符,但是如果直接使用通配符的話,該集合就不是隻能操作數字了。因此我們需要用到設定通配符上限

2、設定通配符下限

既然上面我們已經說了如何設定通配符的上限,那麼設定通配符的下限也不是陌生的事了。直接來看語法吧

//傳遞進來的只能是Type或Type的父類
<? super Type>

設定通配符的下限這並不少見,在TreeSet集合中就有....我們來看一下

public TreeSet(Comparator<? super E> comparator) {
    this(new TreeMap<>(comparator));
}

那它有什麼用呢??我們來想一下,當我們想要創建一個TreeSet<String>類型的變量的時候,並傳入一個可以比較String大小的Comparator。

那麼這個Comparator的選擇就有很多了,它可以是Comparator<String>,還可以是類型參數是String的父類,比如說Comparator<Objcet>....

這樣做,就非常靈活了。也就是說,只要它能夠比較字符串大小,就行了

在泛型的上限和下限中有一個原則:PECS(Producer Extends Consumer Super)

帶有子類限定的可以從泛型讀取【也就是--->(? extend T)】-------->Producer Extends

帶有超類限定的可以從泛型寫入【也就是--->(? super T)】-------->Consumer Super

七、泛型擦除

泛型是提供給javac編譯器使用的,它用於限定集合的輸入類型,讓編譯器在源代碼級別上,即擋住向集合中插入非法數據。但編譯器編譯完帶有泛形的java程序後,生成的class文件中將不再帶有泛形信息,以此使程序運行效率不受到影響,這個過程稱之爲“擦除”。

八、兼容性

JDK5提出了泛型這個概念,但是JDK5以前是沒有泛型的。也就是泛型是需要兼容JDK5以下的集合的。

當把帶有泛型特性的集合賦值給老版本的集合時候,會把泛型給擦除了。

值得注意的是:它保留的就類型參數的上限。

List<String> list = new ArrayList<>();

//類型被擦除了,保留的是類型的上限,String的上限就是Object
List list1 = list;

它也不會報錯,僅僅是提示“未經檢查的轉換”。

九、泛型的應用

當我們寫網頁的時候,常常會有多個Dao,我們要寫每次都要寫好幾個Dao,這樣會有點麻煩。

那麼我們想要的效果是什麼呢??只寫一個抽象Dao,別的Dao只要繼承該抽象Dao,就有對應的方法了。

要實現這樣的效果,肯定是要用到泛型的。因爲在抽象Dao中,是不可能知道哪一個Dao會繼承它自己,所以是不知道其具體的類型的。而泛型就是在創建的時候才指定其具體的類型。

1、抽象dao

package javase.genericity.dao;

import javase.genericity.entity.Worker;

import javax.jms.Session;
import java.lang.reflect.ParameterizedType;

public abstract class BaseDao<T> {
    private Session session;
    private Class clazz;

    //哪個子類調的這個方法,得到的class就是子類處理的類型(非常重要)
    public BaseDao(){
        Class clazz = this.getClass();  //拿到的是子類
        ParameterizedType pt = (ParameterizedType) clazz.getGenericSuperclass();  //BaseDao<Category>
        clazz = (Class) pt.getActualTypeArguments()[0];
        System.out.println(clazz);

    }

    public void add(T t){
        System.out.println(t+",增加");
    }

    public T find(String id){
        System.out.println("查找"+id);
        Worker worker = null;
        return (T)worker;
    }

    public void update(T t){
        System.out.println(t+",更新");
    }

    public void delete(String id){
        System.out.println("刪除"+id);
    }
}

2、WorkerDao ,繼承抽象DAO

該實現類就有對應的增刪改查的方法了。

package javase.genericity.dao;

import javase.genericity.entity.Worker;

public class WorkerDao extends BaseDao<Worker>{
    public static WorkerDao instance = new WorkerDao();
    @Override
    public void add(Worker worker) {
        super.add(worker);
    }

    @Override
    public Worker find(String id) {
        return super.find(id);
    }

    public static void main(String[] args) {
        WorkerDao.instance.add(new Worker(1,"素小暖"));
    }
}
package javase.genericity.dao;

import javase.genericity.entity.Teacher;

public class TeacherDao extends BaseDao<Teacher> {
}

3、Worker 實體類 

package javase.genericity.entity;

public class Worker {
    private int id;
    private String name;

    public Worker(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Worker{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

4、控制檯輸出 

 

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