什么是泛型俗扇?
Java泛型(generics)是JDK5中引入的一種參數(shù)化類型特性
Java泛型(generics)是JDK5中引入的一個新特性又跛,泛型提供了
編譯時類型安全檢測機制,
該機制允許程序員在編譯時檢測到非法的類型
泛型的本質(zhì)是參數(shù)類型,也就是說所操作的數(shù)據(jù)類型被指定為一個參數(shù)
泛型不存在于JVM虛擬機
什么是參數(shù)化類型甘畅?
把類型當(dāng)參數(shù)一樣傳遞
數(shù)據(jù)類型只能是引用類型(泛型的副作用)
舉個例子:
- List<T>中的”T”稱為類型參數(shù)
- List<Person>中的"Person"稱為實際類型參數(shù)
- "List<T>"整個成為泛型類型
- "List<Person>"整個稱為參數(shù)化的類型ParameterizedType
為什么使用泛型,使用泛型的好處往弓?
- 代碼更健壯(只要編譯期沒有警告疏唾,那么運行期就不會出現(xiàn)ClassCastException)
//不使用泛型,運行期報錯
List list = new ArrayList();
list.add("hello");
Integer s = (String) list.get(0);// Causes a ClassCastException to be thrown.
//使用泛型函似,編譯期就檢查
List<String> list = new ArrayList();
list.add("hello");
Integer s = (String) list.get(0);//編譯器就會不通過
- 代碼更簡潔槐脏,不用強轉(zhuǎn)
//不使用泛型,需要強轉(zhuǎn)
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
//使用泛型撇寞,不需要強轉(zhuǎn)
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0); // no cast
- 代碼更靈活顿天,復(fù)用
// java.util.List中的排序方法sort,只要實現(xiàn)了Comparator接口的都可以使用這個方法
default void sort(@Nullable Comparator<? super E> c) {
throw new RuntimeException("Stub!");
}
Java是如何處理泛型的
- 通過運行時獲取的類信息是完全一樣的蔑担。泛型類型被擦除了牌废,擦除后只剩下原始類型,如下面所示的只剩下ArrayList類型啤握。
ArrayList<String> strings = new ArrayList<>();
ArrayList<Integer> integers = new ArrayList<>();
System.out.println(strings.getClass() == integers.getClass());
//result true
泛型擦除
- 功能:保證了泛型不在運行時出現(xiàn)
- 類型消除應(yīng)用的場合:
編譯器會把泛型類型中所有的類型參數(shù)替換為它們的上(下)限鸟缕,如果沒有對類型參數(shù)做出限制,那么就替換為Object類型。因此懂从,編譯出的字節(jié)碼僅僅包含了常規(guī)類授段,接口和方法。
在必要時插入類型轉(zhuǎn)換以保持類型安全番甩。
生成橋方法以在擴展泛型時保持多態(tài)性 - Bridge Methods 橋方法
當(dāng)編譯一個擴展參數(shù)化類的類畴蒲,或一個實現(xiàn)了參數(shù)化接口的接口時,編譯器有可能因此要創(chuàng)建一個合成方法对室,名為橋方法模燥。它是類型擦除過程中的一部分
用一個簡單的例子看一下Java是怎么處理泛型的
- 定義一個泛型接口
public interface Box<T> {
void set(T t);
T get();
}
- 利用javac命令獲取字節(jié)碼文件
public interface Box<T> {
void set(T var1);
T get();
}
- 利用javap -c命令查看生成的字節(jié)碼,我們的T變成了Object類型掩宜。
public abstract interface test3/Box {
public abstract set(Ljava/lang/Object;)V
public abstract get()Ljava/lang/Object;
}
- 我們定義一個類去實現(xiàn)這個接口
public class ConditionalBox<T> implements Box<T> {
private List<T> items = new ArrayList<T>(10);
public ConditionalBox() {
}
@Override
public void set(T t) {
items.add(t);
}
@Override
public T get() {
int index = items.size() - 1;
if (index >= 0) {
return items.get(index);
} else {
return null;
}
}
}
5.用javap -c命令查看生成的字節(jié)碼蔫骂。可以看到我們的set和get還是構(gòu)造方法牺汤,T變成了Object類型辽旋。
public class test3/ConditionalBox implements test3/Box {
// compiled from: ConditionalBox.java
// access flags 0x2
// signature Ljava/util/List<TT;>;
// declaration: items extends java.util.List<T>
private Ljava/util/List; items
// access flags 0x1
public <init>()V
L0
LINENUMBER 10 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
L1
LINENUMBER 8 L1
ALOAD 0
NEW java/util/ArrayList
DUP
BIPUSH 10
INVOKESPECIAL java/util/ArrayList.<init> (I)V
PUTFIELD test3/ConditionalBox.items : Ljava/util/List;
L2
LINENUMBER 11 L2
RETURN
L3
LOCALVARIABLE this Ltest3/ConditionalBox; L0 L3 0
// signature Ltest3/ConditionalBox<TT;>;
// declaration: this extends test3.ConditionalBox<T>
MAXSTACK = 4
MAXLOCALS = 1
// access flags 0x1
// signature (TT;)V
// declaration: void set(T)
public set(Ljava/lang/Object;)V
L0
LINENUMBER 15 L0
ALOAD 0
GETFIELD test3/ConditionalBox.items : Ljava/util/List;
ALOAD 1
INVOKEINTERFACE java/util/List.add (Ljava/lang/Object;)Z (itf)
POP
L1
LINENUMBER 16 L1
RETURN
L2
LOCALVARIABLE this Ltest3/ConditionalBox; L0 L2 0
// signature Ltest3/ConditionalBox<TT;>;
// declaration: this extends test3.ConditionalBox<T>
LOCALVARIABLE t Ljava/lang/Object; L0 L2 1
// signature TT;
// declaration: t extends T
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x1
// signature ()TT;
// declaration: T get()
public get()Ljava/lang/Object;
L0
LINENUMBER 20 L0
ALOAD 0
GETFIELD test3/ConditionalBox.items : Ljava/util/List;
INVOKEINTERFACE java/util/List.size ()I (itf)
ICONST_1
ISUB
ISTORE 1
L1
LINENUMBER 21 L1
ILOAD 1
IFLT L2
L3
LINENUMBER 22 L3
ALOAD 0
GETFIELD test3/ConditionalBox.items : Ljava/util/List;
ILOAD 1
INVOKEINTERFACE java/util/List.get (I)Ljava/lang/Object; (itf)
ARETURN
L2
LINENUMBER 24 L2
FRAME APPEND [I]
ACONST_NULL
ARETURN
L4
LOCALVARIABLE this Ltest3/ConditionalBox; L0 L4 0
// signature Ltest3/ConditionalBox<TT;>;
// declaration: this extends test3.ConditionalBox<T>
LOCALVARIABLE index I L1 L4 1
MAXSTACK = 2
MAXLOCALS = 2
}
- IntelligentBox<T extends Comparable<T>>實現(xiàn)Box接口。代碼如下
public class IntelligentBox<T extends Comparable<T>> implements Box<T> {
private List<T> items = new ArrayList<T>(10);
@Override
public void set(T t) {
items.add(t);
Collections.sort(items);
}
@Override
public T get() {
int index = items.size() - 1;
if (index >= 0) {
return items.get(index);
} else {
return null;
}
}
}
- 用javap -c命令查看IntelligentBox生成的字節(jié)碼
public class test3/IntelligentBox implements test3/Box {
// compiled from: IntelligentBox.java
// access flags 0x2
// signature Ljava/util/List<TT;>;
// declaration: items extends java.util.List<T>
private Ljava/util/List; items
// access flags 0x1
public <init>()V
L0
LINENUMBER 7 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
L1
LINENUMBER 9 L1
ALOAD 0
NEW java/util/ArrayList
DUP
BIPUSH 10
INVOKESPECIAL java/util/ArrayList.<init> (I)V
PUTFIELD test3/IntelligentBox.items : Ljava/util/List;
RETURN
L2
LOCALVARIABLE this Ltest3/IntelligentBox; L0 L2 0
// signature Ltest3/IntelligentBox<TT;>;
// declaration: this extends test3.IntelligentBox<T>
MAXSTACK = 4
MAXLOCALS = 1
// access flags 0x1
// signature (TT;)V
// declaration: void set(T)
public set(Ljava/lang/Comparable;)V
L0
LINENUMBER 13 L0
ALOAD 0
GETFIELD test3/IntelligentBox.items : Ljava/util/List;
ALOAD 1
INVOKEINTERFACE java/util/List.add (Ljava/lang/Object;)Z (itf)
POP
L1
LINENUMBER 14 L1
ALOAD 0
GETFIELD test3/IntelligentBox.items : Ljava/util/List;
INVOKESTATIC java/util/Collections.sort (Ljava/util/List;)V
L2
LINENUMBER 15 L2
RETURN
L3
LOCALVARIABLE this Ltest3/IntelligentBox; L0 L3 0
// signature Ltest3/IntelligentBox<TT;>;
// declaration: this extends test3.IntelligentBox<T>
LOCALVARIABLE t Ljava/lang/Comparable; L0 L3 1
// signature TT;
// declaration: t extends T
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x1
// signature ()TT;
// declaration: T get()
public get()Ljava/lang/Comparable;
L0
LINENUMBER 19 L0
ALOAD 0
GETFIELD test3/IntelligentBox.items : Ljava/util/List;
INVOKEINTERFACE java/util/List.size ()I (itf)
ICONST_1
ISUB
ISTORE 1
L1
LINENUMBER 20 L1
ILOAD 1
IFLT L2
L3
LINENUMBER 21 L3
ALOAD 0
GETFIELD test3/IntelligentBox.items : Ljava/util/List;
ILOAD 1
INVOKEINTERFACE java/util/List.get (I)Ljava/lang/Object; (itf)
CHECKCAST java/lang/Comparable
ARETURN
L2
LINENUMBER 23 L2
FRAME APPEND [I]
ACONST_NULL
ARETURN
L4
LOCALVARIABLE this Ltest3/IntelligentBox; L0 L4 0
// signature Ltest3/IntelligentBox<TT;>;
// declaration: this extends test3.IntelligentBox<T>
LOCALVARIABLE index I L1 L4 1
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x1041
public synthetic bridge get()Ljava/lang/Object;
L0
LINENUMBER 7 L0
ALOAD 0
INVOKEVIRTUAL test3/IntelligentBox.get ()Ljava/lang/Comparable;
ARETURN
L1
LOCALVARIABLE this Ltest3/IntelligentBox; L0 L1 0
// signature Ltest3/IntelligentBox<TT;>;
// declaration: this extends test3.IntelligentBox<T>
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1041
public synthetic bridge set(Ljava/lang/Object;)V
L0
LINENUMBER 7 L0
ALOAD 0
ALOAD 1
CHECKCAST java/lang/Comparable
INVOKEVIRTUAL test3/IntelligentBox.set (Ljava/lang/Comparable;)V
RETURN
L1
LOCALVARIABLE this Ltest3/IntelligentBox; L0 L1 0
// signature Ltest3/IntelligentBox<TT;>;
// declaration: this extends test3.IntelligentBox<T>
MAXSTACK = 2
MAXLOCALS = 2
}
- 可以看到檐迟,有兩處地方進(jìn)行強制類型轉(zhuǎn)換补胚,分別是get和set方法。
INVOKEINTERFACE java/util/List.get (I)Ljava/lang/Object; (itf)
CHECKCAST java/lang/Comparable
CHECKCAST java/lang/Comparable
INVOKEVIRTUAL test3/IntelligentBox.set (Ljava/lang/Comparable;)V
- 可以看到有兩個橋方法
public synthetic bridge set(Ljava/lang/Object;)V
public synthetic bridge get()Ljava/lang/Object;
- 用偽代碼來表示IntelligentBox的過程
public class test3/IntelligentBox implements test3/Box {
public void set(Comparable t) { /* compiled code */ }
public Comparable get() { /* compiled code */ }
@Overide
public synthetic bridge get(){
}
@Overide
public synthetic bridge set(Object t){
set((Comparable)t)
}
}
泛型擦除的殘留
看一下Box的字節(jié)碼文件Box.class和查看生成的字節(jié)碼
public interface Box<T> {
void set(T var1);
T get();
}
- 疑問:不是類型擦除之后變成Object了嗎追迟?怎么這里字節(jié)碼文件還是T類型溶其?其實這里看到的其實是簽名而已,還保留定義的格式敦间,對于分析字節(jié)碼有好處瓶逃。并不是真的擦除了,保存在類的常量池中廓块。
/**
* ParameterizedType
* 具體的范型類型, 如Map<String, String>
* 有如下方法:
*
* Type getRawType(): 返回承載該泛型信息的對象, 如上面那個Map<String, String>承載范型信息的對象是Map
* Type[] getActualTypeArguments(): 返回實際泛型類型列表, 如上面那個Map<String, String>實際范型列表中有兩個元素, 都是String
* Type getOwnerType(): 返回是誰的member.(上面那兩個最常用)
*/
public class TestType {
Map<String, String> map;
//擦除 其實在類常量池里面保留了泛型信息
public static void main(String[] args) throws Exception {
Field f = TestType.class.getDeclaredField("map");
System.out.println(f.getGenericType()); // java.util.Map<java.lang.String, java.lang.String>
System.out.println(f.getGenericType() instanceof ParameterizedType); // true
ParameterizedType pType = (ParameterizedType) f.getGenericType();
System.out.println(pType.getRawType()); // interface java.util.Map
for (Type type : pType.getActualTypeArguments()) {
System.out.println(type); // 打印兩遍: class java.lang.String
}
System.out.println(pType.getOwnerType()); // null
}
}
- java虛擬機規(guī)范中為了響應(yīng)在泛型類中如何獲取傳入的參數(shù)化類型等問題厢绝,引入了signature,LocalVariableTypeTable等新的屬性來記錄泛型信息带猴,所以所謂的泛型類型擦除昔汉,僅僅是對方法的code屬性中的字節(jié)碼進(jìn)行擦除,而原數(shù)據(jù)中還是保留了泛型信息的拴清,這些信息被保存在class字節(jié)碼的常量池中靶病,使用了泛型的代碼調(diào)用處會生成一個signature簽名字段,signature指明了這個常量在常量池的地址贷掖,這樣我們就找到了參數(shù)化類型嫡秕。這樣我們也知道 現(xiàn)在就明白了泛型擦除不是擦除全部
總結(jié)
QUESTION:Java泛型的原理?什么是泛型擦除機制苹威?
ANSWER:Java的泛型是JDK5新引入的特性,為了向下兼容驾凶,虛擬機其實是不支持泛型牙甫,所以Java實現(xiàn)的是一種偽泛型機制掷酗,也就是說Java在編譯期擦除了所有的泛型信息,這樣Java就不需要產(chǎn)生新的類型到字節(jié)碼窟哺,所有的泛型類型最終都是一種原始類型泻轰,在Java運行時根本就不存在泛型信息。
QUESTION:Java編譯器具體是如何擦除泛型的
-
ANSWER:
- 檢查泛型類型且轨,獲取目標(biāo)類型
- 擦除類型變量浮声,并替換為限定類型
如果泛型類型的類型變量沒有限定(<T>),則用Object作為原始類型。如果有限定(<T extends XClass>),則用XClass作為原始類型如果有多個限定(T extends XClass1&XClass2),則使用第一個邊界XClass1作為原始類 - 在必要時插入類型轉(zhuǎn)換以保持類型安全
- 生成橋方法以在擴展時保持多態(tài)性
使用泛型以及泛型擦除帶來的影響(副作用)
泛型類型變量不能使用基本數(shù)據(jù)類型
比如沒有ArrayList<int>,只有ArrayList<Integer>.當(dāng)類型擦除后旋奢,ArrayList的原始類中的類型變量(T)替換成Object,但Object類型不能存放int值
//error報錯泳挥,因為擦除后變成了Object,而Object是無法存放int
ArrayList<int> ints = new ArrayList<int>();
ArrayList<Integer> integerArrayList = new ArrayList<Integer>();
不能使用instanceof 運算符
因為擦除后至朗,ArrayList<String>只剩下原始類型屉符,泛型信息String不存在了,所有沒法使用instanceof
ArrayList<String> stringArrayList = new ArrayList<String>();
//使用ArrayList<?>可以
if (stringArrayList instanceof ArrayList<?>){
}
//因為擦除ArrayList<String>后String丟失了
if (stringArrayList instanceof ArrayList<String>){
}
泛型在靜態(tài)方法和靜態(tài)類中的問題
因為泛型類中的泛型參數(shù)的實例化是在定義泛型類型對象
(比如ArrayList<Integer>)的時候指定的锹引,而靜態(tài)成員是不需要使用對象來調(diào)用的矗钟,所有對象都沒創(chuàng)建,如何確定這個泛型參數(shù)是什么
//下面兩個會報錯嫌变,因為泛型參數(shù)是要創(chuàng)建對象時確定
public static T a;
public static T test1(T t) {
}
//這里不報錯吨艇,因為這是一個泛型方法,此T非彼T test2(T t)的T
public static <T> T test2(T t) {
return t;
}
泛型類型中的方法沖突
因為擦除后兩個equals方法變成一樣的了
//方法沖突腾啥,因為擦除后變一樣了
@Override
public boolean equals(T obj) {
return super.equals(obj);
}
@Override
public boolean equals(Object obj) {
return super.equals(obj);
}
沒法創(chuàng)建泛型實例
因為類型不確定
class Test02 {
//無法創(chuàng)建一個類型參數(shù)的實例秸应。下面會報錯
public static <E> void append(List<E> list) {
// E elem = new E(); // compile-time error
// list.add(elem);
}
//通過反射創(chuàng)建一個參數(shù)化類型的實例
public static <E> void append(List<E> list, Class<E> cls) throws Exception {
E elem = cls.newInstance(); // OK
list.add(elem);
}
}
沒有泛型數(shù)組
因為數(shù)組是協(xié)變,擦除后就沒法滿足數(shù)組協(xié)變的原則
// Plate<Apple>[] applePlates = new Plate<Apple>[10];//不允許
// T[] arr = new T[10];//不允許
Apple[] apples = new Apple[10];
Fruit[] fruits = new Fruit[10];
System.out.println(apples.getClass());
//class [Lcom.zero.genericsdemo02.demo02.Apple;
System.out.println(fruits.getClass());
//class [Lcom.zero.genericsdemo02.demo02.Fruit;
fruits = apples;
// fruits里面原本是放什么類型的碑宴? Fruit or Apple
// Apple[]
fruits[0] = new Banana();//編譯通過软啼,運行報ArrayStoreException
//Fruit是Apple的父類,F(xiàn)ruit[]是Apple[]的父類延柠,這就是數(shù)組的協(xié)變
//如果加入泛型后祸挪,由于擦除機制,運行時將無法知道數(shù)組的類型
Plate<?>[] plates = new Plate<?>[10];//這是可以的
泛型贞间,繼承和子類型
給定兩種具體的類型A和B(例如Fruit和Apple),
無論A和B是否相關(guān)贿条,
MyClass<A>與MyClass<B>都沒半毛錢關(guān)系,
它們的公共父對象是Object
泛型PECS原則
- 如果你只需要從集合中獲得類型T , 使用<? extends T>通配符
- 如果你只需要將類型T放到集合中, 使用<? super T>通配符
- 如果你既要獲取又要放置元素增热,則不使用任何通配符整以。例如List<Apple>
- PECS即 Producer extends Consumer super, 為了便于記憶峻仇。
- 為何要PECS原則公黑?提升了API的靈活性
- <?> 既不能存也不能取
在泛型編程時,使用部分限定的形參時,<? super T>和<? extends T>的使用場景容易混淆凡蚜, PECS原則可以幫助我們很好記住它們:提供者(Provider)使用extends人断,消費者(Consumer) 使用super。通俗地說朝蜘, Provider指的就是該容器從自己的容器里提供T類型或T的子類型的對象供別人使用恶迈; Consumer指的就是該容器把從別處拿到的T類型或T的子類型的對象放到自己的容器。
Kotlin的泛型
- 使用關(guān)鍵字 out 來支持協(xié)變谱醇,等同于 Java 中的上界通配符 ? extends暇仲。
- 使用關(guān)鍵字 in 來支持逆變,等同于 Java 中的下界通配符 ? super副渴。
var textViews: List<out TextView>
var textViews: List<in TextView>
聲明處的 out 和 in
Kotlin 提供了另外一種寫法:可以在聲明類的時候奈附,給泛型符號加上 out 關(guān)鍵字,表明泛型參數(shù) T 只會用來輸出佳晶,在使用的時候就不用額外加 out 了桅狠。
class Producer<out T> {
fun produce(): T {
...
}
}
val producer: Producer<TextView> = Producer<Button>() // ?? 這里不寫 out 也不會報錯
val producer: Producer<out TextView> = Producer<Button>() // ?? out 可以但沒必要
where關(guān)鍵字
Java 中聲明類或接口的時候,可以使用 extends 來設(shè)置邊界轿秧,將泛型類型參數(shù)限制為某個類型的子集中跌,同時這個邊界是可以設(shè)置多個,用 & 符號連接:
//T 的類型必須同時是 B 和 C 的子類型
class A<T extends B & C>{
}
在Kotlin中
//T 的類型必須同時是 B 和 C 的子類型
class A<T> where T : B, T : C
reified關(guān)鍵字
inline fun <reified T> printIfTypeMatch(item: Any) {
if (item is T) { // ?? 這里就不會在提示錯誤了
println(item)
}
}
Kotlin 泛型與 Java 泛型不一致的地方
- Java 里的數(shù)組是支持協(xié)變的菇篡,而 Kotlin 中的數(shù)組 Array 不支持協(xié)變漩符。
這是因為在 Kotlin 中數(shù)組是用 Array 類來表示的,這個 Array 類使用泛型就和集合類一樣驱还,所以不支持協(xié)變嗜暴。
- Java 中的 List 接口不支持協(xié)變,而 Kotlin 中的 List 接口支持協(xié)變议蟆。
Java 中的 List 不支持協(xié)變闷沥,原因在上文已經(jīng)講過了,需要使用泛型通配符來解決咐容。
在 Kotlin 中舆逃,實際上 MutableList 接口才相當(dāng)于 Java 的 List。Kotlin 中的 List 接口實現(xiàn)了只讀操作戳粒,沒有寫操作路狮,所以不會有類型安全上的問題,自然可以支持協(xié)變蔚约。
面試常問
- Array中可以用泛型嗎?
答:不能
- 泛型類型引用傳遞問題
問:你可以把List<String>傳遞給一個接受List<Object>參數(shù)的方法嗎奄妨?
ArrayList<String> arrayList1=new ArrayList<Object>();
ArrayList<Object> arrayList1=new ArrayList<String>();
答:不能。沒有半毛錢關(guān)系
- Java中List<?>和List<Object>之間的區(qū)別是什么?
答:
- List :完全沒有類型限制和賦值限定苹祟。
- List<Object> :看似用法與List一樣砸抛,但是在接受其他泛型賦值時會出現(xiàn)編譯錯誤评雌。
- List<?>:是一個泛型,在沒有賦值前锰悼,表示可以接受任何類型的集合賦值柳骄,但賦值之后不能往里面隨便添加元素团赏,但可以remove和clear箕般,并非immutable(不可變)集合。List<?>一般作為參數(shù)來接收外部集合舔清,或者返回一個具體元素類型的集合丝里,也稱為通配符集合。
- 什么是泛型中的限定通配符和非限定通配符 ?
答:
- 限定通配符<? extends T> <? super T>
- 非限定通配符<?>
5.泛型類型變量不能是基本數(shù)據(jù)類型
//error
ArrayList<double> arr1 = new ArrayList<>();
ArrayList<Double> arr2 = new ArrayList<>();
- 運行時類型查詢
ArrayList<String> arrayList=new ArrayList<String>();
if( arrayList instanceof ArrayList<String>) //擦除
if( arrayList instanceof ArrayList<?>)
- Java 的泛型本身是不支持協(xié)變和逆變的
- 可以使用泛型通配符 ? extends 來使泛型支持協(xié)變体谒,但是「只能讀取不能修改」杯聚,這里的修改僅指對泛型集合添加元素,如果是 remove(int index) 以及 clear 當(dāng)然是可以的抒痒。
- 可以使用泛型通配符 ? super 來使泛型支持逆變幌绍,但是「只能修改不能讀取」,這里說的不能讀取是指不能按照泛型類型讀取故响,你如果按照 Object 讀出來再強轉(zhuǎn)當(dāng)然也是可以的傀广。
- Java中數(shù)組是協(xié)變的