重学Java泛型
一丶从字节码层面看范型擦除
public class Type1<T> {
private T t;
}
使用jclasslib插件查看其字节码:
可以看到 t属性的类型是List<Obeject>
可以知道Java泛型确实通过类型擦除来实现,所以字节码中没有类型信息。
二丶泛型信息存储于常量池
public class Type2 {
List mylist; //mylist 字段的GenericType是Class 而不是ParameterizeType
}
使用idea查看字节码信息
可以看到mylist字段的签名是一个List类型
public class Type3 {
List<String> mylist;
}
对于Type3我们可以看到类型是一个List<String>
签名索引指向常量池中,说明范型信息存储在常量池
三丶复杂类型如何使用Json序列化与其原理
1.使用TypeReference(alibaba fastJson包,其他api有对应的方法)
//复杂类型 List嵌套List
List<List<String>> lists= Arrays.asList(Collections.singletonList("1")
,Collections.singletonList("2")
,Collections.singletonList("3"));
String s = JSON.toJSONString(lists);
List<List<String>> list = (List<List<String>>)JSON.parseObject(s, List.class);//强转并不报错 但是实际类型是List<JsonArray>
TypeReference<List<List<String>>> type = new TypeReference<List<List<String>>>() {}; //注意这里是匿名内部类
List<List<String>> lists1 = JSON.parseObject(s, type);
System.out.println(lists1);//类型就是List<List<String>>
2.原理
new TypeReference<List<List<String>>>() {}
其实是new 了一个对象 这个对象继承自TypeReference,相当于new了一个确切类型(TypeReference<List<List<String>>>
)类的子类,已经明确知道类型了,jvm就不会进行类型擦除,类型信息会被保存下来
如下面这个类 继承子LinkedList<String>
public class Type4 extends LinkedList<String> {
}
对应的字节码内容
在签名信息里确切的知道了父类类型是LinkedList<String>
3.TypeReference源码
protected TypeReference(){
//拿父类的通用类型
Type superClass = getClass().getGenericSuperclass();
//拿父类真实类型的第一个 也就是第一个类型 就是 List<List<String>>
Type type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
//缓存
Type cachedType = classTypeCache.get(type);
if (cachedType == null) {
classTypeCache.putIfAbsent(type, type);
cachedType = classTypeCache.get(type);
}
this.type = cachedType;
}
四丶泛型擦除
1.定义
Java 泛型擦除(类型擦除)是指在编译器处理带泛型定义的类、接口或方法时,会在字节码指令集里抹去全部泛型类型信息,泛型被擦除后在字节码里只保留泛型的原始类型(raw type)。
原始类型是指抹去泛型信息后的类型,在 Java 语言中,它必须是一个引用类型(非基本数据类型),一般而言,它对应的是泛型的定义上界。
示例:<T> 中的 T 对应的原始泛型是 Object,<T extends A> 对应的原始类型就是 A。
2.泛型擦除验证
如下代码,我们定义了一些类,然后反射获取这些类当中create方法的出参,并打印分析
static class A {
}
//返回值上界是A
static abstract class B<T extends A> {
abstract T create();
}
//返回值没有上界,或者可以视为T extends Object
static abstract class C<T> {
abstract T create();
}
// T 要求同时是A的子类且实现Comparable
static abstract class D<T extends A & Comparable<?>> {
abstract T create();
}
//泛型多限制时 类必须在接口前
// static abstract class E<T extends Comparable<A> & A> {
// abstract T create();
// }
//T 要求实现Serializable 和 Comparable
static abstract class E<T extends Serializable & Comparable<?>> {
abstract T create();
}
//T 要求实现 Comparable 和 Serializable
static abstract class F<T extends Comparable<?> & Serializable> {
abstract T create();
}
//反射获取方法返回值 并且大于
static void reflectCreateMethodInfoPrint(Class<?> clazz, String methodName, Class<?>... paramClass) throws Exception {
Method createMethod = clazz.getDeclaredMethod(methodName, paramClass);
System.out.println("===============");
System.out.println("当前class:" + clazz.getSimpleName());
System.out.println(methodName + "方法返回值:" + createMethod.getReturnType().getSimpleName());
}
public static void main(String[] args) throws Exception {
//泛型擦除反射获取方法返回值
reflectCreateMethodInfoPrint(B.class, "create");
reflectCreateMethodInfoPrint(C.class, "create");
reflectCreateMethodInfoPrint(D.class, "create");
reflectCreateMethodInfoPrint(E.class, "create");
reflectCreateMethodInfoPrint(F.class, "create");
}
1)对于B类和D类
打印结果是
我们可以理解为编译器直接将方法的返回值设置为T类型的上界也就是A
2)对于C类
打印结果是
我们可以理解为编译器直接将方法的返回值设置为T类型的上界,泛型没有指定上界 那么就是Object
3)对E和F类
打印结果是
在要求泛型实现多个接口时,编译器默认将返回值设置成最左接口的类型
3.泛型擦除导致的问题
-
泛型类型变量不能是基本数据类型
泛型类型变量只能是引用类型,不能是 Java 中的 8 种基本类型(
char
、byte
、short
、int
、long
、boolean
、float
、double
)。
以 List 为例,只能使用List<Integer>
,但不能使用List<int>
,因为在进行类型擦除后,List 的原始类型会变为 Object,而 Object 类型不能存储 int 类型的值,只能存储引用类型 Integer 的值。 -
类型的丢失
对于泛型对象使用 instanceof 进行类型判断的时候就不能使用具体的类型,而只能使用通配符?
,示例如下所示:
ArrayList<String> list = new ArrayList<>();
System.out.println(list instanceof ArrayList);//true
System.out.println(list instanceof ArrayList<?>);//true
//无法编译
// System.out.println(list instanceof ArrayList<String>);
-
catch中不能使用泛型异常类
假设有一个泛型异常类的定义 MyException<T>, try{ }catch (MyException<String> e1) catch ( MyException<Integer>e2){…} //MyException<String> 和 MyException<Integer> 都会被擦除为 MyException<Object>,因此,两个 catch 的条件就相同了,所以这种写法是不允许的。
//也不允许在 catch 子句中使用泛型变量 public <T extends Throwable> void test(T t) { try{ ... }catch(T e) { //编译错误 ... } catch(IOException e){ } } //假设上述代码能通过编译,由于擦除的存在,T 会被擦除为 Throwable。由于异常捕获的原则为:先捕获子类类型的异常,再捕获父类类型的异常。
即使T擦除之后是下一个catch的子类,也是不行的
-
泛型类的静态方法与属性不能使用类上面的泛型
由于泛型类中的泛型参数的实例化是在实例化对象的时候指定的,而静态变量和静态方法的使用是不需要实例化对象的,显然这二者是矛盾的。
如果没有实例化对象,而直接使用泛型类型的静态变量,那么此时是无法确定其类型的。
-
静态方法使用泛型
五丶桥接方法
1.定义:
桥接方法是 JDK 1.5 引入泛型后,为了使Java的泛型方法生成的字节码和 1.5 版本前的字节码相兼容,由编译器自动生成的方法。
2.如何判断是否是桥接方法
可以通过 Method.isBridge()
来判断一个方法是不是桥接方法。
3.代码分析帮助理解桥接方法
public static void main(String[] args) {
//桥接方法
printMethodByName("getData", BridgeMethodT.class);
printMethodByName("setData", BridgeMethodT.class);
printMethodByName("getData", BridgeMethodSubT.class);
printMethodByName("setData", BridgeMethodSubT.class);
printMethodByName("getData", BridgeMethodString.class);
printMethodByName("setData", BridgeMethodString.class);
}
//反射分析clazz 中的methodName方法
static void printMethodByName(String methodName, Class<?> clazz) {
//所有的放啊
Method[] methods = clazz.getMethods();
System.out.println("=============");
System.out.println("当前分析类:" + clazz.getSimpleName());
System.out.println("当前方法名称:" + methodName);
for (Method method : methods) {
//如果方法名字相同
if (method.getName().equals(methodName)) {
//是否是桥接方法
System.out.println("是否是桥接方法:" + method.isBridge());
//返回值类型
System.out.println("当前方法返回值:" + method.getReturnType());
//所有的方法入参类型
Parameter[] allParams = method.getParameters();
//打印第一个入参 应为setData具备第一个入参
if (allParams.length > 0) {
System.out.println("当前方法入参:" + allParams[0].getType().getSimpleName());
} else {
System.out.println("无入参");
}
//方法分割线
System.out.println("----");
}
}
}
static class BridgeMethodT<T> {
T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
static class BridgeMethodSubT<T> extends BridgeMethodT<T> {
@Override
public T getData() {
return super.getData();
}
@Override
public void setData(T data) {
super.setData(data);
}
}
static class BridgeMethodString extends BridgeMethodT<String> {
@Override
public String getData() {
return super.getData();
}
@Override
public void setData(String data) {
super.setData(data);
}
}
输出
=============
当前分析类:BridgeMethodT
当前方法名称:getData
是否是桥接方法:false
当前方法返回值:class java.lang.Object
无入参
----
=============
当前分析类:BridgeMethodT
当前方法名称:setData
是否是桥接方法:false
当前方法返回值:void
当前方法入参:Object //BridgeMethodT的方法入参被擦除成Object
----
=============
当前分析类:BridgeMethodSubT
当前方法名称:getData
是否是桥接方法:false
当前方法返回值:class java.lang.Object
无入参
----
=============
当前分析类:BridgeMethodSubT
当前方法名称:setData
是否是桥接方法:false
当前方法返回值:void
当前方法入参:Object //方法入参被擦除成Object
----
=============
当前分析类:BridgeMethodString
当前方法名称:getData
是否是桥接方法:true
当前方法返回值:class java.lang.Object //这个是编译器自动生成的桥接方法 实际是调用父类的getData方法
无入参
----
是否是桥接方法:false
当前方法返回值:class java.lang.String //我们重写的方法
无入参
----
=============
当前分析类:BridgeMethodString
当前方法名称:setData
是否是桥接方法:true
当前方法返回值:void
当前方法入参:Object //这个是编译器自动生成的桥接方法 实际是调用父类的setData方法
----
是否是桥接方法:false
当前方法返回值:void
当前方法入参:String //我们重写的方法
----
可以得出结论 对于getData子类BridgeMethodString存在两个方法
Object getData()
String getData()
这似乎违背了方法签名(方法名+参数列表确定为一个一个方法),这里getData只是出参不同怎么可以存在昵——程序员不能写违背方法签名的方法,但是jvm允许
JVM会用参数类型和返回类型来确定一个方法。 一旦编译器通某种方式自己编译出方法签名一样的两个方法 (只能编译器自己来创造这种奇迹,我们程序员却不能人为的编写这种代码)。JVM还是能够分清楚这些方法的,前提是需要返回类型不一样。
看到这里在理解下——桥接方法是 JDK 1.5 引入泛型后,为了使Java的泛型方法生成的字节码和 1.5 版本前的字节码相兼容,由编译器自动生成的方法。
setData的调用可以理解成
BridgeMethodString 存在两个setData 一个接受object类型, 一个接受string 类型
BridgeMethodString.setData("1")
其实是先掉头setData(Object)然后强转成String 后执行setData(String)
4.桥方法带来的问题
1.不小心调用到桥方法
BridgeMethodT b = new BridgeMethodString();
invoke(b,"setData",new Object());
//抛出异常java.lang.ClassCastException: java.lang.Object cannot be cast to java.lang.String
//这里其实就是调用到了父类的setData 然后将Object 强转成String
//反射调用方法
public static void invoke(Object obj,String methodName,Object param1) throws InvocationTargetException, IllegalAccessException {
Method[] methods = obj.getClass().getMethods();
for (Method method : methods) {
if (method.getName().equals(methodName)){
//检验第一个参数类型和入参相同 这个时候上面调用传入的是new Object 这个时候就会掉调用到桥方法 将object强转成String
if (method.getParameters()
[0].getType().equals(param1.getClass())
method.invoke(obj,param1);
break;
}
}
}
看一下hutool是怎么解决的:
public static Method getMethodByName(Class<?> clazz, boolean ignoreCase, String methodName) throws SecurityException {
if (null == clazz || StrUtil.isBlank(methodName)) {
return null;
}
final Method[] methods = getMethods(clazz);
if (ArrayUtil.isNotEmpty(methods)) {
for (Method method : methods) {
if (StrUtil.equals(methodName, method.getName(), ignoreCase)
// 排除桥接方法 false == method.isBridge()
//可以通过method.isBridge() 来判断是否是桥接方法
&& false == method.isBridge()) {
return method;
}
}
}
return null;
}
2.桥方法导致重写方法冲突
六丶如何构造泛型数组
public static <T> T[] newTArray(Class<T> clazz, int len) {
//底层是一个native 方法 malloc开辟 单个clazz对象大小*长度+数据额外的空间
return (T[]) Array.newInstance(clazz, len);
}
public static <T> T[] newTArray2(Class<T> clazz, int len) {
ArrayList<T> ts = new ArrayList<>(len);
//调用 Arrays.copyOf
//底层调用Array.newInstance然后 System.arraycopy
return (T[]) ts.toArray();
}
七丶协变 逆变,通配符
1.定义
逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果A、B表示类型,f(⋅)表示类型转换操作
f(⋅)是逆变(contravariant)的,当A是B的子类的时有f(B)是f(A)的子类成立;
f(⋅)是协变(covariant)的,当A是B的子类时有f(A)是f(B)的子类成立;
f(⋅)是不变(invariant)的,当A是B的子类时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。
2.背景
//工人
abstract static class Worker {
abstract void work();
}
//清洁工
static class Cleaner extends Worker {
void clean() {
}
@Override
void work() {
}
}
//后端
static class BackEnd extends Worker {
void backEnd() {
}
@Override
void work() {
System.out.println("backEnd");
}
}
//java 后端
static class JavaCoder extends BackEnd {
void java() {
System.out.println("java");
}
}
//前端
static class FondEnd extends Worker {
void fondEnd() {
}
@Override
void work() {
System.out.println("fondEnd");
}
}
2.数组是协变的
f(⋅)是协变(covariant)的,当A是B的子类时有f(A)是f(B)的子类成立;
也就是说A是B的子类===》A【】是B【】的子类,如下
//数组支持协变
//A是B的子类 A[] 也是B[]的子类
BackEnd[] backEndArray = new BackEnd[10];
Worker[] workerArray = backEndArray;//说明BackEnd[] 是 Worker[]的子类 数组支持协变
System.out.println(workerArray.getClass().getComponentType());//BackEnd
System.out.println(backEndArray.getClass().isAssignableFrom(workerArray.getClass()));//true
workerArray[0] = new Cleaner();//java.lang.ArrayStoreException
以上代码可以通过编译,但是运行抛出java.lang.ArrayStoreException
3.泛型是不变的
f(⋅)是不变(invariant)的,当A是B的子类时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。
也就是说A是B的子类,List《A》 和List《B》没有任何关系
List<Worker> workerList = new ArrayList<>();
List<BackEnd> backEndList = new ArrayList<>();
workerList = backEndList;//无法编译
backEndList = workerList;//无法编译
//因为泛型擦除的时候 都是List<object> 如果支持这种操作 那么将导致
backEndList.get(1) 可能是一个清洁工 强转成BackEnd 失败
4.泛型上界extends 是协变的
f(⋅)是协变(covariant)的,当A是B的子类时有f(A)是f(B)的子类成立;
//?extends worker的子类 是BackEnd的子类
ArrayList<BackEnd> backEnds = new ArrayList<>();
List<? extends Worker> workers = backEnds;//可以
//但是协变之后 不可使用任何入参是泛型的方法
workers.add(new BackEnd());//编译错误
workers.add(new JavaCoder());//编译错误
//编译器无法确定所持有的具体类型是什么
//所以一旦执行这种类型的向上转型,你就将丢失掉向其中传递任何对象的能力。
5.泛型下界super是逆变的
f(⋅)是逆变(contravariant)的,当A是B的子类的时有f(B)是f(A)的子类成立;
// worker 是 ? super Backend 的子类
List<Worker> workers1 = new ArrayList<>();
//==>List<Worker> 是 List<? super BackEnd>的子类
List<? super BackEnd> backEndArrayList = workers1;
//可以加入任何BackEnd的父类
backEndArrayList.add(new JavaCoder());
//但是遍历的时候 得到的是object 因为Object 也是BackEnd的父类
//无法保证集合里面的元素到底是具体什么类型
for (Object o : backEndArrayList) {
}
6.producer-extends, consumer-super
从数据流来看,extends是限制数据来源的(生产者),而super是限制数据流入的(消费者)
-
作为生产者的时候使用extends
//越界返回 Optional.empty() 反之返回对应位置的optional保证 public static <T> Optional<T> outBoundsReturnNull(List<? extends T> collection, int index) { if (collection == null || index < 0 || index >= collection.size()) { return Optional.empty(); } return Optional.ofNullable(collection.get(index)); }
-
作为消费者使用super
//尝试添加到指定位置 public static <T> boolean tryAddAtAppointIndex(List<? super T> list, int index, T value) { if (list == null) { return false; } if (index < 0 || index > list.size()) { return false; } list.add(index,value); return true; }
八丶反射与泛型
1.Type类
Type是Java当前所有类型的通用的顶层接口,包括
-
原始类型(Type)
不仅仅包含我们平常所指的类,还包括枚举、数组、注解等
-
参数化类型(ParameterizedType)
Map<K,V>
,Set<T>
,`这里的参数化指这些泛型可以像参数一样去指定
-
数组类型(GenericArrayType)
带有泛型的数组,即T[]
-
类型变量(TypeVariable)
比如 T a
-
基本类型(Class)
public interface Type {
//1.8后默认实现
default String getTypeName() {
return toString();
}
}
2.ParameterizedType
ParameterizedType 表示参数化类型,参数化类型即我们通常所说的泛型类型,例如 Collection<String>
。参数化类型在反射方法第一次需要时创建。创建参数化类型 p 时,解析 p 实例化的泛型类型声明,并递归创建 p 的所有类
2.1.getActualTypeArguments():
该方法返回参数化类型<>中的实际参数类型, 如 Map<String,Person> map 这个 ParameterizedType 返回的是 String 类,Person 类的全限定类名的 Type Array。注意: 该方法只返回最外层的<>中的类型,无论该<>内有多少个<>。
2.2.getOwnerType()
返回ParameterizedType类型所在的类的Type。如Map.Entry<String, Object>这个参数化类型返回的事Map(因为Map.Entry这个类型所在的类是Map)的类型。
2.3.getRawType()
返回的是当前这个 ParameterizedType 的类型。 如 Map<String,Person> map 这个 ParameterizedType 返回的是 Map 类的全限定类名的 Type 数组。
3.TypeVariable
类型变量,即泛型中的变量;例如:T、K、V等变量,可以表示任何类,TypeVariable代表着泛型中的变量,而ParameterizedType则代表整个泛型
3.1.getBounds
获取泛型的上限,如果没有指定那么默认是Object
3.2.getGenericDeclaration
获取声明该类型变量实体(即获取类,方法或构造器名)(java 只可以在这三个位置声明泛型)
3.3.getAnnotatedBouds
返回一个AnnotatedType对象的数组,表示使用类型来表示此TypeVariable表示的类型参数的上限。 数组中的对象的顺序对应于type参数的声明中的边界的顺序。 如果type参数声明没有边界,则返回长度为0的数组。
4.GenericArrayType
泛型数组类型,用来描述ParameterizedType、TypeVariable类型的数组;即List
4.1.getGenericComponentType
返回表示此数组的组件类型的Type
对象。
如果数组内部的元素既不是ParameterizedType也不是TypeVariable,那么该数组代表的Type则是一个数组Class,而不是GenericArrayType。
5.WildcardType
WildcardType表示通配符类型表达式,例如?
、? extends Number
、? super Integer
。
5.1.getUpperBounds
获取通配符的所有上边界(使用extends关键字),为了保持扩展返回数组
5.2.getLowerBounds
获取通配符的所有下边界(使用super关键字),为了保持扩展返回数组