Java中的泛型有着很重要的作用,它能够让我们的数据容器类型安全,避免发生转换异常。不过Java中的泛型也为人诟病,它会在编译中被全部转换成Object对象,也就是泛型擦除,这造成了诸多不便,除非你能获取泛型的一个实例,否则我们无法直接获取泛型的实际类型。不过JDK依然提供了一个技巧让我们可以获得泛型的具体类型。

大致原理

虽然泛型会在字节码编译过程中被擦除,但是Class对象会通过java.lang.reflect.Type记录其实现的接口和继承的父类信息。我们以ArrayList<E>为例:

        ArrayList<String> strings = new ArrayList<>();

        Type genericSuperclass = strings.getClass().getGenericSuperclass();
        // genericInterfaces = java.util.AbstractList<E>
        System.out.println("genericSuperclass = " + genericSuperclass);

虽然我们成功打印出来了泛型的占位符E,但是这并不是我们想要的。我们希望能够获取E的具体类型,也就是String

让我们回到java.lang.reflect.Type

Type的实现类型

通过上图可以知道Type有四种类型:

  • GenericArrayType 用来描述一个参数泛型化的数组。

  • WildcardType 用来描述通配符?相关的泛型,包含的?、下界通配符? super E 、上界通配符? extend E

  • Class<T>  用来描述类的Class对象。

  • ParameterizedType 用来描述参数化类型。

我们再来试一试:

ArrayList<String> strings = new ArrayList<>();

Type genericSuperclass = strings.getClass().getGenericSuperclass();

System.out.println( genericSuperclass instanceof ParameterizedType); // true
System.out.println( genericSuperclass instanceof Class); // false
System.out.println( genericSuperclass instanceof WildcardType); // false
System.out.println( genericSuperclass instanceof GenericArrayType); // false

我们来看看参数化类型能不能获取到具体的类型:

        ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
        Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
        // [E]
        System.out.println("actualTypeArguments = " + Arrays.toString(actualTypeArguments));

ParameterizedType可以帮助我们获取参数类型,可惜依然是E。两种方法为什么都只能获取一个泛型占位符呢?

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
// 省略

}

这是因为ArrayList实例化时只指定了自己的泛型类型而没有指定父类AbstractList的具体泛型,所以获取到的就是占位符E。我们先来看一个抽象类例子:

  public abstract  class SuperClass<E> {
        private E e;

        protected SuperClass(E e) {
            this.e = e;
        }

        public E get() {
            return this.e;
        }
    }

构建匿名子类实现:

// 实现
 SuperClass<String> superClassInstance = new SuperClass<String>("试一试") {
        };

Type genericSuperclass1 = superClassInstance.getClass().getGenericSuperclass();
//SuperClass<java.lang.String>
System.out.println(genericSuperclass1);

我们通过大括号{}就可以重写实现父类的方法并指定父类的泛型具体类型。我们可以借助这一特性来获取父类的具体泛型类型,我们还拿ArrayList来试试:

ArrayList<String> strings = new ArrayList<String>(){};
Type genericSuperclass = strings.getClass().getGenericSuperclass();
// genericSuperclass = java.util.ArrayList<java.lang.String>
System.out.println("genericSuperclass = " + genericSuperclass);

证明了我们的猜想是对的。那么问题来了如何封装一个工具类?

封装工具类

我们可以借助于抽象类来定义一个获取java.lang.reflect.ParameterizedType的工具类。好在Spring框架中已经提供了一个很好用的实现:

public abstract class ParameterizedTypeReference<T> {
    private final Type type;

    protected ParameterizedTypeReference() {
        Class<?> parameterizedTypeReferenceSubclass = findParameterizedTypeReferenceSubclass(this.getClass());
        // 获取父类的泛型类 ParameterizedTypeReference<具体类型>
        Type type = parameterizedTypeReferenceSubclass.getGenericSuperclass();
        // 必须是 ParameterizedType
        Assert.isInstanceOf(ParameterizedType.class, type, "Type must be a parameterized type");
        ParameterizedType parameterizedType = (ParameterizedType)type;
        // 获取泛型的具体类型  这里是单泛型
        Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
        Assert.isTrue(actualTypeArguments.length == 1, "Number of type arguments must be 1");    // 设置结果
        this.type = actualTypeArguments[0];
    }
 
    // 这个方法开放出去用来调用 来获取泛型的具体Class类型 
    public Type getType() {
        return this.type;
    }
    
    private static Class<?> findParameterizedTypeReferenceSubclass(Class<?> child) {
        Class<?> parent = child.getSuperclass();
        // 如果父类是Object 就没戏了
        if (Object.class == parent) {
            throw new IllegalStateException("Expected ParameterizedTypeReference superclass");
        } else {
            // 如果父类是工具类本身 就返回否则就递归 直到获取到ParameterizedTypeReference
            return ParameterizedTypeReference.class == parent ? child : findParameterizedTypeReferenceSubclass(parent);
        }
    }
}

其实 Jackson Gson都有类似的实现。

所以今天你又学了一招,而且这一招相当的有创意。这一招在封装一些通用类库的时候非常有用,比如反序列化工具类。看完了别忘关注码农小胖哥并一键四连哦。

后端Java开发如何防御XSS攻击

2021-06-30

Windows 11正式发布,Win10用户可免费升级

2021-06-25

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐