美文网首页Java学习笔记
Think in Java 回顾之泛型

Think in Java 回顾之泛型

作者: aJIEw | 来源:发表于2018-04-19 22:25 被阅读96次
    想去海边玩沙子

    什么是泛型?

    Java SE 5 开始引入了泛型的概念,泛型即参数化类型,利用泛型我们可以编写出更通用的代码(先不指定类型,使用时再指定类型)。泛型出现的最大的目的之一就是用来指定容器要持有的对象的类型,而且这种指定是由编译器来保证其正确性的。来看个例子:

    class Holder<T> {
        private T value;
    
        public Holder() {
        }
    
        public Holder(T value) {
            this.value = value;
        }
    
        public T get() {
            return value;
        }
    
        public void set(T value) {
            this.value = value;
        }
        
        public static void main(String[] args) {
            // 可以不指定类型,类型参数 T 就是 Object,因此会被初始化为 null
            Holder holder = new Holder();
            System.out.println(holder.get());
    
            Holder<String> strHolder = new Holder<>("Aaron");
            System.out.println(strHolder.get());
            
            strHolder.set(1); // Error
        }
    }
    

    以上代码中定义了一个 Holder 类,使用类型参数 T 作为持有的对象的类型。T 可以表示任何对象,所以Holder 类也就具有了持有任何对象的能力。当我们使用它的时候,可以使用 <> 来确定 Holder 所持有的对象的类型,这样我们就可以保证持有对象的类型的正确性。

    泛型方法

    如果普通方法中定义了泛型参数,那么这就是一个泛型方法。泛型方法和与该类是否是泛型类无关,但是如果静态方法想要使用泛型参数,那么它就必须定义为泛型方法,因为静态方法无法访问泛型类中的泛型参数。

    public class DemoGenericMethod<E> {
    
        private E e;
    
        public DemoGenericMethod() {
            e = (E) new Object();
        }
    
        public DemoGenericMethod(E e) {
            this.e = e;
        }
    
        /**
         * 根据泛型类的类型变量返回相应类型的 List
         */
        public List<E> getAsList(E e) {
            System.out.println(e.getClass().getName());
            return new ArrayList<>();
        }
    
        /**
         * 普通泛型方法
         */
        public <T> T printClassName(T t) {
            System.out.println(t.getClass().getName());
            return t;
        }
    
        public <T> void printSelfAndThis(T t) {
            System.out.println("this = " + e.getClass().getName()
                    + ", that = " + t.getClass().getName());
        }
    
        /**
         * 静态方法一旦使用了泛型参数就必须定义为泛型方法。
         * 因为静态方法是独立于类之外的,无法访问类中的泛型参数
         */
        public static <T> void getName(T t) {
            System.out.println(t.getClass().getName());
        }
    
        /**
         * 可变参数列表与泛型方法
         */
        public static <T> List<T> makeList(T... args) {
            List<T> result = new ArrayList<>();
            Collections.addAll(result, args);
            return result;
        }
    }
    

    擦除

    在使用泛型时,你是无法通过代码获得任何有关泛型参数类型的信息的,这其实是因为擦除的存在。在泛型类或泛型方法中,关于泛型参数的任何具体的类型信息都被擦除了(wiped),所以我们只能把类型参数当作一个 Object 使用。

    // ArrayList<String> 被擦除为 ArrayList
    Class c1 = new ArrayList<String>().getClass();
    // ArrayList<Integer> 被擦除为 ArrayList
    Class c2 = new ArrayList<Integer>().getClass();
    // 这两个 Class 对象都被擦除为 ArrayList了,所以是相等的
    System.out.println("c1 == c2? " + (c1 == c2)); // true
    

    擦除意味着无法使用 instanceofnew 或者转型等需要在运行时才能知道确切类型信息的操作。

    边界

    边界允许我们在参数类型上设置限制条件,这样就能部分抵消擦除带来的负面影响。来看代码:

    interface HasColor {
        Color getColor();
    }
    
    /**
     * 用 extend 关键字指定类型参数的边界
     */
    class Colored<T extends HasColor> {
        T element;
    
        Colored(T element) {
            this.element = element;
        }
    
        T getElement() {
            return element;
        }
    
        Color color() {
            // 因为设置了边界,所以调用是安全的
            return element.getColor();
        }
    }
    

    可以看到,我们用 extend 关键字指定泛型边界。当参数类型继承多个边界时,定义的规则与类的继承相同,类在前,接口在后,类与多个接口的连接符用 &,比如:

    class Solid<T extends Dimension & HasColor & Weight> {...}
    

    通配符

    通配符允许我们更加自由地使用泛型类。

    1. 首先是通配符结合 extends 关键字,用于确定泛型类的上边界。
    // 表示"具有任何从 Number 类继承的类型的 List"
    List<? extends Number> numbers = new ArrayList<Integer>();
    

    但是此时往 numbers 中添加任何元素都是不被允许的,因为只声明了上边界,编译器是无法确定捕获(capture)的类型到底是什么,所以无论添加任何对象都是类型不安全的。

    1. 通配符 + super 关键字,即超类型通配符,用于确定泛型类的下边界。
    // 表示边界范围由 "Number 类的任何基类" 来确定
    List<? super Number> numbers = new ArrayList<>();
    

    使用超类型通配符后,由于下边界确定,所以当我们向 list 中添加 Number 类或者其子类的时候,才可能保证是类型安全的。同样地,如果添加的是 Number 类的超类,也是不被允许的,因为编译器无法确定捕获的超类型到底是哪个超类。

    可能文字比较难以理解,来看几个例子,更为直观:

    public static void main(String[] args) {
    
        // 通配符必须是单一边界的,无法使用继承
        //List<? extends Fruit & Something > wildcard = new ArrayList<>();
    
        // Incompatible types,泛型不支持协变返回类型
        //List<Number> numberList = new ArrayList<Integer>();
    
        System.out.println("----------确定泛型的上边界----------");
        // 但是利用通配符和 extends 可以做到这一点
        // 这样的 List 可以看作是“具有任何从 Number 类继承的 List”,即为通配符确定了上界
        List<? extends Number> numbers = new ArrayList<Integer>();
    
        /*
        * 但是这样的List是非常有局限性的,无法添加任何有意义的元素
        * 因为List的参数类型为 ? extends Fruit,也就是任何继承自 Fruit 的对象
        * 编译器无法确定 List 所持有的类型,这样就无法保证类型安全性,所以不允许添加任何有意义的对象
        * */
        //numbers.add(1);
    
        //numbers.add(new Object()); // 甚至连 Object 都无法添加
    
        // 可以添加 null 进去,因为 null 可以表示任何对象,这也说明了此时添加某个对象是不安全的
        numbers.add(null);
    
        // 可以从中取出元素,因为至少可以确定这是一个 Number 类的对象
        Number number = numbers.get(0);
        System.out.println(number);
    
    
        System.out.println("----------泛型的转型----------");
        Holder<Number> numberHolder = new Holder<>(1);
        // Incompatible types,无法向上转型
        //Holder<Integer> intHolder = numberHolder;
    
        // 利用通配符设定边界后可以完成向上转型
        Holder<? extends Number> nHolder = numberHolder;
    
        // 无法完成调用set,原因同上,捕获的 Fruit 类无法应用到 Apple
        //nHolder.set(new Apple());
    
        // 获取 Fruit,但是实际类型为 Apple
        Number num = nHolder.get();
        Integer iNum = (Integer) nHolder.get();
        System.out.println("num: " + num + ", iNum: " + iNum);
        System.out.println("num.equals(iNum) " + num.equals(iNum)); // true
    
    
        System.out.println("----------确定泛型的下边界----------");
        // 用 super 关键字指定下界后才可以添加内容
        List<? super Number> boundedNums = new ArrayList<>();
        boundedNums.add(1);
        writeTo(boundedNums);
        System.out.println(boundedNums);
    
    
        System.out.println("----------无界通配符----------");
        List<?> list = new ArrayList<String>();
        // 捕获的参数类型 capture<?> 无法应用到String
        //list.add("1");
    }
    

    无界通配符

    上面的例子中有关于无界通配符的使用,第一次看到会觉得似乎难以理解,其实它表示的意思是”我可以持有任何类型“,它是更为泛化的参数化类型,但也因此无法像有界的泛型参数那样做更多的事。它最主要的用途就是捕获转换,即捕获未指定的通配符类型,然后将之转换为确切的某种类型。

    public class DemoCaptureConversion {
    
        static <T> void f1(Holder<T> holder) {
            System.out.println(holder.get().getClass().getSimpleName());
        }
    
        // 调用该方法时,发生了类型参数的捕获
        static void f2(Holder<?> holder) {
            f1(holder);
        }
    
        @SuppressWarnings("unchecked")
        public static void main(String[] args) {
            Holder raw = new Holder(1);
            f1(raw);
            f2(raw);
            Holder<?> wildcarded = new Holder<>(1.2f);
            f1(wildcarded);
            f2(wildcarded);
        }
    }
    

    以上代码中,f1() 中的参数是确切的已知的,而 f2() 中使用了无界通配符,参数是未知的。在调用 f2() 的时候,首先会捕获参数类型,然后转换为确切的类型以供 f1() 调用。

    问题

    1. 基本类型无法作为类型参数,必须使用其包装类。
    2. 不能同时实现同一个泛型接口的两种变体,原因是接口的参数类型会被擦除,也就相当于同一个接口被实现了两次。
    interface Pay<T> {}
    
    class Emp implements Pay<Emp> {}
    
    // cannot be inherited with different type arguments
    class Hour extends Emp implements Pay<Hour>{}
    
    1. 对泛型转型有时会产生”unchecked cast“的警告,因为编译期无法确定转型是否安全。
    2. 无法使用泛型参数作为区分两个方法,也就是无法用类型参数作为重载的依据。
    3. 基类劫持接口,最常见的例子就是实现 Comparable 接口。
    // Pet 类把自己作为参数类型传到 Comparable 接口中
    class Pet implements Comparable<Pet> {
    
        private String name;
        private int age;
    
        public Pet(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        @Override
        public int compareTo(@NotNull Pet pet) {
            return Integer.compare(this.age, pet.age);
        }
    }
    

    结语

    在 Think in Java 中,除了以上问题外,还详细讲解了自限定的类型、动态类型安全、泛型在异常中的使用、混型、潜在类型机制的缺失及补偿、将函数对象用作策略等,想要深入了解泛型的,不妨仔细阅读下这部分内容,如果有新的感受记得留言交流哦~


    参考资料:

    • Think in Java 第4版

    相关文章

      网友评论

        本文标题:Think in Java 回顾之泛型

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