Java泛型小結
什麼是泛型
泛型, 可以理解為對型別的抽象。指定了泛型引數的型別就是一個具體化了的型別。一個泛型和其指定了泛型引數的型別的關係,在概念上更像是父類和子類的關係,是一種抽象與具體的關係。我自己覺得他是在面向物件程式設計思想多型的特點上對實際情況的更高一層的抽象。
Java為什麼引入泛型
Java中的泛型是在SE5中加入的概念,也就是說Java一開始是不支援泛型的。那麼Java為什麼要引入泛型機制呢?
程式碼的複用性
首先為了程式碼的複用性,也就是為了使使用者能更加泛化自己的程式碼。正如前面所說的,泛型是面向物件程式設計思想多型的特點上對實際情況更高一層的抽象,這種抽象更凌駕於父類-子類這種關係之上。因此提供了更高層的抽象能力。這是其第一個目的,但是不是其最重要的目的。我們知道在Java中Object類是所有類的父類。那麼,如果單純為了程式碼的複用性,我們是不是通過只持有Object類的物件就可以解決了:
class MyClass{
private Object object;
public setObject(Object object){
this.object=object;
}
public getObject(){
return object;
}
}
這個類可以持有Java中的所有物件,如果單單從泛化的角度來說,這其實就夠了。那麼為什麼還要引入泛型呢?這就引出了引入泛型概念的第二個目的-編譯期型別檢查
編譯期型別檢查
還以我們前面定義的MyClass為例子,雖然它可以持有Java中的所有物件,但是要看一下我們在程式中使用時是多麼的麻煩。
public class TestA {
public String aTalk(){
return "this is TestA";
}
}
public class TestB {
public String bTalk(){
return "this is TestB";
}
}
public class Main {
public static void main(String[] args){
MyClass myClass=new MyClass();
myClass.setObject(new TestA());
TestA testA=(TestA)myClass.getObject();
System.out.println(testA.aTalk());
myClass.setObject(new TestB());
TestB testB=(TestB)myClass.getObject();
System.out.println(testB.bTalk());
}
}
可以看到,雖然程式可以執行正確,但前提是在我們一路強轉,並且一路強轉正確的情況下程式才能執行正確的。但實際用在工程中呢?這段程式存在兩個比較大的問題:a、程式強制轉換太多,很容易強轉出錯,導致程式執行異常。b、即使程式強轉出錯了,在編譯器在編譯期也檢查不出來,只能在程式執行的時候拋異常。雖然大部分程式猿是很靠譜的,但是不排除那一部分不靠譜的。嗯哼,怎麼拯救這些程式猿呢?OK引入泛型吧。看下用泛型重寫以上程式碼的結果吧。
public class MyClass1<T> {
private T object;
public void setObject(T object){
this.object=object;
}
public T getObject(){
return object;
}
}
public class Main {
public static void main(String[] args){
MyClass1<TestA> myClass=new MyClass1<TestA>();
myClass.setObject(new TestA());
TestA testA=myClass.getObject();
System.out.println(testA.aTalk());
/*myClass.setObject(new TestB()); //編譯錯誤
TestB testB=(TestB)myClass.getObject();
System.out.println(testB.bTalk());*/
}
}
可以看到在這個例子中,並沒有像上例中那樣人為地對元素進行強轉。從而避免了人為因素導致的強轉錯誤而程式執行失敗。而且對於本身就錯誤的程式碼,編譯器可以在編譯期就給出錯誤提示,而不用讓我們等到執行時才提示錯誤。之所以泛型能讓我們這麼省事,其實是編譯器幫忙做了很多工作。這裡先不談這些編譯器為我們做了哪些工作,因為涉及到Java是如何實現泛型的問題,文章最後會提到。接下來我們看Java都對泛型做了哪些支援。
Java對泛型的支援型別
泛型可應用的場合
泛型類
使用方法如上節中的MyClass。
泛型介面
和泛型類的使用方法相同,只不過類宣告的時候使用class而介面使用interface。
泛型方法
在Java中,上一節中MyClass就是典型的泛型類的使用,除此之外還有影響範圍更小的泛型方法可以使用。泛型方法的使用和泛型類的使用是相互獨立的。並且在TIJ裡面明確指出如果可以使用泛型方法來解決的問題,就不要使用泛型類了。究其原因,我想是由於如果使用泛型類,那麼如果想處理另外一種型別的話,免不了要生成一個針對那種型別的新類。但是如果使用泛型方法就不用,他只是在呼叫泛型方法的時候確定引數型別,不用生成新類。ok,泛型方法的定義一般如下所示:
public class GernericMethod {
public <T> void printClazz(T clazz){
System.out.println(clazz.getClass().getName());
}
}
注意格式,泛型引數要寫在返回型別前面哦。其使用方法如下:
public class Main {
public static void main(String[] args){
GernericMethod gernericMethod=new GernericMethod();
gernericMethod.printClazz(1);
gernericMethod.printClazz("天啦嚕!");
}
}
其輸出結果如下:
java.lang.Integer
java.lang.String
程式在執行gernericMethod.printClazz(1)這一句的時候,由於Java的泛型不支援源生型別如int,long等,因此Java通過autoBoxing將1包裝成了Integer型別的了。細心點的同學可能發現了,上面在對泛型方法的呼叫中,並沒有什麼跡象表明這是在呼叫一個泛型方法。可以看到,在使用泛型類時,在宣告的時候我們需要使用MyClass[TestA]這種形式來顯示的宣告泛型類的型別引數,但是在使用泛型方法的時候卻完全沒有使用到相關的型別資訊。這是為什麼呢?Java中的泛型方法有一種叫做argument inference的能力,在呼叫泛型方法的時候,他可以通過你的呼叫過程來推斷出自己的型別引數是什麼,而不用我們來指定。在很多情況下,泛型方法的這個特性可以用來簡化對Java容器類的宣告過程。例如在Guava中的Maps等容器工具類中,都有構造JDK容器類的靜態工具方法如Maps.new HashMap()。其實內部就是利用的泛型方法的型別推斷機制。
但是有一點需要注意,大家不要以為泛型方法這種argument inference的能力是萬能的,Type inference doesn’t work for anything other than assignment.
只有在賦值語句或者是上面這種情形下,這種能力才能生效,很多情況下如:其返回值作為引數交給其他方法去處理的時候就不生效了。但是JDK1.8中對於Java中的型別推導做了很大的改進。Java的整個型別推導有了很大的改善。不論是泛型類的宣告還是使用都變的很智慧了。詳見JDK1.8泛型型別推導。
泛型宣告的方式
普通宣告方式
普通的宣告方式就如同上例中的MyClass一樣:
public class MyClass<T> {
private T object;
public void setObject(T object) {
this.object = object;
}
public T getObject() {
return object;
}
}
帶有限定符的宣告方式
帶有限定符的宣告方式會把型別引數限定為某一個類的子類,其一般宣告方式如下:
public class MyClass<T extends TestB> {
private T object;
public void setObject(T object) {
this.object = object;
}
public T getObject() {
return object;
}
public void action() {
object.bTalk();
}
public static void main(String[] args) {
// MyClass<TestA> myClass = new MyClass<TestA>();
// myClass.setObject(new TestA());
// TestA testA = myClass.getObject();
// System.out.println(testA.aTalk());
// myClass.setObject(new TestB()); //編譯錯誤 TestB testB=(TestB)myClass.getObject();
// System.out.println(testB.bTalk());
MyClass<TestB> testBMyClass = new MyClass<TestB>();
testBMyClass.setObject(new TestB());
TestB testB = testBMyClass.getObject();
System.out.println(testB.bTalk());
}
}
如上,如果在宣告MyClass時,限定了其型別引數為T extend TestB,那麼就限制了MyClass在使用的時候所能持有的型別必須為TestB型別或者其子型別。如果還想持有TestA型別,就如main方法中第一行所示。編譯期直接會報錯Type parameter "TestA" is not within its bound, should extend TestB
。
但是也有一個好處,如MyClass類中新加的那個action方法,現在可以呼叫持有型別的bTalk方法了。為什麼沒有限定引數T的時候不能呼叫bTalk方法呢?因為Java型別擦除機制的原因(稍後會解釋)。型別引數T直接被認為成Object來處理。因此程式並不能知道其持有物件有bTalk方法。而當限定了泛型引數為TestB的子類時,Java就會把型別引數T當成TestB來處理,因為他知道MyClass能持有的型別最高只能是TestB類(不是和TestB一系的物件,在編譯期是放不到MyClass中去的,直接編譯報錯)。後面型別擦除機制的時候會詳細講解。
自限定泛型
在帶有限定符的泛型宣告方式中有一朵奇葩,那就是自限定型別的泛型,其最為常見的一個例子就是JDK中的Enum類,可以去原始碼中看一下它的宣告方式:
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable {
private final String name;
public final int compareTo(E o){......}
public final Class<E> getDeclaringClass() {......}
....
....
}
單看這一句class Enum<E extends Enum<E>>
是不是有一種Enum型別是一個無線遞迴的概念。理解起來就像Enum類的引數型別是E,而E是繼承自Enum[E]的,而它繼承的這個Enum[E]中的E又是繼承自Enum[E]的……。但是這種宣告方式並沒有遞迴的意思在裡面。容我喝口水一一道來。
在Java裡面,Enum是所有列舉類的父類,所有Enum型別都會被編譯器解釋成繼承自Enum類的一個子類。而Enum存在的意義就是為所有的這些子類提供其公用的方法。舉個栗子,假設有個Color的列舉型別宣告如下:
enum Color {RED, BLUE, GREEN}
而編譯器則會把他編譯成類似下面的這種方式。注意是編譯器把Color硬寫成Color extends Enum[Color]的,而不是說Color extends Enum[somethin else extends Enum]就不行,後面有個例子。
public final class Color extends Enum<Color> {
public static final Color[] values() { return (Color[])$VALUES.clone(); }
public static Color valueOf(String name) { ... }
private Color(String s, int i) { super(s, i); }
public static final Color RED;
public static final Color BLUE;
public static final Color GREEN;
private static final Color $VALUES[];
static {
RED = new Color("RED", 0);
BLUE = new Color("BLUE", 1);
GREEN = new Color("GREEN", 2);
$VALUES = (new Color[] { RED, BLUE, GREEN });
}
}
Color繼承自Enum的意思也就是說Color擁有了Enum所有的方法。嗯哼,重點來了,Enum是有compareTo方法的啊,那麼Color從Enum裡面繼承過來的compareTo方法,應該有一個怎樣的引數啊。從直觀上來講,Color只能跟Color比吧,不能跟其他的什麼不是列舉類或者其它列舉比吧,因此compareTo的引數也必須支援變更為Enum的各種子類。但是呢compareTo是列舉類大家公有的方法,必須寫在Enum裡面,因此把compareTo方法的引數設為了泛型已支援Enum的各種子型別。那總得有個什麼方法來限制程式設計師,讓他不能在繼承Enum的時候,亂寫其型別引數吧。恩,對的,這個目的就是通過上面的宣告達到的。也就是說,Enum的類似遞迴的宣告其實是在表達:任何繼承自我的類,他給我傳的的型別引數都必須是我,或者我的子類。下面通過一個例子來解釋:
class SelfBounded<T extends SelfBounded<T>>{
T element;
SelfBounded<T> set(T arg){
this.element=arg;
return this;
}
T get(){
return element;
}
}
class A extends SelfBounded<A>{}
class B extends SelfBounded<A>{}
class C extends SelfBounded<C>{
C setAndGet(C arg){
set(arg);
return get();
}
}
class D{}
//class E extends SelfBounded<D>{}
class F extends SelfBounded{}
public class Main {
public static void main(String[] args) {
A a=new A();
a.set(new A());
a=a.set(new A()).get();
a=a.get();
C c=new C();
c=c.setAndGet(new C());
F f = new F();
SelfBounded selfBounded = f.get();
}
}
如上,我們聲明瞭自限定型別SelfBounded,然後聲明瞭型別A。注意這個時候SelfBoundes的型別引數裡面只能填A,因為SelfBounded到現在只有A一個子類。但是在宣告B的時候,可以看到,我們不一定非要給其型別引數裡面傳入B,只要傳入的是SelfBounded的子類就可以了,我們傳了A。注意看在宣告A的時候,我們傳給型別引數的是E,編譯器會報錯,編譯不通過,因為D不是SelfBounded的子類。另外我們聲明瞭F,什麼型別引數都沒有傳給它,那編譯期預設就是用的是預設左邊界SelfBounded。
要區分兩個概念,class SelfBounded<T extends SelfBounded<T>>
這種宣告方式只是限定了SelfBounded的型別引數只能是其子類。而沒有表示非得是繼承他的子類本身,如例子中宣告的B類。而Java中,我們宣告的列舉類如Color,是一種特殊情況,被編譯成public final class Color extends Enum<Color>
是編譯器限定死了型別引數就是繼承Enum的那個類本身Color,畢竟這樣做是有意義的,因為Color只能和Color列舉比。
泛型的使用方式
普通方式
普通方式被稱為exact方式,就是說List[A] alist=new List[A]()
這樣,型別完全一樣的泛型之間的相互賦值。
上界萬用字元
我們來看一個例子
package com.person.yecheng.li.blogcases;
import java.util.ArrayList;
import java.util.List;
/**
* Version: 1.0.0 Date:2017-08-05 Time: 17:44 Author: yecheng.li.
*/
class Fruit {
public String isWhat() {
return "Fruit";
}
}
class Apple extends Fruit {
@Override
public String isWhat() {
return "Apple";
}
}
class Orange extends Fruit {
@Override
public String isWhat() {
return "Orange";
}
}
public class WildCards {
public static void main(String[] args) {
List<Apple> apples = new ArrayList<Apple>();
// List<Fruit> fruits=apples;
Apple[] arrayApples = new Apple[10];
Fruit[] arrayFruits = arrayApples;
arrayFruits[0] = new Orange();
}
}
WildCards中,我們先聲明瞭一個Apple的List apples,然後我們準備將apples賦值給一個Fruit的List fruits。但是編譯器不讓,給出一個報錯。這是為什麼呢?作為對比,我們拿一個在Java中和List非常相近的資料結構-array做了同樣的操作,一個apple的陣列賦值給一個fruit的陣列完全沒有問題。其實直觀上來講也很合理,一堆蘋果不也就是一堆水果嘛,將其賦於一個更大的概念沒什麼錯。然後在fruitsArray裡面,我們新建一個橘子物件。這句程式碼看起來也沒什麼問題。但是執行一下呢?報錯了,最後一行報了ArrayStoreException
。關於陣列的這三行程式碼,每一行單單拎出來其實都沒什麼問題,但是這三行程式碼組合起來就有問題了。這三行程式碼一起其實是在往一個Apple的數組裡面塞一個Orange物件。要知道陣列可是Java正經一開始就支援的功能,其中儲存了它在宣告的時候,指定要儲存的型別資訊。所以在arrayFruits往其中塞Orange的時候會報錯。
類比到上面的List中,現在知道為什麼List不讓將一個Apple的List賦值給一個Fruit的List了吧,因為它怕你往裡面放橘子。並且比陣列更糟糕的是,由於型別擦除的原因,它根本就不知道它裡面應該放點什麼。用陣列的時候,往arrayApples裡面放個Orange,系統還會報個異常出來。如果是往apple的List裡面放個orange,系統可是不會報異常的哦。所以為了杜絕這種情況的發生。泛型乾脆就不讓這樣相互賦值。
但是呢,上面說的 一堆蘋果不也就是一堆水果嘛,將其賦於一個更大的概念沒什麼錯這句話是有道理的啊。有些情況下就是要將一個小的概念賦值給一個大的概念啊。不要慌,Java考慮到了這種情況。這也就是這一小節要說的上界萬用字元的用法,還是剛才的那個例子,這次我們換個方法來賦值,如下:
public class WildCards {
public static void main(String[] args) {
List<Apple> apples = new ArrayList<Apple>();
// List<Fruit> fruits=apples;
List<? extends Fruit> fruits = apples;
// fruits.add(new Apple());
// fruits.add(new Orange());
Fruit fruit = fruits.get(0);
fruits.contains(new Apple());
fruits.indexOf(new Orange());
Apple[] arrayApples = new Apple[10];
Fruit[] arrayFruits = arrayApples;
arrayFruits[0] = new Orange();
}
}
我們使用List<? extends Fruit> fruits = apples;
這句來將apple的List賦值給fruit的List。但是接下來的事情出乎了我們的意料,fruits裡面不能add東西了,任何東西都不能往裡面放了,編譯器會報錯。為啥呢?聯想一下上面陣列的例子你心裡還能沒點數麼。剛剛就是因為往apples裡面放了不該放的東西導致錯誤。這次List能再這種賦值的情況下,阻止往fruits裡面加東西也純屬正常,因為你根本就不知道當初付給fruits的List具體是什麼,也許是apples,也許是oranges,還也許是bananas呢。既然執行的時候Java已經不知道你往裡面放什麼是安全的了,拿乾脆就不讓你往裡面add了。那是fruits所有的方法都不能調了嘛?並不是,get,contains,indexOf三個方法調起來確實沒什麼問題啊。那是編譯期能檢測到我們那個方法是要修改儲存內容的?然後把這部分方法遮蔽掉了不讓用?其實並不是,我們來看下ArrayList的原始碼是怎麼寫著四個方法的:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
public E get(int index) {
rangeCheck(index);
checkForComodification();
return ArrayList.this.elementData(offset + index);
}
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
}
可以看到,其實除了add的引數是E引數型別,其他三個方法的引數都是Object及以下的型別。關鍵就在於這裡。當一個泛型的引數型別被宣告為? extends Fruit以後,他對應所有使用到引數型別的方法的引數也都會變成? extends Fruit,這個時候方法也表示很無奈啊,你只說了是Fruit的一個子型別,我怎麼知道具體應該是哪個型別啊?所以就直接會編譯失敗。從這裡我們可以得出來一個結論。如果不想讓宣告為上界萬用字元的類執行的方法,大可以將其引數設定成為引數型別E,但是如果想被執行的方法,引數型別就要是Object型別的了。
但是,有一點啊!如果泛型被宣告為? extends Fruit以後,是不是說明其持有的物件的上界就是Fruit,因此泛型類的方法的返回值是型別引數的,返回值一律按照上邊界Fruit來。這也就是List的get方法能返回一個Fruit的原因。
下界萬用字元
這個萬用字元的功能也是用來接受一個類的,舉個栗子吧
class Fruit {
public String isWhat() {
return "Fruit";
}
}
class Apple extends Fruit {
@Override
public String isWhat() {
return "Apple";
}
}
class Orange extends Fruit {
@Override
public String isWhat() {
return "Orange";
}
}
class Fuji extends Apple {
@Override
public String isWhat() {
return "Fuji";
}
}
public class WildCards {
public static void main(String[] args) {
List<Fruit> fruits = new ArrayList<Fruit>();
List<? super Apple> appleSupers=fruits;
appleSupers.add(new Fuji());
Object object = appleSupers.get(0);
}
}
還是一開始的那些類,加了個Apple的子類Fuji(紅富士)。這次我們聲明瞭一個fruit的List,List<? super Apple> appleSupers=fruits;
這句話將其賦給了appleSupers,appleSupers表示的就是一個下界萬用字元,它表示:我正持有的類是Apple的某個父類,具體是哪個我不太清楚。然後我們往appleSupers裡面新增元素Fuji,結果證明是可以新增成功的,為什麼呢?因為add的引數是? super Apple ,這能說明什麼?說明其現在正在持有的物件最少是Apple型別的,那麼我現在往其中新增Apple或者是Apple的子類時不會破壞List中持有物件的原則的。接著我們往外取資料,啊哦!又出事了,我們從List裡面拿出來的東西變成了Object型別的啦。為啥?因為我們剛開了get的返回值是E型別的,對應到appleSupers物件上,就是? super Apple型別的。具體型別它也不太清楚,反正就是大於Apple的型別。那Java就只能把他當其左邊界Object來處理啦!
無界萬用字元
無界萬用字元集合了上界下界萬用字元的優點,同時他們的缺點也幾種到了一起。不論什麼樣引數型別的泛型類都可以付給帶有無界萬用字元的泛型類(上下界萬用字元的優點)。但是!賦值就賦值了,你可千萬別想再通過它呼叫泛型類中引數值是泛型引數的方法(上界萬用字元的缺點),也別想從它這裡拿到某個具體型別了,所有以引數型別為返回值的方法都將返回Object(下界萬用字元的缺點)。下面還是以一個例子為例:
public class WildCards {
public static void main(String[] args) {
List<Fruit> fruits = new ArrayList<Fruit>();
List<?> appleSupers=fruits;
//appleSupers.add(new Fuji());
Object object = appleSupers.get(0);
}
}
可以看到,例子證明了以上闡釋。
無界萬用字元與RawType的區別與聯絡
無界萬用字元指的就是像List<?>
這樣的,RawType指的是List,不指定泛型型別這樣的。他們最終其實持有的都是Object型別的物件。 這是他們的聯絡,但是還是有一些區別的,以TIJ中的一個例子來詳細闡述。
class Holder<T> {
private T value;
public Holder() {
}
public Holder(T val) {
value = val;
}
public void set(T val) {
value = val;
}
public T get() {
return value;
}
public boolean equals(Object obj) {
return value.equals(obj);
}
}
public class WildCarsInAll {
}
class Wildcards {
// Raw argument:
static void rawArgs(Holder holder, Object arg) {
// holder.set(arg); // Warning:
// Unchecked call to set(T) as a
// member of the raw type Holder
// holder.set(new Wildcards()); // Same warning
// Can’t do this; don’t have any ‘T’:
// T t = holder.get();
// OK, but type information has been lost:
Object obj = holder.get();
}
// Similar to rawArgs(), but errors instead of warnings:
static void unboundedArg(Holder<?> holder, Object arg) {
// holder.set(arg); // Error:
// set(capture of ?) in Holder<capture of ?>
// cannot be applied to (Object)
// holder.set(new Wildcards()); // Same error
// Can’t do this; don’t have any ‘T’:
// T t = holder.get();
// OK, but type information has been lost:
Object obj = holder.get();
}
static <T> T exact1(Holder<T> holder) {
T t = holder.get();
return t;
}
static <T> T exact2(Holder<T> holder, T arg) {
holder.set(arg);
T t = holder.get();
return t;
}
static <T> T wildSubtype(Holder<? extends T> holder, T arg) {
// holder.set(arg); // Error:
// set(capture of ? extends T) in
// Holder<capture of ? extends T>
// cannot be applied to (T)
T t = holder.get();
return t;
}
static <T> void wildSupertype(Holder<? super T> holder, T arg) {
holder.set(arg);
// T t = holder.get(); // Error:
// Incompatible types: found Object, required T
// OK, but type information has been lost:
Object obj = holder.get();
}
public static void main(String[] args) {
Holder raw = new Holder<Long>();
// Or:
raw = new Holder();
Holder<Long> qualified = new Holder<Long>();
Holder<?> unbounded = new Holder<Long>();
Holder<? extends Long> bounded = new Holder<Long>();
Long lng = 1L;
rawArgs(raw, lng);
rawArgs(qualified, lng);
rawArgs(unbounded, lng);
rawArgs(bounded, lng);
unboundedArg(raw, lng);
unboundedArg(qualified, lng);
unboundedArg(unbounded, lng);
unboundedArg(bounded, lng);
// Object r1 = exact1(raw); // Warnings:
// Unchecked conversion from Holder to Holder<T>
// Unchecked method invocation: exact1(Holder<T>)
// is applied to (Holder)
Long r2 = exact1(qualified);
Object r3 = exact1(unbounded); // Must return Object
Long r4 = exact1(bounded);
// Long r5 = exact2(raw, lng); // Warnings:
// Unchecked conversion from Holder to Holder<Long>
// Unchecked method invocation: exact2(Holder<T>,T)
// is applied to (Holder,Long)
Long r6 = exact2(qualified, lng);
// Long r7 = exact2(unbounded, lng); // Error:
// exact2(Holder<T>,T) cannot be applied to
// (Holder<capture of ?>,Long)
// Long r8 = exact2(bounded, lng); // Error:
// exact2(Holder<T>,T) cannot be applied
// to (Holder<capture of ? extends Long>,Long)
// Long r9 = wildSubtype(raw, lng); // Warnings:
// Unchecked conversion from Holder
// to Holder<? extends Long>
// Unchecked method invocation:
// wildSubtype(Holder<? extends T>,T) is
// applied to (Holder,Long)
Long r10 = wildSubtype(qualified, lng);
// OK, but can only return Object:
Object r11 = wildSubtype(unbounded, lng);
Long r12 = wildSubtype(bounded, lng);
// wildSupertype(raw, lng); // Warnings:
// Unchecked conversion from Holder
// to Holder<? super Long>
// Unchecked method invocation:
// wildSupertype(Holder<? super T>,T)
// is applied to (Holder,Long)
wildSupertype(qualified, lng);
// wildSupertype(unbounded, lng); // Error:
// wildSupertype(Holder<? super T>,T) cannot be
// applied to (Holder<capture of ?>,Long)
// wildSupertype(bounded, lng); // Error:
// wildSupertype(Holder<? super T>,T) cannot be
// applied to (Holder<capture of ? extends Long>,Long)
}
}
從上面的例子我們至少可以得出兩個結論:
1. RawType表示,我可以持有任意物件。而無界限定型別則表示,我持有的物件是一旦確定了(賦值了)就是固定的,只是現在還沒有確定。
2. RawType表示,你隨便調我方法,我認慫算我輸!但是我可能還是會對於你不檢查型別嗶嗶你兩句(warning)。而無界限定型別則表示:你儘管調我方法,如果我能讓你調通我以型別引數為引數的方法的話,算我輸!如果你調了我以型別引數為返回值的方法,而我返回的不是Object也算我輸。
應用場景
帶有萬用字元的限定型別一般用於方法的引數或者類的域上。這些方法或類一般不知道自己具體要處理那種型別的變數,只知道要處理資料的上界或者下界,亦或者上下界都不知道(無界)。並且在引數或者域一旦被指定之後,想要呼叫其方法就要遵循以上所說的那些原則了!
型別擦除機制
型別擦除機制的原因
文章一開始就說了,Java一開始並沒有泛型的概念,是在SE5中才引入的。那麼就存在一個問題,應該如何向用戶透明的引入泛型的概念呢?換句話說,如何讓使用了泛型客戶端程式碼(SE5以後的程式碼)和沒有使用過泛型的程式碼(SE5以前的程式碼)相互呼叫不出問題呢(前後相容)?這是個很大的問題。
理想情況下,我們可能期望能有單獨的一天,讓我們來對所有的程式碼升級,讓其都支援泛型。但現實是,即使從SE5出現的那一刻起,大家都只寫支援泛型的程式,但依然會有程式需要用到SE5之前那些不支援泛型的庫。而那些庫的作者可能根本就不想對其庫進行升級。
所以Java的泛型不但要支援向前相容-已經存在的程式碼在SE5之後依然是合法的程式碼,並且其表示的意義不變。同時還要支援移植相容性-即保證現有的庫(不支援泛型的)如果想要遷移到支援泛型的庫的時候,只有他自己需要修改,不至於影響到其使用方。原文如下:
The core motivation for erasure is that it allows generified clients to be used with non-generified libraries, and vice versa. This is often called migration compatibility. In the ideal world, we would have had a single day when everything was generified at once. In reality, even if programmers are only writing generic code, they will have to deal with non-generic libraries that were written before Java SE5. The authors of those libraries may never have the incentive to generify their code, or they may just take their time in getting to it.
So Java generics not only must support backwards compatibility—existing code and class files are still legal, and continue to mean what they meant before—but also must support migration compatibility, so that libraries can become generic at their own pace, and when a library does become generic, it doesn’t break code and applications that depend upon it. After deciding that this was the goal, the Java designers and the various groups working on the problem decided that erasure was the only feasible solution. Erasure enables this migration towards generics by allowing non-generic code to coexist with generic code.
基於以上的問題,Java引入了型別擦除機制來實現泛型。因為型別擦除機制可以解決以上說的相容性問題。
什麼是型別擦除機制
實現泛型機制一般有兩種方法。其中之一以C++中的模板(Code Specialization)方法為代表,C++編譯器會為每一個泛型類例項生成一份執行程式碼。執行程式碼中integer list和string list是兩種不同的型別。這樣會導致程式碼膨脹(code bloat)。但是好處是,這種泛型相對於Java中的偽泛型來說是“真泛型”,其型別引數資訊可以保留到執行時。另外一種便以Java型別擦除機制為代表。型別擦除指的是通過型別引數合併,將泛型型別例項關聯到同一份位元組碼上(Code Sharing)。編譯器只為泛型型別生成一份位元組碼,並將其例項關聯到這份位元組碼上。型別擦除的關鍵在於從泛型型別中清除型別引數的相關資訊,並且再必要的時候新增型別檢查和型別轉換的方法。
型別擦除過程可以理解為將泛型類程式設計普通的Java程式碼的過程。一般包括兩個步驟:
1. 將所有的泛型引數用其最左邊界(最頂級的父型別)型別替換。
2. 移除所有的型別引數。
煮個栗子,還以剛才的MyClass為例。
public class MyClass<T extends TestB> {
private T object;
public void setObject(T object) {
this.object = object;
}
public T getObject() {
return object;
}
public void action() {
object.bTalk();
}
public static void main(String[] args) {
//MyClass<TestA> myClass = new MyClass<TestA>();
// myClass.setObject(new TestA());
// TestA testA = myClass.getObject();
// System.out.println(testA.aTalk());
// myClass.setObject(new TestB()); //編譯錯誤 TestB testB=(TestB)myClass.getObject();
// System.out.println(testB.bTalk());
MyClass<TestC> testBMyClass = new MyClass<TestC>();
testBMyClass.setObject(new TestC());
TestC testC = testBMyClass.getObject();
System.out.println(testC.cTalk());
}
}
在型別擦除之後,你可以理解它變成了這樣
public class MyClass {
private TestB object;
public void setObject(TestB object) {
this.object = object;
}
public TestB getObject() {
return object;