Loading

重学Java泛型

系列文章目录和关于我

一丶从字节码层面看范型擦除

public class Type1<T> {
    private T t;
}

使用jclasslib插件查看其字节码:

image-20220418191442894

可以看到 t属性的类型是List<Obeject>可以知道Java泛型确实通过类型擦除来实现,所以字节码中没有类型信息。

二丶泛型信息存储于常量池

public class Type2 {

    List mylist; //mylist 字段的GenericType是Class  而不是ParameterizeType
}

使用idea查看字节码信息

image-20220418192254161

可以看到mylist字段的签名是一个List类型

public class Type3 {

    List<String> mylist;
}

image-20220418192426737

对于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> {
}

对应的字节码内容image-20220418194055227

在签名信息里确切的知道了父类类型是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类

打印结果是

image-20220416152043366

image-20220416152313452

我们可以理解为编译器直接将方法的返回值设置为T类型的上界也就是A

2)对于C类

打印结果是

image-20220416152223422

我们可以理解为编译器直接将方法的返回值设置为T类型的上界,泛型没有指定上界 那么就是Object

3)对E和F类

打印结果是

image-20220416152345573

在要求泛型实现多个接口时,编译器默认将返回值设置成最左接口的类型

3.泛型擦除导致的问题

  1. 泛型类型变量不能是基本数据类型

    泛型类型变量只能是引用类型,不能是 Java 中的 8 种基本类型(charbyteshortintlongbooleanfloatdouble)。
    以 List 为例,只能使用 List<Integer>,但不能使用 List<int>,因为在进行类型擦除后,List 的原始类型会变为 Object,而 Object 类型不能存储 int 类型的值,只能存储引用类型 Integer 的值。

  2. 类型的丢失

对于泛型对象使用 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>);
  1. 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的子类,也是不行的

    image-20220416154110971

  2. 泛型类的静态方法与属性不能使用类上面的泛型

    image-20220416154234805

    由于泛型类中的泛型参数的实例化是在实例化对象的时候指定的,而静态变量和静态方法的使用是不需要实例化对象的,显然这二者是矛盾的。

    如果没有实例化对象,而直接使用泛型类型的静态变量,那么此时是无法确定其类型的。

  3. 静态方法使用泛型

    image-20220416154703510

    image-20220416154907525

五丶桥接方法

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.桥方法导致重写方法冲突

image-20220416163158301

image-20220416163744324

六丶如何构造泛型数组

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是限制数据流入的(消费者

  1. 作为生产者的时候使用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));
    }
    
  2. 作为消费者使用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();
    }
}

image-20220417094803722

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[] 、T[]等

4.1.getGenericComponentType

返回表示此数组的组件类型的Type对象。

如果数组内部的元素既不是ParameterizedType也不是TypeVariable,那么该数组代表的Type则是一个数组Class,而不是GenericArrayType。

5.WildcardType

WildcardType表示通配符类型表达式,例如?? extends Number? super Integer

5.1.getUpperBounds

获取通配符的所有上边界(使用extends关键字),为了保持扩展返回数组

5.2.getLowerBounds

获取通配符的所有下边界(使用super关键字),为了保持扩展返回数组

posted @ 2022-09-25 18:58  Cuzzz  阅读(283)  评论(0编辑  收藏  举报