泛型与反射

一、泛型

1 概述

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

1.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. 通配符限定

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.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);
}

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的子类型, 得到这些就是一个父类引用指向子类对象.

2.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类中的方法访问时, 使用无边界通配符;

  • 当你需要一个既能读又能写的对象时, 就不要使用通配符了.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值