扒一拔:Java 中的泛型(一)
目錄
@
1 泛型
泛型的本質是引數化型別,也就是說所操作的資料型別被指定為一個引數。這種引數型別可以用在類、介面和方法的建立中,分別稱為泛型類、泛型介面、泛型方法。
1.1 為什麼需要泛型
泛型是JDK1.5才出來的, 在泛型沒出來之前, 我們可以看看集合框架中的類都是怎麼樣的。
以下為JDK1.4.2的 HashMap
可以看到, 在該版本中, 引數和返回值(引用型別)的都是 Object
物件。 而在 Java 中, 所有的類都是 Object
子類, 實用時, 可能需要進行強制型別轉換。 這種轉換在編譯階段並不會提示有什麼錯誤, 因此, 在使用時, 難免會出錯。
而有了泛型之後, HashMap
通過泛型, 我們可以傳入相同的引數又能返回相同的引數, 由編譯器為我們來進行這些檢查。
這樣可以減少很多無關程式碼的書寫。
因此, 泛型可以使得型別引數化, 泛型有如下的好處
- 型別引數化, 實現程式碼的複用
- 強制型別檢查, 保證了型別安全,可以在編譯時就發現程式碼問題, 而不是到在執行時才發現錯誤
- 不需要進行強制轉換。
1.2 型別引數命名規約
按照慣例,型別引數名稱是單個大寫字母。 通過規約, 我們可以容易區分出型別變數和普通類、介面。
- E - 元素
- T - 型別
- N - 數字
- K - 鍵
- V - 值
- S,U,V - 第2種類型, 第3種類型, 第4種類型
2 泛型的簡單實用
2.1 最基本最常用
最早接觸的泛型, 應該就是集合框架中的泛型了。
List<Integer> list = new ArrayList<Integer>();
list.add(100086); //OK
list.add("Number"); //編譯錯誤
在以上的例子中, 將 String
加入時, 會提示錯誤。 編譯器不會編譯通過, 從而保證了型別安全。
2.2 簡單泛型類
2.2.1 非泛型類
先來定義一個簡單的類
public class SimpleClass {
private Object obj;
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
}
這麼寫是沒問題的。 但是在使用上可能出現如下的錯誤:
public static void main(String[] args) {
SimpleClass simpleClass = new SimpleClass();
simpleClass.setObj("ABC");// 傳入 String 型別
Integer a = (Integer) simpleClass.getObj(); // Integer 型別接受
}
以上寫是不會報錯的, 但是在執行時會出現報錯
java.lang.ClassCastException
如果是一個人使用, 那確實有可能會避免類似的情況。 但是, 如果是多人使用, 則你不能保證別人的用法是對的。 其存在著隱患。
2.2.2 泛型類的定義
我們可以使用泛型來強制型別限定
public class GenericClass<T> {
private T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
}
2.2.3 泛型類的使用
在使用時, 在類的後面, 使用尖括號指明引數的型別就可以
@Test
public void testGenericClass(){
GenericClass<String> genericClass = new GenericClass<>();
genericClass.setObj("AACC");
/* Integer str = genericClass.getObj();//*/
}
如果型別不符, 則編譯器會幫我們發現錯誤, 導致編譯不通過。
2.3 簡單泛型介面
2.3.1 定義
與類相似, 以 JDK 中的 Comparable
介面為例
package java.lang;
import java.util.*;
public interface Comparable<T> {
public int compareTo(T o);
}
2.3.2 實現
在實現時, 指定具體的引數型別即可。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
...
public int compareTo(String anotherString) {
byte v1[] = value;
byte v2[] = anotherString.value;
if (coder() == anotherString.coder()) {
return isLatin1() ? StringLatin1.compareTo(v1, v2)
: StringUTF16.compareTo(v1, v2);
}
return isLatin1() ? StringLatin1.compareToUTF16(v1, v2)
: StringUTF16.compareToLatin1(v1, v2);
}
...
}
2.4 簡單泛型方法
泛型方法可以引入自己的引數型別, 如同宣告泛型類一樣, 但是其型別引數我的範圍只是在宣告的方法本身。 靜態方法和非靜態方法, 以及建構函式都可以使用泛型。
2.4.1 泛型方法宣告
泛型方法的宣告, 型別變數放在修飾符之後, 在返回值之前
public class EqualMethodClass {
public static <T> boolean equals(T t1, T t2){
return t1.equals(t2);
}
}
如上所示, 其中 是不能省略的。 而且可以是多種型別, 如
<K, V>
public class Util {
public static <K, V> boolean sameType(K k, V v) {
return k.getClass().equals(v.getClass());
}
}
2.4.2 泛型方法的呼叫
呼叫時, 在方法之前指定引數的型別
@Test
public void equalsMethod(){
boolean same = EqualMethodClass.<Integer>equals(1,1);
System.out.println(same);
}
3 型別變數邊界
3.1 定義
如果我們需要指定型別是某個類(介面)的子類(介面)
<T extends BundingType>
使用 extends
, 表示 T
是 BundingType
的子類, 兩者都可以是類或介面。
此處的 extends
和繼承中的是不一樣的。
如果有多個邊界限定:
<T extends Number & Comparable>
使用的是 &
符號。
注意事項
如果邊界型別中有類, 則類必須是放在第一個
也就是說
<T extends Comparable & Number> // 編譯錯誤
會報錯
3.2 示例
有時, 我們需要對型別進行一些限定, 比如說, 我們要獲取陣列的最小元素
public class ArrayUtils {
public static <T> T min(T[] arr) {
if (arr == null || arr.length == 0) {
return null;
}
T smallest = arr[0];
for (int i = 0; i < arr.length; i++) {
if (smallest.compareTo(arr[i]) > 0) {
smallest = arr[i];
}
}
return smallest;
}
}
上面的是報錯的。 因為, 在該函式中, 我們需要使用 compareTo
函式, 但是, 並不是所欲的類都有這個函式的。 因此, 我們可以這樣子限定
將 轉換成
<T extends Comparable
即可。
測試
@Test
public void testMin() {
Integer a[] = {1, 4, 5, 6, 0, 2, -1};
Assertions.assertEquals(ArrayUtils.<Integer>min(a), Integer.valueOf(-1));
}
4 泛型, 繼承和子型別
4.1 泛型和繼承
在 Java 繼承中, 如果變數 A 是 變數 B 的子類, 則我們可以將 A 賦值給 B。 但是, 在泛型中則不能進行類似的賦值。
對繼承來說, 我們可以這樣做
public class Box<T> {
List<T> boxs = new ArrayList<>();
public void add(T element) {
boxs.add(element);
}
public static void main(String[] args) {
Box<Number> box = new Box<Number>();
box.add(new Integer(10)); // OK
box.add(new Double(10.1)); // OK
}
}
但是, 在泛型中, Box
不能賦值給 Box
(即兩個不是子類或父類的關係)。
可以使用下圖來進行闡釋
注意:
對於給定的具體型別 A 和 B(如 Number 和 Integer),
MyClass
與MyClass
沒有任何的關係, 不管 A 和 B 之間是否有關係。
4.2 泛型和子型別
在 Java 中, 我們可以通過繼承或實現來獲得一個子型別。 以 Collection
為例
由於 ArrayList
實現了 List
, 而 List
繼承了Collection
。 因此, 只要型別引數沒有更改(如都是 String 或 都是 Integer), 則型別之間子父類關係會一直保留。
5 型別推斷
型別推斷並不是什麼高大上的東西, 我們日常中其實一直在用到。它是 Java 編譯器的能力, 其檢視每個方法呼叫和相應宣告來決定型別引數, 以便呼叫時相容。
值得注意的是, 型別推斷演算法僅僅是在呼叫引數, 目標型別和明顯的預期返回型別時使用。
5.1 型別推斷和泛型方法
在下面的泛型方法中
public class Box<T> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
public class BoxDemo {
public static <U> void addBox(U u,
List<Box<U>> boxes) {
Box<U> box = new Box<>();
box.set(u);
boxes.add(box);
}
public static <U> void outputBoxes(List<Box<U>> boxes) {
int counter = 0;
for (Box<U> box: boxes) {
U boxContents = box.get();
System.out.println("Box #" + counter + " contains [" +
boxContents.toString() + "]");
counter++;
}
}
public static void main(String[] args) {
ArrayList<Box<Integer>> listOfIntegerBoxes =
new ArrayList<>();
BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes);
BoxDemo.outputBoxes(listOfIntegerBoxes);
}
}
輸出
Box #0 contains [10]
Box #1 contains [20]
Box #2 contains [30]
我們可以看到, 泛型方法 addBox 中定義了一個型別引數 U, 在泛型方法的呼叫時, Java 編譯器可以推斷出該型別引數。 因此, 很多時候, 我們不需要指定他們。
如上面的例子, 我們可以顯示的指出
BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
也可以省略, 這樣, Java 編譯器可以從方法引數中推斷出
BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
由於方法引數是 Integer, 因此, 可以推斷出型別引數就是 Integer。
5.2 泛型類的型別推斷和例項化
這是我們最常用到的型別推斷了: 將建構函式中的型別引數替換成<>
(該符號被稱為“菱形(The diamond)”), 編譯器可以從上下文中推斷出該型別引數。
比如說, 正常情況先, 我們是這樣子宣告的
Map<String, List<String>> myMap = new HashMap<String, List<String>>();
但是, 實際上, 建構函式的型別引數是可以推斷出來的。 因此, 這樣子寫即可
Map<String, List<String>> myMap = new HashMap<>();
但是, 不能將 <>
去掉, 否則編譯器會報警告。
Map<String, List<String>> myMap = new HashMap(); // 警告
5.3 類的型別推斷和建構函式
在泛型類和非泛型類中, 建構函式都是可以宣告自己的型別引數的。
class MyClass<X> {
<T> MyClass(T t) {
// ...
}
public static void main(String[] args) {
MyClass<Integer> myObject = new MyClass<>("");
}
}
在以上程式碼 main 函式中, X
對應的型別是 Integer
, 而 T
對應的型別是 String
。
那麼, 菱形 <>
對應的是 X
還是 T
呢?
在 Java SE 7 之前, 其對應的是建構函式的型別引數。 而在 Java SE 7及以後, 其對應的是類的型別引數。
也就是說, 如果類不是泛型, 則程式碼是這樣子寫的
class MyClass{
<T> MyClass(T t) {
// ...
}
public static void main(String[] args) {
MyClass myObject = new MyClass("");
}
}
T
的實際型別, 編譯器根據方法的引數推斷出來。
5.4 型別推斷和目標型別
Java 編譯器利用目標型別來推斷泛型方法呼叫的型別引數。 表示式的目標型別就是 Java 編譯器所期望的資料型別, 根據該資料型別, 我們可以推斷出泛型方法的型別。
以 Collections
中的方法為例
static <T> List<T> emptyList();
我們在賦值時, 是這樣子
List<String> listOne = Collections.emptyList();
該表示式想要得到 List
的例項, 那麼, 該資料型別就是目標型別。 由於 emptyList
的返回值是 List
, 因此, 編譯器就推斷, T
對應的實際型別就是 String
。
當然, 我們也可以顯示的指定該型別引數
List<String> listOne = Collections.<String>emptyList();
6 萬用字元
在泛型中, 使用 ?
作為萬用字元, 其代表的是未知的型別。
6.1 設定萬用字元的下限
有時候, 我們想寫一個方法, 它可以傳遞 List
, List
和List
。 此時, 可以使用萬用字元來幫助我們了。
設定萬用字元的上限
使用?
, 其後跟隨著 extends
, 再後面是 BundingType
(即上邊界)
<? extends BundingType>
示例
class MyClass{
public static void process(List<? extends Number> list) {
for (Number elem : list) {
System.out.println(elem.getClass().getName());
}
}
public static void main(String[] args) {
List<Integer> integers = new LinkedList<>(Arrays.asList(1));
List<Double> doubles = new LinkedList<>(Arrays.asList(1.0));
List<Number> numbers = new LinkedList<>(Arrays.asList(1));
process(integers);
process(doubles);
process(numbers);
}
}
輸出
java.lang.Integer
java.lang.Double
java.lang.Integer
也就是說, 我們通過萬用字元, 可以將List
, List
和List
作為引數傳遞到同一個函式中。
6.2 設定萬用字元的下限
上限萬用字元是限定了引數的型別是指定的型別或者是其子類, 使用 extends
來進行。
而下限萬用字元, 使用的是 super
關鍵字, 限定了未知的型別是指定的型別或者其父類。
設定萬用字元的下限
<? super bundingType>
在 ?
後跟著 super
, 在跟上對應的邊界型別。
示例
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}
對於該方法, 由於我們是要將整型新增到列表中, 因此, 需要傳入的列表必須是整型或者其父類。
6.3 未限定的萬用字元
當然, 我們也可以使用未限定的萬用字元。 如List<?>
, 表示未知型別的列表。
使用萬用字元的情景
- 所寫的方法需要使用 Object 類所提供的功能
- 所寫的方法, 不依賴於具體的型別引數。 比較常見的是反射中, 用
Class<?>
而非Class
, 因為絕大部分方法都不依賴於具體的型別。
那麼, 為什麼不使用 List