
面試題:說說你對泛型的理解?
面試考察點
考察目的:了解求職者對于Java基礎知識的掌握程度,
考察范圍:作業1-3年的Java程式員,
背景知識
Java中的泛型,是JDK5引入的一個新特性,
它主要提供的是編譯時期型別的安全檢測機制,這個機制允許程式在編譯時檢測到非法的型別,從而進行錯誤提示,
這樣做的好處,一方面是告訴開發者當前方法接識訓回傳的引數型別,另一方面是避免程式運行時的型別轉換錯誤,
泛型的設計推演
舉一個比較簡單的例子,首先我們來看一下ArrayList這個集合,部分代碼定義如下,
public class ArrayList{
transient Object[] elementData; // non-private to simplify nested class access
}
在ArrayList中,存盤元素所使用的結構是一個Object[]物件陣列,意味著可以存盤任何型別的資料,
當我們使用這個ArrayList來做下面這個操作時,
public class ArrayExample {
public static void main(String[] args) {
ArrayList al=new ArrayList();
al.add("Hello World");
al.add(1001);
String str=(String)al.get(1);
System.out.println(str);
}
}
運行程式后,會得到如下的執行結果
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at org.example.cl06.ArrayExample.main(ArrayExample.java:11)
這種型別轉換錯誤,相信大家在開發中有遇到過,總的來說,在沒有泛型的情況下,會有兩個比較嚴重的問題
- 需要對型別進行強制轉換
- 使用不方便,容易出錯
怎么解決上面這個問題呢?要解決這個問題,就得思考這個問題背后的需求是什么?
我簡單總結兩點:
- 要能支持不同型別的資料存盤
- 還需要保證存盤資料型別的統一性
基于這兩個點不難發現,對于一個資料容器中要存盤什么型別的資料,其實是由開發者自己決定的,因此,為了解決這個問題,在JDK5中就引入了泛型的機制,
其定義形式是:ArrayList<E>,它相當于給ArrayList提供了一個型別輸入的模板E,E可以是任意型別的物件,它的定義方式如下,
public class ArrayList<E>{
transient E[] elementData; // non-private to simplify nested class access
}
在ArrayList這個類的定義中,使用<>語法,并傳入一個用來表示任意型別的物件E,這個E可以隨便定義,你可以定義成A、B、C都可以,
接著,把用來存盤元素的陣列elementData的型別,設定為E型別,
有了這個配置之后,ArrayList這個容器中,你想存盤什么型別的資料,是由使用者自己決定,比如我希望ArrayList只存盤String型別,那么它可以這么實作
public class ArrayExample {
public static void main(String[] args) {
ArrayList<String> al=new ArrayList();
al.add("Hello World");
al.add(1001);
String str=(String)al.get(1);
System.out.println(str);
}
}
在定義ArrayList時,傳入一個String型別,這樣寫意味著后續往ArrayList這個實體物件al中添加元素,必須是String型別,否則會提示如下的語法錯誤,

同理,如果需要保存其他型別的資料,可以這么寫:
- ArrayList
- ArrayList
總結:所謂泛型定義,其實本質上就是一種型別模板,在實際開發中,我們把一個容器或者一個物件中需要保存的屬性的型別,通過模板定義的方式,給到呼叫者來決定,從而保證了型別的安全性,
泛型的定義
泛型定義可以從兩個維度來說明:
- 泛型類
- 泛型方法
泛型類
泛型類指的是在類名后面添加一個或多個型別引數,一個泛型引數,也被稱為一個型別變數,是用于指定一個泛型型別名稱的識別符號,因為他們接受一個或多個引數,這些類被稱為引數化的類或引數化的型別,
型別變數的表示標記,常用的是:E(element),T(type)、K(key),V(value),N(number)等,這只是一個表示符號,可以是任何字符,沒有強制要求,
下面的代碼是關于泛型類的定義,
該類接收一個T標記符的型別引數,該類中有一個成員變數,使用T型別,
public class Response <T>{
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = https://www.cnblogs.com/mic112/p/data;
}
}
使用方式如下:
public static void main(String[] args) {
Response<String> res=new Response<>();
res.setData("Hello World");
}
泛型方法
泛型方法是指指定方法級別的型別引數,這個方法在呼叫時可以接收不同的引數型別,根據傳遞給泛型方法的引數型別,編譯器適當地處理每一個方法呼叫,
下面的代碼表示泛型方法的定義,用到了JDK提供的反射機制,來生成動態代理類,
public interface IHelloWorld {
String say();
}
定義getProxy方法,它用來生成動態代理物件,但是傳遞的引數型別是T,也就是說,這個方法可以完成任意介面的動態代理實體的構建,
在這里,我們針對IHelloWorld這個介面,構建了動態代理實體,代碼如下,
public class ArrayExample implements InvocationHandler {
public <T> T getProxy(Class<T> clazz){
// clazz 不是介面不能使用JDK動態代理
return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class<?>[]{ clazz }, ArrayExample.this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return "Hello World";
}
public static void main(String[] args) {
IHelloWorld hw=new ArrayExample().getProxy(IHelloWorld.class);
System.out.println(hw.say());
}
}
運行結果:
Hello World
關于泛型方法的定義規則,簡單總結如下:
- 所有泛型方法的定義,都有一個用
<>表示的型別引數宣告,這個型別引數宣告部分在方法回傳型別之前, - 每一個型別引數宣告部分包含一個或多個型別引數,引數間用逗號隔開,一個泛型引數,也被稱為一個型別變數,是用于指定一個泛型型別名稱的識別符號,
- 型別引數能被用來宣告回傳值型別,并且能作為泛型方法得到的實際引數型別的占位符
- 泛型方法體的宣告和其他方法一樣,注意型別引數只能代表參考型型別,不能是原始型別(像 int、double、char 等),##
多型別變數定義
上在我們只定義了一個泛型變數T,那如果我們需要傳進去多個泛型要怎么辦呢?
我們可以這么寫:
public class Response <T,K,V>{
}
每一個引數宣告符號代表一種型別,
注意,在多變數型別定義中,泛型變數最好是定義成能夠簡單理解具有含義的字符,否則型別太多,呼叫者比較容易搞混,
有界型別引數
在有些場景中,我們希望傳遞的引數型別屬于某種型別范圍,比如,一個運算元字的方法可能只希望接受Number或者Number子類的實體,怎么實作呢?
泛型通配符上邊界
上邊界,代表型別變數的范圍有限,只能傳入某種型別,或者它的子類,
我們可以在泛型引數上,增加一個extends關鍵字,表示該泛型引數型別,必須是派生自某個實作類,示例代碼如下,
public class TypeExample<T extends Number> {
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
public static void main(String[] args) {
TypeExample<String> t=new TypeExample<>();
}
}
上述代碼,宣告了一個泛型引數T,該泛型引數必須是繼承Number這個類,表示后續實體化TypeExample時,傳入的泛型型別應該是Number的子類,
所以,有了這個規則后,上面這個測驗代碼,會提示java: 型別引數java.lang.String不在型別變數T的范圍內錯誤,
泛型通配符下邊界
下邊界,代表型別變數的范圍有限,只能傳入某種型別,或者它的父類,
我們可以在泛型引數上,增加一個super關鍵字,可以設定泛型通配符的上邊界,實體代碼如下,
public class TypeExample<T> {
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
public static void say(TypeExample<? super Number> te){
System.out.println("say: "+te.getT());
}
public static void main(String[] args) {
TypeExample<Number> te=new TypeExample<>();
TypeExample<Integer> te2=new TypeExample<>();
say(te);
say(te2);
}
}
在say方法上宣告TypeExample<? super Number> te,表示傳入的TypeExample的泛型型別,必須是Number以及Number的父型別別,
在上述代碼中,運行時會得到如下錯誤:
java: 不兼容的型別: org.example.cl06.TypeExample<java.lang.Integer>無法轉換為org.example.cl06.TypeExample<? super java.lang.Number>
如下圖所示,表示Number這個類的類關系圖,通過super關鍵字限定后,只能傳遞Number以及父類Serializable,

型別通配符?
型別通配符一般是使用 ? 代替具體的型別引數,例如 List<?> 在邏輯上是 List
來看下面這段代碼的定義,在say方法中,接受一個TypeExample型別的引數,并且泛型型別是<?>,代表接收任何型別的泛型型別引數,
public class TypeExample<T> {
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
public static void say(TypeExample<?> te){
System.out.println("say: "+te.getT());
}
public static void main(String[] args) {
TypeExample<Integer> te1=new TypeExample<>();
te1.setT(1111);
TypeExample<String> te2=new TypeExample<>();
te2.setT("Hello World");
say(te1);
say(te2);
}
}
運行結果如下
say: 1111
say: Hello World
同樣,型別通配符的引數,也可以通過extends來做限定,比如:
public class TypeExample<T> {
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
public static void say(TypeExample<? extends Number> te){ //修改,增加extends
System.out.println("say: "+te.getT());
}
public static void main(String[] args) {
TypeExample<Integer> te1=new TypeExample<>();
te1.setT(1111);
TypeExample<String> te2=new TypeExample<>();
te2.setT("Hello World");
say(te1);
say(te2);
}
}
由于say方法中的引數TypeExample,在泛型型別定義中使用了<? extends Number>,所以后續在傳遞引數時,泛型型別必須是Number的子型別,
因此上述代碼運行時,會提示如下錯誤:
java: 不兼容的型別: org.example.cl06.TypeExample<java.lang.String>無法轉換為org.example.cl06.TypeExample<? extends java.lang.Number>
注意: 構建泛型實體時,如果省略了泛型型別,則默認是通配符型別,意味著可以接受任意型別的引數,
泛型的繼承
泛型型別引數的定義,是允許被繼承的,比如下面這種寫法,
表示子類SayResponse和父類Response使用同一種泛型型別,
public class SayResponse<T> extends Response<T>{
private T ox;
}
JVM是如何實作泛型的?
在JVM中,采用了型別擦除Type erasure generics)的方式來實作泛型,簡單來說,就是泛型只存在.java原始碼檔案中,一旦編譯后就會把泛型擦除.
我們來看ArrayExample這個類,編譯之后的位元組指令,
public class ArrayExample implements InvocationHandler {
public <T> T getProxy(Class<T> clazz){
// clazz 不是介面不能使用JDK動態代理
return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class<?>[]{ clazz }, ArrayExample.this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return "Hello World";
}
public static void main(String[] args) {
IHelloWorld hw=new ArrayExample().getProxy(IHelloWorld.class);
System.out.println(hw.say());
}
}
通過javap -v ArrayExample.class查看位元組指令如下,
public <T extends java.lang.Object> T getProxy(java.lang.Class<T>);
descriptor: (Ljava/lang/Class;)Ljava/lang/Object;
flags: ACC_PUBLIC
Code:
stack=5, locals=2, args_size=2
0: aload_1
1: invokevirtual #2 // Method java/lang/Class.getClassLoader:()Ljava/lang/ClassLoader;
可以看到,getProxy在編譯之后,泛型T已經被擦除了,引數型別替換成了java.lang.Object.
并不是所有型別都會轉換為java.lang.Object,比如如果是
,則引數型別是java.lang.String,
同時,為了保證IHelloWorld hw=new ArrayExample().getProxy(IHelloWorld.class);這段代碼的準確性,編譯器還會在這里插入一個型別轉換的機制,
下面這個代碼是ArrayExample.class反編譯之后的呈現,
IHelloWorld hw = (IHelloWorld)(new ArrayExample()).getProxy(IHelloWorld.class);
System.out.println(hw.say());
泛型型別擦除實作帶來的缺陷
擦除方式實作泛型,還是會存在一些缺陷的,簡單舉幾個案例說明,
不支持基本型別
由于泛型型別擦除后,變成了java.lang.Object型別,這種方式對于基本型別如int/long/float等八種基本型別來說,就比較麻煩,因為Java無法實作基本型別到Object型別的強制轉換,
ArrayList<int> list=new ArrayList<int>();
如果這么寫,會得到如下錯誤
java: 意外的型別
需要: 參考
找到: int
所以,在泛型定義中,只能使用參考型別,
但是作為參考型別,如果保存基本型別的資料時,又會涉及到裝箱和拆箱的程序,比如
List<Integer> list = new ArrayList<Integer>();
list.add(10); // 1
int num = list.get(0); // 2
在上述代碼中,宣告了一個List<Integer>泛型型別的集合,
在標記1的位置,添加了一個int型別的數字10,這個程序中,會涉及到裝箱操作,也就是把基本型別int轉換為Integer.
在標記2的位置,編譯器首先要把Object轉換為Integer型別,接著再進行拆箱,把Integer轉換為int,因此上述代碼等同于
List list = new ArrayList();
list.add(Integer.valueOf(10));
int num = ((Integer) list.get(0)).intValue();
增加了一些執行步驟,對于執行效率來說還是會有一些影響,
運行期間無法獲取泛型實際型別
由于編譯之后,泛型就被擦除,所以在代碼運行期間,Java 虛擬機無法獲取泛型的實際型別,
下面這段代碼,從原始碼上兩個 List 看起來是不同型別的集合,但是經過泛型擦除之后,集合都變為 ArrayList,所以 if陳述句中代碼將會被執行,
public static void main(String[] args) {
ArrayList<Integer> li = new ArrayList<>();
ArrayList<Float> lf = new ArrayList<>();
if (li.getClass() == lf.getClass()) { // 泛型擦除,兩個 List 型別是一樣的
System.out.println("型別相同");
}
}
運行結果:
型別相同
這就使得,我們在做方法多載時,無法根據泛型型別來定義重寫方法,
也就是說下面這種方式無法實作重寫,
public void say(List<Integer> a){}
public void say(List<String> b){}
另外還會給我們在實際使用中帶來一些限制,比如說我們沒辦法直接實作以下代碼
public <T> void say(T a){
if(a instanceof T){
}
T t=new T();
}
上述代碼會存在編譯錯誤,
既然通過擦除的方式實作泛型有這么多缺陷,那為什么要這么設計呢?
要回答這個問題,需要知道泛型的歷史,Java的泛型是在Jdk 1.5 引入的,在此之前Jdk中的容器類等都是用Object來保證框架的靈活性,然后在讀取時強轉,但是這樣做有個很大的問題,那就是型別不安全,編譯器不能幫我們提前發現型別轉換錯誤,會將這個風險帶到運行時, 引入泛型,也就是為解決型別不安全的問題,但是由于當時java已經被廣泛使用,保證版本的向前兼容是必須的,所以為了兼容老版本jdk,泛型的設計者選擇了基于擦除的實作,
問題解答
面試題:說說你對泛型的理解?
回答: 泛型是JDK5提供的一個新特性,它主要提供的是編譯時期型別的安全檢測機制,這個機制允許程式在編譯時檢測到非法的型別,從而進行錯誤提示,
問題總結
深入理解Java泛型是程式員最基礎的必備技能,雖然面試很卷,但是實力仍然很重要,
關注[跟著Mic學架構]公眾號,獲取更多精品原創

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/349478.html
標籤:Java
