文章目錄
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);
}
}
如上所示, 其中 <T>
是不能省略的。 而且可以是多種類型, 如 <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>
轉換成 <T extends Comparable<T>>
即可。
測試
@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<Intager>
不能賦值給 Box<Number>
(即兩個不是子類或父類的關係)。
可以使用下圖來進行闡釋
注意:
對於給定的具體類型 A 和 B(如 Number 和 Integer),
MyClass<A>
與MyClass<B>
沒有任何的關係, 不管 A 和 B 之間是否有關係。
4.2 泛型和子類型
在 Java 中, 我們可以通過繼承或實現來獲得一個子類型。 以 Collection
爲例
由於 ArrayList<E></code> 實現了
List, 而 List<E>
繼承了Collection<E>
。 因此, 只要類型參數沒有更改(如都是 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<String>
的實例, 那麼, 該數據類型就是目標類型。 由於 emptyList
的返回值是 List<T>
, 因此, 編譯器就推斷, T
對應的實際類型就是 String
。
當然, 我們也可以顯示的指定該類型參數
List<String> listOne = Collections.<String>emptyList();
6 通配符
在泛型中, 使用 ?
作爲通配符, 其代表的是未知的類型。
6.1 設定通配符的下限
有時候, 我們想寫一個方法, 它可以傳遞 List<Integer>
, List<Double>
和List<Number>
。 此時, 可以使用通配符來幫助我們了。
設定通配符的上限
使用?
, 其後跟隨着 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<Integer>
, List<Double>
和List<Number>
作爲參數傳遞到同一個函數中。
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<T>
, 因爲絕大部分方法都不依賴於具體的類型。
那麼, 爲什麼不使用 List<Object>
進行替代呢?
public static void printList(List<Object> list) {
for (Object elem : list)
System.out.println(elem + " ");
System.out.println();
}
在以上的方法中, 我們想帶引出列表的各項。 但是以上的函數只能輸出的是 Object
的實例(我們只能傳入List<Object>
, 而不是 List<Interger>
等, 因爲不是子類和父類的關係)。
而更改爲通配符之後
public static void printList(List<?> list) {
for (Object elem: list)
System.out.print(elem + " ");
System.out.println();
}
我們可以傳入任意的 List
.
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));
printList(integers);
printList(doubles);
printList(numbers);
}
以上的代碼運行正常。
6.4 通配符和子類型
在泛型和子類型中, 我們論證了
對於給定的具體類型 A 和 B(如 Number 和 Integer),
MyClass<A>
與MyClass<B>
沒有任何的關係, 不管 A 和 B 之間是否有關係
但是, 通配符可以在類或接口之間創建關係。 實現了子類和父類的關係。 因爲 Integer
是Number
的子類, 因此, 可以有如下的關係。
正因爲如此, 我們在前面進行參數傳遞時, 纔可以進行多種類型參數的傳遞。
6.5 通配符捕獲
我們想編寫一個方法, 該方法
public class WildcardError {
void foo(List<?> i) {
? t = i.get(0); // 錯誤
i.set(0, t);
}
}
我們需要取得傳入的類型, 但是, 在編寫時, 不能使用 “?” 來作爲一種類型。 此時, 我們可以使用類型捕獲來解決幹問題。
public class WildcardError {
void foo(List<?> i) {
fooHelper(i);
}
private <T> void fooHelper(List<T> l) {
T t = l.get(0); // 錯誤
l.set(0, t);
}
}
在此過程中, fooHelper 是泛型方法, 而 foo 方法不是, 它具有固定類型的參數。 在此情況下, T 捕獲通配符。 它不知道具體的類型是哪一個, 但是, 這是一個明確的類型。
慣例上, helper 方法, 被命名爲 xxxHelper。
7 類型擦除
爲了實現泛型, 編譯器使用類型擦除:
- 替換所有的類型爲其邊界類或 沒有邊界則爲
Object
。 因此, 其所產生的字節碼, 僅僅 包含的是原始的類,接口, 方法。 - 在必要的地方插入類型轉換以保證類型安全
- 生成橋接方法以保留擴展泛型類型的多態。
也就是說, 經過編譯之後, 任何的類型都會被擦除。 因此, List<Integer>
和List<String>
在運行時是一樣的類型, 進行類型擦除之後, 都是 List
。
7.1 類型擦除
定義一個泛型類
public class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
public static void main(String[] args) {
Node<String> node = new Node<>("11", null);
System.out.println(node.getData());
}
}
對其進行反編譯, 可以獲得:
public class Node
{
private Object data;
private Node next;
public Node(Object data, Node next)
{
this.data = data;
this.next = next;
}
public Object getData()
{
return data;
}
public static void main(String args[])
{
Node node = new Node("11", null);
System.out.println((String)node.getData());
}
}
可以看到, 類型已經被替換成 Object, 然後在 main 方法中, 將 Object 轉換爲 String, 因爲我們傳入的是 String 類型。
同理, 將
public class Node<T> {
替換爲
public class Node<T extends Serializable> {
則, 反編譯後, 替換 T 爲邊界類型
public class Node
{
private Serializable data;
private Node next;
public Node(Serializable data, Node next)
{
this.data = data;
this.next = next;
}
public Serializable getData()
{
return data;
}
public static void main(String args[])
{
Node node = new Node("11", null);
System.out.println((String)node.getData());
}
}
方法的類型擦除也是一樣的。
7.2 類型擦除和橋接方法
正因爲有類型擦除的存在, 因此, 任何在運行時需要知道確切類型信息的操作都無法工作。
有時候也會導致一些我們無法預料到的情況。
在方法的重寫時, 我們會遇到這樣的情況
聲明一個泛型類
public class Node<T> {
public T data;
public Node(T data) { this.data = data; }
public T getData() {
return data;
}
public void setData(T data) {
System.out.println("Node.setData");
this.data = data;
}
}
繼承泛型類, 並指明瞭它的類型爲 Integer
public class MyNode extends Node<Integer> {
public MyNode(Integer data) {
super(data);
}
@Override
public Integer getData() {
return super.getData();
}
@Override
public void setData(Integer data) {
super.setData(data);
}
public static void main(String[] args) {
Class<?> clazz = MyNode.class;
for (Method m:
clazz.getDeclaredMethods()) {
System.out.println(m + ":" + m.isBridge());
}
}
}
那麼, 這個時候, 由於類型擦除,Node
類變成了這樣子
public class Node
{
public Object data;
public Node(Object data)
{
this.data = data;
}
public Object getData()
{
return data;
}
public void setData(Object data)
{
System.out.println("Node.setData");
this.data = data;
}
}
那麼問題就出現了。 如果沒有任何的情況, 對於 setData 方法來說, 在父類中
public void setData(Object data)
{
System.out.println("Node.setData");
this.data = data;
}
在子類中
public void setData(Integer data) {
super.setData(data);
}
顯然, 這兩個方法並不是重寫的關係。
爲了解決這個問題, 以便在泛型擦除之後保持多態性, 編譯器會產生橋接方法, 以保證子類運行時正確的。
生成的橋接方法:
public volatile void setData(Object obj){
setData((Integer)obj);
}
先寫到這吧, 後面在繼續深入。已經太長了!