@
目錄
- 1 泛型
- 1.1 為什麼需要泛型
- 1.2 類型參數命名規約
- 2 泛型的簡單實用
- 2.1 最基本最常用
- 2.2 簡單泛型類
- 2.2.1 非泛型類
- 2.2.2 泛型類的定義
- 2.2.3 泛型類的使用
- 2.3 簡單泛型接口
- 2.3.1 定義
- 2.3.2 實作
- 2.4 簡單泛型方法
- 2.4.1 泛型方法聲明
- 2.4.2 泛型方法的調用
- 3 類型變量邊界
- 3.1 定義
- 3.2 示例
- 4 泛型, 繼承和子類型
- 4.1 泛型和繼承
- 4.2 泛型和子類型
- 5 類型推斷
- 5.1 類型推斷和泛型方法
- 5.2 泛型類的類型推斷和執行個體化
- 5.3 類的類型推斷和構造函數
- 5.4 類型推斷和目标類型
- 6 通配符
- 6.1 設定通配符的下限
- 6.2 設定通配符的下限
- 6.3 未限定的通配符
- 6.4 通配符和子類型
- 6.5 通配符捕獲
- 7 類型擦除
- 7.1 類型擦除
- 7.2 類型擦除和橋接方法
泛型的本質是參數化類型,也就是說所操作的資料類型被指定為一個參數。這種參數類型可以用在類、接口和方法的建立中,分别稱為泛型類、泛型接口、泛型方法。
泛型是JDK1.5才出來的, 在泛型沒出來之前, 我們可以看看集合架構中的類都是怎麼樣的。
以下為JDK1.4.2的
HashMap

可以看到, 在該版本中, 參數和傳回值(引用類型)的都是
Object
對象。 而在 Java 中, 所有的類都是
Object
子類, 實用時, 可能需要進行強制類型轉換。 這種轉換在編譯階段并不會提示有什麼錯誤, 是以, 在使用時, 難免會出錯。
而有了泛型之後,
HashMap
的中使用泛型來進行類型的檢查
通過泛型, 我們可以傳入相同的參數又能傳回相同的參數, 由編譯器為我們來進行這些檢查。
這樣可以減少很多無關代碼的書寫。
是以, 泛型可以使得類型參數化, 泛型有如下的好處
- 類型參數化, 實作代碼的複用
- 強制類型檢查, 保證了類型安全,可以在編譯時就發現代碼問題, 而不是到在運作時才發現錯誤
- 不需要進行強制轉換。
按照慣例,類型參數名稱是單個大寫字母。 通過規約, 我們可以容易區分出類型變量和普通類、接口。
- E - 元素
- T - 類型
- N - 數字
- K - 鍵
- V - 值
- S,U,V - 第2種類型, 第3種類型, 第4種類型
最早接觸的泛型, 應該就是集合架構中的泛型了。
List<Integer> list = new ArrayList<Integer>();
list.add(100086); //OK
list.add("Number"); //編譯錯誤
在以上的例子中, 将
String
加入時, 會提示錯誤。 編譯器不會編譯通過, 進而保證了類型安全。
先來定義一個簡單的類
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
如果是一個人使用, 那确實有可能會避免類似的情況。 但是, 如果是多人使用, 則你不能保證别人的用法是對的。 其存在着隐患。
我們可以使用泛型來強制類型限定
public class GenericClass<T> {
private T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
}
在使用時, 在類的後面, 使用尖括号指明參數的類型就可以
@Test
public void testGenericClass(){
GenericClass<String> genericClass = new GenericClass<>();
genericClass.setObj("AACC");
/* Integer str = genericClass.getObj();//*/
}
如果類型不符, 則編譯器會幫我們發現錯誤, 導緻編譯不通過。
與類相似, 以 JDK 中的
Comparable
接口為例
package java.lang;
import java.util.*;
public interface Comparable<T> {
public int compareTo(T o);
}
在實作時, 指定具體的參數類型即可。
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);
}
...
}
泛型方法可以引入自己的參數類型, 如同聲明泛型類一樣, 但是其類型參數我的範圍隻是在聲明的方法本身。 靜态方法和非靜态方法, 以及構造函數都可以使用泛型。
泛型方法的聲明, 類型變量放在修飾符之後, 在傳回值之前
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());
}
}
調用時, 在方法之前指定參數的類型
@Test
public void equalsMethod(){
boolean same = EqualMethodClass.<Integer>equals(1,1);
System.out.println(same);
}
如果我們需要指定類型是某個類(接口)的子類(接口)
<T extends BundingType>
使用
extends
, 表示
T
是
BundingType
的子類, 兩者都可以是類或接口。
此處的
extends
和繼承中的是不一樣的。
如果有多個邊界限定:
<T extends Number & Comparable>
使用的是
&
符号。
注意事項
如果邊界類型中有類, 則類必須是放在第一個
也就是說
<T extends Comparable & Number> // 編譯錯誤
會報錯
有時, 我們需要對類型進行一些限定, 比如說, 我們要擷取數組的最小元素
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));
}
在 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>
沒有任何的關系, 不管 A 和 B 之間是否有關系。
MyClass<B>
在 Java 中, 我們可以通過繼承或實作來獲得一個子類型。 以
Collection
為例
由于
ArrayList<E></code> 實作了
List, 而
List<E>
繼承了
Collection<E>
。 是以, 隻要類型參數沒有更改(如都是 String 或 都是 Integer), 則類型之間子父類關系會一直保留。
類型推斷并不是什麼高大上的東西, 我們日常中其實一直在用到。它是 Java 編譯器的能力, 其檢視每個方法調用和相應聲明來決定類型參數, 以便調用時相容。
值得注意的是, 類型推斷算法僅僅是在調用參數, 目标類型和明顯的預期傳回類型時使用。
在下面的泛型方法中
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。
這是我們最常用到的類型推斷了: 将構造函數中的類型參數替換成
<>
>(該符号被稱為“菱形(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(); // 警告
在泛型類和非泛型類中, 構造函數都是可以聲明自己的類型參數的。
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
的實際類型, 編譯器根據方法的參數推斷出來。
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();
在泛型中, 使用
?
作為通配符, 其代表的是未知的類型。
有時候, 我們想寫一個方法, 它可以傳遞
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>
作為參數傳遞到同一個函數中。
上限通配符是限定了參數的類型是指定的類型或者是其子類, 使用
extends
來進行。
而下限通配符, 使用的是
super
關鍵字, 限定了未知的類型是指定的類型或者其父類。
設定通配符的下限
<? super bundingType>
在
?
後跟着
super
, 在跟上對應的邊界類型。
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}
對于該方法, 由于我們是要将整型添加到清單中, 是以, 需要傳入的清單必須是整型或者其父類。
當然, 我們也可以使用未限定的通配符。 如
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);
}
以上的代碼運作正常。
在泛型和子類型中, 我們論證了
MyClass<A>
沒有任何的關系, 不管 A 和 B 之間是否有關系
MyClass<B>
但是, 通配符可以在類或接口之間建立關系。 實作了子類和父類的關系。 因為
Integer
Number
的子類, 是以, 可以有如下的關系。
正因為如此, 我們在前面進行參數傳遞時, 才可以進行多種類型參數的傳遞。
我們想編寫一個方法, 該方法
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。
為了實作泛型, 編譯器使用類型擦除:
- 替換所有的類型為其邊界類或 沒有邊界則為
。 是以, 其所産生的位元組碼, 僅僅 包含的是原始的類,接口, 方法。Object
- 在必要的地方插入類型轉換以保證類型安全
- 生成橋接方法以保留擴充泛型類型的多态。
也就是說, 經過編譯之後, 任何的類型都會被擦除。 是以,
List<Integer>
List<String>
在運作時是一樣的類型, 進行類型擦除之後, 都是
List
定義一個泛型類
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());
}
}
方法的類型擦除也是一樣的。
正因為有類型擦除的存在, 是以, 任何在運作時需要知道确切類型資訊的操作都無法工作。
有時候也會導緻一些我們無法預料到的情況。
在方法的重寫時, 我們會遇到這樣的情況
聲明一個泛型類
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);
}
先寫到這吧, 後面在繼續深入。已經太長了!
作者:阿進的寫字台
出處:https://www.cnblogs.com/homejim/
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。