泛型

作者: JBryan | 来源:发表于2020-03-05 10:58 被阅读0次
    1.什么是泛型

    泛型就是定义一种模板,例如ArrayList<T>,然后在代码中为用到的类创建对应的ArrayList<类型>:

    ArrayList<String> strList = new ArrayList<String>();
    

    由编译器针对类型作检查:

    strList.add("hello"); // OK
    String s = strList.get(0); // OK
    strList.add(new Integer(123)); // compile error!
    Integer n = strList.get(0); // compile error!
    

    泛型就是编写模板代码来适应任意类型;
    泛型的好处是使用时不必对类型进行强制转换,它通过编译器对类型进行检查;
    注意泛型的继承关系:可以把ArrayList<Integer>向上转型为List<Integer>(T不能变!),但不能把ArrayList<Integer>向上转型为ArrayList<Number>(T不能变成父类)。

    2.使用泛型

    除了ArrayList<T>使用了泛型,还可以在接口中使用泛型。例如,Arrays.sort(Object[])可以对任意数组进行排序,但待排序的元素必须实现Comparable<T>这个泛型接口:

    public interface Comparable<T> {
        /**
         * 返回-1: 当前实例比参数o小
         * 返回0: 当前实例与参数o相等
         * 返回1: 当前实例比参数o大
         */
        int compareTo(T o);
    }
    
    package com.ljessie.designpattern.java;
    
    public class Person implements Comparable<Person>{
    
        String name;
    
        public Person(String name){
            this.name = name;
        }
        @Override
        public int compareTo(Person o) {
            return this.name.compareTo(o.name);
        }
    
        @Override
        public String toString() {
            return name;
        }
    }
    package com.ljessie.designpattern.java;
    
    import java.util.Arrays;
    import java.util.StringJoiner;
    
    public class Test {
        public static void main(String[] args) {
            Person[] person = new Person[] {
                new Person("Jessie"),
                new Person("Bob")
            };
            Arrays.sort(person);
            System.out.println(Arrays.toString(person));
        }
    
    }
    

    使用泛型时,把泛型参数<T>替换为需要的class类型,例如:ArrayList<String>,ArrayList<Number>等;
    可以省略编译器能自动推断出的类型,例如:List<String> list = new ArrayList<>();;
    不指定泛型参数类型时,编译器会给出警告,且只能将<T>视为Object类型;
    可以在接口中定义泛型类型,实现此接口的类必须实现正确的泛型类型。

    3.编写泛型

    编写泛型类比普通类要复杂。通常来说,泛型类一般用在集合类中,例如ArrayList<T>,我们很少需要编写泛型类。
    编写泛型类时,要特别注意,泛型类型<T>不能用于静态方法。例如:

    public class Pair<T> {
        private T first;
        private T last;
        public Pair(T first, T last) {
            this.first = first;
            this.last = last;
        }
        public T getFirst() { ... }
        public T getLast() { ... }
        // 对静态方法使用<T>:
        public static Pair<T> create(T first, T last) {
            return new Pair<T>(first, last);
        }
    }
    

    上述代码会导致编译错误,我们无法在静态方法create()的方法参数和返回类型上使用泛型类型T。
    泛型还可以定义多种类型。例如,我们希望Pair不总是存储两个类型一样的对象,就可以使用类型<T, K>:

    public class Pair<T, K> {
        private T first;
        private K last;
        public Pair(T first, K last) {
            this.first = first;
            this.last = last;
        }
        public T getFirst() { ... }
        public K getLast() { ... }
    }
    

    使用的时候,需要指出两种类型:

    Pair<String, Integer> p = new Pair<>("test", 123);
    

    Java标准库的Map<K, V>就是使用两种泛型类型的例子。它对Key使用一种类型,对Value使用另一种类型。
    编写泛型时,需要定义泛型类型<T>;
    静态方法不能引用泛型类型<T>,必须定义其他类型(例如<K>)来实现静态泛型方法;
    泛型可以同时定义多种类型,例如Map<K, V>。

    4.擦拭法

    泛型是一种类似”模板代码“的技术,不同语言的泛型实现方式不一定相同。Java语言的泛型实现方式是擦拭法(Type Erasure)。所谓擦拭法是指,虚拟机对泛型其实一无所知,所有的工作都是编译器做的。
    例如,我们编写了一个泛型类Pair<T>,这是编译器看到的代码:

    public class Pair<T> {
        private T first;
        private T last;
        public Pair(T first, T last) {
            this.first = first;
            this.last = last;
        }
        public T getFirst() {
            return first;
        }
        public T getLast() {
            return last;
        }
    }
    

    而虚拟机根本不知道泛型。这是虚拟机执行的代码:

    public class Pair {
        private Object first;
        private Object last;
        public Pair(Object first, Object last) {
            this.first = first;
            this.last = last;
        }
        public Object getFirst() {
            return first;
        }
        public Object getLast() {
            return last;
        }
    }
    

    因此,Java使用擦拭法实现泛型,导致了:
    编译器把类型<T>视为Object;
    编译器根据<T>实现安全的强制转型。
    Java的泛型是由编译器在编译时实行的,编译器内部永远把所有类型T视为Object处理,但是,在需要转型的时候,编译器会根据T的类型自动为我们实行安全地强制转型。
    了解了Java泛型的实现方式——擦拭法,我们就知道了Java泛型的局限:
    局限一:<T>不能是基本类型,例如int,因为实际类型是Object,Object类型无法持有基本类型:

    Pair<int> p = new Pair<>(1, 2); // compile error!
    

    局限二:无法取得带泛型的Class
    局限三:无法判断带泛型的Class

    Pair<Integer> p = new Pair<>(123, 456);
    // Compile error:
    if (p instanceof Pair<String>.class) {
    }
    

    局限四:不能实例化T类型:

    public class Pair<T> {
        private T first;
        private T last;
        public Pair() {
            // Compile error:
            first = new T();
            last = new T();
        }
    }
    

    要实例化T类型,我们必须借助额外的Class<T>参数:

    public class Pair<T> {
        private T first;
        private T last;
        public Pair(Class<T> clazz) {
            first = clazz.newInstance();
            last = clazz.newInstance();
        }
    }
    

    上述代码借助Class<T>参数并通过反射来实例化T类型,使用的时候,也必须传入Class<T>。例如:

    Pair<String> pair = new Pair<>(String.class);
    

    有些时候,一个看似正确定义的方法会无法通过编译。例如:

    public class Pair<T> {
        public boolean equals(T t) {
            return this == t;
        }
    }
    

    这是因为,定义的equals(T t)方法实际上会被擦拭成equals(Object t),而这个方法是继承自Object的,编译器会阻止一个实际上会变成覆写的泛型方法定义。
    换个方法名,避开与Object.equals(Object)的冲突就可以成功编译:

    public class Pair<T> {
        public boolean same(T t) {
            return this == t;
        }
    }
    

    子类可以获取父类的泛型类型<T>。

    5.extends通配符

    使用<? extends Number>的泛型定义称之为上界通配符(Upper Bounds Wildcards),即把泛型类型T的上界限定在Number了。
    除了可以传入Pair<Integer>类型,我们还可以传入Pair<Double>类型,Pair<BigDecimal>类型等等,因为Double和BigDecimal都是Number的子类。
    如果我们考察对Pair<? extends Number>类型调用getFirst()方法,实际的方法签名变成了:

    <? extends Number> getFirst();
    

    即返回值是Number或Number的子类,因此,可以安全赋值给Number类型的变量:

    Number x = p.getFirst();
    

    然后,我们不可预测实际类型就是Integer,例如,下面的代码是无法通过编译的:

    Integer x = p.getFirst();
    

    这是因为实际的返回类型可能是Integer,也可能是Double或者其他类型,编译器只能确定类型一定是Number的子类(包括Number类型本身),但具体类型无法确定。
    方法参数签名setFirst(? extends Number)无法传递任何Number类型给setFirst(? extends Number)。
    这里唯一的例外是可以给方法参数传入null:

    p.setFirst(null); // ok, 但是后面会抛出NullPointerException
    p.getFirst().intValue(); // NullPointerException
    

    extends通配符的作用
    extends通配符放在方法参数位置,限制此方法内部,只可读取传入参数,不可修改参数里面的内容。
    如果我们考察Java标准库的java.util.List<T>接口,它实现的是一个类似“可变数组”的列表,主要功能包括:

    public interface List<T> {
        int size(); // 获取个数
        T get(int index); // 根据索引获取指定元素
        void add(T t); // 添加一个新元素
        void remove(T t); // 删除一个已有元素
    }
    

    现在,让我们定义一个方法来处理列表的每个元素:

    int sumOfList(List<? extends Integer> list) {
        //list.add(new Integer(5)); 此行代码放开注释,会编译错误。若方法参数是List<Integer>,则不会。
        int sum = 0;
        for (int i=0; i<list.size(); i++) {
            Integer n = list.get(i);
            sum = sum + n;
        }
        return sum;
    }
    

    为什么我们定义的方法参数类型是List<? extends Integer>而不是List<Integer>?
    从方法内部代码看,传入List<? extends Integer>或者List<Integer>是完全一样的,但是,注意到List<? extends Integer>的限制:
    允许调用get()方法获取Integer的引用;
    不允许调用set(? extends Integer)方法并传入任何Integer的引用(null除外)。
    因此,方法参数类型List<? extends Integer>表明了该方法内部只会读取List的元素,不会修改List的元素(因为无法调用add(? extends Integer)、remove(? extends Integer)这些方法。换句话说,这是一个对参数List<? extends Integer>进行只读的方法(恶意调用set(null)除外)。
    使用extends限定T类型
    在定义泛型类型Pair<T>的时候,也可以使用extends通配符来限定T的类型:

    public class Pair<T extends Number> { ... }
    

    现在,我们只能定义:

    Pair<Number> p1 = null;
    Pair<Integer> p2 = new Pair<>(1, 2);
    Pair<Double> p3 = null;
    

    因为Number、Integer和Double都符合<T extends Number>。
    非Number类型将无法通过编译:

    Pair<String> p1 = null; // compile error!
    Pair<Object> p2 = null; // compile error!
    

    因为String、Object都不符合<T extends Number>,因为它们不是Number类型或Number的子类。

    6.Super通配符

    和extends通配符相反,这次,我们希望接受Pair<Integer>类型,以及Pair<Number>、Pair<Object>,因为Number和Object是Integer的父类,setFirst(Number)和setFirst(Object)实际上允许接受Integer类型。
    我们使用super通配符来改写这个方法:

    void set(Pair<? super Integer> p, Integer first, Integer last) {
        p.setFirst(first);
        p.setLast(last);
    }
    

    Pair<? super Integer>表示,方法参数接受所有泛型类型为Integer或Integer父类的Pair类型。
    因此,使用<? super Integer>通配符表示:
    允许调用set(? super Integer)方法传入Integer的引用;
    不允许调用get()方法获得Integer的引用。
    唯一例外是可以获取Object的引用:Object o = p.getFirst()。
    换句话说,使用<? super Integer>通配符作为方法参数,表示方法内部代码对于参数只能写,不能读。
    对比extends和super通配符
    我们再回顾一下extends通配符。作为方法参数,<? extends T>类型和<? super T>类型的区别在于:
    <? extends T>允许调用读方法T get()获取T的引用,但不允许调用写方法set(T)传入T的引用(传入null除外);
    <? super T>允许调用写方法set(T)传入T的引用,但不允许调用读方法T get()获取T的引用(获取Object除外)。
    一个是允许读不允许写,另一个是允许写不允许读。
    先记住上面的结论,我们来看Java标准库的Collections类定义的copy()方法:

    public class Collections {
        // 把src的每个元素复制到dest中:
        public static <T> void copy(List<? super T> dest, List<? extends T> src) {
            for (int i=0; i<src.size(); i++) {
                T t = src.get(i);
                dest.add(t);
            }
        }
    }
    

    它的作用是把一个List的每个元素依次添加到另一个List中。它的第一个参数是List<? super T>,表示目标List,第二个参数List<? extends T>,表示要复制的List。我们可以简单地用for循环实现复制。在for循环中,我们可以看到,对于类型<? extends T>的变量src,我们可以安全地获取类型T的引用,而对于类型<? super T>的变量dest,我们可以安全地传入T的引用。
    这个copy()方法的定义就完美地展示了extends和super的意图:
    copy()方法内部不会读取dest,因为不能调用dest.get()来获取T的引用;
    copy()方法内部也不会修改src,因为不能调用src.add(T)。
    这个copy()方法的另一个好处是可以安全地把一个List<Integer>添加到List<Number>,但是无法反过来添加:

    // copy List<Integer> to List<Number> ok:
    List<Number> numList = ...;
    List<Integer> intList = ...;
    Collections.copy(numList, intList);
    // ERROR: cannot copy List<Number> to List<Integer>:
    Collections.copy(intList, numList);
    

    而这些都是通过super和extends通配符,并由编译器强制检查来实现的。
    无限定通配符
    我们已经讨论了<? extends T>和<? super T>作为方法参数的作用。实际上,Java的泛型还允许使用无限定通配符(Unbounded Wildcard Type),即只定义一个?:

    void sample(Pair<?> p) {
    }
    

    因为<?>通配符既没有extends,也没有super,因此:
    不允许调用set(T)方法并传入引用(null除外);
    不允许调用T get()方法并获取T引用(只能获取Object引用)。
    换句话说,既不能读,也不能写,那只能做一些null判断:

    static boolean isNull(Pair<?> p) {
        return p.getFirst() == null || p.getLast() == null;
    }
    

    大多数情况下,可以引入泛型参数<T>消除<?>通配符:

    static <T> boolean isNull(Pair<T> p) {
        return p.getFirst() == null || p.getLast() == null;
    }
    

    <?>通配符有一个独特的特点,就是:Pair<?>是所有Pair<T>的超类。无限定通配符<?>很少使用,可以用<T>替换,同时它是所有<T>类型的超类。

    7.泛型和反射

    Java的部分反射API也是泛型。例如:Class<T>就是泛型:

    // compile warning:
    Class clazz = String.class;
    String str = (String) clazz.newInstance();
    // no warning:
    Class<String> clazz = String.class;
    String str = clazz.newInstance();
    

    调用Class的getSuperclass()方法返回的Class类型是Class<? super T>:

    Class<? super String> sup = String.class.getSuperclass();
    

    构造方法Constructor<T>也是泛型:

    Class<Integer> clazz = Integer.class;
    Constructor<Integer> cons = clazz.getConstructor(int.class);
    Integer i = cons.newInstance(123);
    

    我们可以声明带泛型的数组,但不能用new操作符创建带泛型的数组:

    Pair<String>[] ps = null; // ok
    Pair<String>[] ps = new Pair<String>[2]; // compile error!
    

    必须通过强制转型实现带泛型的数组:

    @SuppressWarnings("unchecked")
    Pair<String>[] ps = (Pair<String>[]) new Pair[2];
    

    使用泛型数组要特别小心,因为数组实际上在运行期没有泛型,编译器可以强制检查变量ps,因为它的类型是泛型数组。但是,编译器不会检查变量arr,因为它不是泛型数组。因为这两个变量实际上指向同一个数组,所以,操作arr可能导致从ps获取元素时报错,例如,以下代码演示了不安全地使用带泛型的数组:

    Pair[] arr = new Pair[2];
    Pair<String>[] ps = (Pair<String>[]) arr;
    ps[0] = new Pair<String>("a", "b");
    arr[1] = new Pair<Integer>(1, 2);
    // ClassCastException:
    Pair<String> p = ps[1];
    String s = p.getFirst();
    

    要安全地使用泛型数组,必须扔掉arr的引用:

    @SuppressWarnings("unchecked")
    Pair<String>[] ps = (Pair<String>[]) new Pair[2];
    

    相关文章

      网友评论

          本文标题:泛型

          本文链接:https://www.haomeiwen.com/subject/vwvilhtx.html