泛型与反射

泛型与反射获取泛型信息

一、泛型

1 概述
1) 泛型?为什么要使用泛型?
    泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
方法
public void set(int a) {}
public void set(String b) {}ArrayList<E>
传入Integer则为ArrayList<Integer>类
传入String则为ArrayList<String>
    **泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)**。这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

    主要是为了提高代码可读性和安全性。
// 不会报错,运行时容易产生ClassCastException
List arrayList = new ArrayList(); 
arrayList.add("aaaa");
arrayList.add(100);
2) 类型擦除

类型擦除指的是,你在给类型参数<T>赋值时,编译器会将实参类型擦除为Object(这里假设没有限定符,限定符下面会讲到),即泛型只在编译阶段有效。

所以这里我们要明白一个东西:虚拟机中没有泛型类型对象的概念,在它眼里所有对象都是普通对象

List List1 = new ArrayList(); 
List1.add("aaaa");
List List2 = new ArrayList(); 
List2.add(100);
list1.getclass()==list2.getClass()
public class EraseDemo<T> {
    private T t;
}
//类型擦除后
public class EraseDemo<T> {
    private Object t;
}


public class EraseDemo<T extends Animal> {
    private T t;
}
//类型擦除后
public class EraseDemo {
    private Animal t;
}
2 通配符限定
1) 常用的 T,E,K,V,?

本质上这些个都是通配符,是编码时的一种约定俗成的东西。比如上述代码中的 T ,我们可以换成 A-Z 之间的任何一个 字母都可以,并不会影响程序的正常运行,但是如果换成其他的字母代替 T ,在可读性上可能会弱一些。通常情况下,T,E,K,V,?是这样约定的:

  • ?表示不确定的 Java 类型(类型通配符),一般用于方法声明,表示泛型方法形参(无需定义即可使用),不用于定义类和泛型方法。

    List<?> list = new ArrayList<>();
    List<T> list = new ArrayList<>();
    
    //当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。
    public static void printList(List<?> list) {
        for (Object o : list) {
            //可以使用Object的方法
            System.out.println(o);
        }
    }
    public static void main(String[] args) {
        List<String> l1 = new ArrayList<>();
        l1.add("aa");
        l1.add("bb");
        l1.add("cc");
        printList(l1);
        
        List<Integer> l2 = new ArrayList<>();
        l2.add(11);
        l2.add(22);
        l2.add(33);
        printList(l2);
    }
    
    
  • T (type) 表示具体的一个Java类型(类型参数),通常用于泛型类和泛型方法的定义

    public class A<T> {}  //上界为Object
    public class B<T entends Number> {}   //上界为Number
    
  • K V (key value) 分别代表Java键值中的Key Value

  • E (element) 代表Element

2) 无边界的通配符?

无边界的通配符的主要作用就是让泛型能够接受未知类型的数据。

/**
* String 是 Object 的子类,但是 List<String> 不是 List<Object> 的子类。
* 例如:如下代码会导致编译错误。
* 如果将 List<Object> 换成 List<?>,则可以编译通过。
*/
public static void func(List<?> list) {
}

public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    func(list); // 编译错误.
}

下面这个例子,List不是List的父类同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的

static int countLegs (List<? extends Animal> animals ) {
    int retVal = 0;
    for (Animal animal : animals)
    {
        retVal += animal.countLegs();
    }
    return retVal;
}

static int countLegs1 (List<Animal> animals){
    int retVal = 0;
    for ( Animal animal : animals )
    {
        retVal += animal.countLegs();
    }
    return retVal;
}
public static void main(String[] args) {
    
  List<Animal> Animals = new ArrayList<>();
    // 不会报错
    countLegs(Animals);
    // 不会报错
    countLegs1(Animals);
    
  List<Dog> dogs = new ArrayList<>();
    List<Dog> dogs = new ArrayList<>();
     // 不会报错
    countLegs(dogs);
    // 编译报错
    countLegs1(dogs);
}

我们不能对List<?>使用add方法, 仅有一个例外, 就是add(null).

为什么呢?  因为我们不确定该List的类型, 不知道add什么类型的数据才对,只有null是所有引用数据类型都具有的元素。

List<?>也不能使用get方法, 只有Object类型是个例外.

为什么呢?  因为我们不知道传入的List是什么泛型的, 所以无法接受得到的get,但是Object是所有数据类型的父类, 所以只有接受他可以。
public static void addTest(List<?> list) {
    Object o = new Object();
    // list.add(o); // 编译报错
    // list.add(1); // 编译报错
    // list.add("ABC"); // 编译报错
    list.add(null);
}

// 过使用强制转换,就没有必要使用?,直接用List<T>或者list<Integer>
public static void getTest(List<?> list) {
    // String s = list.get(0); // 编译报错
    // Integer i = list.get(1); // 编译报错
    Object o = list.get(2);
}
3)固定上边界的通配符
    使用固定上边界的通配符的泛型, 就能够接受指定类及其子类类型的数据。 要声明使用该类通配符, 采用<? extends E>的形式, 这里的E就是该泛型的上边界。
public static double sumOfList(List<? extends Number> list) {
    double s = 0.0;
    for (Number n : list) {
        // 注意这里得到的n是其上边界类型的, 也就是Number, 需要将其转换为double.
        s += n.doubleValue();
    }
    return s;
}

public static void main(String[] args) {
    List<Integer> list1 = Arrays.asList(1, 2, 3, 4);
    System.out.println(sumOfList(list1));
    List<Double> list2 = Arrays.asList(1.1, 2.2, 3.3, 4.4);
    System.out.println(sumOfList(list2));
}

有一点我们需要记住的是, List<? extends E>不能使用add方法, 请看如下代码:

public static void addTest2(List<? extends Number> l) {
    // l.add(1); // 编译报错
    // l.add(1.1); //编译报错
    l.add(null);
}

为什么? 泛型<? extends E>指的是E及其子类, 这里传入的可能是Integer, 也可能是Double, 我们在写这个方法时不能确定传入的什么类型的数据, 如果我们调用:

List<Integer> list = new ArrayList<>();
addTest2(list);

那么我们之前写的add(1.1)就会出错, 反之亦然, 所以除了null之外什么也不能add。

但是get的时候是可以得到一个Number, 也就是上边界类型的数据的, 因为不管存入什么数据类型都是Number的子类型, 得到这些就是一个父类引用指向子类对象.

4)固定下边界的通配符
使用固定下边界的通配符的泛型, 就能够接受指定类及其父类类型的数据. 要声明使用该类通配符, 采用<? super E>的形式, 这里的E就是该泛型的下边界。(不能同时指定上下边界)
public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}

public static void main(String[] args) {
    List<Object> list1 = new ArrayList<>();
    addNumbers(list1);
    System.out.println(list1);
    
    List<Number> list2 = new ArrayList<>();
    addNumbers(list2);
    
    System.out.println(list2);
    List<Double> list3 = new ArrayList<>();
    // addNumbers(list3); // 编译报错
}

我们看到, List<? super E>是能够调用add方法的, 因为我们在addNumbers所add的元素就是Integer类型的,而传入的list不管是什么,都一定是Integer或其父类泛型的List,这时add一个Integer元素是没有任何疑问的。

但是, 我们不能使用get方法, 请看如下代码:

public static void getTest2(List<? super Integer> list) {
    // Integer i = list.get(0); //编译报错
    Object o = list.get(1);
}

public static void addNumbers(List<? super Dog> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(new PoodleDog());//Dog及其子类不报错
        list.add(new Animal());//编译报错
    }
}

这个原因也是很简单的, 因为我们所传入的类都是Integer的类或其父类, 所传入的数据类型可能是Integer到Object之间的任何类型, 这是无法预料的, 也就无法接收. 唯一能确定的就是Object, 因为所有类型都是其子类型。

总结:

对于extends和super的使用有PECS(即"Producer Extends, Consumer Super"),"in out"等原则, 总的来说就是:

  • in或者producer就是你要读取出数据以供随后使用(想象一下List的get), 这时使用extends关键字, 固定上边界的通配符. 你可以将该对象当做一个只读对象;
  • out或者consumer就是你要将已有的数据写入对象(想象一下List的add), 这时使用super关键字, 固定下边界的通配符. 你可以将该对象当做一个只能写入的对象;
  • 当你希望in或producer的数据能够使用Object类中的方法访问时, 使用无边界通配符;
  • 当你需要一个既能读又能写的对象时, 就不要使用通配符了.
3 泛型基本使用
1) 泛型类
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,建议指定T的具体类型(也可不指定)
public class Generic<T>{ 
    //key这个成员变量的类型为T,T的类型由外部指定  
    private T key;
    public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
        this.key = key;
    }

    public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
        return key;
    }
}
//泛型的类型参数只能是类类型(包括自定义类),不能是简单类型
//传入的实参类型需与泛型的类型参数类型相同,即为Integer.
Generic<Integer> genericInteger = new Generic<Integer>(123456);

//传入的实参类型需与泛型的类型参数类型相同,即为String.
Generic<String> genericString = new Generic<String>("key_vlaue");
Log.d("泛型测试","key is " + genericInteger.getKey());
Log.d("泛型测试","key is " + genericString.getKey());
Generic generic = new Generic("111111");
Generic generic1 = new Generic(4444);
Generic generic2 = new Generic(55.55);
Generic generic3 = new Generic(false);
idea会黄色提示Raw use of parameterized class 'Generic' 
2) 泛型接口

泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,可以看一个例子:

//定义一个泛型接口
public interface Generator<T> {
    public T next();
}

当实现泛型接口的类,未传入泛型实参时:

/**
 * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
 * 即:class FruitGenerator<T> implements Generator<T>{
 * 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
 */
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}
interface FruitGenerator<T> implements Generator<T>{
}

当实现泛型接口的类,传入泛型实参时:

/**
 * 传入泛型实参时:
 * 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
 * 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
 * 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
 */
  public class FruitGenerator implements Generator<String> {
  private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

  @Override
  public String next() {
      Random rand = new Random();
      return fruits[rand.nextInt(3)];
  }
 }
3) 泛型方法
/**

 * 泛型方法的基本介绍
 * @param tClass 传入的泛型实参
 * @return T 返回值为T类型
 * 说明:
 * 1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
 * 2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
 * 3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
 * 4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
    */
   public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
     IllegalAccessException{
    T instance = tClass.newInstance();
    return instance;
   }
 class GenerateTest<T>{
        
        private T key;

        public void setKey(T key) {
            this.key = key;
        }

        //重点这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法。
        //这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
        //所以在这个方法中才可以继续使用 T 这个泛型。
        public T getKey(){
            return key;
        }

        //在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
        //由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
        public <E> void show(E t){
            System.out.println(t.toString());
        }
    }

二、反射获取泛型

1 反射获取类成员的泛型信息
public class Test02 {
    
    public Test02(List<String> list) {
    }
    
    public List<String> stringList = new ArrayList<>();
    
    public void parameterTest(Map<Integer, Dog> map){
    }

    public Map<Integer,Dog> returnTest(){
        return new HashMap<Integer, Dog>();
    }
}

1)获取方法参数中的泛型信息
@Test  //获取方法的参数中的泛型信息
    public void test1() throws NoSuchMethodException {
        Method method = Test02.class.getMethod("parameterTest", Map.class);
        //获取方法的参数类型
         Type[] genericParameterTypes = method.getParameterTypes();//
        Type[] genericParameterTypes = method.getGenericParameterTypes();
        for (Type genericParameterType : genericParameterTypes) {
            System.out.println("type:"+genericParameterType);
            //ParameterizedType:表示一种参数化类型,比如Collection<Object>
            if(genericParameterType instanceof ParameterizedType){
                Type[] actualTypeArguments = ((ParameterizedType)                                                                 genericParameterType).getActualTypeArguments();
                for (Type parameterType : actualTypeArguments) {
                    System.out.println(parameterType);
                }
            }
        }
    }
2)获取方法返回值中的泛型信息
@Test //获取方法的返回值中的泛型信息
    public void test2() throws NoSuchMethodException {
        Method method = Test02.class.getMethod("returnTest");
        //获取方法的返回值类型
        Type genericReturnType = method.getGenericReturnType();
        if(genericReturnType instanceof ParameterizedType){
            Type[] actualTypeArguments = ((ParameterizedType)                                                                 genericReturnType).getActualTypeArguments();
            for (Type parameterType : actualTypeArguments) {
                System.out.println(parameterType);
            }
        }
    }
3)获取类成员变量的泛型信息
 @Test //获取类成员变量的泛型信息
    public void test3() throws NoSuchMethodException {
        Field field = Test02.class.getField("stringList");
        Type genericsFieldType=field.getGenericType();
        if(genericsFieldType instanceof ParameterizedType){
            ParameterizedType parameterizedType=(ParameterizedType) genericsFieldType;
            Type[] fieldArgTypes=parameterizedType.getActualTypeArguments();
            for (Type fieldArgType:fieldArgTypes){
                Class fieldArgClass=(Class) fieldArgType;
                System.out.println("泛型字段的类型:"+fieldArgClass);
            }
        }
    }
4)获取类构造方法参数的泛型信息
    @Test //获取类构造函数参数的泛型信息
    public void test4() throws NoSuchMethodException {
        Constructor<SapTest> constructor = Test02.class.getConstructor(List.class);
        Type[] genericParameterTypes = constructor.getGenericParameterTypes();
        Type type = ((ParameterizedType)(genericParameterTypes[0])).getActualTypeArguments()[0];
        System.out.println(type);

    }

2 获取类的泛型信息
1)获取类的泛型信息
    Java的泛型是伪泛型(C++语言真泛型),只是在编译的时候进行类型检查,编译后,所有的泛型信息都会被擦除,因此在运行时无法使用反射获取泛型的类型。比如对于泛型类List<String> ,在编译的时候会检测所有加入到list里面的对象是否是String类型,编译后的class文件中是不会有泛型信息的。

子类继承泛型类的方法可以获得父类的泛型信息

public static abstract class ParentClass<T> {
        T name;
        public T getName() {
            return name;
        }
        public void setName(T name) {
            this.name = name;
        }
    }

public static class Child extends ParentClass<String> {
}

// 子类没有实例化T,则无法获取T的实际类型;
public static class ChildNew<T> extends ParentClass<T> {
}

//对于ArrayList<T>
public static class Child extends ArrayList<String> {
}

    /**
     * 通过父类去获取泛型类型
     */
    public static void getActualType() {
        Child child = new Child();
        Type type = child.getClass().getGenericSuperclass();
        if (type instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) type;
            Type actualtype = parameterizedType.getActualTypeArguments()[0];
            System.out.println(actualtype);//class java.lang.String

        }
    }
    Java在编译的时候会检测父类的泛型信息,因为子类声明了泛型的具体类型,所以子类的class文件中记录该子类声明的泛型类型,所以只有在这种情况下才能在运行时通过反射API获取到该泛型的具体类型。
2)反射获取泛型信息的机制
 在JDK1.5后Signature属性被增加到了Class文件规范中,它是一个可选的定长属性,可以出现在类、字段表和方法表结构的属性表中。在JDK1.5中大幅度增强了Java语言的语法,在此之后,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Singature属性会为它记录泛型签名信息。Signature属性就是为了弥补擦除法的缺陷而增设的,Java可以通过反射获得泛型类型,最终的数据来源也就是这个属性。

类Child<String>的字节码文件中类Signature属性存储有String这部分泛型信息。

ParentClass<String>这种泛型类在编译时是会擦除所有泛型信息的,字节码中没有存储类型泛型信息,只存储了泛型名称,所以必须通过子类继承泛型类的方式在子类的字节码中存储泛型信息。

主要参考以下大佬的文章

https://www.cnblogs.com/minikobe/p/11547220.html

https://blog.csdn.net/s10461/article/details/53941091

https://www.cnblogs.com/wxw7blog/p/7517343.html
————————————————
版权声明:本文为CSDN博主「天府三街的it打工仔」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_38997816/article/details/115708489

  • 7
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值