Java核心技术读书笔记8-泛型程序设计

1.泛型概述

泛型设计在JDK5被引入,它提供了一个类型参数,提前指示某种数据结构将操纵的数据类型,即可以在编译时检测出非法类型。
List list = new ArrayList<>();
泛型的设计可以使程序有更强的可读性和安全性。

类型参数不能是基本数据类型

2.泛型类

一个泛型类就是有一个或多个泛型变量的类。泛型参数是一种类似于占位符的功能,在调用时指定。

public class Pair<T> { //指定了一个类型参数T,若有多个类型参数用逗号隔开如:<T,U>
    private T first; //类型参数可以指定域和局部变量
    private T second;
    public Pair() {}
    public Pair(T first, T second) { //作为方法入参
        this.first = first;
        this.second = second;
    }
    public T getFirst() {return first;} //作为方法返回值
    public void setFirst(T first) {this.first = first;}
    public T getSecond() {return second;}
    public void setSecond(T second) {this.second = second;}
}
//使用泛型构造一个类,对泛型参数传入一个具体类型,则所有同泛型参数位置将被替换为该类型
public class Test {
    public static void main(String[] args) {
        Pair<String> pair = new Pair<>();
        pair.setFirst("1");
        pair.setSecond("2");
        pair.getSecond();
    }
}

所以,泛型类可以看做普通类的工厂。

2.泛型方法

类似于泛型类,类型参数在方法修饰符后面定义,作用域为该方法。泛型方法也可以在非泛型类中定义。

    public static <T> T[] testMethod(T... t){
        System.out.println(t.getClass().getName());
        return t;
    }
    public static void main(String[] args) {
        Test.<Double>testMethod(521d); //编译器可以根据参数推断出T的类型括号可以省略,但若不省略则要在方法前加上调用者
        testMethod(521d);
        testMethod(100, 512d, 2f, 10000L); //对于这种情况,编译器会认为T为这几参数类型的共同类型即Number
        testMethod(new ArrayList<>(), "1234", 1234); //Serializable
    }

3.类型变量的限定

如果我们相对类型变量T t1做一些操作。比如调用Comparable接口的compareTo方法与其他T类型变量作比较,如何确保作为通用类型的T实现了Comparable接口呢?这就需要对类型变量作出限定:
//注意即使Comparable也一样固定使用extends而不是implements
这样就要求传入的类型变量所属类必须实现Comparable接口。若有多个限定则使用&隔开:
<T extends A & B>

4.泛型代码与虚拟机

虚拟机没有泛型对象,泛型类型都会被擦除,若泛型类型有限定则替换为第一个限定类型,否则替换为Object,对于返回值被擦除为Object的,使用时系统会自动进行强转:
<T extends Comparable & AutoCloseable> 替换为Comparable类型 //若使用close方法,系统会在使用时做强转。
替换为Object类型
pair.getSecond(); 替换为(String)pair.getSecond();
类型擦除带来的问题
-由于擦除后得到的结果只是一个普通类或普通方法。同时在类型擦除时可能会合成桥方法来保持多态。

因此,不能使用a instanceof Pair语句或者做强制类型转换Pair p = (Pair)a,毕竟泛型会被擦除,虚拟机中没有泛型相关的类型。
new Pair().getClass == new Pair().getClass //返回true

-同时创建使用参数化的类型数组(使用变量则没问题Pair[]),如new Pair[10];可以使用ArrayList:ArrayList<Pair>代替。

-不能实例化类型变量,如new T(..)、new T[..]、T.class

-不能构造泛型数组:T[]

-不能在静态域或方法中引用类型变量

-泛型类不能扩展Throwable,所以更不可能捕获或抛出泛型类对象。

-注意类型擦除后的冲突

     @Override
    public boolean equals(Object obj) {return super.equals(obj);}
    public boolean equals(T obj) {return super.equals(obj);} //类型擦除后会引起冲突

5.泛型类型的继承规则

泛型类之间与参数类型之间的关系无关。

数组协变,超类数组变量可以引用子类数组变量Employee[] e = new Manager[10]; 但之后只能向数组传入Manager类型变量(类似于多态)。
而原始类型变量可以引用一个泛型类型构建的对象。但可能会产生类型错误。
Pair mp = new Pair<>();
Pair p = mp;
p.setFirst(new File(..)); //只有警告,不会报错。
泛型类型与对应的原始类型没有类间的关系,如ArrayList与String

6.有界通配符

通配符可以实现参数类型的限定继承关系问题,由于泛型类之间没有继承关系(Array与Array),所以如果想要处理作为形参的变量,其类型参数必须是Employee的子类怎么办呢?这就需要使用通配符类型:
上界通配符:<? extends A> //类型参数必须是A或A的子类,同时<? extends A>是<A>的子类。
下界通配符:<? super A> //类型参数必须是A或A的超类,同时<? super A>是<A>的超类。

6.1 有界通配符的特性:
· 通配符后只能有一个引用:<? extends A & B>非法
· 通配符只能出现在形参和引用(List<? extends Employee> list = new ArrayList<>();)中,不允许出现在泛型定义中,即泛型接口、类、方法。
· 通配符可以作为参数说明,但对使用通配符的变量的数据存取会有一定限制。带有超类限定的通配符可以向泛型对象写入,带有子类限定的通配符可以从泛型对象读取。

怎么理解这句话?
首先要明白向上转型即,Employee e = new Manager(),超类类型变量可以引用子类类型对象。
试想,对于方法:aMethod(List<? extends Employee> list)其参数,数组列表list。我们考虑为list添加值,我能只能为其添加其元素类型的同类型或更低的元素类型,但是其元素类型是一个以Employee为上界的不确定类型,这无法办到(其实也可以用Object得到)。
同时对于aMethod(List<? super Employee> list)的list,我们想取出他的值,那么就要用一个层级更高的类型接收其元素,但很可惜,其元素类型是没有上界的,这同样无法办到。
相反,List<? extends Employee> list,为元素类型指定了一个上界,我们可以很简单的用Employee类型(及Employee的父类)变量接收list中的元素,即带有子类限定的通配符可以从泛型对象读取
而,List<? super Employee> list,为元素类型指定了一个下界,我们将任何低于Employee类层级的类型元素加入到集合中而进行向上转型,即带有超类限定的通配符可以向泛型对象写入

    public static void printElement(List<? extends Employee> list){
        for (Employee e : list) { //指定上界后,可以用任何高于上界类型的元素接收值
            System.out.println(e.toString()); 
        }
    }

    public static void addElementTolist(List<? super Employee> list ,Employee e){
        list.add(e); //指定下界后,可以将任何低于下界类型的元素添加到集合中
    }

    public static void main(String[] args) {
        List<Employee> list = new ArrayList<>();
        addElementTolist(list, new Employee(1000, "王工"));
        addElementTolist(list, new Manager(10000, "李总"));
        printElement(list);
    }

7.无界通配符

无解通配符相当于上界为Object的有界通配符。即List<?>等价于List<? extends Object>。因此其合法操作只有读操作。

8.捕获通配符

因为我们对于<?>,不能用?作为一种参数类型,所以有需要取值的时该怎么办呢?如:

    public static void swapElements(List<?> list){
        Object o = list.get(0);
        list.set(0, list.get(1)); //无法设置值
        list.set(1, o); //同样无法设置值
    }

这时候就需要用到具体泛型T去捕获实际类型了

    public static void swapElements(List<?> list){
        swapHelper(list);
    }

    public static <E> void swapHelper(List<E> list){ //使用泛型T捕获值
        E e = list.get(0);
        list.set(0, list.get(1));
        list.set(1, e);
    }

    public static void main(String[] args) {
        List<Employee> list = new ArrayList<>();
        addElementTolist(list, new Employee(1000, "王工"));
        addElementTolist(list, new Manager(10000, "李总"));
        swapElements(list);
        System.out.println(list);
    }

9.反射与泛型

Class类是泛型的。例如,String.class实际上是一个Class<String>类的对象。类型参数十分有用,它允许Class<T>方法的返回类型更加具有针对性。比如对于参数Class<T> c。调用c.newInstance()实际上就是T.class.newInstance()。

10.虚拟机中的泛型信息。

Java可以通过反射获得类与方法的泛型相关信息,如T。但是不会知道对于特定的对象和方法调用,如何即使类型参数,如具体的<String>
在反射中,提供了Type接口,包括:
· Class类,描述具体类型
· TypeVariable接口,描述类型变量(T extends Comparable<? super T>)
· WildcardType接口,描述通配符(? super T)
· ParameterizedType接口,描述泛型类或接口类型(Comparable<? super T>)
· GenericArrayType接口,描述泛型数组(T[])



posted @ 2021-11-14 19:10  芝芝与梅梅  阅读(46)  评论(0)    收藏  举报