欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-05-23 15:21:59
...

泛型

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

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


一.泛型通配符(重点)

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