Java泛型的約束與局限性

2021-01-10 全棧技術資源社區

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

不能用類型參數代替基本類型:例如,沒有Pair,只有Pair,其原因是類型擦除。擦除之後,Pair類含有Object類型的域,而Object不能存儲double值。這體現了Java語言中基本類型的獨立狀態。

運行時類型查詢只適用於原始類型(raw type)

運行時:通常指在Classloader裝載之後,JVM執行之時

類型查詢:instanceof、getClass、強制類型轉換

原始類型:即(raw type),泛型類型經編譯器類型擦除後是Object或泛型參數的限定類型(例如Pair,Comparable就是T的限定類型,轉化後泛型的原始類型就是Comparable,所以Pair類不帶泛型是Pair),即Pair類含有Comparable類型的域

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

//加入Java開發交流君樣:756584822一起吹水聊天

不能創建參數化類型的數組(泛型數組)

參數化類型的數組:指類型帶有泛型參數的數組,也即泛型數組,如Pair[] 、 T[]

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

Pair<String> table = new Pair<String>[10]; //ERROR

在這裡我們假設可以實例化,那麼經編譯器類型擦除後,table的類型是Pair[],我們再讓它協變為Object[]:

Object[] objArray = table;

而一般來說,數組會記住他的元素類型Pair,我們如果試圖存儲其他類型的元素,就會拋出異常(數組存儲檢查),例如:

objArray[0] = "Hello"; //ERROR--component type is Pair

但是,對於泛型類型Pair,類型擦除會使這種不同類檢查機制無效,這就是不能實例化泛型數組的原因!

objArray[0] = new Pair<Employee>();

//如果泛型機制允許我們實例化數組,那麼這一步就沒理由出錯了!

//而這違背了我們的初衷(限定類型)

數組存儲只會檢查擦除後的類型,又因為Java語言設計數組可以協變,所以可以通過編譯

能夠通過數組存儲檢查,不過仍會導致一個類型錯誤,故不允許創建參數化類型的數組

注意,聲明類型為Pair[]的變量是合法的,只是不能創建這些實例(我們應該直接用new Pair[10]{…}來初始化這個變量)

泛型數組的間接實現:

通過泛型數組包裝器,如ArrayList類,維護一個Object數組,然後通過進出口方法set、get來限定類型和強制轉換數組類型,從而間接實現泛型數組,

例如:ArrayList: ArrayList<Pair<T>>、ArrayList<T>

不能實例化類型變量T

即不能使用

new T(..) , new T[..] 或 T.class

這樣的表達式中的類型變量

例如:

public Pair() { first = new T(); } //ERROR!

類型擦除將T改變成Object,調用非本意的new Object()

不能使用

new T(..)

但是,可通過反射調用

Class.newInstance

方法來構造泛型對象(要注意表達式T.class是非法的)

public static <T> Pair<T> makePair(Class<T> cl){

try{ return new Pair<>(cl.newInstance() , cl.newInstance()); }

catch(Exception ex) { return null; }

}

//這個方法可以按照下列方式調用:

Pair<String> p = Pair.makePair(String.class);

注意:Class類本身是泛型。String.class是一個Class的實例,因此makePair方法能夠推斷出pair的類型

不能使用new T[…]

解決方案:使用泛型數組包裝器,例如

ArrayList

然而,當在設計一個泛型數組包裝器時,例如方法minmax返回一個T[]數組,則泛型數組包裝器無法施展,因為類型擦除,

return (T [])new Object

是沒有意義的強轉不了。此時只好利用反射,調用

Array.newInstance

import java.lang.reflect.*;

...

public static <T extends Comparable> T[] minmax(T... a){

T[] mm = (T[]) Array.newInstance(a.getClass().getComponentType() , 2);

【API文檔描述】public Class<?> getComponentType() 返回表示數組組件類型的 Class。如果此類不表示數組類,則此方法返回 null。

而ArrayList類中的toArray方法的實現就麻煩了

public Object[] toArray() 無參,返回Object[]數組即可 

public Object[] toArray() {

return Arrays.copyOf(elementData, size);

【API文檔描述】public static T[] copyOf(T[] original,int newLength)

複製指定的數組,截取或用 null 填充(如有必要),以使副本具有指定的長度。對於在原數組和副本中都有效的所有索引,這兩個數組將包含相同的值。對於在副本中有效而在原數組無效的所有索引,副本將包含 null。若且唯若指定長度大於原數組的長度時,這些索引存在。所得數組和原數組屬於完全相同的類。

public T[] toArray(T[] a) a - 要存儲列表元素的T[]數組(如果它足夠大)否則分配一個具有相同運行時類型的新數組,返回該T[]數組

@SuppressWarnings("unchecked")

public <T> T[] toArray(T[] a) {

if (a.length < size)

// Make a new array of a's runtime type, but my contents:

return (T[]) Arrays.copyOf(elementData, size, a.getClass()); //a.getClass()得運行時目的數組的運行時類型//加入Java開發交流君樣:756584822一起吹水聊天

System.arraycopy(elementData, 0, a, 0, size);

if (a.length > size)

a[size] = null;

return a;

【API文檔描述】

public static <T,U> T[] copyOf(U[] original,int newLength, Class<? extends T[]> newType)

複製指定的數組,截取或用 null 填充(如有必要),以使副本具有指定的長度。對於在原數組和副本中都有效的所有索引,這兩個數組將包含相同的值。對於在副本中有效而在原數組無效的所有索引,副本將包含 null。若且唯若指定長度大於原數組的長度時,這些索引存在。所得數組屬於 newType 類。

泛型類的靜態上下文中類型變量無效

泛型類不能在靜態域或靜態方法中引用類型變量

public class Singleton<T>{

private static T singleInstance; //ERROR

public static T getSingleInstance(){...} //ERROR

類型擦除後只剩下Singleton類,因為靜態所以他只包含一個singleInstance域,如果能運行則以Singleton類為模板生成不同類型的域,因此產生了衝突

不能throws或catch泛型類的實例(有關異常)

泛型類繼承Throwable類不合法,如

public class Problem<T> extends Exception {...}

//ERROR 不能通過編譯

catch

子句不能使用類型變量

public static <T extends Throwable> void doWork(Class<T> t){

try{

do work

}catch (T e){ // ERROR

Logger.global.info(...)

不過,在異常規範中使用類型變量是允許的:

public static <T extends Throwable> void doWork(T t) throws T { //此時可以throws T

try{//加入Java開發交流君樣:756584822一起吹水聊天

}catch (Throwable realCause){ //捕獲到具體實例

t.initCause(realCause);

throw t; //這時候拋具體實例,所以throw t 和 throws T 是可以的!

此特性作用:可以利用泛型類、類型擦除、SuppressWarnings標註,來消除對已檢查(checked)異常的檢查,

unchecked和checked異常: Java語言規範將派生於Error類或RuntimeException的所有異常稱為未檢查(unchecked)異常,其他的是已檢查(checked)異常

Java異常處理原則:必須為所有已檢查(checked)異常提供一個處理器,即一對一個,多對多個

//SuppressWarning標註很關鍵,使得編譯器認為T是unchecked異常從而不強迫為每一個異常提供處理器

public static <T extends Throwable> void throwAs(Throwable e) throws

T{ //因為泛型和類型擦除,可以傳遞任意checked異常,例如RuntimeException類異常

throw (T) e;

假設該方法放在類Block中,如果調用 Block.throwAs(t); 編譯器就會認為t是一個未檢查的異常

public abstract class Block{

public abstract void body() throws Exception;

public Thread toThread(){

return new Thread(){

public void run(){

body();

}catch(Throwable t){

Block.<RuntimeException>throwAs(t);

}//加入Java開發交流君樣:756584822一起吹水聊天

};

public static <T extends Throwable> void throwAs(Throwable e) throws T{

再寫個測試類

public class Test{

public static void main(String[] args){

new Block(){

public void body() throws Exception{

//不存在ixenos文件將產生IOException,checked異常!

Scanner in = new Scanner(new File("ixenos"));

while(in.hasNext())

System.out.println(in.next());

}.toThread().start();

啟動線程後,throwAs方法將捕獲線程run方法所有checked異常,「處理」成uncheckedException(其實只是騙了編譯器)後拋出;有什麼意義?正常情況下,因為run()方法聲明為不拋出任何checked異常,所以必須捕獲所有checked異常並「包裝」到未檢查的異常中;意義:而我們這樣處理後,就不必去捕獲所有並包裝到

unchecked

異常中,我們只是拋出異常並「哄騙」了編譯器而已

注意擦除後的衝突

Java泛型規範有個原則:「要想支持擦除的轉換,就需要強行限制一個泛型類或類型變量T不能同時成為兩個接口類型的子類,而這兩個接口是統一接口的不同參數化」

注意:非泛型類可以同時實現同一接口,畢竟沒有泛型,很好處理

class Calender implements Comparable<Calender>{...}

class GGCalender extends Calender implements Comparable<GGCalender>{...} //ERROR

123在這裡GGCalender類會同時實現Comparable 和 Comparable,這是同一接口的不同參數化

相關焦點

  • Java泛型的特點與優缺點,泛型擦除是怎麼回事?
    所有參數化容器類都被擦除成非參數化的(raw type);如List<E>、List<List<E>>都被擦除成List;2)所有參數化數組都被擦除成非參數化的數組;如List<E>[],被擦除成List[];3)Raw type的容器類,被擦除成其自身,如List 被擦除成List;4)原生類型(int,String還有wrapper類)都擦除成他們的自身;6)所有約束參數如
  • 泛型Java程式設計師必備基礎
    其所操作的數據類型被指定為一個參數(type parameter)這種參數類型可以用在類、接口和方法的創建中,分別稱為泛型類、泛型接口、泛型方法。泛型類泛型類(generic class) 就是具有一個或多個類型變量的類。
  • Java之使用泛型與未使用泛型的區別
    各位小夥伴們大家好,在之前的文章中,小編有介紹過Java之泛型的概念,這次小編要介紹的是,Java程序中,使用泛型與未使用泛型的區別。代碼如下:public class Demo01Generic {public static void main(String[] args) { show01(); show02(); } /*創建集合對象,使用泛型好處:1.避免了類型轉換的麻煩,存儲的是什麼類型的數據,取出的就是什麼類型的數據 2.把運行期異常
  • Java 泛型背後的原理
    作者 | 的一幕  來源 | https://www.jianshu.com/p/dd34211f2565這一節主要講的內容是java中泛型的應用,通過該篇讓大家更好地理解泛型,以及面試中經常說的泛型類型擦除是什麼概念,今天就帶著這幾個問題一起看下:舉一個簡單的例子:這裡可以看出來在代碼編寫階段就已經報錯了,不能往string類型的集合中添加int類型的數據。
  • Java泛型背後是什麼
    這一節主要講的內容是java中泛型的應用,通過該篇讓大家更好地理解泛型,以及面試中經常說的泛型類型擦除是什麼概念,今天就帶著這幾個問題一起看下:舉一個簡單的例子:這裡可以看出來在代碼編寫階段就已經報錯了,不能往string類型的集合中添加int類型的數據。
  • Java反射,泛型在Json中的運用
    最近項目中遇到了Json數據自動獲取的功能,不然令人想起java的反射,已經很長時間沒複習java了正好一塊連java的這一塊內容一起過一遍。java中的反射無疑就相當於java開發者的春天,在眾多的框架中也能看到它的身影,可以在運行時檢查類,接口、變量和方法等信息,可以實例化調用方法以及設置變量值等。本文主要以代碼的形式直接將反射,泛型的運用展現出來。java中的反射首先新建一個基礎類Author。
  • Java總結篇系列:Java泛型
    泛型概念的提出(為什麼需要泛型)?因為編譯階段正常,而運行時會出現「java.lang.ClassCastException」異常。因此,導致此類錯誤編碼過程中不易發現。在如上的編碼過程中,我們發現主要存在兩個問題:1.當我們將一個對象放入集合中,集合不會記住此對象的類型,當再次從集合中取出此對象時,改對象的編譯類型變成了Object類型,但其運行時類型任然為其本身類型。
  • Java泛型總結
    什麼是泛型泛型是jdk5引入的類型機制,就是將類型參數化,它是早在1999年就制定的jsr14的實現。泛型機制將類型轉換時的類型檢查從運行時提前到了編譯時,使用泛型編寫的代碼比雜亂的使用object並在需要時再強制類型轉換的機制具有更好的可讀性和安全性。
  • Java泛型解析,了解泛型使用
    Java 泛型泛型的本質是參數化類型。簡單解釋就是,將參數的數據類型也當作參數對待。泛型的目的就是為了寫一套代碼,可以到處通用,不用擔心類型安全的問題。泛型可以用在類、接口、方法中對應的就是泛型類、泛型接口和泛型方法。一、為什麼要引入泛型?
  • 為什麼我們需要Java中的泛型類型?
    泛型類型在Java集合中廣泛使用。為什麼我們需要Java中的泛型類型?理解這個問題可以幫助我們更好地理解許多相關概念。在本文中,我將使用一個非常簡短的示例來說明Generic為什麼有用。1.泛型概述實現泛型的目的是在編譯時而不是運行時中發現錯誤。在編譯時查找錯誤可以節省調試Java程序的時間,因為編譯時錯誤更容易查找和修復。泛型類型僅在編譯時存在。這是學習Java泛型時要記住的最重要的事情。2.如果沒有泛型怎麼辦?
  • Java 泛型詳解-絕對是對泛型方法講解最詳細的,沒有之一
    本文參考java 泛型詳解、Java中的泛型方法、 java泛型詳解概述泛型在java中有很重要的地位,在面向對象編程及各種設計模式中有非常廣泛的應用。什麼是泛型?為什麼要使用泛型?泛型,即「參數化類型」。一提到參數,最熟悉的就是定義方法時有形參,然後調用此方法時傳遞實參。那麼參數化類型怎麼理解呢?
  • JAVA總結篇系列-泛型
    泛型概念的提出(為什麼需要泛型)?2.因此,//1處取出集合元素時需要人為的強制類型轉化到具體的目標類型,且很容易出現「java.lang.ClassCastException」異常。那麼有沒有什麼辦法可以使集合能夠記住集合內元素各類型,且能夠達到只要編譯時不出現問題,運行時就不會出現「java.lang.ClassCastException」異常呢?答案就是使用泛型。
  • 3 分鐘帶你徹底搞懂 Java 泛型背後的秘密
    作者 | 的一幕來源 | www.jianshu.com/p/dd34211f2565這一節主要講的內容是java中泛型的應用
  • Java的「泛型」特性,你以為自己會了?(萬字長文)
    - (表示不確定的java類型)但是泛型的參數只能是類類型,不能是基本的數據類型,他的類型一定是自Object的注意:泛型不接受基本數據類型,換句話說,只有引用類型才能作為泛型方法的實際參數2. 為什麼要使用泛型?
  • Java基礎教程:Java泛型概述及應用(Java難點)
    就要把迭代出來的對象轉成String類型 String str = (String) it.next(); System.out.println(str.length()); }}}程序在運行時發生了問題java.lang.ClassCastException
  • java泛型的基本使用案例,重基礎
    持續更新中……在上幾篇圖文中已經介紹了容器集合的大部分的基本使用,接下來按照知識圖譜的順序來介紹一下java泛型的使用。在下面的例子中講創建一個繼承結構,然後用基本類型的類型來聲明容器,看看容器表現是否表現正常,並且創建一個泛型方法,來觀察對類型的處理。下面通過代碼展示:
  • Java 泛型詳解
    看完了泛型類,接下來我們來了解一下泛型方法。類型擦除就是說Java泛型只能用於在編譯期間的靜態類型檢查,然後編譯器生成的代碼會擦除相應的類型信息,這樣到了運行期間實際上JVM根本就知道泛型所代表的具體類型。這樣做的目的是因為Java泛型是1.5之後才被引入的,為了保持向下的兼容性,所以只能做類型擦除來兼容以前的非泛型代碼。對於這一點,如果閱讀Java集合框架的源碼,可以發現有些類其實並不支持泛型。
  • Java 泛型 T,E,K,V,?,傻傻分不清?
    泛型的本質是參數化類型,也就是說所操作的數據類型被指定為一個參數。 # 泛型帶來的好處 在沒有泛型的情況的下,通過對類型 Object 的引用來實現參數的「任意化」,「任意化」帶來的缺點是要做顯式的強制類型轉換,而這種轉換是要求開發者對實際參數類型可以預知的情況下進行的。
  • 掃盲:Kotlin 的泛型
    : java.lang.Integer cannot be cast to java.lang.String這個體驗非常糟糕,我們真正需要的是在代碼編譯的時候就能發現錯誤,而不是讓錯誤的代碼發布到生產環境中。
  • Java基礎-今日內容介紹(集合、Iterator、增強for循環 泛型)
    泛型就是需要我們指定存儲的數據類型。Java的泛型是偽泛型,編譯後的文件裡是沒有泛型的。2.Iterator迭代器知識點:java中有很多集合,但是我們不可能每個集合都去寫一個迭代器,所以,我們使用有一個通用的迭代器Iterator迭代器遍歷集合元素3.增強for循環:增強for循環,好處是代碼少,弊端是不能操作裡面的元素,比如排序啊等等。