1. 程式人生 > >Java基礎學習——泛型(generics)二

Java基礎學習——泛型(generics)二

萬用字元(Wildcard)

考慮一個列印集合內所有元素的問題。下面這個可能是在Java舊版本中的寫法:

void printCollection(Collection c) {
    Iterator i = c.iterator();
    for (k = 0; k < c.size(); k++) {
        System.out.println(i.next());
    }
}

下面是一個用泛型的天真嘗試(同時使用foreach語法):

void printCollection(Collection<Object> c) {
    for
(Object e : c) { System.out.println(e); } }

問題是新版本並不比舊版本更有益處。而舊版程式碼可以使用任何型別集合當引數傳入,而新的程式碼確只能接受* Collection<Object>*型別,正如上文介紹,Collection<Object>並不是其他任何泛型集合的父型別。

那麼什麼才是所有泛型集合的父型別呢?那就是被寫成Collection<?>(表示“未知型別的集合” “collection of unknown”)的集合,一種元素型別可以匹配任何型別的集合。這就是它被叫做通配型別(wildcard type)的直白原因。我們可以這樣寫:

void printCollection(Collection<?> c) {
    for (Object e : c) {
        System.out.println(e);
    }
}

現在,我們可以用任何泛型集合當引數呼叫了。注意一下,在printCollection函式內部,我們依然是用Object型別去讀取集合內的元素。無論集合內真實型別是什麼,這樣做是安全的,因為集合真的包含了Object。但是隨意新增Object物件卻不一定是安全的。

Collection<?> c = new ArrayList<String>();
c.add(new
Object()); // Compile time error

因為我們不知道c的元素型別時,我們是不能往裡面新增物件的。方法add需要一個E型別的引數,E表示集合元素型別。什麼時候實際型別引數是?呢?它代表著一些未知的型別(unknown type)。我們傳入add方法的引數都必須是未知型別的子型別。因為我們不知道未知型別是什麼,所以我們不能傳入任何東西。唯一的例外就是null,null是所有型別的成員。

另外一方面,給定List<?>,我們能夠呼叫get()方法,並且利用返回值。返回值是一個未知型別,但我們總是能知道它是一個物件。因此,我們總是能安全地將get()的結果賦值給一個Object型別變數或者將它用在任何期望Object型別的地方。

受限萬用字元(Bounded Wildcards)

假想一下,有一個簡單的繪畫程式,它可以畫出一些圖形,比如矩形或者圓。為了在程式裡表示這些圖形,我們定義下面的類層次結構:

public abstract class Shape {
    public abstract void draw(Canvas c);
}

public class Circle extends Shape {
    private int x, y, radius;
    public void draw(Canvas c) {
        ...
    }
}

public class Rectangle extends Shape {
    private int x, y, width, height;
    public void draw(Canvas c) {
        ...
    }
}

這些類都可以在畫布上繪出:

public class Canvas {
    public void draw(Shape s) {
        s.draw(this);
   }
}

一個圖紙總是會包含很多形狀。假設圖紙用一個list表示,有個方面的函式Canvas可以畫出所有圖形:

public void drawAll(List<Shape> shapes) {
    for (Shape s: shapes) {
        s.draw(this);
   }
}

現在,型別規則說明drawAll()函式只能以Shape的list為引數被呼叫。它實際上被不能以List<Circle>為引數。這是不幸的,因為所有方法做的就是從List裡讀取shape,所以,它也應該能接受List<Circle>為引數。其實,我們真實的想法,是接受一個shape子類型別的list:

public void drawAll(List<? extends Shape> shapes) {
    ...
}

上面的程式碼有一個很小卻很重要的區別:我們用List

public void addRectangle(List<? extends Shape> shapes) {
    // Compile-time error!
    shapes.add(0, new Rectangle());
}

你應該能指出上面的程式碼為啥不能被允許。因為add方法的第二個引數型別是? extends Shape——Shape的未知子型別。因為我們不知道型別是啥,我們並不知道它是不是Rectangle的父型別。它可能是,也可能不是一個超型別,所以,這個地方傳入Rectangle是不安全的。

受限萬用字元(Bounded wildcards)正好解決之前那個從人口普查局(the census bureau)傳送資料給DMV的例子。之前的例子假設,資料是存放在map裡的,用人名作key,人員資訊(可以用Person類或其子類,比如Driver,代表)為value。Map<K,V>是一個有兩個型別引數的泛型例子,兩個引數代表map的key與value。

再次提醒,引數型別的命名慣例是:K表示key,V表示values。

public class Census {
    public static void addRegistry(Map<String, ? extends Person> registry) {
}
...

Map<String, Driver> allDrivers = ... ;
Census.addRegistry(allDrivers);

泛型方法(Generic Methods)

考慮一下,需要寫個將一個Object陣列的元素都寫入另外一個collection(集合)的方法。下面是第一次嘗試:

static void fromArrayToCollection(Object[] a, Collection<?> c) {
    for (Object o : a) { 
        // 編譯錯誤
        c.add(o); // compile-time error
    }
}

到目前為止,你可能已經學會避免剛開始時用Collection<Object>來代替所有集合的錯誤。你可能已經認識到,也可能還沒有,Collection

static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
    for (T o : a) {
        c.add(o); // Correct
    }
}

我們可以用任何型別的集合呼叫此函式,只要集合的元素型別是陣列元素型別的父型別。

Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<Object>();

// T inferred to be Object
// 將T自動推導為Object
fromArrayToCollection(oa, co); 

String[] sa = new String[100];
Collection<String> cs = new ArrayList<String>();

// T inferred to be String
// 將T自動推導為String
fromArrayToCollection(sa, cs);

// T inferred to be Object
// T 自動推導為Object
fromArrayToCollection(sa, co);

Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection<Number> cn = new ArrayList<Number>();

// T inferred to be Number
// T 自動推導為Number
fromArrayToCollection(ia, cn);

// T inferred to be Number
// T 自動推導為Number
fromArrayToCollection(fa, cn);

// T inferred to be Number
// T 自動推導為Number
fromArrayToCollection(na, cn);

// T inferred to be Object
// T 自動推導為Object
fromArrayToCollection(na, co);

// compile-time error
// 編譯錯誤
fromArrayToCollection(na, cs);

注意一下,我們並沒有直接傳入型別給泛型函式。Java編譯器會根據實際引數的型別自動推導T的型別。

但是有一個問題:什麼時候我們需要泛型函式,什麼時候我們需要萬用字元型別。為了搞清楚這個問題。我們拿出幾個jdk裡的Collection類方法作為例子。

interface Collection<E> {
    public boolean containsAll(Collection<?> c);
    public boolean addAll(Collection<? extends E> c);
}

這裡我們也可以使用泛型函式。

interface Collection<E> {
    public <T> boolean containsAll(Collection<T> c);
    public <T extends E> boolean addAll(Collection<T> c);
    // Hey, type variables can have bounds too!
}

但是,在containsAll和addAl函式裡,型別引數T都僅僅被用過一次。返回值既不依賴型別引數,也沒有其他引數依賴此型別(在這個例子裡,方法只有一個引數)。這便是告訴我們這裡的型別引數只是用於多型,僅僅為了可以讓方法被各種各樣的引數型別呼叫。如果是上面這種情況,就是應該使用萬用字元(wildcards)。萬用字元正是設計用來支援靈活的子型別,像上面的例子那樣。

泛型方法用來使型別引數可以體現多個引數之間或引數與返回值之間的型別依賴關係。如果沒有這些依賴關係,則不應該使用泛型方法。

泛型方法和萬用字元是可以串聯使用的。比如Collections.copy():

class Collections {
    public static <T> void copy(List<T> dest, List<? extends T> src) {
    ...
}

注意一下,上面的依賴關係是兩個引數型別之間的依賴。list src裡的元素都必須可以賦值給dst list的元素型別T。所以,src的元素型別可以是T的任何型別,我們也不關心具體是哪個子型別。copy函式的宣告體現了型別引數的依賴,並且在第二個引數的型別使用了萬用字元。

我們也可以用完全不用萬用字元的方式,改寫上面函式的宣告:

class Collections {
    public static <T, S extends T> void copy(List<T> dest, List<S> src) {
    ...
}

改寫後,第一個引數型別,既用在了dst,同時又是第二個型別引數S的上界,同時,S僅僅只在src裡用了一次。這便是用萬用字元替換S的標誌。這裡,使用萬用字元則比型別引數更加簡潔清晰。因此,如果合適,則應首選萬用字元。

萬用字元同時還有其他優點,它可以在方法外面被使用,比如成員變數,區域性變數和陣列等。這裡有個例子。
回到之前的繪畫程式的例子。假設我們想保留繪畫的歷史操作。我們可以使用一個靜態變數保留歷史,然後,在drawAll()函式中,將操作儲存到歷史靜態變數裡。

static List<List<? extends Shape>> 
    history = new ArrayList<List<? extends Shape>>();

public void drawAll(List<? extends Shape> shapes) {
    history.addLast(shapes);
    for (Shape s: shapes) {
        s.draw(this);
    }
}

在結束前,讓我們再次強調下,型別引數的命名約定。當我們沒有更加詳細內容來說明的型別,我們使用T代表型別。在泛型函式裡,經常出現這種情況。如果有多個引數型別,我們就是T旁邊的字面,比如S。如果一個泛型方法出現在泛型類(generic class)裡,為了避免混淆,最好不要使用相同的型別引數名稱。這也使用於泛型巢狀類。

相關推薦

Java基礎學習——(generics)

萬用字元(Wildcard) 考慮一個列印集合內所有元素的問題。下面這個可能是在Java舊版本中的寫法: void printCollection(Collection c) { Iterator i = c.iterator(); fo

Java基礎學習——(generics)學習

概述 在JDK 5.0,Java語言引入了好幾個新的功能,其中很重要的一個就是泛型(generics)。 本文就是對泛型的一個概述。你可以很熟悉其他語言中的類似結構,比如C++裡的模板(templates)。如果這樣,你將會看到兩者之間有相似,同時也有很大的

Java基礎學習——

() logs 調用 自定義 sta class string [] lis 一、泛型方法 1 /** 2 3 自定義泛型:自定義泛型可以理解為是一個數據類型的占位符,或者理解為是一個數據類型的變量。 4 5 泛型方法: 6 泛型方法的自定義格式:修飾符&

java入門基礎學習----

概念:泛型就是引數化型別,使用廣泛的型別 作用:        1. 安全:在編譯的時候檢查型別安全;  2.省心:所有的強制轉換都是自動和隱式的,提高程式碼的重用率 一、  泛型類:宣告時使用泛型   字母:   T Type 表示型別   K V 分別代表兼職中

Java基礎

ret 原來 不能 使用 自定義泛型 編程 讀取數組 yum 實現   同樣是面試當中遇到的問題,在平常寫代碼的過程當中,經常有使用到泛型編程,比如用到的各種集合方式,如Arraylist、hashmap、List、等,都有使用到泛型。但是當面試官讓自己系統的介紹一下泛型編

14. Java基礎

remove 角度 分享 The 兩個 -a 類型形參 建議 set 引用自:https://www.cnblogs.com/lwbqqyumidi/p/3837629.html , https://blog.csdn.net/s10461/article/details/

java基礎學習總結(十):深入理解java內部類

內部類 內部類也是語法糖,是因為它僅僅是一個編譯時的概念,outer.java裡面定義了一個內部類inner,一旦編譯成功,就會生成兩個完全不同的.class檔案了,分別是outer.class和outer$inner.class。所以內部類的名字完全可以和它的外部類名字相同。 內部類分為四

java基礎——3——+異常+IO

在Java釋出的JDK1.5版本中增加了泛型支援,所謂的泛型就是為了讓集合在很大程度上記住元素的資料型別。在沒有使用泛型之前,一旦把物件存放進Java集合中,集合只會將元素作為Object物件進行處理。當從集合中取出物件時就需要進行強制型別轉換。如果轉換錯誤,則將會引起ClassCastEx

java基礎-04

介紹 泛型就是資料型別的引數化表示,泛型的本質是引數化型別,常用E代表任何資料型別,在實際使用的時候把實際的資料型別傳遞給E。 泛型的好處是設計通用的功能,多個數據型別可以共用。 泛型型別E只能代表Object型別,不能代表 int,double等基本型別,要使用Integer,Double代

Java基礎Demo -- 上界的示例

<T extends SuperClass> 泛型上界的定義 <? extends SuperClass> 有界萬用字元的運用 普通泛型方法的運用 靜態泛型方法的運用 class Grandpa { private int x,y; public Gran

Java基礎Demo -- 介面的示例

泛型介面 public interface MyInterface<T>{} 的簡單入門示例 /** * 類實現了一個泛型介面,並擴容下 */ interface AI<T> { void sss(T t); } class BI<T,K> imple

Java基礎Demo -- 類的示例

泛型類 public class MyClass<T>{} 的簡單入門示例 /** * 泛型類 class MyClass<T>{} */ class MyGen<T> { private T t; public void setValue(T t){

java基礎總結 -- 在類、介面、方法、匿名類、元組等使用 堆疊例子 商店模型

為什麼使用泛型:     在面向物件程式語言中,多型算是一種泛化機制。例如,你可以將方法的引數型別設為基類,那麼 該方法就可以接受從這個基類中匯出的任何類作為引數,這樣的方法更通用一些,可應用的地方也多一點。     在類的內部也是如此,凡是能夠使用基類,確實是能夠具備更好

java基礎總結 --- 擦除、邊界、萬用字元、

* 擦除的問題 * 為什麼要擦除: 1.5版本才出現泛型 為了相容之前地程式碼 * 它使得泛化的客戶端可以用非泛化的類庫來使用。 * 以及不破壞現有類庫的情況下,將泛型融入java語言。 * 擦除使得現有的非泛型客戶端程式碼能夠在不改變的情況繼續使用,直至客戶端準

Java基礎記錄概要

泛型記錄概要 泛型出現重要原因之一:創造容器類 泛型主要目的之一:指定容器持有什麼型別的物件,保證編譯的正確性 泛型簡單例項 class GenericsClass<T>{ private T property; public

Java基礎18----工具類--JDK1.5新特性

18-1,泛型-概述 1,泛型是JDK1.5出現的新技術,新技術的出現是為了解決問題。 2,泛型可以用於明確一個集合中儲存什麼型別的元素 ArrayList<String> al = new ArrayList<String>(); al.add("

java基礎(28)--與型別擦除、與繼承

【泛型與型別擦除】 泛型是JDK 1.5的一項新特性,它的本質是引數化型別(Parameterized Type)的應用,也就是說所操作的資料型別被指定為一個引數。這種引數型別可以用在類、介面和方法的建立中,分別稱為泛型類、泛型介面和泛型方法。 泛

JAVA基礎與集合

一:泛型: 泛型類的定義格式: class class-name  <type -parma-list>{} 例項化泛型類的格式: class-name<type-parma-list> obj = new class-name<type

JAVA基礎知識|

一、什麼是泛型? 泛型,即“引數化型別”。 比如定義一個變數A,我們可以通過如下方式將這個變數定義為字串型別或者整形。 S

java基礎學習總結(九):深入理解Java

一、什麼是泛型         “泛型” 意味著編寫的程式碼可以被不同型別的物件所重用。泛型的提出是為了編寫重用性更好的程式碼。泛型的本質是引數化型別,也就是說所操作的資料型別被指定為一個引數。 比如常見的集合類 LinkedList: publi