泛型:工作原理及其重要性
作者:Josh Juneau
深入了解 Java SE 8 中的泛型。
2014 年 7 月發布
Java SE 8 的發布曾在 Java 界引起轟動。該版本中新增的和更新的語言特性可減少需要編寫的代碼量并使代碼更易于使用,從而提高開發人員的工作效率。要充分了解一些新特性(如 lambda)的實現,您需要先了解該語言的核心概念。其中一個在許多 Java SE 8 特性中發揮了重要作用的概念是泛型。
本文首先簡單解釋泛型,連帶介紹一些基本概念。了解基本概念之后,我們將深入介紹一些場景,演示泛型的用法。最后,我們將看到泛型如何成為 Java SE 8 中一些新增構造的重要組成部分。
注:GitHub?上提供了本文的完整源代碼。
泛型是什么?
考慮以下場景:您希望開發一個用于在應用中傳遞對象的容器。但對象類型并不總是相同。因此,需要開發一個能夠存儲各種類型對象的容器。
鑒于這種情況,要實現此目標,顯然最好的辦法是開發一個能夠存儲和檢索?Object?類型本身的容器,然后在將該對象用于各種類型時進行類型轉換。清單 1 中的類演示了如何開發此類容器。
public class ObjectContainer {private Object obj;/*** @return the obj*/public Object getObj() {return obj;}/*** @param obj the obj to set*/public void setObj(Object obj) {this.obj = obj;}}清單 1
雖然這個容器會達到預期效果,但就我們的目的而言,它并不是最合適的解決方案。它不是類型安全的,并且要求在檢索封裝對象時使用顯式類型轉換,因此有可能引發異常。清單 2 中的代碼演示如何使用該容器存儲和檢索值。
ObjectContainer myObj = new ObjectContainer();// store a string myObj.setObj("Test"); System.out.println("Value of myObj:" + myObj.getObj()); // store an int (which is autoboxed to an Integer object) myObj.setObj(3); System.out.println("Value of myObj:" + myObj.getObj());List objectList = new ArrayList(); objectList.add(myObj); // We have to cast and must cast the correct type to avoid ClassCastException! String myStr = (String) ((ObjectContainer)objectList.get(0)).getObj(); System.out.println("myStr: " + myStr);清單 2
可以使用泛型開發一個更好的解決方案,在實例化時為所使用的容器分配一個類型,也稱泛型類型,這樣就可以創建一個對象來存儲所分配類型的對象。泛型類型是一種類型參數化的類或接口,這意味著可以通過執行泛型類型調用?分配一個類型,將用分配的具體類型替換泛型類型。然后,所分配的類型將用于限制容器內使用的值,這樣就無需進行類型轉換,還可以在編譯時提供更強的類型檢查。
清單 3 中的類演示了如何創建與先前創建的容器相同的容器,但這次使用泛型類型參數,而不是?Object?類型。
public class GenericContainer<T> {private T obj;public GenericContainer(){}// Pass type in as parameter to constructorpublic GenericContainer(T t){obj = t;}/*** @return the obj*/public T getObj() {return obj;}/*** @param obj the obj to set*/public void setObj(T t) {obj = t;} }清單 3
最顯著的差異是類定義包含?<T>,類字段?obj?不再是?Object?類型,而是泛型類型?T。類定義中的尖括號之間是類型參數部分,介紹類中將要使用的類型參數(或多個參數)。T?是與此類中定義的泛型類型關聯的參數。
要使用泛型容器,必須在實例化時使用尖括號表示法指定容器類型。因此,以下代碼將實例化一個?Integer?類型的?GenericContainer,并將其分配給?myInt?字段。
GenericContainer<Integer> myInt = new GenericContainer<Integer>();如果我們嘗試在已經實例化的容器中存儲其他類型的對象,代碼將無法編譯:
myInt.setObj(3); // OK myInt.setObj("Int"); // Won't Compile使用泛型的好處
上面的示例已經演示了使用泛型的一些好處。一個最重要的好處是更強的類型檢查,因為避開運行時可能引發的?ClassCastException?可以節省時間。
另一個好處是消除了類型轉換,這意味著可以用更少的代碼,因為編譯器確切知道集合中存儲的是何種類型。例如,在清單 4 所示代碼中,我們來看看將?Object?容器實例存儲到集合中與存儲?GenericContainer?實例之間的差異。
List myObjList = new ArrayList();// Store instances of ObjectContainer for(int x=0; x <=10; x++){ObjectContainer myObj = new ObjectContainer();myObj.setObj("Test" + x);myObjList.add(myObj); } // Get the objects we need to cast for(int x=0; x <= myObjList.size()-1; x++){ObjectContainer obj = (ObjectContainer) myObjList.get(x); System.out.println("Object Value: " + obj.getObj()); }List<GenericContainer> genericList = new ArrayList<GenericContainer>();// Store instances of GenericContainer for(int x=0; x <=10; x++){GenericContainer<String> myGeneric = new GenericContainer<String>();myGeneric.setObj(" Generic Test" + x);genericList.add(myGeneric); } // Get the objects; no need to cast to Stringfor(GenericContainer<String> obj:genericList){String objectString = obj.getObj();// Do something with the string...here we will print itSystem.out.println(objectString); }清單 4
注意,使用?ArrayList?時,我們可以使用括號表示法 (<GenericContainer>) 在創建時指定集合類型,指明我們將存儲?GenericContainer?實例。該集合將只能存儲?GenericContainer?實例(或?GenericContainer?的子類),無需在從集合檢索對象時使用顯式類型轉換。
將泛型與 Collections API 結合使用的概念讓我們能獲得泛型提供的另外一個好處:允許開發可根據手頭的任務定制的泛型算法。Collections API 本身是使用泛型開發的,如果不使用,Collections API 將永遠無法容納參數化類型。
分析泛型
以下各節將探討泛型的更多特性。
如何使用泛型?
泛型有許多不同用例。本文的第一個示例介紹了生成泛型對象類型的用例。這對于在類和接口層面了解泛型語法是個很好的起點。研究下代碼,類簽名包含一個類型參數部分,包括在類名后的尖括號 (< >) 內,例如:
public class GenericContainer<T> { ...類型參數(又稱類型變量)用作占位符,指示在運行時為類分配類型。根據需要,可能有一個或多個類型參數,并且可以用于整個類。根據慣例,類型參數是單個大寫字母,該字母用于指示所定義的參數類型。下面列出每個用例的標準類型參數:
- E:元素
- K:鍵
- N:數字
- T:類型
- V:值
- S、U、V?等:多參數情況中的第 2、3、4 個類型
在上面的示例中,T?指示將分配的類型,因此可在實例化時為?GenericContainer?分配任何有效類型。注意,T?參數用于整個類,指示實例化時指定的類型。使用下面這行代碼實例化對象時,將用?String?類型替換所有?T?參數:
GenericContainer<String> stringContainer = new GenericContainer<String>();泛型也可用于構造函數中,傳遞類域初始化所需的類型參數。GenericContainer?的構造函數允許在實例化時傳遞任意類型:
GenericContainer gc1 = new GenericContainer(3); GenericContainer gc2 = new GenericContainer("Hello");注意,未分配類型的泛型稱為原始類型。例如,要創建原始類型的?GenericContainer,可以使用以下代碼:
GenericContainer rawContainer = new GenericContainer();原始類型有時對于實現向后兼容很有用,但并不適用于日常代碼。原始類型在編譯時無需執行類型檢查,導致代碼在運行時易于出錯。
多種泛型類型
有時,能夠在類或接口中使用多種泛型類型很有幫助。通過在尖括號之間放置一個逗號分隔的類型列表,可在類或接口中使用多個類型參數。清單 5 中的類使用一個接受以下兩種類型的類演示了此概念:T?和?S。
如果我們回顧上一節中列出的標準類型命名約定,T?是第一種類型的標準標識符,S?是第二種類型的標準標識符。使用這兩種類型生成一個使用泛型存儲多個值的容器。
public class MultiGenericContainer<T, S> {private T firstPosition;private S secondPosition;public MultiGenericContainer(T firstPosition, S secondPosition){this.firstPosition = firstPosition;this.secondPosition = secondPosition;}public T getFirstPosition(){return firstPosition;}public void setFirstPosition(T firstPosition){this.firstPosition = firstPosition;}public S getSecondPosition(){return secondPosition;}public void setSecondPosition(S secondPosition){this.secondPosition = secondPosition;}}清單 5
MultiGenericContainer?類可用于存儲兩個不同對象,每個對象的類型可在實例化時指定。容器的用法如清單 6 所示。
MultiGenericContainer<String, String> mondayWeather =new MultiGenericContainer<String, String>("Monday", "Sunny"); MultiGenericContainer<Integer, Double> dayOfWeekDegrees = new MultiGenericContainer<Integer, Double>(1, 78.0);String mondayForecast = mondayWeather.getFirstPosition(); // The Double type is unboxed--to double, in this case. More on this in next section! double sundayDegrees = dayOfWeekDegrees.getSecondPosition();清單 6
類型推斷和尖括號運算符
如前所述,泛型無需進行類型轉換。例如,使用清單 5 中所示的?MultiGenericContainer?示例,如果調用?getFirstPosition()?或?getSecondPosition(),用于存儲結果的字段必須與容器中該位置存儲的對象的類型相同。
在清單 7 所示的示例中,我們看到實例化時分配給該容器的類型在檢索值時無需進行類型轉換。
MultiGenericContainer<String, String> mondayWeather =new MultiGenericContainer<String, String>("Monday", "Sunny"); MultiGenericContainer<Integer, Double> dayOfWeekDegrees = new MultiGenericContainer<Integer, Double>(1, 78.0); String mondayForecast = mondayWeather.getFirstPosition(); // Works fine with String // The following generates "Incompatible types" error and won't compile int mondayOutlook = mondayWeather.getSecondPosition(); double sundayDegrees = dayOfWeekDegrees.getSecondPosition(); // Unboxing occurs清單 7
考慮清單 7 中的第三行代碼,由于?getSecondPosition()?的結果存儲到?double?類型的字段中,因此無需進行類型轉換。MultiGenericContainer?是用?MultiGenericContainer<String, Double>?實例化的,這怎么可能呢?借助將引用類型自動轉換為原始類型的拆箱?操作,即可實現。同樣,通過構造函數存儲值時,使用自動裝箱?操作將原始類型的?double?值存儲為?Double?引用類型。
注:無法將原始類型用于泛型;只能使用引用類型。自動裝箱和拆箱操作能夠在使用泛型對象時將值存儲為原始類型并檢索原始類型的值。
類型引用可以在分配?getFirstPosition()?或?getSecondPosition()?調用結果時避免顯式類型轉換。根據 Oracle 文檔,類型引用?是 Java 編譯器的一項功能,可查看每種方法調用和對應的聲明,從而確定支持調用的類型參數。換言之,編譯器根據對象實例化過程中分配的類型確定可以使用的類型,在本例中,為?<String, String>?和?<Integer, Double>。引用算法嘗試找到適用于所有參數的最特定的類型。
看看?MuliGenericContainer?的實例化,也可以使用類型引用避免重復類型聲明。不必指定對象類型兩次,只要編譯器可以從上下文推斷類型,即可以指定尖括號運算符?<>。因此,可以在實例化對象時使用尖括號運算符,如清單 8 可見。
MultiGenericContainer<String, String> mondayWeather =new MultiGenericContainer<>("Monday", "Sunny"); MultiGenericContainer<Integer, Double> dayOfWeekDegrees = new MultiGenericContainer<>(1, 78.0);清單 8
如果使用集成開發環境 (IDE)(如 NetBeans IDE),IDE 將指示何處可以使用類型引用。考慮?MultiGenericContainer?原始實例化;我們兩次指定類型,NetBeans 將顯示指示器和提示,如圖 1 所示。
圖 1. NetBeans 類型引用提示
我的目標是什么?
被稱為目標類型化?的概念允許編譯器推斷泛型調用的類型參數。目標類型是編譯器希望的數據類型,具體取決于用于實例化泛型對象的類型、表達式出現的位置等因素。
在下面的代碼行中,值的目標類型是?Double,因為?getSecondPosition()?方法返回?S?類型的值,其中?S?在本例中為?Double。如前所述,由于拆箱操作,我們能夠將調用的值分配給?double?類型的基元。
double sundayDegrees = dayOfWeekDegrees.getSecondPosition();有界類型
我們經常會遇到這種情況,需要指定泛型類型,但希望控制可以指定的類型,而非不加限制。有界類型?在類型參數部分指定?extends?或?super?關鍵字,分別用上限或下限限制類型,從而限制泛型類型的邊界。例如,如果希望將某類型限制為特定類型或特定類型的子類型,請使用以下表示法:
<T extends UpperBoundType>同樣,如果希望將某個類型限制為特定類型或特定類型的超類型,請使用以下表示法:
<T super LowerBoundType>在清單 9 的示例中,我們用先前使用的?GenericContainer?類,通過指定一個上限,將其泛型類型限制為?Number?或?Number?的子類。注意,GenericNumberContainer?這個新類指定泛型類型必須擴展?Number?類型。
public class GenericNumberContainer <T extends Number> {private T obj;public GenericNumberContainer(){}public GenericNumberContainer(T t){obj = t;}/*** @returnthe obj*/public T getObj() {return obj;}/*** @param obj the obj to set*/public void setObj(T t) {obj = t;} }清單 9
該類可以很好地將其字段類型限制為?Number,但如果您嘗試指定一個不在邊界內的類型(如清單 10 所示),將引發編譯器錯誤。
GenericNumberContainer<Integer> gn = new GenericNumberContainer<Integer>(); gn.setObj(3);// Type argument String is not within the upper bounds of type variable T GenericNumberContainer<String> gn2 = new GenericNumberContainer<String>();清單 10
泛型方法
有時,我們可能不知道傳入方法的參數類型。在方法級別應用泛型可以解決此類問題。方法參數可以包含泛型類型,方法也可以包含泛型返回類型。
假設我們要開發一個接受?Number?類型的計算器類。泛型可用于確保可將任何?Number?類型作為參數傳遞給此類的計算方法。例如,清單 11 中的?add()?方法演示了如何使用泛型限制兩個參數的類型,確保其包含?Number?的上限:
public static <N extends Number> double add(N a, N b){double sum = 0;sum = a.doubleValue() + b.doubleValue();return sum; }清單 11
通過將類型限制為?Number,您可以將?Number?子類的任何對象作為參數傳遞。此外,通過將類型限制為?Number,我們還可以確保傳遞給該方法的任何參數將包含?doubleValue()?方法。要查看實際效果,如果您想添加一個?Integer?和一個?Float,可以按如下所示調用該方法:
double genericValue1 = Calculator.add(3, 3f);通配符
某些情況下,編寫指定未知類型的代碼很有用。問號 (?) 通配符可用于使用泛型代碼表示未知類型。通配符可用于參數、字段、局部變量和返回類型。但最好不要在返回類型中使用通配符,因為確切知道方法返回的類型更安全。
假設我們想編寫一個方法來驗證指定的?List?中是否存在指定的對象。我們希望該方法接受兩個參數:一個是未知類型的?List,另一個是任意類型的對象。參見清單 12。
public static <T> void checkList(List<?> myList, T obj){if(myList.contains(obj)){System.out.println("The list contains the element: " + obj);} else {System.out.println("The list does not contain the element: " + obj);}}清單 12
清單 13 中的代碼演示如何利用此方法。
// Create List of type Integer List<Integer> intList = new ArrayList<Integer>(); intList.add(2); intList.add(4); intList.add(6);// Create List of type String List<String> strList = new ArrayList<String>(); strList.add("two"); strList.add("four"); strList.add("six");// Create List of type Object List<Object> objList = new ArrayList<Object>(); objList.add("two"); objList.add("four"); objList.add(strList);checkList(intList, 3); // Output: The list [2, 4, 6] does not contain the element: 3checkList(objList, strList); /* Output: The list [two, four, [two, four, six]] contains the element: [two, four, six] */checkList(strList, objList); /* Output: The list [two, four, six] does not contain the element: [two, four, [two, four, six]] */清單 13
有時要使用上限或下限限制通配符。與指定帶邊界的泛型類型極其相似,指定?extends?或?super?關鍵字加上通配符,后面跟用于上限或下限的類型,即可聲明帶邊界的通配符類型。例如,如果我們要更改?checkList?方法使其只接受擴展?Number?類型的?List,可按清單 14 所示編寫代碼。
public static <T> void checkNumber(List<? extends Number> myList, T obj){if(myList.contains(obj)){System.out.println("The list " + myList + " contains the element: " + obj);} else {System.out.println("The list " + myList + " does not contain the element: " + obj);} }清單 14
在 Java SE 8 構造中使用泛型
我們已經看到了泛型的用法和重要性。現在,我們來看看泛型在 Java SE 8 中的新構造 lambda 表達式的用例。Lambda 表達式表示一個匿名函數,它實現函數接口的單一抽象方法。有許多函數接口可供使用,其中許多利用了泛型。我們來看一個示例。
假設我們要遍歷書名 (String) 列表,比較書名,這樣我們可以返回包含指定搜索詞的所有書名。為此,我們可以開發一個方法,它有兩個參數:書名列表和用于執行比較的謂詞。Predicate?函數接口可用于比較,返回一個?boolean,指示給定對象是否滿足測試要求。Predicate?接口可用于所有類型的對象,因為它有以下泛型簽名:
@FunctionalInterface public interface Predicate<T>{ ... }如果我們要遍歷每個書名,查找包含文本“Java EE”的書名,可以傳遞?contains("Java EE")?作為謂詞參數。清單 15 所示方法可用于遍歷給定的書名列表,并應用這樣的謂詞打印那些匹配的書名。在這種情況下,接受的參數使用泛型指示?String?的?List,并使用一個謂詞測試每個?String。
public static void compareStrings(List<String> list, Predicate<String> predicate) {list.stream().filter((n) -> (predicate.test(n))).forEach((n) -> {System.out.println(n + " ");}); }清單 15
清單 16 中的代碼可用于填充書名列表,然后打印所有包含文本“Java EE”的書名。
List<String> bookList = new ArrayList<>(); bookList.add("Java 8 Recipes"); bookList.add("Java EE 7 Recipes"); bookList.add("Introducing Java EE 7"); bookList.add("JavaFX 8: Introduction By Example"); compareStrings(bookList, (n)->n.contains("Java EE"));清單 16
更進一步
我們已經看到了如何通過引用類型使用泛型,了解泛型在使用應用特定的類型中的實際應用可能很有幫助。本文的完整源代碼包括了咖啡店應用的源代碼。
咖啡店的示例使用泛型處理咖啡店出售的各種不同口味的咖啡,每種口味用一種不同的 Java 類型。在該場景中,客戶將購買各種袋裝或杯裝咖啡,我們需要分解購買細節,以確定不同咖啡類型的數量,這樣我們就可以更新店內庫存信息,更多地了解客戶。
該應用利用包含泛型類型的方法執行一些任務。清單 17 中的代碼演示了一個示例,用于計算一次購買中所含的咖啡類型數目,其中?purchase?表示所有咖啡銷售的列表。
public <T> long countTypes(T coffeeType) {long count = purchase.stream().filter((sale) -> (sale.getType().getType().equals(coffeeType))).count();return count; }清單 17
此方法返回給定購買的指定咖啡類型的計數。為了有效執行此任務,該方法接受一個泛型類型參數,這意味著可將任意對象傳遞給該方法。然后該方法將搜索購買列表,查看其中包含了多少次給定類型的購買。
由于泛型方法應引入自己的類型參數,該參數的范圍限于該方法的主體。類型參數必須出現在方法的返回類型之前。在?countTypes?的情況下,只用一個?<T>?表示泛型類型。
如前所述,可以使用有界類型限制可為泛型類型指定的類型。如果您查看?GitHub 上的代碼中的?JavaHouse?類中的?addToPurchase()?方法,將看到它接受一個泛型?List。
這種情況下,List?必須包含擴展?CoffeeSaleType?的元素,因此?CoffeeSaleType?是上限。換句話說,只能使用擴展?CoffeeSaleType?的對象列表作為此方法參數。參見清單 18。
public <T extends CoffeeSaleType> void addToPurchase(List<T> saleList) {for (CoffeeSaleType sale : saleList) {purchase.add(sale);} }清單 18
咖啡店示例包括各種泛型實現。要模擬咖啡店的購買交易,請執行?JavaHouseVisit?類,了解?main?方法中調用的每種方法。
總結
有了泛型,我們可以使用更強的類型檢查、無需進行類型轉換,并且能夠開發泛型算法。沒有泛型,我們今天在 Java 中使用的許多特性都不可能實現。
在本文中,我們看到了一些基本示例,展示如何使用泛型實現一個可提供強類型檢查和類型靈活性的解決方案。我們還看到泛型在算法中所起的重要作用,以及泛型在用于實現 lambda 表達式的 Collections API 和函數接口中起到的重要作用。
本文只是介紹了泛型的一點皮毛,若想深入了解,有許多在線資源可供參考。我建議您下載本文源代碼,通過使用了解更多有關泛型的信息,以及如何在自己的解決方案中使用它們。
另請參見
- Java 教程?泛型課程
- NetBeans IDE
關于作者
Josh Juneau 擔任應用開發人員、系統分析師和數據庫管理員。他主要使用 Java 和其他 Java 虛擬機 (JVM) 語言開發。他是 Oracle 技術網和?Java Magazine?的技術作家,與人合著了《The Definitive Guide to Jython》和《PL/SQL Recipes》(均為 Apress,2010)和《Java 7 Recipes》(Apress,2011)。Josh 最近撰寫了《Java EE 7 Recipes》和《Introducing Java EE 7》(均為 Apress,2013),他目前正在寫一本 Apress 的書《Java 8 Recipes》,將于今年晚些時候出版。
from:?http://www.oracle.com/technetwork/cn/articles/java/juneau-generics-2255374-zhs.html
總結
以上是生活随笔為你收集整理的泛型:工作原理及其重要性的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java深度历险(五)——Java泛型
- 下一篇: java线程池使用