重学Java泛型

系列文章目录和关于我

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

public class Type1 {
    private T t;
}

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

重学Java泛型

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

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

public class Type2 {

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

使用idea查看字节码信息

重学Java泛型

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

public class Type3 {

    List<string> mylist;
}
</string>

重学Java泛型

对于Type3我们可以看到类型是一个 List<string></string> 签名索引指向常量池中,说明范型信息存储在常量池

三丶复杂类型如何使用Json序列化与其原理

1.使用TypeReference(alibaba fastJson包,其他api有对应的方法)

//复杂类型 List嵌套List
List> lists= Arrays.asList(Collections.singletonList("1")
        ,Collections.singletonList("2")
        ,Collections.singletonList("3"));
String s = JSON.toJSONString(lists);
List> list = (List>)JSON.parseObject(s, List.class);//强转并不报错 但是实际类型是List
TypeReference>> type = new TypeReference>>() {}; //注意这里是匿名内部类
List> lists1 = JSON.parseObject(s, type);
System.out.println(lists1);//类型就是List>

2.原理

new TypeReference>>() {}

其实是new 了一个对象 这个对象继承自TypeReference,相当于new了一个确切类型( TypeReference<list<list<string>>></list<list<string>)类的子类,已经明确知道类型了,jvm就不会进行类型擦除,类型信息会被保存下来

如下面这个类 继承子 LinkedList<string></string>

public class Type4 extends LinkedList {
}

重学Java泛型

在签名信息里确切的知道了父类类型是 LinkedList<string></string>

3.TypeReference源码

protected TypeReference(){
    //拿父类的通用类型
    Type superClass = getClass().getGenericSuperclass();
    //拿父类真实类型的第一个 也就是第一个类型 就是 List>
    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 对应的原始泛型是 Object, 对应的原始类型就是 A。

2.泛型擦除验证

如下代码,我们定义了一些类,然后反射获取这些类当中create方法的出参,并打印分析

  static class A {

    }
    //返回值上界是A
    static abstract class B {
        abstract T create();
    }
    //返回值没有上界,或者可以视为T extends Object
    static abstract class C {
        abstract T create();
    }

  // T 要求同时是A的子类且实现Comparable
    static abstract class D> {
        abstract T create();
    }

    //泛型多限制时 类必须在接口前
//    static abstract class E & A> {
//        abstract T create();
//    }

    //T 要求实现Serializable 和 Comparable
    static abstract class E> {
        abstract T create();
    }
  //T 要求实现   Comparable 和 Serializable
    static abstract class F & 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类

打印结果是

重学Java泛型

重学Java泛型

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

2)对于C类

打印结果是

重学Java泛型

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

3)对E和F类

打印结果是

重学Java泛型

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

3.泛型擦除导致的问题

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

对于泛型对象使用 instanceof 进行类型判断的时候就不能使用具体的类型,而只能使用通配符 &#xFF1F;,示例如下所示:

ArrayList list = new ArrayList<>();
System.out.println(list instanceof ArrayList);//true
System.out.println(list instanceof ArrayList);//true
//无法编译
// System.out.println(list instanceof ArrayList);
  1. catch中不能使用泛型异常类
假设有一个泛型异常类的定义 MyException,
try{
}catch (MyException e1)
catch ( MyExceptione2){...}
//MyException 和 MyException 都会被擦除为 MyException,因此,两个 catch 的条件就相同了,所以这种写法是不允许的。
//也不允许在 catch 子句中使用泛型变量
public  void test(T t) {
    try{
        ...

    }catch(T e) {  //编译错误
        ...

    } catch(IOException e){
    }
}
//假设上述代码能通过编译,由于擦除的存在,T 会被擦除为 Throwable。由于异常捕获的原则为:先捕获子类类型的异常,再捕获父类类型的异常。

重学Java泛型
重学Java泛型
重学Java泛型

五丶桥接方法

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 data;

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

static class BridgeMethodSubT extends BridgeMethodT {
    @Override
    public T getData() {
        return super.getData();
    }

    @Override
    public void setData(T data) {
        super.setData(data);
    }
}

static class BridgeMethodString extends BridgeMethodT {
    @Override
    public String getData() {
        return super.getData();
    }

    @Override
    public void setData(String data) {
        super.setData(data);
    }
}
输出

=============
当前分析类:BridgeMethodT
当前方法名称:getData
是否是桥接方法:false
当前方法返回值:class java.lang.Object
无入参
=============
当前分析类:BridgeMethodSubT
当前方法名称:getData
是否是桥接方法:false
当前方法返回值:class java.lang.Object
无入参
=============
当前分析类:BridgeMethodString
当前方法名称:getData
是否是桥接方法:true
当前方法返回值:class java.lang.Object  //这个是编译器自动生成的桥接方法 实际是调用父类的getData方法
无入参
=============
当前分析类:BridgeMethodString
当前方法名称:setData
是否是桥接方法:true
当前方法返回值:void
当前方法入参:Object  //这个是编译器自动生成的桥接方法 实际是调用父类的setData方法

可以得出结论 对于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.桥方法导致重写方法冲突

重学Java泛型

重学Java泛型

六丶如何构造泛型数组

public static  T[] newTArray(Class clazz, int len) {
    //底层是一个native 方法 malloc开辟 单个clazz对象大小*长度+数据额外的空间
    return (T[]) Array.newInstance(clazz, len);
}
public static  T[] newTArray2(Class clazz, int len) {

    ArrayList 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 workerList = new ArrayList<>();
List backEndList = new ArrayList<>();
workerList = backEndList;//无法编译
backEndList = workerList;//无法编译
//因为泛型擦除的时候 都是List 如果支持这种操作 那么将导致
backEndList.get(1) 可能是一个清洁工 强转成BackEnd 失败

4.泛型上界extends 是协变的

f(⋅)是 协变(covariant)的,当A是B的子类时有f(A)是f(B)的子类成立;

//?extends worker的子类 是BackEnd的子类
ArrayList backEnds = new ArrayList<>();
List workers = backEnds;//可以

//但是协变之后 不可使用任何入参是泛型的方法
    workers.add(new BackEnd());//编译错误
        workers.add(new JavaCoder());//编译错误
    //编译器无法确定所持有的具体类型是什么
        //所以一旦执行这种类型的向上转型,你就将丢失掉向其中传递任何对象的能力。

5.泛型下界super是逆变的

f(⋅)是 逆变(contravariant)的,当A是B的子类的时有f(B)是f(A)的子类成立;

// worker 是 ? super Backend 的子类
 List workers1 = new ArrayList<>();
//==>List 是 List的子类
        List 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  Optional outBoundsReturnNull(List collection, int index) {
    if (collection == null || index < 0 || index >= collection.size()) {
        return Optional.empty();
    }
    return Optional.ofNullable(collection.get(index));
}
  1. 作为消费者使用super
//尝试添加到指定位置
public static  boolean tryAddAtAppointIndex(List 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></k,v>, Set<t></t>,` 这里的参数化指这些泛型可以像参数一样去指定
  • 数组类型(GenericArrayType) 带有泛型的数组,即T[]
  • 类型变量(TypeVariable) 比如 T a
  • 基本类型(Class)
public interface Type {
//1.8后默认实现
    default String getTypeName() {
        return toString();
    }
}

重学Java泛型

2.ParameterizedType

ParameterizedType 表示参数化类型,参数化类型即我们通常所说的泛型类型,例如 Collection<string></string>。参数化类型在反射方法第一次需要时创建。创建参数化类型 p 时,解析 p 实例化的泛型类型声明,并递归创建 p 的所有类

2.1.getActualTypeArguments():

该方法返回参数化类型<>中的实际参数类型, 如 Map

2.2.getOwnerType()

返回ParameterizedType类型所在的类的Type。如Map.Entry

2.3.getRawType()

返回的是当前这个 ParameterizedType 的类型。 如 Map

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关键字),为了保持扩展返回数组

Original: https://www.cnblogs.com/cuzzz/p/16728495.html
Author: Cuzzz
Title: 重学Java泛型

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/715329/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球