Java基礎系列二:Java泛型

 該系列博文會告訴你如何從入門到進階,一步步地學習Java基礎知識,並上手進行實戰,接着瞭解每一個Java知識點背後的實現原理,更完整地瞭解整個Java技術體系,造成本身的知識框架。java

 

1、泛型概述

一、定義:

所謂泛型,就是容許在定義類、接口、方法時使用類型形參,這個類型形參(或叫泛型)將在聲明變量、建立對象、調用方法時動態地指定(即傳入實際的類型參數,也可稱爲類型實參)。Java5改寫了集合框架中的所有接口和類,爲這些接口、類增長了泛型支持,從而能夠在聲明集合變量、建立集合對象時傳入類型實參。面試

 

二、泛型初體驗:一個被舉了無數次的栗子

1
2
3
4
5
6
7
8
List arrayList =  new  ArrayList();
arrayList.add( "aaaa" );
arrayList.add( 100 );
 
for ( int  i =  0 ; i< arrayList.size();i++){
     String item = (String)arrayList.get(i);
     Log.d( "泛型測試" , "item = "  + item);
}

  

運行上述代碼,咱們能夠在控制檯看到這樣的錯誤信息:編程

1
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

  

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

 

三、泛型的特性:

先思考以下一段代碼:緩存

1
2
3
List<String> sList= new  ArrayList<String>();
List<Integer> iList= new  ArrayList<Integer>();
System.out.println(sList.getClass()==iList.getClass());  

 

先不要看結果,本身思考一下。安全

 

結果:app

1
true

  

咱們看到輸出的結果爲true,經過上面的例子能夠證實,在編譯以後程序會採起去泛型化的措施。也就是說Java中的泛型,只在編譯階段有效。在編譯過程當中,正確檢驗泛型結果後,會將泛型的相關信息擦出,而且在對象進入和離開方法的邊界處添加類型檢查和類型轉換的方法。也就是說,泛型信息不會進入到運行時階段。(泛型的這一特性在下述文字中會有詳解介紹)框架

小結:泛型類型在邏輯上看以當作是多個不一樣的類型,實際上都是相同的基本類型。

 

 

2、泛型的使用

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

一、泛型類:

泛型類型用於類的定義中,被稱爲泛型類。經過泛型能夠完成對一組類的操做對外開放相同的接口。最典型的就是各類容器類,如:List、Set、Map。ide

 

泛型的基本寫法:

1
2
3
4
5
6
class  類名稱 <泛型標識:能夠隨便寫任意標識號,標識指定的泛型的類型>{
   private  泛型標識  /*(成員變量類型)*/  var;
   .....
 
   }
}

  

看不懂?看不懂就對了,下面咱們來看一個栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public  class  Apple<T> {
     
     //使用T類型定義變量
     private  T info;
     public  Apple() {}
     
     //下面方法使用T類型定義構造器
     public  Apple(T info){
         this .info=info;
     }
 
     public  T getInfo() {
         return  info;
     }
 
     public  void  setInfo(T info) {
         this .info = info;
     }
     
     public  static  void  main(String[] args) {
         //因爲傳給T形參的是String,因此構造器參數只能是String
         Apple<String> apple= new  Apple<String>( "蘋果" );
         System.out.println(apple.getInfo());
         
         //因爲傳給T形參的是Double,因此構造器參數只能是Double
         Apple<Double> apple2= new  Apple<Double>( 5.56 );
         System.out.println(apple2.getInfo());
         
     }
     
 
}

  

這裏的T能夠寫成任意符合,經常使用的有以下幾個符合:

  • ?:表示不肯定的 java 類型
  • T (type): 表示具體的一個java類型
  • K V (key value): 分別表明java鍵值中的Key Value
  • E (element) :表明Element

 

二、泛型接口:

泛型接口與泛型類的定義及使用基本相同。下面是Java5改寫後的List接口,Map接口的代碼片斷

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public  interface  List<E>{
     //在該接口中,E能夠做爲類型使用
     //下面方法可使用E做爲參數類型
     void  add(E x);
     Iterator<E> iterator();
}
 
 
//定義該接口時指定了兩個泛型形參,其參數名爲K,V
public  interface  Map<K,V>{
     //在該接口中K,V徹底能夠做爲類型使用
     Set<K> keySet()
     V put(K key,V value);
}

  

下面咱們來看一個泛型案例:

定義一個泛型接口:

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

  

如今有一個類實現了這個泛型接口,類的代碼以下:

1
2
3
4
5
6
class  FruitGenerator<T>  implements  Generator<T>{
     @Override
     public  T next() {
         return  null ;
     }
}

  

咱們看到了這個類中也使用了泛型,並未傳入實際的參數

未傳入泛型實參時,與泛型類的定義相同,在聲明類的時候,需將泛型的聲明也一塊兒加到類中,即:class FruitGenerator<T> implements Generator<T>

若是不聲明泛型,如:class FruitGenerator implements Generator<T>,編譯器會報錯:"Unknown class"

 

1
2
3
4
5
6
7
8
9
10
public  class  FruitGenerator  implements  Generator<String> {
 
     private  String[] fruits =  new  String[]{ "Apple" "Banana" "Pear" };
 
     @Override
     public  String next() {
         Random rand =  new  Random();
         return  fruits[rand.nextInt( 3 )];
     }
}

  

這段代碼也是實現了Generator接口,不一樣的是傳入了實際的類型String

傳入泛型實參時:定義一個生產器實現這個接口,雖然咱們只建立了一個泛型接口Generator<T>可是咱們能夠爲T傳入無數個實參,造成無數種類型的Generator接口。在實現類實現泛型接口時,如已將泛型類型傳入實參類型,則全部使用泛型的地方都要替換成傳入的實參類型即:Generator<T>,public T next();中的的T都要替換成傳入的String類型。

 

三、泛型通配符:

爲何要使用泛型通配符:

正如前面講的,當使用一個泛型類時(包括聲明變量和建立對象兩種狀況),都應該爲這個泛型類傳入一個類型實參。若是沒有傳入類型實際參數,編譯器就會提出泛型警告。假設如今須要定義一個方法,該方法裏有一個集合形參,集合形參的元素類型是不肯定的,那應該怎樣定義呢?

考慮以下代碼:

1
2
3
4
5
public  void  test(List c) {
     for ( int  i= 0 ;i<c.size;i++) {
         System.out.println(c.get(i));
     }
}

  

上面程序固然沒有問題:這是一段最普通的遍歷List集合的代碼。問題是上面程序中List是一個有泛型聲明的接口,此處使用List 接口時沒有傳入實際類型參數,這將引發泛型警告。爲此,考慮爲List 接口傳入實際的類型參數——由於List集合裏的元素類型是不肯定的

 

泛型通配符的使用:

爲了表示各類泛型List的父類,可使用類型通配符,類型通配符是一個問號(?),將一個問號做爲類型實參傳給List集合,寫做:List<?>(意思是元素類型未知的List)。這個問號(?)被稱爲通配符,它的元素類型能夠匹配任何類型。能夠將上面方法改寫爲以下形式:

1
2
3
4
5
public  void  test(List<?> c) {
     for ( int  i= 0 ;i<c.size;i++) {
         System.out.println(c.get(i));
     }
}

  

這樣就不會出現警告了,但這種帶通配符的List僅表示它是各類泛型List的父類,並不能將其餘元素加入到其中,例如將String放入其中

List<?> c=new ArrayList<String>();//這段代碼將報錯

由於程序沒法肯定c集合中元素的類型,因此不能向其中添加對象。根據前面的List<E>接口定義的代碼能夠發現:add0方法有類型參數E做爲集合的元素類型,因此傳給add的參數必須是E類的對象或者其子類的對象。但由於在該例中不知道E是什麼類型,因此程序沒法將任何對象「丟進」該集合。惟一的例外是null,它是全部引用類型的實例。

 

設置類型通配符的上限:

如今想實現一個簡單的繪圖程序,代碼以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public  abstract  class  Shape{
     public  abstract  void  draw(Canvas c);
}
 
//定義Shape的子類Circle
public  class  Circle  extends  Shape{
     //實現畫圖方法,以打印字符串來模擬畫圖方法實現
     public  void  draw(Canvas c)
     {
         System.out.println( "在畫布" +c+ "上畫一個圓" );
     }
}
 
//定義Shape的子類Rectangle
public  class  Rectangle  extends  Shape{
     //實現畫圖方法,以打印字符串來模擬畫圖方法實現
     public  void  draw(Canvas c)
     {
         System.out.print1n( "把一個矩形畫在畫布" +c+ "上" );
     }
}

  

上面定義了三個形狀類,其中Shape是一個抽象父類,該抽象父類有兩個子類:Circle和Rectangle。接下來定義一個Canvas類,該畫布類能夠畫數量不等的形狀(Shape子類的對象),那應該如何定義這個Canvas類呢?考慮以下的Canvas實現類。

1
2
3
4
5
//同時在畫布上繪製多個形狀
public  void  drawAll(List<Shape>shapes) {
     for (Shape s:shapes)
         s.draw( this );
}

  

注意上面的drawAll()方法的形參類型是List<Shape>,而List<Circle>並非List<Shape>的子類型,所以,下面代碼將引發編譯錯誤。

1
2
3
4
List<Circle> circleList= new  ArrayList<Circle>();
Canvas c= new  Canvas();
//不能把List<Circle>當成List<Shape>使用,因此下面代碼引發編譯錯誤
c.drawAll(circleList); 

  

這時咱們能夠經過通配符的上限來解決這個問題:

1
List<?  extends  Shape>

  

List<? extends Shape>是受限制通配符的例子,此處的問號(?)表明一個未知的類型,就像前面看到的通配符同樣。可是此處的這個未知類型必定是Shape的子類型(也能夠是Shape自己),所以能夠把Shape稱爲這個通配符的上限(upper bound)。

 

設置類型通配符的下限:

除能夠指定通配符的上限以外,Java也容許指定通配符的下限,通配符的下限用<?super類型>的方式來指定,通配符下限的做用與通配符上限的做用剛好相反。

指定通配符的下限就是爲了支持類型型變。好比Foo是Bar的子類,當程序須要一個A<? super Bar>變量時,程序能夠將A<Foo>、A<Object>賦值給A<? super Bar>類型的變量,這種型變方式被稱爲逆變。

對於逆變的泛型集合來講,編譯器只知道集合元素是下限的父類型,但具體是哪一種父類型則不肯定。所以,這種逆變的泛型集合能向其中添加元素(由於實際賦值的集合元素老是逆變聲明的父類),從集合中取元素時只能被當成Object類型處理(編譯器沒法肯定取出的究竟是哪一個父類的對象)。

 

四、泛型方法:

前面介紹了在定義類、接口時可使用泛型形參,在該類的方法定義和成員變量定義、接口的方法定義中,這些泛型形參可被當成普通類型來用。在另一些狀況下,定義類、接口時沒有使用泛型形參,但定義方法時想本身定義泛型形參,這也是能夠的,Java5還提供了對泛型方法的支持。

 

(1)、泛型方法的基本用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
  * 泛型方法的基本介紹
  * @param tClass 傳入的泛型實參
  * @return T 返回值爲T類型
  * 說明:
  *     1)public 與 返回值中間<T>很是重要,能夠理解爲聲明此方法爲泛型方法。
  *     2)只有聲明瞭<T>的方法纔是泛型方法,泛型類中的使用了泛型的成員方法並非泛型方法。
  *     3)<T>代表該方法將使用泛型類型T,此時才能夠在方法中使用泛型類型T。
  *     4)與泛型類的定義同樣,此處T能夠隨便寫爲任意標識,常見的如T、E、K、V等形式的參數經常使用於表示泛型。
  */
     public  <T> T genericMethod(Class<T> tClass) throws  InstantiationException ,
       IllegalAccessException{
             T instance = tClass.newInstance();
             return  instance;
     }
 
Object obj = genericMethod(Class.forName( "com.test.test" ));

  

(2)、類中的泛型方法:

泛型方法能夠出現雜任何地方和任何場景中使用。可是有一種狀況是很是特殊的,當泛型方法出如今泛型類中時,咱們再經過一個例子看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//注意泛型類先寫類名再寫泛型,泛型方法先寫泛型再寫方法名
//類中聲明的泛型在成員和方法中可用
class  A <T, E>{
     {
         T t1 ;
     }
     A (T t){
         this .t = t;
     }
     T t;
 
     public  void  test1() {
         System.out.println( this .t);
     }
 
     public  void  test2(T t,E e) {
         System.out.println(t);
         System.out.println(e);
     }
}
@Test
public  void  run () {
     A <Integer,String > a =  new  A<>( 1 );
     a.test1();
     a.test2( 2 , "ds" );
//        1
//        2
//        ds
}
 
static  class  B <T>{
     T t;
     public  void  go () {
         System.out.println(t);
     }
}

  

(3)、泛型方法和可變參數:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public  class  泛型和可變參數 {
     @Test
     public  void  test () {
         printMsg( "dasd" , 1 , "dasd" , 2.0 , false );
         print( "dasdas" , "dasdas" "aa" );
     }
     //普通可變參數只能適配一種類型
     public  void  print(String ... args) {
         for (String t : args){
             System.out.println(t);
         }
     }
     //泛型的可變參數能夠匹配全部類型的參數
     public  <T>  void  printMsg( T... args){
         for (T t : args){
             System.out.println(t);
         }
     }
         //打印結果:
     //dasd
     //1
     //dasd
     //2.0
     //false
 
}

  

(4)、靜態方法與泛型

靜態方法有一種狀況須要注意一下,那就是在類中的靜態方法使用泛型:靜態方法沒法訪問類上定義的泛型;若是靜態方法操做的引用數據類型不肯定的時候,必需要將泛型定義在方法上。

即:若是靜態方法要使用泛型的話,必須將靜態方法也定義成泛型方法 。

1
2
3
4
5
6
7
8
9
10
11
12
13
public  class  StaticGenerator<T> {
     ....
     ....
     /**
      * 若是在類中定義使用泛型的靜態方法,須要添加額外的泛型聲明(將這個方法定義成泛型方法)
      * 即便靜態方法要使用泛型類中已經聲明過的泛型也不能夠。
      * 如:public static void show(T t){..},此時編譯器會提示錯誤信息:
           "StaticGenerator cannot be refrenced from static context"
      */
     public  static  <T>  void  show(T t){
 
     }
}

  

 

總結:

泛型方法能使方法獨立於類而產生變化,如下是一個基本的指導原則:

不管什麼時候,若是你能作到,你就該儘可能使用泛型方法。也就是說,若是使用泛型方法將整個類泛型化,那麼就應該使用泛型方法。另外對於一個static的方法而已,沒法訪問泛型類型的參數。因此若是static方法要使用泛型能力,就必須使其成爲泛型方法。

 

 

3、泛型的類型擦除:

一、什麼是類型擦除:

還記得咱們在文章開始介紹的代碼嗎?咱們如今再來看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public  class  Test {
 
     public  static  void  main(String[] args) {
 
         ArrayList<String> list1 =  new  ArrayList<String>();
         list1.add( "abc" );
 
         ArrayList<Integer> list2 =  new  ArrayList<Integer>();
         list2.add( 123 );
 
         System.out.println(list1.getClass() == list2.getClass());
     }
 
}

  

在這個例子中,咱們定義了兩個ArrayList數組,不過一個是ArrayList<String>泛型類型的,只能存儲字符串;一個是ArrayList<Integer>泛型類型的,只能存儲整數,最後,咱們經過list1對象和list2對象的getClass()方法獲取他們的類的信息,最後發現結果爲true。這就是java的泛型擦除。

 

下面咱們再來看一個例子加深一下理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
public  class  Test001 {
     
     public  static  void  main(String[] args)  throws  Exception {
         ArrayList<Integer> list= new  ArrayList<Integer>();
         list.add( 1 );
         
         list.getClass().getMethod( "add" ,Object. class ).invoke(list,  "asd" );
         for ( int  i= 0 ;i<list.size();i++) {
             System.out.println(list.get(i));
         }
     }
 
}

  

上面這段代碼首先建立了一個ArrayList,泛型類型實例化爲Integer對象,若是咱們直接調用add()方法,那麼只能添加Integer類型的值,可是如今咱們利用反射,發現能夠往裏面加入String類型的值,這也說明了java的泛型擦除。

 

定義:

Java在編譯期間,全部的泛型信息都會被擦掉,這就是泛型擦除。

正確理解泛型概念的首要前提是理解類型擦除。Java的泛型基本上都是在編譯器這個層次上實現的,在生成的字節碼中是不包含泛型中的類型信息的,使用泛型的時候加上類型參數,在編譯器編譯的時候會去掉,這個過程成爲類型擦除。

 

二、類型擦除後保留的原始類型

原始類型 就是擦除去了泛型信息,最後在字節碼中的類型變量的真正類型,不管什麼時候定義一個泛型,相應的原始類型都會被自動提供,類型變量擦除,並使用其限定類型(無限定的變量用Object)替換。

例一、

1
2
3
4
5
6
7
8
9
class  Pair<T> { 
     private  T value; 
     public  T getValue() { 
         return  value; 
    
     public  void  setValue(T  value) { 
         this .value = value; 
    
}

  

Pair的原始類型爲:Object

1
2
3
4
5
6
7
相關文章
相關標籤/搜索