深入解析 Java 泛型的魅力與機制
在 Java 的廣袤編程世界中,有一個特性如同閃耀的星辰,為代碼的編寫帶來了深刻的變革和無盡的優(yōu)勢,它就是泛型。當我們踏入 Java 泛型的領域,就仿佛打開了一扇通往更高層次編程境界的大門。
泛型的出現(xiàn),不僅僅是一種語法上的創(chuàng)新,更是對編程思維和代碼質(zhì)量的一次重大提升。它讓我們在處理各種數(shù)據(jù)類型時能夠更加得心應手,賦予了代碼更強的通用性、安全性和可讀性。無論是構建復雜的數(shù)據(jù)結構,還是實現(xiàn)高效的算法邏輯,泛型都在背后默默發(fā)揮著至關重要的作用。
一、泛型使用示例
1. 泛型接口
接口定義,可以看到我們只需在接口上增加泛型聲明<T>即可,后續(xù)我們在繼承時可以具體指明類型約束實現(xiàn)類,同樣也可以不指明。
/**
* 泛型接口
*
* @param <T>
*/
public interface GeneratorInterface<T> {
T getVal();
}
下面就是不指明類型的實現(xiàn)類,通過這種抽象的方式在后續(xù)的使用時,我們就可以靈活設置類型了。
/**
* 實現(xiàn)泛型接口不指定類型
* @param <T>
*/
public class GeneratorImpl<T> implements GeneratorInterface<T> {
@Override
public T getVal() {
return null;
}
}
就像下面這樣,在創(chuàng)建示例時指明:
public class Main {
public static void main(String[] args) {
GeneratorInterface<String> generatorInterface=new GeneratorImpl<>();
generatorInterface.getVal();
}
}
當然我們也可以在創(chuàng)建這個類時指明:
/**
* 泛型接口指定類型
*/
public class GeneratorImpl2 implements GeneratorInterface<String> {
@Override
public String getVal() {
return null;
}
}
2. 泛型方法
聲明泛型方法的方式很簡單,只需在返回類型前面增加一個<E> 即可:
public class GeneratorMethod {
/**
* 泛型方法
* @param array
* @param <E>
*/
public static <E> void printArray(List<E> array){
for (E e : array) {
System.out.println(e);
}
}
}
如此一來,我們就可以在使用時靈活指定元素類型:
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("11");
list.add("11");
list.add("11");
list.add("11");
list.add("11");
list.add("11");
GeneratorMethod.printArray(list);
}
3. 泛型類
與泛型接口用法差不多,在類名后面增加<T>即可。
/**
* 泛型類的用法
* @param <T>
*/
public class GenericObj<T> {
private T key;
public T getKey() {
return key;
}
public void setKey(T key) {
this.key = key;
}
}
通過抽象泛型,在創(chuàng)建時靈活指令類型,從而約束泛型類的參數(shù)類型:
public class Main {
public static void main(String[] args) {
GenericObj<Integer> obj=new GenericObj();
obj.setKey(1);
}
}
二、泛型的使用場景
泛型大部分是應用于項目開發(fā)中通用對象例如我們常用的Map:
public interface Map<K,V> {
//......略
//通過泛型接口的泛型約束鍵值對類型
V put(K key, V value);
}
三、詳解java泛型常見知識點
1. 為什么說Java是一門偽泛型語言
Java本質(zhì)就一門偽泛型語言,泛型的作用僅僅在編譯期間進行類型檢查的,一旦生成字節(jié)碼之后,關于泛型的一切都會消失。
如下所示,Integer類型數(shù)組我們完全可以通過反射將字符串存到列表中。
public static void main(String[] args) throws Exception {
List<Integer> list=new ArrayList<>();
list.add(1);
// list.add("s"); 報錯
Class<? extends List> clazz=list.getClass();
// java的泛型時偽泛型,運行時就會被擦除
Method add = clazz.getDeclaredMethod("add", Object.class);
add.invoke(list,"k1");
System.out.println(list);
}
同樣的,設計者將Java泛型在編譯器后擦除的原因還有如下原因:
- 避免引入泛型創(chuàng)建沒必要的新類型
- 節(jié)約虛擬機開銷
這一點我們用如下的例子就能看出,相同參數(shù)不通泛型的方法根本不能重載
2. 泛型存在的意義
說到這里很多讀者可能會問:既然編譯器要把泛型擦除,為什么還要用泛型呢?用Object不行嘛?
使用泛型后便于集合的取操作,且提高的代碼的可讀性。
如下代碼所示,雖然一下代碼在編譯后會擦除為Object類型,但是通過泛型限定后,JVM就會自動將其強轉為Comparable類型,減少我們編寫一些沒必要的代碼。
public class Test2 {
public static void main(String[] args) {
List<? extends Comparable> list=new ArrayList<>();
for (Comparable comparable : list) {
comparable.compareTo("1");
}
}
}
3. 橋方法是什么
橋方法其實并不是什么高大上的概念,無非是繼承泛型類并指定泛型類型,IDE會自動為我們創(chuàng)建構造方法調(diào)用父類的有參構造函數(shù)確保泛型多態(tài)類。
泛型類,指定了一個有參構造函數(shù):
public class Node<T> {
public T data;
public Node(T data) {
this.data = data;
}
public void setData(T data) {
System.out.println("Node.setData");
this.data = data;
}
}
實現(xiàn)類,自動補充構造方法并調(diào)用父類構造方法確保實現(xiàn)泛型的多態(tài)性。
public class MyNode extends Node<Integer>{
//繼承泛型類后自動添加的,用于保證泛型的多態(tài)性
public MyNode(Integer data) {
super(data);
}
}
4. 泛型的限制
泛型不可以被實例化:泛型會在編譯器擦除,所以泛型在編譯器還未知,所以不可被實例化。
泛型參數(shù)不可以是基本類型:我們都知道泛型僅在編譯器存在,當編譯結束泛型就會被擦除,對象就會編程Object類型,所以基本類型作為泛型參數(shù)ide就會直接報錯。
泛型無法被實例化,無論是泛型變量還是泛型數(shù)組,從上文我們就知道泛型會在編譯期完成后被擦除,這正是因為JVM不想為泛型創(chuàng)建新的類型造成沒必要的開銷。
不能拋出或者捕獲T類型的泛型異常,之所以catch使用泛型會編譯失敗,是因為若引入泛型后,編譯器無法直到這個錯誤是否是后續(xù)catch類的父類。
不能聲明泛型錯誤:如下所示,泛型會在編譯器被擦除,那么下面這段代碼的catch就等于catch兩個一樣的錯誤,出現(xiàn)執(zhí)行矛盾。
try{
}catch(Problem<String> p){
}catch(Problem<Object> p){
}
不能聲明兩個參數(shù)一樣泛型不同的方法:編譯器擦除后,參數(shù)一樣,所以編譯失敗
泛型不能被聲明為static,泛型只有在類創(chuàng)建時才知曉,而靜態(tài)變量在類加載無法知曉,故無法通過編譯。
5. 泛型的通配符
(1) 什么是通配符
道通配符是解決泛型之間無法協(xié)變的問題,當我們使用一種類型作為泛型參數(shù)時,卻無法使用他的父類或者子類進行賦值,而通配符就是解決這種問題的對策。
(2) 上界通配符
有時我們不知道子類的具體類型,上界通配符就是用于解決那些父類引用指向子類泛型引用的場景,所以上界通配符的設計增強了代碼的通用性。
對此我們給出一段示例,首先定義父類。
/**
* 水果父類
*/
public class Fruit {
}
對應的子類代碼如下。
/**
* 水果的子類 蘋果
*/
public class Apple extends Fruit {
}
然后封裝水果類的容器代碼。
/**
* 容器類
* @param <T>
*/
public class Container<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
測試代碼如下,可以看到上界通配符使得蘋果類可以作為水果類的指向引用,即上界通配符是用于子類的上界,方便父類管理子類。
/**
* 泛型測試
*/
public class TestParttern {
public static void main(String[] args) {
//上界通配符限定引用的父類,使得container可以指向繼承Fruit的Apple
Container<? extends Fruit> container=new Container<Apple>();
Fruit data = container.getData();
//報錯,下文贅述
container.setData(new Apple());
}
}
那么問題來了。為什么上界通配符只能get不能set?如上代碼所示,當我們用上界通配符? extends Fruit,我們用其子類作為泛型參數(shù),這只能保證我們get到的都是這個子類的對象。
但我們卻忘了一點,當我們用子類apple作為泛型參數(shù)時,泛型的工作機制僅僅是對這個對象加個一個編號CAP#1,當我set一個新的對象,編譯器無法識別這個對象類型是否和編號匹配。
更通俗的理解,上界通配符決定可以指向的容器,但是真正使用是并不知曉這個容器是哪個子類容器。所以無法set。
(3) 下界通配符
還是以上文的例子進行演示,只不過通配符改為下界通配符:
/**
* 泛型測試
*/
public class TestParttern {
public static void main(String[] args) {
Container<? super Apple> container1=new Container<Fruit>();
}
}
下界通配符決定了泛型的最大粒度的上限,通俗來說只要是蘋果類的父親都可以作為被指向的引用,通過super聲明,它可以很直觀的告訴我們泛型參數(shù)必須傳super后的父類如下所示
Container<? super Apple> container1=new Container<Fruit>();
為什么下界通配符只能set不能get(或者說get的是object)?原因如下:
- 下界通配符決定泛型的類型上限,所有水果類的父親都可以作為指向的引用
- get時無法知曉其具體為哪個父親,所以取出來的類型只能是object
Container<? super Apple> container1=new Container<Fruit>();
Object data = container1.getData();
6. 如何獲取泛型類型
public class GenericType<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public static void main(String[] args) {
//注意這個類要使用子類,筆者為了方便期間使用了 {}
GenericType<String> genericType = new GenericType<String>() {};
Type superclass = genericType.getClass().getGenericSuperclass();
//getActualTypeArguments 返回確切的泛型參數(shù), 如Map<String, Integer>返回[String, Integer]
Type type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
System.out.println(type);//class java.lang.String
}
}
7. 判斷泛型是否編譯通過
(1) 基于泛型類型比較
public final class Algorithm {
public static <T> T max(T x, T y) {
return x > y ? x : y;
}
}
答:錯誤,T類型未知,無法比較,編譯失敗
(2) 基于泛型創(chuàng)建靜態(tài)單例
public class Singleton<T> {
public static T getInstance() {
if (instance == null)
instance = new Singleton<T>();
return instance;
}
private static T instance = null;
}
答案
不能,泛型不能被static修飾
四、常見面試題
1. 什么是泛型?有什么好處
通過指定泛型的提升代碼抽象等級,提升代碼復用性。
泛型界定元素的類型避免各種強轉操作,且在編譯器即可完成泛型檢查,提升程序安全性。
2. 什么是類型擦除
java是一門偽泛型語言,我們?yōu)樵刂付ǚ盒椭?,實際執(zhí)行原理是編譯后將object強轉為泛型設定的類型。
3. 泛型中上下界限定符extends 和 super有什么區(qū)別
extend T 決定類型的上界,它決定這個泛型的類型必須是T或者T的子類,super決定傳入類型的超類型必須是指定的類型。
使用時,我們也一般使用PECS原則,即生產(chǎn)時(從元素中獲取數(shù)據(jù))使用extends 這樣讀取時的類型可以指定為extends的父類及其子類型:
List<? extends Number> list = new ArrayList<>();
//從列表中讀取,可以理解為列表在生產(chǎn),這里面拿到的元素可以是Number類型的子類
for (Number number : list) {
//do something
}
消費時使用super。這樣添加元素時就可以可以指定超類型是T的類及其子類:
List<? super Number> list = new ArrayList<>();
//add 寫入可以理解為列表在消費元素,通過super可以傳入超類型為Number的元素
list.add(1);
list.add(2.0);