夯實JAVA基本之一——泛型詳解(2):高級進階(轉)

上一篇給你們初步講解了泛型變量的各類應用環境,這篇將更深刻的講解一下有關類型綁定,通配符方面的知識。數組

1、類型綁定
一、引入
咱們從新看上篇寫的一個泛型:
class Point<T> {
private T x; // 表示X座標
private T y; // 表示Y座標

public void setX(T x) {
this.x = x;
}

public void setY(T y) {
this.y = y;
}

public T getX() {
return this.x;
}

public T getY() {
return this.y;
}
}

//使用
Point<Integer> p1 = new Point<Integer>();
p1.setX(new Integer(100));
System.out.println(p1.getX());
首先,咱們要知道一點,任何的泛型變量(好比這裏的T)都是派生自Object,因此咱們在填充泛型變量時,只能使用派生自Object的類,好比String,Integer,Double,等而不能使用原始的變量類型,好比int,double,float等。
而後,問題來了,那在泛型類Point<T>內部,利用泛型定義的變量T x能調用哪些函數呢?
private T x;
固然只能調用Object所具備的函數,由於編譯器根本不知道T具體是什麼類型,只有在運行時,用戶給什麼類型,他才知道是什麼類型。編譯器惟一能肯定的是,不管什麼類型,都是派生自Object的,因此T確定是Object的子類,因此T是能夠調用Object的方法的。
那麼問題又來了,若是我想寫一個找到最小值的泛型類;因爲不知道用戶會傳什麼類型,因此要寫一個接口,讓用戶實現這個接口來自已對比他所傳遞的類型的大小。
接口以下:
public interface Comparable<T>{
public boolean compareTo(T i);
}
但若是咱們直接利用T的實例來調用compareTo()函數的話,會報錯,編譯器截圖以下:數據結構

這是由於,編譯器根本沒法得知T是繼承自Comparable接口的函數。那怎麼樣才能讓編譯器知道,T是繼承了Comparable接口的類型呢?
這就是類型綁定的做用了。app

二、類型綁定:extends
(1)、定義
有時候,你會但願泛型類型只能是某一部分類型,好比操做數據的時候,你會但願是Number或其子類類型。這個想法其實就是給泛型參數添加一個界限。其定義形式爲:
<T extends BoundingType>
此定義表示T應該是BoundingType的子類型(subtype)。T和BoundingType能夠是類,也能夠是接口。另外注意的是,此處的」extends「表示的子類型,不等同於繼承。
必定要很是注意的是,這裏的extends不是類繼承裏的那個extends!兩個根本沒有任何關聯。在這裏extends後的BoundingType能夠是類,也能夠是接口,意思是說,T是在BoundingType基礎上建立的,具備BoundingType的功能。目測是JAVA的開發人員不想再引入一個關鍵字,因此用已有的extends來代替而已。
(2)、實例:綁定接口
一樣,咱們還使用上面對比大小的接口來作例子
首先,看加上extends限定後的min函數:
public interface Comparable<T> {
public boolean compareTo(T i);
}
//添加上extends Comparable以後,就能夠Comparable裏的函數了
public static <T extends Comparable> T min(T...a){
T smallest = a[0];
for(T item:a){
if (smallest.compareTo(item)){
smallest = item;
}
}
return smallest;
}
這段代碼的意思就是根據傳進去的T類型數組a,而後調用其中item的compareTo()函數,跟每一項作對比,最終找到最小值。
從這段代碼也能夠看出,類型綁定有兩個做用:一、對填充的泛型加以限定 二、使用泛型變量T時,可使用BoundingType內部的函數。
這裏有一點很是要注意的是,在這句中smallest.compareTo(item),smallest和item所有都是T類型的,也就是說,compareTo對比的是同一種類型。
而後咱們實現一個派生自Comparable接口的類:
public class StringCompare implements Comparable<StringCompare> {
private String mStr;

public StringCompare(String string){
this.mStr = string;
}

@Override
public boolean compareTo(StringCompare str) {
if (mStr.length() > str.mStr.length()){
return true;
}
return false;
}
}
在這段代碼,你們可能會疑惑爲何把T也填充爲StringCompare類型,記得咱們上面說的嗎:smallest.compareTo(item),smallest和item是同一類型!!因此compareTo的參數必須是與調用者自身是同一類型,因此要把T填充爲StringCompare;
在這段代碼中compareTo的實現爲,對比當前mstr的長度與傳進來實例的mstr長度進行比較,若是超過,則返回true,不然返回false;
最後是使用min函數:
StringCompare result = min(new StringCompare("123"),new StringCompare("234"),new StringCompare("59897"));
Log.d(TAG,"min:"+result.mStr);
結果以下:ide

這裏有extends接口,咱們開篇說過,extends表示綁定,後面的BindingType便可以是接口,也能夠是類,下面咱們就再舉個綁定類的例子。函數

源碼在文章底部給出
(3)、實例:綁定類
咱們假設,咱們有不少種類的水果,須要寫一個函數,打印出填充進去水果的名字:
爲此,咱們先建一個基類來設置和提取名字:ui

class Fruit {
private String name;

public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
而後寫個泛型函數來提取名字:
public static <T extends Fruit> String getFruitName(T t){
return t.getName();
}
這裏泛型函數的用法就出來了,因爲咱們已知水果都會繼承Fruit基類,因此咱們利用<T extends Fruit>就能夠限定填充的變量必須派生自Fruit的子類。一來,在T中,咱們就能夠利用Fruit類中方法和函數;二來,若是用戶填充進去的類沒有派生自Fruit,那編譯器就會報錯。
而後,咱們新建兩個類,派生自Fruit,並填充進去它們本身的名字:
class Banana extends Fruit{
public Banana(){
setName("bababa");
}
}
class Apple extends Fruit{
public Apple(){
setName("apple");
}
}
最後調用:
String name_1 = getFruitName(new Banana());
String name_2 = getFruitName(new Apple());
Log.d(TAG,name_1);
Log.d(TAG,name_2);
結果以下:this

源碼在文章底部給出
(4)、綁定多個限定
上面咱們講了,有關綁定限定的用法,其實咱們能夠同時綁定多個綁定,用&鏈接,好比:
public static <T extends Fruit&Serializable> String getFruitName(T t){
return t.getName();
}
再加深下難度,若是咱們有多個泛型,每一個泛型都帶綁定,那應該是什麼樣子的呢:
public static <T extends Comparable & Serializable, U extends Runnable> T foo(T a, U b){
…………
}
你們應該看得懂,稍微講一下:這裏有兩個泛型變量T和U,將T與Comparable & Serializable綁定,將U與Runnable綁定。
好了,這部分就講完了,下面講講有關通配符的用法。
2、通配符
通配符是一個很是使人頭疼的一個功能,理解與掌握難度比較大,下面我盡力去講明白它與泛型變量的區別與用法。
一、引入
從新來看咱們上篇用的Point泛型定義:
class Point<T> {
private T x;
private T y;

public Point(){

}
public Point(T x,T y){
this.x = x;
this.y = y;
}

public void setX(T x) {
this.x = x;
}

public void setY(T y) {
this.y = y;
}

public T getX() {
return this.x;
}

public T getY() {
return this.y;
}
}
這段代碼很簡單,引入了一個泛型變量T,而後是有兩個構造函數,最後分別是利用set和get方法來設置和獲取x,y的值。這段代碼沒什麼難度,再也不細講。
咱們看看下面這段使用的代碼:
Point<Integer> integerPoint = new Point<Integer>(3,3);
…………
Point<Float> floatPoint = new Point<Float>(4.3f,4.3f);
…………
Point<Double> doublePoint = new Point<Double>(4.3d,4.90d);
…………
Point<Long> longPoint = new Point<Long>(12l,23l);
…………
在這段代碼中,咱們使用Point<T>生成了四個實例:integerPoint,floatPoint,doublePoint和longPoint;
在這裏,咱們生成四個實例,就得想四個名字。若是咱們想生成十個不一樣類型的實例呢?那不得想十個名字。
光想名字就是個事,(其實我並不以爲想名字是個什麼大事…… T _ T ,沒辦法,想不出更好的例子了…… )
那有沒有一種辦法,生成一個變量,能夠將不一樣類型的實例賦值給他呢?
二、無邊界通配符:?
(1)、概述
先不講無邊界通配符是什麼,一樣拿上面的例子來看,若是咱們這樣實現:
Point<?> point;

point = new Point<Integer>(3,3);
point = new Point<Float>(4.3f,4.3f);
point = new Point<Double>(4.3d,4.90d);
point = new Point<Long>(12l,23l);
在這裏,咱們首先,利用下面的代碼生成一個point實例,注意到,在填充泛型時,用的是?
Point<?> point;
而後,各類類型的Point實例,均可以賦值給point了:
point = new Point<Integer>(3,3);
point = new Point<Float>(4.3f,4.3f);
point = new Point<Double>(4.3d,4.90d);
point = new Point<Long>(12l,23l);
這裏的?就是無邊界通配符。通配符的意義就是它是一個未知的符號,能夠是表明任意的類。
因此這裏可能你們就明白了,這裏不光能將泛型變量T填充爲數值類型,其實任意Point實例都是能夠傳給point的:好比這裏的Point<String>(),Point<Object>()都是能夠的.net

(2)、?與T的區別
你們可能會有疑問,那無邊界通配符?與泛型變量T有什麼區別呢?
答案是:他們倆沒有任何聯繫!!!!!
泛型變量T不能在代碼用於建立變量,只能在類,接口,函數中聲明之後,才能使用。
好比:3d

public class Box<T> {
public T get(){
…………
};
public void put(T element){
…………
};
}
而無邊界通配符?則只能用於填充泛型變量T,表示通配任何類型!!!!再重複一遍:?只能用於填充泛型變量T。它是用來填充T的!!!!只是填充方式的一種!!!
好比:
//無邊界通配符填充
Box<?> box;
//其它類型填充
Box<String> stringBox;
(3)、通配符只能用於填充泛型變量T,不能用於定義變量
你們必定要記得,通配符的使用位置只有:
Box<?> box;
box = new Box<String>();
即填充泛型變量T的位置,不能出如今後面String的位置!!!!
下面的第三行,第四行,都是錯誤的。通配符不能用於定義變量。對象

再次強調,?只能出如今Box<?> box;中,其它位置都是不對的。

三、通配符?的extends綁定
(1)、概述
從上面咱們能夠知道通配符?能夠表明任意類型,但跟泛型同樣,若是不加以限定,在後期的使用中編譯器可能不會報錯。因此咱們一樣,要對?加以限定。
綁定的形式,一樣是經過extends關鍵字,意義和使用方法都用泛型變量一致。
一樣,以咱們上面的Point<T>泛型類爲例,由於Point在實例意義中,其中的值是數值纔有意義,因此將泛型變量T填充爲Object類型、String類型等都是不正確的。
因此咱們要對Point<?> point加以限定:只有數值類型才能賦值給point;
咱們把代碼改爲下面的方式:


咱們給通配符加上限定: Point<? extends Number> point;
此時,最後兩行,當將T填充爲String和Object時,賦值給point就會報錯!
這裏雖然是指派生自Number的任意類型,但你們注意到了沒: new Point<Number>();也是能夠成功賦值的,這說明包括邊界自身。
再重複一遍:無邊界通配符只是泛型T的填充方式,給他加上限定,只是限定了賦值給它(好比這裏的point)的實例類型。
若是想從根本上解決亂填充Point的問題,須要從Point泛型類定義時加上<T extends Number>:

class Point<T extends Number> {
private T x; // 表示X座標
private T y; // 表示Y座標

…………
}
(2)注意:利用<? extends Number>定義的變量,只可取其中的值,不可修改
看下面的代碼:

明顯在point.setX(Integer(122));時報編譯錯誤。但point.getX()卻不報錯。
這是爲何呢?
首先,point的類型是由Point<? extends Number>決定的,並不會由於point = new Point<Integer>(3,3);而改變類型。
即使point = new Point<Integer>(3,3);以後,point的類型依然是Point<? extends Number>,即派生自Number類的未知類型!!!這一點很好理解,若是在point = new Point<Integer>(3,3);以後,point就變成了Point<Integer>類型,那後面point = new Point<Long>(12l,23l);操做時,確定會由於類型不匹配而報編譯錯誤了,正由於,point的類型始終是Point<? extends Number>,所以能繼續被各類類型實例賦值。
回到正題,如今說說爲何不能賦值
正由於point的類型爲 Point<? extends Number> point,那也就是說,填充Point的泛型變量T的爲<? extends Number>,這是一個什麼類型?未知類型!!!怎麼可能能用一個未知類型來設置內部值!這徹底是不合理的。
但取值時,正因爲泛型變量T被填充爲<? extends Number>,因此編譯器能肯定的是T確定是Number的子類,編譯器就會用Number來填充T
也就是說,編譯器,只要能肯定通配符類型,就會容許,若是沒法肯定通配符的類型,就會報錯。

四、通配符?的super綁定
(1)、概述
若是說 <? extends XXX>指填充爲派生於XXX的任意子類的話,那麼<? super XXX>則表示填充爲任意XXX的父類!
咱們先寫三個類,Employee,Manager,CEO,分別表明工人,管理者,CEO
其中Manager派生於Employee,CEO派生於Manager,代碼以下:
class CEO extends Manager {
}

class Manager extends Employee {
}

class Employee {
}
而後,若是我這樣生成一個變量:
List<? super Manager> list;
它表示的意思是將泛型T填充爲<? super Manager>,即任意Manager的父類;也就是說任意將List<T>中的泛型變量T填充爲Manager父類的List變量,均可以賦值給list;

從上面的代碼中能夠看出new ArrayList<Employee>(),new ArrayList<Manager>()都是正確的,而new ArrayList<CEO>()卻報錯,固然是由於CEO類已經再也不是Manager的父類了。因此會報編譯錯誤。
這裏還要注意一個地方,從代碼中能夠看出new ArrayList<Manager>()是能夠成功賦值給 List<? super Manager> list的,可見,super關鍵字也是包括邊界的。即邊界類型(這裏是Manager)組裝的實例依然能夠成功賦值。
(2)、super通配符實例內容:能存不能取
上面咱們講了,extends通配符,能取不能存,那super通配符狀況又怎樣呢?咱們試試看:

 

先看存的部分:

List<? super Manager> list;
list = new ArrayList<Employee>();
//存
list.add(new Employee()); //編譯錯誤
list.add(new Manager());
list.add(new CEO());
首先,須要聲明的是,與Point<? extends Number> point中point的類型是由Point<? extends Number>肯定的,相同的是list的類型是也是由List<? super Manager> ;list的item的類型始終是<? super Manager>,即Manager類的任意父類,便可能是Employee或者Object.
你們可能疑惑的地方在於,爲何下面這兩個是正確的!而list.add(new Employee()); 倒是錯誤的!
list.add(new Manager());
list.add(new CEO());
由於list裏item的類型是<? super Manager>,即Manager的任意父類,咱們假如是Employee,那下面這段代碼你們能理解了吧:
List<Employee> list = new ArrayList<Employee>();
list.add(new Manager());
list.add(new CEO());
在這裏,正由於Manager和CEO都是Employee的子類,在傳進去list.add()後,會被強制轉換爲Employee!
如今回過頭來看這個:
List<? super Manager> list;
list = new ArrayList<Employee>();
//存
list.add(new Employee()); //編譯錯誤
list.add(new Manager());
list.add(new CEO());
編譯器沒法肯定<? super Manager>的具體類型,但惟一能夠肯定的是Manager()、CEO()確定是<? super Manager>的子類,因此確定是能夠add進去的。但Employee不必定是<? super Manager>的子類,因此不能肯定,不能肯定的,確定是不容許的,因此會報編譯錯誤。
最後再來看看取:

在這段代碼中,Object object = list.get(0);是不報錯的,而Employee employee = list.get(0);是報錯的;
咱們知道list中item的類型爲<? super Manager>,那編譯器能確定的是<? super Manager>確定是Manger的父類;但不能肯定,它是Object仍是Employee類型。但不管是填充爲Object仍是Employee,它必然是Object的子類!
因此Object object = list.get(0);是不報錯的。由於 list.get(0);確定是Object的子類;
而編譯器沒法判斷list.get(0)是否是Employee類型的,因此Employee employee = list.get(0);是報錯的。
這裏雖然看起來是能取的,但取出來一個Object類型,是毫無心義的。因此咱們認爲super通配符:能存不能取;

五、通配符?總結
總結 ? extends 和 the ? super 通配符的特徵,咱們能夠得出如下結論:
◆ 若是你想從一個數據類型裏獲取數據,使用 ? extends 通配符(能取不能存)
◆ 若是你想把對象寫入一個數據結構裏,使用 ? super 通配符(能存不能取)
◆ 若是你既想存,又想取,那就別用通配符。

六、常見問題注意
(1)、Point與Point<T>構造泛型實例的區別
一樣以Point泛型類爲例:
class Point<T> {
private T x; // 表示X座標
private T y; // 表示Y座標

public Point(){

}
public Point(T x,T y){
this.x = x;
this.y = y;
}

public void setX(T x) {
this.x = x;
}

public void setY(T y) {
this.y = y;
}

public T getX() {
return this.x;
}

public T getY() {
return this.y;
}
}
咱們來看看下面這種構造Point泛型實例有什麼區別:
//使用Point<?>
Point<?> point1 = new Point(new Integer(23),new Integer(23));
Point<?> point2 = new Point(new String(""),new String(""));
//直接使用Point
Point point3 = new Point(new Integer(23),new Integer(23));
Point point4 = new Point(new String(""),new String(""));
上面的四行代碼中,point1,point2生成的是Point<?>的實例,填充的是無邊界通配符。而point3和point4則很是奇怪,沒有了泛型的<>標識,直接使用Point生成的實例,那它填充的是什麼呢?
這四行代碼在編譯和運行時,都沒有報錯,並且輸出結果也同樣!
那麼問題就來了:
Point<?> point1 = new Point(new Integer(23),new Integer(23));
Point<?> point2 = new Point(new String(""),new String(""));
在上面的代碼中,使用了無界通配符,因此可以將各類Point實例賦值給Point<?> point1
而省略了泛型標識的構造方法,依然能將各類Point實例賦值給它:
Point point3 = new Point(new Integer(23),new Integer(23));
Point point4 = new Point(new String(""),new String(""));
這說明:構造泛型實例時,若是省略了填充類型,則默認填充爲無邊界通配符!
因此下面這兩個是對等的:
Point point3 = new Point(new Integer(23),new Integer(23));
Point<?> point3 = new Point(new Integer(23),new Integer(23));
最後重複一遍:構造泛型實例時,若是省略了填充類型,則默認填充爲無邊界通配符!


好了,快累死了,這部分真是太難講了,有關通配符捕獲和編譯器類型擦除的知識,就不講了,在實際項目中基本用不到,有興趣的同窗能夠自行去補充下。
下篇給你們講下反射。


若是本文有幫到你,記得加關注哦

本文涉及源碼下載地址:http://download.csdn.net/detail/harvic880925/9275551

請你們尊重原創者版權,轉載請標明出處:http://blog.csdn.net/harvic880925/article/details/49883589 謝謝

相關文章
相關標籤/搜索