Java泛型(generics)是JDK5中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数类型,也就是说所操作的数据类型被指定为一个参数。为了向下兼容,虚拟机其实是不支持泛型,所以Java实现的是一种伪泛型机制,也就是说Java在编译期擦除了所有的泛型信息,这样Java就不需要产生新的类型到字节码,所有的泛型类型最终都是一种原始类型,在Java运行时根本就不存在泛型信息。
为什么使用泛型?
泛型可以增强编译时错误检测,减少因类型问题引发的运行时异常(只要编译期没有警告,那么运行期就不会出现ClassCastException
)
泛型具有更强的类型检查
泛型可以避免类型转换
泛型可以泛型算法,增加代码复用性
Java中的泛型
泛型类
泛型接口
泛型方法
泛型类格式:class name<T1, T2, ..., Tn>
定义格式:private <K,V> boolean compare(Pair<K,V> p1, Pair<K,V> p2)
调用格式:Util.<K, V>compare(p1,p2)
常见类型变量名称
最常见的类型变量名有:
E:元素(在Java集合框架中有广泛的应用)
K:键
N:数字
T:类型
V:值
S,U,V 等:第二,第三,第四个类型
参数化类型
- 参数化类型:
把类型当参数一样传递
<数据类型>只能是引用类型(泛型的副作用)
- 举个例子:
Plate<T>
中的”T”称为类型参数
Plate<Banana>
中的”Banana”称为实际类型参数
Plate<T>
整个称为泛型类型
Plate<Banana>
整个称为参数化的类型ParameterizedType
类型参数VS类型实参
“类型参数”与“类型变量”的不同
Foo<T>
中的T为类型参数
Foo<String>
中的String为类型变量
The Diamond钻石运算符
JDK7以下版本
Box<Integer> integerBox = new Box<Integer>();
JDK7及以上版本
Box<Integer> integerBox1 = new Box<>();// The Diamond(菱形) 类型推断
原始类型
缺少实际类型变量的泛型就是一个原始类型
举例:
class Box<T>{}
Box b = new Box(); //这个Box就是Box<T>的原始类型
泛型擦除
ArrayList<Int> arr1 = new ArrayList();
ArrayList<String> arr2 = new ArrayList();
//result true
System.out.println(arr1.getClass() == arr2.getClass()); // true
由此可见通过运行时获取的类信息是完全一致的,泛型类型被擦除了。擦除后只留下原始类型,这里也就是ArrayList
- 代码查看:
- 示例:
ConditionalPlate
package com;
import java.util.ArrayList;
import java.util.List;
public class ConditionalPlate<T> implements Plate<T> {
private List<T> items = new ArrayList<T>(10);
public ConditionalPlate(){
}
@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;
}
}
@Override
public String toString() {
return "Plate{" +
"items=" + items +
'}';
}
// @Override
// public boolean equals(T t) {
// return super.equals(t);
// }
@Override
public boolean equals(Object obj) {
return super.equals(obj);
}
}
Plate
package com;
public interface Plate<T> {
public void set(T t);
public T get();
}
-
javac
编译源文件 -
javap -c
查看生成的字节码
PS E:\project\com> javac .\ConditionalPlate.java .\Plate.java
PS E:\project\com> javap -c .\ConditionalPlate.class
Compiled from "ConditionalPlate.java"
public class com.ConditionalPlate<T> implements com.Plate<T> {
public com.ConditionalPlate();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: new #2 // class java/util/ArrayList
8: dup
9: bipush 10
11: invokespecial #3 // Method java/util/ArrayList."<init>":(I)V
14: putfield #4 // Field items:Ljava/util/List;
17: return
public void set(T);
Code:
0: aload_0
1: getfield #4 // Field items:Ljava/util/List;
4: aload_1
5: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
10: pop
11: return
public T get();
Code:
0: aload_0
1: getfield #4 // Field items:Ljava/util/List;
4: invokeinterface #6, 1 // InterfaceMethod java/util/List.size:()I
9: iconst_1
10: isub
11: istore_1
12: iload_1
13: iflt 27
16: aload_0
17: getfield #4 // Field items:Ljava/util/List;
20: iload_1
21: invokeinterface #7, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
26: areturn
27: aconst_null
28: areturn
public java.lang.String toString();
Code:
0: aload_0
1: getfield #4 // Field items:Ljava/util/List;
4: invokedynamic #8, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/util/List;)Ljava/lang/String;
9: areturn
public boolean equals(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: invokespecial #9 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
5: ireturn
}
1: invokespecial #1 // Method java/lang/Object."<init>":()V
可以看到ConditionalPlate构造函数里显示的类型已经是object
功能:保证了泛型不在运行时出现
- 类型消除应用的场合:
编译器会把泛型类型中所有的类型参数替换为它们的上(下)限,如果没有对类型参数做出限制,那么就替换为Object类型。因此,编译出的字节码仅仅包含了常规类,接口和方法。
在必要时插入类型转换以保持类型安全。
生成桥方法以在扩展泛型时保持多态性
- Bridge Methods 桥方法
当编译一个扩展参数化类的类,或一个实现了参数化接口的接口时,编译器有可能因此要创建一个合成方法,名为桥方法。它是类型擦除过程中的一部分
- 示例代码查看桥方法:
BananaPlate
package com;
import java.util.ArrayList;
import java.util.List;
public class BananaPlate implements Plate<Banana> {
private List<Banana> items = new ArrayList<>(10);
@Override
public void set(Banana banana) {
items.add(banana);
}
@Override
public Banana get() {
return items.get(0);
}
}
javap -c
查看字节码
PS E:\project\com> javap -c .\BananaPlate.class
Compiled from "BananaPlate.java"
public class com.BananaPlate implements com.Plate<com.Banana> {
public com.BananaPlate();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: new #2 // class java/util/ArrayList
8: dup
9: bipush 10
11: invokespecial #3 // Method java/util/ArrayList."<init>":(I)V
14: putfield #4 // Field items:Ljava/util/List;
17: return
public void set(com.Banana);
Code:
0: aload_0
1: getfield #4 // Field items:Ljava/util/List;
4: aload_1
5: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
10: pop
11: return
public com.Banana get();
Code:
0: aload_0
1: getfield #4 // Field items:Ljava/util/List;
4: iconst_0
5: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
10: checkcast #7 // class com/Banana
13: areturn
public java.lang.Object get();
Code:
0: aload_0
1: invokevirtual #8 // Method get:()Lcom/Banana;
4: areturn
public void set(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: checkcast #7 // class com/Banana
5: invokevirtual #9 // Method set:(Lcom/Banana;)V
8: return
}
查看字节码可以看到有两个get
函数,一个是自定义返回值的get
,一个是object默认的get
public com.Banana get();
10: checkcast #7 // class com/Banana
通过字节码可以看到这里的返回值类型被强转了
泛型擦除的残留
查看class文件的时候泛型是没有擦除的,还是能看到泛型
这里看到的其实是签名而已,还保留了定义的格式,这样子,对于分析字节码是有好处的
泛型方法的类型擦除
消除方法:同对泛型类的处理
无限制:替换为Object
有限制:替换为第一受限类型
Java编译器具体是如何擦除泛型的?
- 检查泛型类型,获取目标类型
- 擦除类型变量,并替换为限定类型
如果泛型类型的类型变量没有限定(<T>
),则用Object作为原始类型
如果有限定(<T extends XClass>
),则用XClass作为原始类型
如果有多个限定(T extends XClass1&XClass2)
,则使用第一个边界XClass1作为原始类 - 在必要时插入类型转换以保持类型安全
- 生成桥方法以在扩展时保持多态性
不可具体化类型
可具体化类型和不可具体化类型的定义:
可具体化类型:就是一个可在整个运行时可知其类型信息的类型。
包括:基本类型、非泛型类型、原始类型和调用的非受限通配符。
不可具体化类型:无法整个运行时可知其类型信息的类型,其类型信息已在编译时被擦除:
例如:List<String>
和List<Number>
,JVM无法在运行时分辨这两者
堆污染:
发生时机:当一个参数化类型变量引用了一个对象,而这个对象并非此变量的参数化类型时,堆污染就会发生。
分模块对代码进行分别编译,那就很难检测出潜在的堆污染,应该同时编译
带泛型的可变参数问题:
T...将会被翻译为T[]
,根据类型擦除,进一步会被处理为Object[]
,这样就可能造成潜在的堆污染
避免堆污染警告
@SafeVarargs
:当你确定操作不会带来堆污染时,使用此注释关闭警告
@SuppressWarnings({"unchecked", "varargs"})
:强制关闭警告弹出(不建议这么做)
泛型,继承和子类型
给定两种具体的类型A和B(例如Fruit和Apple),
无论A和B是否相关,
MyClass<A>
与MyClass<B>
都没半毛钱关系,
它们的公共父对象是Object
受限的类型参数
功能:对泛型变量的范围作出限制
格式:
单一限制:<U extends Number>
多种限制:<U extends A & B & C>
extends表达的意义:这里指的是广义上“扩展”,兼有“类继承”和“接口实现”之意
多种限制下的格式语法要求:如果上限类型是一个类,必须第一位标出,否则编译错误
泛型算法实现的关键:利用受限类型参数
通配符
泛型中的问号符“?”名为“通配符”
受上下限控制的通配符
通配符匹配
通配符的适用范围:
参数类型
字段类型
局部变量类型
返回值类型(但返回一个具体类型的值更好)
非受限通配符
两个关键使用场合:
写一个方法,而这方法的实现可以利用Object类中提供的功能时
泛型类中的方法不依赖类型参数时
如List.size()
方法,它并不关心List
中元素的具体类型
List<XXX>
是List<?>
的一个子类型
理解List<Object>
和List<?>
的不同:差在NULL
处理,前者不支持,而后者却可接受一个null
入表
受上限控制的通配符
语法格式:<? extends XXX>
优点:扩大兼容的范围
List<XXX>
要比List<? extends XXX>
更加严格,因为前者仅能匹配XXX列表,然而后者却可同时匹配XXX及其子类的列表
关键词:及其
缺点:
不能set任何元素,但是可以set(null)
可以
get类型也不是随便一个子类都能接收
Plate<? extends Fruit> fruitPlate = xiaoLiMa.getSnack(applePlate);
//这时候小明再从盘子里面那苹果吃,发现不行了
xiaoMing.eat((Apple) fruitPlate.get());
//实际上
Fruit fruit = fruitPlate.get();
Object object = fruitPlate.get();
但是这种不是严格的限制,反射可破
public Plate<? extends Fruit> getSnack(Plate<Apple> applePlate){
Plate<? extends Fruit> fruitPlate = applePlate;
//不能存放任何元素
try{
Method set = fruitPlate
.getClass()
.getMethod("set",Object.class);
set.invoke(fruitPlate,new Banana());
//set.invoke(fruitPlate,new Beef());//什么都能放了 安全没法保证
}catch(Exception e){}
// fruitPlate.set(new Apple());
// fruitPlate.set(new Banana());
//放null还是可以
fruitPlate.set(null);
return fruitPlate;
}
<? extends T>
上界通配符 相当于”只读“,但是通过反射可以写数据进去
反射破坏了泛型的特性了,当拿出来的时候不能转成T
就会报错了
有下限通配符
功能:限定了类型的下限,也就它必须为某类型的父类
格式:<? super A>
List<XXX>
比List<? super XXX>
要更加严格。因为前者仅仅兼容XXX类型的列表,而后者却兼容XXX及其任何XXX超类的列表
关键词:及其
缺点:
只能set
数据,不能get
数据
public static void scene03() {
Plate<? super Fruit> lowerfruitPlate = new AIPlate<Food>();
lowerfruitPlate.set(new Apple());
lowerfruitPlate.set(new Banana());
// lowerfruitPlate.set(new Food());
// Fruit newFruit1 = lowerfruitPlate.get();
// Apple newFruit3 = lowerfruitPlate.get();
Object newFruit2 = lowerfruitPlate.get();
}
可以把Plate<Fruit>
以及它的基类Plate<Food>
转成Plate<? super Fruit>
它可以存数据但是取出来后 泛型信息丢失了,只能用Object存放
<?>
不能存也不能取
public static void scene05() {
//<?> == <? extends Object>
Plate<?> fruitPlate = new AIPlate<Apple>();
// Fruit fruit = fruitPlate.get();
// fruitPlate.set(new Apple());
fruitPlate.toString();
Object object = fruitPlate.get();
fruitPlate.set(null);
}
Plate<?>
其实就是Plate<? extends Object>
Java泛型PECS原则
如果你只需要从集合中获得类型T , 使用<? extends T>
通配符
如果你只需要将类型T放到集合中, 使用<? super T>
通配符
如果你既要获取又要放置元素,则不使用任何通配符。例如List<Apple>
PECS即 Producer extends Consumer super, 为了便于记忆。
- 为何要PECS原则?
提升了API的灵活性
示例:
public static void scen07() {
List<Apple> src = new ArrayList<>();
src.add(new Apple(1));
List<Apple> dest = new ArrayList<>(10);
dest.add(new Apple(2));
System.out.println(dest);
copy(dest,src);
System.out.println(dest);
List<Banana> src1 = new ArrayList<>();
src1.add(new Banana(1));
List<Banana> dest1 = new ArrayList<>(10);
dest1.add(new Banana(2));
copy1(dest1,src1);
List<Fruit> dest2 = new ArrayList<>(10);
dest2.add(new Banana());
// List<Apple> src = new ArrayList<>();
// List<Food> dest2 = new ArrayList<>(10);
// Test1.<Food>copy2(dest2,src);
Test1.<Fruit>copy3(dest2,src1);
}
public static void copy(List<Apple> dest, List<Apple> src) {
Collections.copy(dest,src);
}
public static <T> void copy1(List<T> dest, List<T> src) {
Collections.copy(dest,src);
}
public static <T> void copy2(List<? super T> dest, List<T> src) {
Collections.copy(dest,src);
}
public static <T> void copy3(List<? super T> dest, List<? extends T> src) {
Collections.copy(dest,src);
}
通配符捕获
void foo(List<?> i) {
fooHelper(i);
}
/**
* 在此示例中,代码正在尝试执行安全操作,那么如何解决编译器错误?
* 你可以通过编写捕获通配符的私有帮助器方法来修复它。在这种情况下,
* 你可以通过创建私有帮助器方法fooHelper来解决此问题
*
* 由于使用了辅助方法,编译器在调用中使用推断来确定T是CAP#1(捕获变量)。该示例现在可以成功编译。
* 按照约定,辅助方法通常命名为originalMethodNameHelper
*/
private <T> void fooHelper(List<T> l) {
l.set(0, l.get(0));
}
泛型不是被擦除了吗? 那为何还与反射有关?
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Map;
/**
* 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
}
}
泛型约束
- 无法利用原始类型来创建泛型
解决方法:使用它们的包装类
- 无法创建类型参数的实例
变通方案:利用反射就是可以
- 无法创建参数化类型的静态变量
原因:静态变量是类所有,共享决定了其必须能确定。但多个类型作为参数传入此类的多个实例时,就会无法确定究竟赋予此静态变量哪个实例对象所传入的参数了
无法对参数化类型使用转换或者instanceof关键字 ,但需要注意通配符的情况
无法创建参数化类型的数组
无法创建、捕获或是抛出参数化类型对象 ,但却可以在throw后使用类型参数
当一个方法的所有重载方法的形参类型擦除后,如果它们具有了相同的原始类型,那么此方法不可重载
原因:此情境下,类型擦除会产生两个同签名的方法
使用泛型以及泛型擦除带来的影响(副作用)
泛型类型变量不能使用基本数据类型
比如没有ArrayList<int>
,只有ArrayList<Integer>
.当类型擦除后,ArrayList
的原始类中的类型变量(T)替换成Object,但Object类型不能存放int值
不能使用instanceof 运算符
ArrayList<String> strings = new ArrayList<>();
if(strings instanceof ArrayList<?>){} //ArrayList<?>可以
// if(strings instanceof ArrayList<String>){ } ArrayList<String> 不可以
因为擦除后,ArrayList<String>
只剩下原始类型,泛型信息String不存在了,所有没法使用instanceof
泛型在静态方法和静态类中的问题
class Test2<T>{
// public static T one; //不可以
// public static T test(T t){} //不可以
public static <T> T test1(T t){return t;}
}
因为泛型类中的泛型参数的实例化是在定义泛型类型对象(比如ArrayList<Integer>
)的时候指定的,而静态成员是不需要使用对象来调用的,所有对象都没创建,如何确定这个泛型参数是什么
泛型类型中的方法冲突
// @Override
// public boolean equals(T t) {
// return super.equals(t);
// }
@Override
public boolean equals(Object obj) {
return super.equals(obj);
}
因为擦除后两个equals方法变成一样的了
没法创建泛型实例
因为类型不确定
//2. Cannot Create Instances of Type Parameters 无法创建类型参数的实例
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);
}
}
没有泛型数组
因为数组是协变,擦除后就没法满足数组协变的原则
public static <T> void scene05() {
// 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];//这是可以的
}