首頁  >  文章  >  Java  >  Java泛型程式設計最全總結

Java泛型程式設計最全總結

高洛峰
高洛峰原創
2016-12-19 15:11:491552瀏覽

1介紹

Java泛型程式設計是JDK1.5版本後引進的。泛型讓程式設計師能夠使用型別抽象,通常用於集合裡面。以下是一個不用泛型範例:

List myIntList=new LinkedList(); //1  
myIntList.add(newInteger(0)); //2  
Integer x=(Integer)myIntList.iterator().next(); //3

注意第3行程式碼,但這是讓人很不爽的一點,因為程式設計師肯定知道自己儲存在List裡面的物件類型是Integer,但在傳回清單中元素時,還是必須強制轉換類型,這是為什麼?原因在於,編譯器只能保證迭代器的next()方法回傳的是Object類型的對象,為確保Integer變數的型別安全,所以必須強制轉換。

這種轉換不僅顯得混亂,更可能導致類型轉換異常ClassCastException,運行時異常往往讓人難以檢測到。確保清單中的元素為一個特定的資料型別,這樣就可以取消型別轉換,減少發生錯誤的機會, 這也是泛型設計的初衷。以下是一個使用了泛型的範例:

Listc0f559cc8d56b43654fcbe4aa9df7b4a myIntList=newLinkedListc0f559cc8d56b43654fcbe4aa9df7b4a(); //1’  
myIntList.add(newInteger(0)); //2’  
Integerx=myIntList.iterator().next(); //3’

 在第1行程式碼中指定List中儲存的物件類型為Integer,這樣在取得清單中的物件時,就不必強制轉換類型了。 

2定義簡單的泛型

下面是一個引用自java.util套件中的介面List和Iterator的定義,其中用到了泛型技術。

public interface List1a4db2c2c2313771e5742b6debf617a1 {  
5144dad4cd9c9023410d94caa0f34ebc    54bdf357c58b8a65c66d7c19c8e4d114void add(E x);  
5144dad4cd9c9023410d94caa0f34ebc    54bdf357c58b8a65c66d7c19c8e4d114Iterator1a4db2c2c2313771e5742b6debf617a1 iterator();  
}  
public interface Iterator1a4db2c2c2313771e5742b6debf617a1 {  
5144dad4cd9c9023410d94caa0f34ebc    54bdf357c58b8a65c66d7c19c8e4d114E next();  
5144dad4cd9c9023410d94caa0f34ebc    54bdf357c58b8a65c66d7c19c8e4d114boolean hasNext();  
}

     這跟原生型別沒有什麼差別,只是在介面後面加入了一個尖括號,尖括號裡面是一個型別參數(定義時就是一個格式化的型別參數,在呼叫時會用一個特定的型別來替換該類型)。

       或許可以這樣認為,List表示List中的型別參數E會被替換為Integer。

public interface IntegerList {  
<span style="white-space: pre;">    </span>void add(Integer x)  
<span style="white-space: pre;">    </span>Iterator<Integer> iterator();  
}

       類型擦除指的是透過型別參數合併,將泛型型別實例關聯到同一份字節碼上。編譯器只為泛型類型產生一份字節碼,並將其實例關聯到這份字節碼上,因此泛型類型中的靜態變數是所有實例共享的。此外,需要注意的是,一個static方法,無法存取泛型類別的型別參數,因為類別還沒有實例化,所以,若static方法需要使用泛型能力,必須使其成為泛型方法。類型擦除的關鍵在於從泛型類型中清除類型參數的相關信息,並且再必要的時候添加類型檢查和類型轉換的方法。在使用泛型時,任何特定的類型都被擦除,唯一知道的是你在使用一個物件。例如:List和List在運作事實上是相同的型別。他們都被擦除成他們的原生類型,即List。因為編譯的時候會有類型擦除,所以不能透過同一個泛型類別的實例來區分方法,如下面的例子編譯時會出錯,因為類型擦除後,兩個方法都是List類型的參數,因此並不能根據泛型類別的類型來區分方法。

 public class Erasure{  
            public void test(List<String> ls){  
                System.out.println("Sting");  
            }  
            public void test(List<Integer> li){  
                System.out.println("Integer");  
            }  
  }

      那麼這就有個問題了,既然在編譯的時候會在方法和類別中擦除實際類型的信息,那麼在返回對象時又是如何知道其具體類型的呢?如List編譯後會擦除掉String訊息,那麼在執行時期透過迭代器回傳List中的物件時,又是如何知道List中儲存的是String類型物件呢?

       擦除在方法體中移除了類型訊息,所以在運行時的問題是邊界:即物件進入和離開方法的地點,這正是編譯器在編譯期執行類型檢查並插入轉型程式碼的地點。泛型中的所有動作都發生在邊界處:對傳遞進來的值進行額外的編譯期檢查,並插入對傳遞出去的值的轉換。

 

3.泛型和子類型

為了徹底理解泛型,這裡看個例子:(Apple為Fruit的子類

List<Apple> apples = new ArrayList<Apple>(); //1  
List<Fruit> fruits = apples; //2

第1行代码显然是对的,但是第2行是否对呢?我们知道Fruit fruit = new Apple(),这样肯定是对的,即苹果肯定是水果,但是第2行在编译的时候会出错。这会让人比较纳闷的是一个苹果是水果,为什么一箱苹果就不是一箱水果了呢?可以这样考虑,我们假定第2行代码没有问题,那么我们可以使用语句fruits.add(new Strawberry())(Strawberry为Fruit的子类)在fruits中加入草莓了,但是这样的话,一个List中装入了各种不同类型的子类水果,这显然是不可以的,因为我们在取出List中的水果对象时,就分不清楚到底该转型为苹果还是草莓了。

通常来说,如果Foo是Bar的子类型,G是一种带泛型的类型,则G4ee996100bf04ab273e687539c860625不是Gad40e550a33cb99ea30eede96e03e60e的子类型。这也许是泛型学习里面最让人容易混淆的一点。

 

4.通配符

4.1通配符?

先看一个打印集合中所有元素的代码。

void printCollection(Collection c) {                  
<span style="white-space: pre;">    </span>Iterator i=c.iterator();  
<span style="white-space: pre;">    </span>for (k=0;k < c.size();k++) {  
<span style="white-space: pre;">        </span>System.out.println(i.next());  
<span style="white-space: pre;">    </span>}  
}
void printCollection(Collection<Object> c) {  
for (Object e:c) {  
System.out.println(e);  
}  
}

   很容易发现,使用泛型的版本只能接受元素类型为Object类型的集合如ArrayLista87fdacec66f0909fc0757c19f2d2b1d();如果是ArrayListf7e83be87db5cd2d9a8a0b8117b38cd4,则会编译时出错。因为我们前面说过,Collectiona87fdacec66f0909fc0757c19f2d2b1d并不是所有集合的超类。而老版本可以打印任何类型的集合,那么如何改造新版本以便它能接受所有类型的集合呢?这个问题可以通过使用通配符来解决。修改后的代码如下所示:

//使用通配符?,表示可以接收任何元素类型的集合作为参数  
void printCollection(Collection<?> c) {  
<span style="white-space: pre;">    </span>for (Object e:c) {  
<span style="white-space: pre;">        </span>System.out.println(e);  
<span style="white-space: pre;">    </span>}  
}

这里使用了通配符?指定可以使用任何类型的集合作为参数。读取的元素使用了Object类型来表示,这是安全的,因为所有的类都是Object的子类。这里就又出现了另外一个问题,如下代码所示,如果试图往使用通配符?的集合中加入对象,就会在编译时出现错误。需要注意的是,这里不管加入什么类型的对象都会出错。这是因为通配符?表示该集合存储的元素类型未知,可以是任何类型。往集合中加入元素需要是一个未知元素类型的子类型,正因为该集合存储的元素类型未知,所以我们没法向该集合中添加任何元素。唯一的例外是null,因为null是所有类型的子类型,所以尽管元素类型不知道,但是null一定是它的子类型。

Collection<?> c=new ArrayList<String>();  
c.add(newObject()); //compile time error,不管加入什么对象都出错,除了null外。  
c.add(null); //OK

另一方面,我们可以从List6b3d0130bba23ae47fe2b8e8cddf0195 lists中获取对象,虽然不知道List中存储的是什么类型,但是可以肯定的是存储的类型一定是Object的子类型,所以可以用Object类型来获取值。如for(Object obj: lists),这是合法的。

4.2边界通配符

   1)?extends通配符

假定有一个画图的应用,可以画各种形状的图形,如矩形和圆形等。为了在程序里面表示,定义如下的类层次:

public abstract class Shape {  
<span style="white-space: pre;">    </span>public abstract void draw(Canvas c);  
}  
  
public class Circle extends Shape {  
<span style="white-space: pre;">    </span>private int x,y,radius;  
<span style="white-space: pre;">    </span>public void draw(Canvas c) { ... }  
}  
  
public class Rectangle extends Shape  
<span style="white-space: pre;">    </span>private int x,y,width,height;  
<span style="white-space: pre;">    </span>public void draw(Canvasc) { ... }  
}

为了画出集合中所有的形状,我们可以定义一个函数,该函数接受带有泛型的集合类对象作为参数。但是不幸的是,我们只能接收元素类型为Shape的List对象,而不能接收类型为Listbd5f0b6be71dcbe6e9e8a42cca8c5100的对象,这在前面已经说过。为了解决这个问题,所以有了边界通配符的概念。这里可以采用public void drawAll(Listd77674685afc6c34e7798aac36bbe2b7shapes)来满足条件,这样就可以接收元素类型为Shape子类型的列表作为参数了。

//原始版本  
public void drawAll(List<Shape> shapes) {  
<span style="white-space: pre;">    </span>for (Shapes:shapes) {  
<span style="white-space: pre;">        </span>s.draw(this);  
<span style="white-space: pre;">    </span>}  
}
//使用边界通配符的版本  
public void drawAll(List<?exends Shape> shapes) {  
<span style="white-space: pre;">    </span>for (Shapes:shapes) {  
<span style="white-space: pre;">        </span>s.draw(this);  
<span style="white-space: pre;">    </span>}  
}

这里就又有个问题要注意了,如果我们希望在List3713e1cf25733a9d265f2d98a96f6589 shapes中加入一个矩形对象,如下所示:

shapes.add(0, new Rectangle()); //compile-time error

那么这时会出现一个编译时错误,原因在于:我们只知道shapes中的元素时Shape类型的子类型,具体是什么子类型我们并不清楚,所以我们不能往shapes中加入任何类型的对象。不过我们在取出其中对象时,可以使用Shape类型来取值,因为虽然我们不知道列表中的元素类型具体是什么类型,但是我们肯定的是它一定是Shape类的子类型。

 

2)?super通配符

       这里还有一种边界通配符为?super。比如下面的代码:

List<Shape> shapes = new ArrayList<Shape>();  
List<? super Cicle> cicleSupers = shapes;  
cicleSupers.add(new Cicle()); //OK, subclass of Cicle also OK  
cicleSupers.add(new Shape()); //ERROR

       这表示cicleSupers列表存储的元素为Cicle的超类,因此我们可以往其中加入Cicle对象或者Cicle的子类对象,但是不能加入Shape对象。这里的原因在于列表cicleSupers存储的元素类型为Cicle的超类,但是具体是Cicle的什么超类并不清楚。但是我们可以确定的是只要是Cicle或者Circle的子类,则一定是与该元素类别兼容。

 

3)边界通配符总结

437fcc348f7b1c54dc1b8f8cb5743cbcl         04c6c2c265887a55041405a81efec09a如果你想从一个数据类型里获取数据,使用 ? extends 通配符

437fcc348f7b1c54dc1b8f8cb5743cbcl         04c6c2c265887a55041405a81efec09a如果你想把对象写入一个数据结构里,使用 ? super 通配符

437fcc348f7b1c54dc1b8f8cb5743cbcl         04c6c2c265887a55041405a81efec09a如果你既想存,又想取,那就别用通配符。

 

5.泛型方法

考虑实现一个方法,该方法拷贝一个数组中的所有对象到集合中。下面是初始的版本:

static void fromArrayToCollection(Object[]a, Collection<?> c) {  
<span style="white-space: pre;">    </span>for (Object o:a) {  
<span style="white-space: pre;">        </span>c.add(o); //compile time error  
<span style="white-space: pre;">    </span>}  
}

可以看到显然会出现编译错误,原因在之前有讲过,因为集合c中的类型未知,所以不能往其中加入任何的对象(当然,null除外)。解决该问题的一种比较好的办法是使用泛型方法,如下所示:

static <T> void fromArrayToCollection(T[] a, Collection<T>c){  
<span style="white-space: pre;">    </span>for(T o : a) {  
<span style="white-space: pre;">        </span>c.add(o);// correct  
<span style="white-space: pre;">    </span>}  
}

注意泛型方法的格式,类型参数8742468051c85b06f0a0af9e3e506b5c需要放在函数返回值之前。然后在参数和返回值中就可以使用泛型参数了。具体一些调用方法的实例如下:

Object[] oa = new Object[100];  
Collection<Object>co = new ArrayList<Object>();  
fromArrayToCollection(oa, co);// T inferred to be Object  
String[] sa = new String[100];  
Collection<String>cs = new ArrayList<String>();  
fromArrayToCollection(sa, cs);// T inferred to be String  
fromArrayToCollection(sa, co);// T inferred to be Object  
Integer[] ia = new Integer[100];  
Float[] fa = new Float[100];  
Number[] na = new Number[100];  
Collection<Number>cn = new ArrayList<Number>();  
fromArrayToCollection(ia, cn);// T inferred to be Number  
fromArrayToCollection(fa, cn);// T inferred to be Number  
fromArrayToCollection(na, cn);// T inferred to be Number  
fromArrayToCollection(na, co);// T inferred to be Object  
fromArrayToCollection(na, cs);// compile-time error

注意到我们调用方法时并不需要传递类型参数,系统会自动判断类型参数并调用合适的方法。当然在某些情况下需要指定传递类型参数,比如当存在与泛型方法相同的方法的时候(方法参数类型不一致),如下面的一个例子:

public  <T> void go(T t) {  
    System.out.println("generic function");  
}  
public void go(String str) {  
    System.out.println("normal function");  
}  
public static void main(String[] args) {  
        FuncGenric fg = new FuncGenric();  
        fg.go("haha");//打印normal function  
        fg.<String>go("haha");//打印generic function  
        fg.go(new Object());//打印generic function  
        fg.<Object>go(new Object());//打印generic function  
}

如例子中所示,当不指定类型参数时,调用的是普通的方法,如果指定了类型参数,则调用泛型方法。可以这样理解,因为泛型方法编译后类型擦除,如果不指定类型参数,则泛型方法此时相当于是public void go(Object t)。而普通的方法接收参数为String类型,因此以String类型的实参调用函数,肯定会调用形参为String的普通方法了。如果是以Object类型的实参调用函数,则会调用泛型方法。

6.其他需要注意的小点

1)方法重载

在JAVA里面方法重载是不能通过返回值类型来区分的,比如代码一中一个类中定义两个如下的方法是不容许的。但是当参数为泛型类型时,却是可以的。如下面代码二中所示,虽然形参经过类型擦除后都为List类型,但是返回类型不同,这是可以的。

/*代码一:编译时错误*/   
public class Erasure{  
            public void test(int i){  
                System.out.println("Sting");  
            }  
            public int test(int i){  
                System.out.println("Integer");  
            }  
  }
/*代码二:正确 */  
 public class Erasure{  
            public void test(List<String> ls){  
                System.out.println("Sting");  
            }  
            public int test(List<Integer> li){  
                System.out.println("Integer");  
            }  
  }

2)泛型类型是被所有调用共享的

       所有泛型类的实例都共享同一个运行时类,类型参数信息会在编译时被擦除。因此考虑如下代码,虽然ArrayListf7e83be87db5cd2d9a8a0b8117b38cd4和ArrayListc0f559cc8d56b43654fcbe4aa9df7b4a类型参数不同,但是他们都共享ArrayList类,所以结果会是true。

List<String>l1 = new ArrayList<String>();  
List<Integer>l2 = new ArrayList<Integer>();  
System.out.println(l1.getClass() == l2.getClass()); //True

3)instanceof

不能对确切的泛型类型使用instanceOf操作。如下面的操作是非法的,编译时会出错。

Collection cs = new ArrayList<String>();  
if (cs instanceof Collection<String>){…}// compile error.如果改成instanceof Collection<?>则不会出错。

4)泛型数组问题

不能创建一个确切泛型类型的数组。如下面代码会出错。

Listf7e83be87db5cd2d9a8a0b8117b38cd4[] lsa = new ArrayListf7e83be87db5cd2d9a8a0b8117b38cd4[10]; //compile error.

       因为如果可以这样,那么考虑如下代码,会导致运行时错误。

List<String>[] lsa = new ArrayList<String>[10]; // 实际上并不允许这样创建数组  
Object o = lsa;  
Object[] oa = (Object[]) o;  
List<Integer>li = new ArrayList<Integer>();  
li.add(new Integer(3));  
oa[1] = li;// unsound, but passes run time store check  
String s = lsa[1].get(0); //run-time error - ClassCastException

       因此只能创建带通配符的泛型数组,如下面例子所示,这回可以通过编译,但是在倒数第二行代码中必须显式的转型才行,即便如此,最后还是会抛出类型转换异常,因为存储在lsa中的是Listc0f559cc8d56b43654fcbe4aa9df7b4a类型的对象,而不是Listf7e83be87db5cd2d9a8a0b8117b38cd4类型。最后一行代码是正确的,类型匹配,不会抛出异常。

ist<?>[] lsa = new List<?>[10]; // ok, array of unbounded wildcard type  
Object o = lsa;  
Object[] oa = (Object[]) o;  
List<Integer>li = new ArrayList<Integer>();  
li.add(new Integer(3));  
oa[1] = li; //correct  
String s = (String) lsa[1].get(0);// run time error, but cast is explicit  
Integer it = (Integer)lsa[1].get(0); // OK



更多Java泛型编程最全总结相关文章请关注PHP中文网!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn