泛型高级进阶二:泛型的高级使用

泛型

泛型高级进阶一:泛型的基本使用

泛型高级进阶二:泛型的高级使用


一.泛型通配符(重点)

1、定义
泛型中的问号符“?”名为“通配符”

2、通配符的适用范围:
参数类型
字段类型
局部变量类型
返回值类型(但返回一个具体类型的值更好)

3、语法格式

<?>  
<? extends XXX>
<? super XXX>

1.Java泛型PESC原则

1、定义:PECS即 Producer extends Consumer super

  • 如果你既要获取又要放置元素,则不使用任何通配符。例如List ,参考: “2.非受限通配符”
  • 如果你只需要将类型T放到集合中, 使用<? super T>通配符,参考下面:“3.固定下限通配符
  • 如果你只需要从集合中获得类型T , 使用<? extends T>通配符,参考下面:“4.固定上限通配符”

2、优点

提升了API的灵活性

2.非受限通配符

1、定义:
不受具体对象的影响

<?>

2、使用场合
写一个方法,而这方法的实现可以利用Object类中提供的功能时泛型类中的方法不依赖类型参数时
如List.size()方法,它并不关心List中元素的具体类型

代码示例

public static void printList(List<?> list) {
    for (Object o : list) {
        System.out.println(o);
    }
}

public static void main(String[] args) {
    List<String> st= new ArrayList<>();
    st.add("aa");
    st.add("cc");
    printList(st);
    List<Integer> number = new ArrayList<>();
    number .add(11);
    number .add(22);
    printList(number);
}
  • 这种使用List<?>的方式就是父类引用指向子类对象. 注意, 这里的printList方法不能写成public static void printList(List list)的形式, 原因我在上一篇博文中已经讲过, 虽然Object类是所有类的父类, 但是List跟其他泛型的List如List, List不存在继承关系, 因此会报错.
  • 我们不能对List<?>使用add方法, 仅有一个例外, 就是add(null). 为什么呢? 因为我们不确定该List的类型, 不知道add什么类型的数据才对, 只有null是所有引用数据类型都具有的元素

3.固定下限通配符

1、限定了类型的下限,也就它必须为某类型的父类
定义:

<? super A>
List<XXX>比List<? super XXX>要更加严格。因为前者仅仅兼容XXX类型的列表,而后者却兼容任何XXX超类的列表

在这里插入图片描述
2、代码示例

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

4.固定上限通配符

1、限定了类型的上限,也就它必须为某类型的子类

<? super XXX>
List<XXX>要比List<? extends XXX>更加严格,因为前者仅能匹配XXX列表,然而后者却可同时匹配XXX及其子类的列表

在这里插入图片描述

代码示例

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, 我们在写这个方法时不能确定传入的什么类型的数据, 如果我们调用:

5.第三方使用案例

使用? super E还有个常见的场景就是Comparator. TreeSet有这么一个构造方法:

TreeSet(Comparator<? super E> comparator) 

例如

public class Person {
    private String name;
    private int age;
    /*
     * 构造函数与getter, setter省略
     */
}

public class Student extends Person {
    public Student() {}

    public Student(String name, int age) {
        super(name, age);
    }
}

class comparatorTest implements Comparator<Person>{
    @Override
    public int compare(Student s1, Student s2) {
        int num = s1.getAge() - s2.getAge();
        return num == 0 ? s1.getName().compareTo(s2.getName()) :  num;
    }
}

public class GenericTest {
    public static void main(String[] args) {
        TreeSet<Person> ts1 = new TreeSet<>(new comparatorTest());
        ts1.add(new Person("Tom", 20));
        ts1.add(new Person("Jack", 25));
        ts1.add(new Person("John", 22));
        System.out.println(ts1);

        TreeSet<Student> ts2 = new TreeSet<>(new comparatorTest());
        ts2.add(new Student("Susan", 23));
        ts2.add(new Student("Rose", 27));
        ts2.add(new Student("Jane", 19));
        System.out.println(ts2);
    }
}

5.总结

我们要记住这么几个使用原则, 有人将其称为PECS(即"Producer Extends, Consumer Super", 网上翻译为"生产者使用extends, 消费者使用super", 我觉得还是不翻译的好). 也有的地方写作"in out"原则, 总的来说就是:

  • in或者producer就是你要读取出数据以供随后使用(想象一下List的get), 这时使用extends关键字, 固定上边界的通配符. 你可以将该对象当做一个只读对象;
  • out或者consumer就是你要将已有的数据写入对象(想象一下List的add), 这时使用super关键字, 固定下边界的通配符. 你可以将该对象当做一个只能写入的对象;
  • 当你希望in或producer的数据能够使用Object类中的方法访问时, 使用无边界通配符;
  • 当你需要一个既能读又能写的对象时, 就不要使用通配符了.

二.泛型擦除

1.定义

Java在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除

证明示例

  • 例1.原始类型相等
public class Test {

    public static void main(String[] args) {

        ArrayList<String> list1 = new ArrayList<String>();
        list1.add("abc");

        ArrayList<Integer> list2 = new ArrayList<Integer>();
        list2.add(123);

        System.out.println(list1.getClass() == list2.getClass());
    }

}

在这个例子中,我们定义了两个ArrayList数组,不过一个是ArrayList<String>泛型类型的,只能存储字符串;一个是ArrayList<Integer>泛型类型的,只能存储整数,最后,我们通过list1对象和list2对象的getClass()方法获取他们的类的信息,最后发现结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下原始类型。

  • 例2.通过反射添加其它类型元素
public class Test {

    public static void main(String[] args) throws Exception {

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

        list.add(1);  //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer

        list.getClass().getMethod("add", Object.class).invoke(list, "asd");

        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }

}

在程序中定义了一个ArrayList泛型类型实例化为Integer对象,如果直接调用add()方法,那么只能存储整数数据,不过当我们利用反射调用add()方法的时候,却可以存储字符串,这说明了Integer泛型实例在编译之后被擦除掉了,只保留了原始类型。

2.擦除后的原始类型:

原始类型 就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换。
举例:

class Pair<T> {  
    private T value;  
    public T getValue() {  
        return value;  
    }  
    public void setValue(T  value) {  
        this.value = value;  
    }  
}

Pair的原始类型为:

class Pair {  
    private Object value;  
    public Object getValue() {  
        return value;  
    }  
    public void setValue(Object  value) {  
        this.value = value;  
    }  
}

因为在Pair中,T 是一个无限定的类型变量,所以用Object替换,其结果就是一个普通的类,如同泛型加入Java语言之前的已经实现的样子。在程序中可以包含不同类型的Pair,如Pair或Pair,但是擦除类型后他们的就成为原始的Pair类型了,原始类型都是Object。

例如.通过查看编译后的代码

Class Plate<T>{}
Plate<Interger> interger =new Plate<>();

通过命令编译后:
在这里插入图片描述

3.泛型擦除带来的问题及解决

  • 先检查,再编译以及编译的对象和引用传递问题
    Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。
  • 自动类型转换
    因为类型擦除的问题,所以所有的泛型类型变量最后都会被替换为原始类型。
    既然都被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢?
    源码做了强制转换:
public E get(int index) {  

    RangeCheck(index);  

    return (E) elementData[index];  

}
  • 类型擦除与多态的冲突和解决方法
    Bridge Methods 桥方法,桥方法以在扩展泛型时保持多态性
    当编译一个扩展参数化类的类,或一个实现了参数化接口的接口时,编译器有可能因此要创建一个合成方法,名为桥方法。它是类型擦除过程中的一部分

  • 泛型类型变量不能使用基本数据类型
    比如没有ArrayList,只有ArrayList.当类型擦除后, ArrayList的原始类中的类型变量(T)替 换成Object,但Object类型不能 存放int值
    在这里插入图片描述

  • 不能使用instanceof 运算符
    因为擦除后,ArrayList只剩下原始类型,泛型信息String不存在了,所有没法使用instanceof
    在这里插入图片描述

  • 泛型在静态方法和静态类中的问题
    因为泛型类中的泛型参数的实例化是在定义泛型类型对象(比如ArrayList)的时候指定的,而静态成员是不需要使用对象来调用的,所有对象都没创建,如何确定这个泛型参数是什么
    在这里插入图片描述

  • 泛型类型中的方法冲突
    因为擦除后两个equals方法变成一样的了
    在这里插入图片描述

  • 没法创建泛型实例
    因为类型不确定
    在这里插入图片描述

  • 没有泛型数组
    因为数组是协变,擦除后就没法满足数组协变的原则
    在这里插入图片描述

相关参考

泛型通配符部分参考: https://www.cnblogs.com/wxw7blog/p/7517343.html
泛型擦除部分参考: https://blog.csdn.net/wisgood/article/details/11762427

猜你喜欢

转载自blog.csdn.net/android_koukou/article/details/108429749