什么是泛型?
-
Java泛型(generics)是JDK5中引入的一种参数化类型特性
-
Java泛型(generics)是JDK5中引入的一个新特性,泛型提供了
编译时类型安全检测机制,
该机制允许程序员在编译时检测到非法的类型
泛型的本质是参数类型,也就是说所操作的数据类型被指定为一个参数
泛型不存在于JVM虚拟机
什么是参数化类型?
把类型当参数一样传递
数据类型只能是引用类型(泛型的副作用)
举个例子:
- List<T>中的”T”称为类型参数
- List<Person>中的"Person"称为实际类型参数
- "List<T>"整个成为泛型类型
- "List<Person>"整个称为参数化的类型ParameterizedType
为什么使用泛型,使用泛型的好处?
- 代码更健壮(只要编译期没有警告,那么运行期就不会出现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);//编译器就会不通过
- 代码更简洁,不用强转
//不使用泛型,需要强转
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
//使用泛型,不需要强转
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0); // no cast
- 代码更灵活,复用
// java.util.List中的排序方法sort,只要实现了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
泛型擦除
- 功能:保证了泛型不在运行时出现
- 类型消除应用的场合:
编译器会把泛型类型中所有的类型参数替换为它们的上(下)限,如果没有对类型参数做出限制,那么就替换为Object类型。因此,编译出的字节码仅仅包含了常规类,接口和方法。
在必要时插入类型转换以保持类型安全。
生成桥方法以在扩展泛型时保持多态性 - Bridge Methods 桥方法
当编译一个扩展参数化类的类,或一个实现了参数化接口的接口时,编译器有可能因此要创建一个合成方法,名为桥方法。它是类型擦除过程中的一部分
用一个简单的例子看一下Java是怎么处理泛型的
- 定义一个泛型接口
public interface Box<T> {
void set(T t);
T get();
}
- 利用javac命令获取字节码文件
public interface Box<T> {
void set(T var1);
T get();
}
- 利用javap -c命令查看生成的字节码,我们的T变成了Object类型。
public abstract interface test3/Box {
public abstract set(Ljava/lang/Object;)V
public abstract get()Ljava/lang/Object;
}
- 我们定义一个类去实现这个接口
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命令查看生成的字节码。可以看到我们的set和get还是构造方法,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>>实现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生成的字节码
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
}
- 可以看到,有两处地方进行强制类型转换,分别是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的字节码文件Box.class和查看生成的字节码
public interface Box<T> {
void set(T var1);
T get();
}
- 疑问:不是类型擦除之后变成Object了吗?怎么这里字节码文件还是T类型?其实这里看到的其实是签名而已,还保留定义的格式,对于分析字节码有好处。并不是真的擦除了,保存在类的常量池中。
/**
* 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虚拟机规范中为了响应在泛型类中如何获取传入的参数化类型等问题,引入了signature,LocalVariableTypeTable等新的属性来记录泛型信息,所以所谓的泛型类型擦除,仅仅是对方法的code属性中的字节码进行擦除,而原数据中还是保留了泛型信息的,这些信息被保存在class字节码的常量池中,使用了泛型的代码调用处会生成一个signature签名字段,signature指明了这个常量在常量池的地址,这样我们就找到了参数化类型。这样我们也知道 现在就明白了泛型擦除不是擦除全部
总结
-
QUESTION:Java泛型的原理?什么是泛型擦除机制?
-
ANSWER:Java的泛型是JDK5新引入的特性,为了向下兼容,虚拟机其实是不支持泛型,所以Java实现的是一种伪泛型机制,也就是说Java在编译期擦除了所有的泛型信息,这样Java就不需要产生新的类型到字节码,所有的泛型类型最终都是一种原始类型,在Java运行时根本就不存在泛型信息。
-
QUESTION:Java编译器具体是如何擦除泛型的
-
ANSWER:
- 检查泛型类型,获取目标类型
- 擦除类型变量,并替换为限定类型
如果泛型类型的类型变量没有限定(<T>),则用Object作为原始类型。如果有限定(<T extends XClass>),则用XClass作为原始类型如果有多个限定(T extends XClass1&XClass2),则使用第一个边界XClass1作为原始类 - 在必要时插入类型转换以保持类型安全
- 生成桥方法以在扩展时保持多态性
使用泛型以及泛型擦除带来的影响(副作用)
泛型类型变量不能使用基本数据类型
比如没有ArrayList<int>,只有ArrayList<Integer>.当类型擦除后,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>){
}
泛型在静态方法和静态类中的问题
因为泛型类中的泛型参数的实例化是在定义泛型类型对象
(比如ArrayList<Integer>)的时候指定的,而静态成员是不需要使用对象来调用的,所有对象都没创建,如何确定这个泛型参数是什么
//下面两个会报错,因为泛型参数是要创建对象时确定
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);
}
没法创建泛型实例
因为类型不确定
class Test02 {
//无法创建一个类型参数的实例。下面会报错
public static <E> void append(List<E> list) {
// E elem = new E(); // compile-time error
// list.add(elem);
}
//通过反射创建一个参数化类型的实例
public static <E> void append(List<E> list, Class<E> cls) throws Exception {
E elem = cls.newInstance(); // OK
list.add(elem);
}
}
没有泛型数组
因为数组是协变,擦除后就没法满足数组协变的原则
// 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的父类,Fruit[]是Apple[]的父类,这就是数组的协变
//如果加入泛型后,由于擦除机制,运行时将无法知道数组的类型
Plate<?>[] plates = new Plate<?>[10];//这是可以的
泛型,继承和子类型
给定两种具体的类型A和B(例如Fruit和Apple),
无论A和B是否相关,
MyClass<A>与MyClass<B>都没半毛钱关系,
它们的公共父对象是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的泛型
- 使用关键字 out 来支持协变,等同于 Java 中的上界通配符 ? extends。
- 使用关键字 in 来支持逆变,等同于 Java 中的下界通配符 ? super。
var textViews: List<out TextView>
var textViews: List<in TextView>
声明处的 out 和 in
Kotlin 提供了另外一种写法:可以在声明类的时候,给泛型符号加上 out 关键字,表明泛型参数 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关键字
Java 中声明类或接口的时候,可以使用 extends 来设置边界,将泛型类型参数限制为某个类型的子集,同时这个边界是可以设置多个,用 & 符号连接:
//T 的类型必须同时是 B 和 C 的子类型
class A<T extends B & C>{
}
在Kotlin中
//T 的类型必须同时是 B 和 C 的子类型
class A<T> where T : B, T : C
reified关键字
inline fun <reified T> printIfTypeMatch(item: Any) {
if (item is T) { // 👈 这里就不会在提示错误了
println(item)
}
}
Kotlin 泛型与 Java 泛型不一致的地方
- Java 里的数组是支持协变的,而 Kotlin 中的数组 Array 不支持协变。
这是因为在 Kotlin 中数组是用 Array 类来表示的,这个 Array 类使用泛型就和集合类一样,所以不支持协变。
- Java 中的 List 接口不支持协变,而 Kotlin 中的 List 接口支持协变。
Java 中的 List 不支持协变,原因在上文已经讲过了,需要使用泛型通配符来解决。
在 Kotlin 中,实际上 MutableList 接口才相当于 Java 的 List。Kotlin 中的 List 接口实现了只读操作,没有写操作,所以不会有类型安全上的问题,自然可以支持协变。
面试常问
- Array中可以用泛型吗?
答:不能
- 泛型类型引用传递问题
问:你可以把List<String>传递给一个接受List<Object>参数的方法吗?
ArrayList<String> arrayList1=new ArrayList<Object>();
ArrayList<Object> arrayList1=new ArrayList<String>();
答:不能。没有半毛钱关系
- Java中List<?>和List<Object>之间的区别是什么?
答:
- List :完全没有类型限制和赋值限定。
- List<Object> :看似用法与List一样,但是在接受其他泛型赋值时会出现编译错误。
- List<?>:是一个泛型,在没有赋值前,表示可以接受任何类型的集合赋值,但赋值之后不能往里面随便添加元素,但可以remove和clear,并非immutable(不可变)集合。List<?>一般作为参数来接收外部集合,或者返回一个具体元素类型的集合,也称为通配符集合。
- 什么是泛型中的限定通配符和非限定通配符 ?
答:
- 限定通配符<? extends T> <? super T>
- 非限定通配符<?>
5.泛型类型变量不能是基本数据类型
//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 的泛型本身是不支持协变和逆变的
- 可以使用泛型通配符 ? extends 来使泛型支持协变,但是「只能读取不能修改」,这里的修改仅指对泛型集合添加元素,如果是 remove(int index) 以及 clear 当然是可以的。
- 可以使用泛型通配符 ? super 来使泛型支持逆变,但是「只能修改不能读取」,这里说的不能读取是指不能按照泛型类型读取,你如果按照 Object 读出来再强转当然也是可以的。
- Java中数组是协变的
网友评论