zoukankan      html  css  js  c++  java
  • Java基础(015):泛型

    注:本文大部分参考自Java官方文档,也参考了其他的一些文章,但是看着很多博客的例子和官方文档又是类似的,而又没有列出详细的出处,最后还是建议多参考官方文档的说明,详见最后罗列出来的参考链接,另外还有《Effective Java》和《On Java 8》也值得参考。谨以此文记录自己对 Java 泛型的粗浅认识。 

    引言

      Java 5 中正式引入了泛型编程来处理类型安全对象,它通过在编译时检测类型错误来使代码稳定。泛型是 Java 5 及之后一个非常重要的知识点,在面向对象编程及各种设计模式中有非常广泛的应用。用 Java 做项目开发的,就算没深入研究过泛型,或多或少都会听到过,也基本肯定是用过的,而且可能还用得不少,特别是 Java 的集合框架,像下面这些:
    // Java 5 - Java 6 的写法
    List<String> strList = new ArrayList<String>();
    // Java 7 及之后可以类型推导,后面只需要<>,不用写上类型
    List<String> strList = new ArrayList<>();
    Map<String, String> strMap = new HashMap<>();

      Java 泛型设计涉及到的知识点及问题相对较多,本文目录安排如下:

    1、泛型的定义

      什么是泛型(Generics)?
    • 泛型,意味着类型参数化,或者说参数化类型 parameterized types ,允许将类型当作参数(类型参数,type parameter)传递给方法、构造器、类、接口等,通过使用泛型,我们便可以创建适用于不同数据类型的类、接口、方法、构造器。一提到参数,最熟悉的就是方法定义中的形参,调用时传入具体的实参。而泛型则是将原来具体的类型参数化,类似于方法中的变量参数,此时将类型也定义成参数形式(称之为 类型形参),进行泛化,然后同样是在实际使用/调用时传入具体的类型(类型实参)。
    • 泛型的本质就是参数化类型,即在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型。也就是说在泛型使用过程中,将操作的数据类型定义为一个参数,这种参数类型可以用在类、接口、方法、构造器中,分别称为泛型类、泛型接口、泛型方法、泛型构造器。
    • 泛型主要设计目的是在编译时进行类型检查,确保类型安全。 

    2、为什么使用泛型

      为什么要使用泛型?泛型可以解决什么问题?看下官方文档的说法 [2]Why Use Generics?
      泛型使类型(类和接口)在定义类、接口和方法时成为参数。与方法声明中使用的形参非常相似,类型参数提供了一种在不同输入中重用相同代码的方法。不同之处在于形参的输入是值,而类型参数的输入是类型。
      与非泛型代码相比,使用泛型的代码有很多优点:
    • 在编译时进行强类型检查
      • Java编译器对泛型代码应用强类型检查,并在代码违反类型安全时发出编译错误提示。编译时错误往往比运行时错误更容易修复,后者很难找到。
    • 无需强制类型转换
    List<String> strList = new ArrayList<>();
    strList.add("1");
    // 无需使用 (String) strList.get(0) 进行显式的强制类型转换
    String str = strList.get(
    • 更容易实现泛型算法,实现不同数据类型类型的算法复用
      • 使用泛型,程序员可以实现泛型算法,这些算法处理不同类型的集合,可以自定义、类型安全且更易于阅读。
      笔者注:为什么要使用泛型?泛型可以解决什么问题?笔者觉得,泛型主要是编译时进行强类型检查类型,提前发现类型安全错误,避免运行时的类型转换异常。Java 5 之前都是用 Object 去接收,然后再根据需要进行强制类型转换,这样就可能在运行时出现 ClassCastException 异常,而泛型编程避免了这种情况,至于可以减少强制类型转换,那是编译器自动处理的,另外就是提高了代码的复用性。 

    3、泛型的应用场景

      泛型的类型参数可用于类、接口、方法、构造器,因此泛型涉及到的应用场景有:泛型类泛型接口泛型方法泛型构造器。 

    3.1、泛型类

      泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开
    。泛型类的定义方式:

    class name<T1, T2, ..., Tn> { /* ... */ }
      可以看到,泛型类和普通类定义的差别在于多了类型参数声明部分,即尖括号 <> 和类型参数以及逗号(多个类型参数时进行分隔)。
      参考官方文档 [3]Generic Types ,下面通过一个简单的 Box 类来进行说明。首先是一个普通的 Box 类:

    public class Box {
        private Object object;
    
        public void set(Object object) { this.object = object; }
        public Object get() { return object; }
    }

      由于 Box 的方法接收和返回的都是 Object ,因此使用时可以传入任意类型。而由于无法在编译期验证这些类型,当代码中的一部分可能存入 Integer 并期待从中获取整数,而另一部分则错误地传入 String 时,就会导致运行时错误 ClassCastException 。另一方面,每次获取 object 后可能还会根据需要强制转换成预期的类型。这个时候就需要泛型类来处理了。

      Box 的泛型方式如下:

    /**
     * Generic version of the Box class.
     * @param <T> the type of the value being boxed
     */
    public class Box<T> {
        // T stands for "Type"
        private T t;
    
        public void set(T t) { this.t = t; }
        public T get() { return t; }
    }

      使用的时候则是这样,各自的类型,而且获取使用时无需进行强制类型转换: 

    // 只能添加 Integer 类型的 Box 实例
    Box<Integer> integerBox = new Box<Integer>();
    // Java 7 及之后依靠类型推断可以省略后面的类型信息
    // Box<Integer> integerBox = new Box<>();
    integerBox.add(1);
    Integer ib = integerBox.get();
    
    // 只能添加 Double 类型的 Box 实例
    Box<Double> doubleBox = new Box<Double>();
    doubleBox.add(1d);
    Double db = doubleBox.get();
    
    // 只能添加 String 类型的 Box 实例
    Box<String> stringBox = new Box<String>();
    stringBox.add("1");
    String sb = stringBox.get();

    3.2、泛型接口

      与泛型类定义类似,也是加上类型参数,此不赘述。 

    3.3、泛型方法

      泛型方法的一些定义规则(参考:https://www.tutorialspoint.com/java/java_generics.htm):
    • 所有泛型方法声明都有一个类型参数部分,该部分由尖括号 <> 分隔,且位于方法的返回类型(如示例中为<E>)之前。
    • 每个类型参数部分包含一个或多个用逗号分隔的类型参数。类型参数(也称为类型变量)是指定泛型类型名称的标识符。
    • 类型参数可用于声明返回类型,并充当传递给泛型方法的参数类型(称为实际类型参数)的占位符。
    • 泛型方法体的声明方式与任何其他方法一样。请注意,类型参数只能表示引用类型,而不能表示原始类型(如int、double和char)。
      泛型方法的定义方式类似,就是加上类型参数,参考官方文档 [4]Generic Methods ,案例如下: 
    public class Util {
        public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
            return p1.getKey().equals(p2.getKey()) &&
                   p1.getValue().equals(p2.getValue());
        }
    }
    
    public class Pair<K, V> {
    
        private K key;
        private V value;
    
        public Pair(K key, V value) {
            this.key = key;
            this.value = value;
        }
    
        public void setKey(K key) { this.key = key; }
        public void setValue(V value) { this.value = value; }
        public K getKey()   { return key; }
        public V getValue() { return value; }
    }

      调用方式也比较简单: 

    Pair<Integer, String> p1 = new Pair<>(1, "apple");
    Pair<Integer, String> p2 = new Pair<>(2, "pear");
    boolean same = Util.<Integer, String>compare(p1, p2);

      Java 7 及之后可以根据 [5]Type Inference 进行类型推断,自动推导出相应的类型参数:

    Pair<Integer, String> p1 = new Pair<>(1, "apple");
    Pair<Integer, String> p2 = new Pair<>(2, "pear");
    boolean same = Util.compare(p1, p2);

    3.4、泛型构造器

    注:可以使用泛型类的类型参数,也可以像泛型方法一样单独声明类型参数。此不赘述 

    3.5、泛型类型参数的命名约定

      根据 [3]Generic Types 中提到的,泛型编程中的一些常见标识符的命名约定如下:
    • T : Type,类型
    • E : Element,元素
    • K : Key,键
    • N : Number,数字
    • V : Value,值
    • S、U、V 等:多参数情况中的第 2、3、4 个类型 

    3.6、类型推断和菱形语法

      根据 [3]Generic Types ,Java 7 及之后,只要编译器可以从上下文中确定或推断类型参数的,就可以用一组空的类型参数(<>)替换调用泛型类的构造函数所需的类型参数。这对尖括号 <> 非正式地称为 diamond ,用大家熟悉的说法就是 diamond operator,菱形操作符,也称为菱形语法。例如,可以使用以下语句创建 Box<Integer> 的实例: 
    Box<Integer> integerBox = new Box<>();

      泛型方法也是可以进行类型推断。关于类型推断 Type Inference ,可以参考 [5]Type Inference 中的具体说明。 

    3.7、原始类型

      根据 [10]Raw Types ,原始类型 Raw Types ,就是在使用泛型类或者泛型接口时不加上类型参数。
      对于前面提到的 Box<T> ,正常情况下我们是这么使用的: 
    Box<Integer> intBox = new Box<>();

      如果我们把两边的类型参数都去掉,包括 <> ,则变成这样: 

    Box rawBox = new Box();
    // 下面这种写法已经被当成是 Object 了,运行时可以添加任意类型
    // Box rawBox = new Box<Integer>();

      这样就变成了原始类型。因此说 Box 是泛型类 Box<T> 的原始类型。需要注意的是,非泛型类或者非泛型接口是没有所谓的原始类型的。

      原始类型主要是出现在遗留代码中,因为在 Java 5 之前,许多 API 类(如 Collections 类)不是泛型的。在使用原始类型时,本质上是预泛型行为(pre-generics behavior),Box 默认提供 Object 对象。为了向后兼容,允许将参数化类型分配给其原始类型:

    Box<String> stringBox = new Box<>();
    Box rawBox = stringBox;
      反过来,让已经没有类型信息的原始类型,则会得到一个警告:

    Box rawBox = new Box();           // rawBox is a raw type of Box<T>
    Box<Integer> intBox = rawBox;     // warning: unchecked conversion
      另外,通过原始类型来调用原来定义的泛型方法也会得到警告:

    Box<String> stringBox = new Box<>();
    Box rawBox = stringBox;
    rawBox.set(8);  // warning: unchecked invocation to set(T)
      该警告显示原始类型绕过泛型类型检查,将不安全代码的捕获推迟到运行时。因此,我们应该避免使用原始类型。
      更多 Java 编译器如何使用原始类型的,可自行参考 [6]Type Erasure。 

    4、类型系统

      主要是泛型及类型参数相关的继承关系。参考 [12]Generics, Inheritance, and Subtypes[13]Wildcards and Subtyping[24]Java 深度历险(五)——Java 泛型
      在 Java 中,大家比较熟悉的是通过继承机制而产生的类型体系结构。比如 String 继承自 Object。根据 Liskov 替换原则,子类是可以替换父类的。当需要 Object 类的引用的时候,如果传入一个 String 对象是没有任何问题的。但是反过来的话,用父类的引用替换子类引用的时候,就需要进行强制类型转换。编译器并不能保证运行时刻这种转换一定是合法的。这种自动的子类替换父类的类型转换机制,对于数组也是适用的。 String[] 可以替换 Object[] (只是在运行时类型错误会出现 ArrayStoreException 异常)。但是泛型的引入,对于这个类型系统产生了一定的影响。例如 List<String> 不是 List<Object> 的子类型,不能替换掉 List<Object> 。我们可以往 List<Object> 添加任意类型,但是只能往 List<String> 中添加 String 类型,当 List<Object> 的引用指向 List<String> 并从中添加了 Object 时,再次从 List<String> 获取就会有 ClassCastException 问题,因此从类型安全的角度来看这是不能替换的,编译器提示:

    List<Object> cannot be converted to List<String>
      看下这个例子就大概清楚了,泛型编程很大程度上就是为了在编译期提前进行类型检查,防止运行时的类型转换异常,这种类型未决行为会破坏类型安全。如果允许这么处理,那就与类型安全的初衷相悖,因此,编译器为了阻止这种情况发生,便直接报错。

    List<String> ls = new ArrayList<String>(); // 1
    List<Object> lo = ls; // 2 
    lo.add(new Object()); // 3
    String s = ls.get(0); // 4: Attempts to assign an Object to a String!
      引入泛型之后的类型系统增加了两个维度:一个是类型参数自身的继承体系结构,另外一个是泛型类或接口自身的继承体系结构。第一个指的是对于 List<String> 和 List<Object> 这样的情况,类型参数 String 是继承自 Object 的。而第二种指的是 List 接口继承自 Collection 接口。对于这个类型系统,有如下的一些规则:
    • 相同类型参数的泛型类的关系取决于泛型类自身的继承体系结构。即 List<String> 是 Collection<String> 的子类型,List<String> 可以替换 Collection<String>。这种情况也适用于带有上下界的类型声明。
    • 当泛型类的类型声明中使用了通配符的时候, 其子类型可以在两个维度上分别展开。如对 Collection<? extends Number> 来说,其子类型可以在 Collection 这个维度上展开,即 List<? extends Number> 和 Set<? extends Number> 等;也可以在 Number 这个层次上展开,即 Collection<Double> 和 Collection<Integer> 等。如此循环下去,ArrayList<Long> 和 HashSet<Double> 等也都算是 Collection<? extends Number> 的子类型。
    • 如果泛型类中包含多个类型参数,则对于每个类型参数分别应用上面的规则。
      对于想要 List<String> 、 List<Integer> 等都通用的,可以使用 List<?> 而非 List<Object> ,因为 List<?> 是像 Object 一般的基类,如下:

     
      从另一个角度讲, List<?> 可以接收,因为其无法写入,而且只能从中读取 Object ,这些限制初步保证了类型安全。具体可以看下文通配符的相关分析。
     

    5、通配符和边界限定符与PECS原则

    5.1、通配符

      根据 [7]Wildcards[8]Unbounded Wildcards ,在泛型编程中,问号 ? 被称为通配符 wildcard ,又称为无限定的通配符,代表的是未知的类型,即类型未知。通配符可以在多种情况下使用:作为参数、字段或局部变量的类型;有时会用作返回类型(尽管使用更具体的编程实践更好)。通配符从不用作泛型方法调用、泛型类实例创建或超类型的类型实参。
      例如 List<?> 表示的是未知类型的一个列表。通配符对于以下2个场景会是一种比较有用的处理方式:
    • 如果是只需要使用由 Object 提供的基础功能实现的方法。
    • 当代码使用泛型类中不依赖于类型参数的方法时。例如, List.size 或 List.clear 。事实上,之所以经常使用 Class<> ,是因为 Class<T> 中的大多数方法都不依赖于 T ,Class<String> 代表的是 String.class 。
      考虑如下案例中的 printList 方法:
    public static void printList(List<Object> list) {
        for (Object elem : list)
            System.out.println(elem + " ");
        System.out.println();
    }

      printList 方法的目的是打印任意类型的一个列表,但是明显这里只能打印是 Object 的列表,并不适用于 List<Integer>, List<String>, List<Double> 等,因为他们都不是 List<Object> 的子类型。此时可以使用 List<?> 来重写这个泛型方法:

    public static void printList(List<?> list) {
        for (Object elem: list)
            System.out.print(elem + " ");
        System.out.println();
    }
      因为对于具体的 List<Integer>, List<String>, List<Double> ,他们都是 List<?> 的子类型,此时的 printList 方法可以用于任意类型的打印。

    List<Integer> li = Arrays.asList(1, 2, 3);
    List<String>  ls = Arrays.asList("one", "two", "three");
    printList(li);
    printList(ls);
      注意的是,List<Object> 和 List<?> 是不一样的,List<Object> 可以写入 Object 及其任意子类型,但是将 List<?> 当作引用时,是无法写入的(null 除外)。编译器只知道会是某种类型,但是编译器不清楚具体是哪种类型,因此从类型安全考虑,编译器是不允许写入的。例如 List<?> 被称为未知类型的列表。

    List<?> list = new ArrayList<String>();
      如何根据使用场景选择具体的通配符类型,可以参考 [9]Guidelines for Wildcard Use
      在使用泛型类的时候,既可以指定一个具体的类型,如 List<String> 就声明了具体的类型是 String ;也可以用通配符 ? 来表示未知类型,如 List<?> 就声明了 List 中包含的元素类型是未知的。 ? 通配符所代表的其实是一组类型,但具体的类型是未知的。List<?> 所声明的就是所有类型都是可以的。但是 List<?> 并不等同于 List<Object> ,List<?> 是 List<Object> 的基类。List<Object> 实际上确定了 List 中包含的是 Object 及其子类,在使用的时候都可以通过 Object 来进行引用。而 List<?> 则其中所包含的元素类型是不确定。其中可能包含的是 String,也可能是 Integer。如果它包含了 String 的话,往里面添加 Integer 类型的元素就是错误的。正因为类型未知,就不能通过 new ArrayList<?>() 的方法来创建一个新的 ArrayList 对象。因为编译器无法知道具体的类型是什么。但是对于 List<?> 中的元素确总是可以用 Object 来引用的,因为虽然类型未知,但肯定是 Object 及其子类。考虑下面的代码:

    public void wildcard(List<?> list) {
        // 编译错误
        list.add(1);
    }
      如上所示,试图对一个带通配符的泛型类进行操作的时候,总是会出现编译错误。其原因在于通配符所表示的类型是未知的。

    5.2、通配符 ? 与类型 T 的区别的一些思考

      泛型编程主要涉及两方面:
    • 1、声明:例如泛型类、泛型方法
    • 2、使用:泛型类的实例化、泛型方法调用、返回
       T 是一个形参/类型参数(a type parameter),表示类型参数占位符/标识符,T 只是一种命名约定定义。它只适用于作为泛型类、泛型方法中的参数类型声明、属性、方法形参声明,表示都是同一种类型,保证类型一致,是一种类型约束,使用时肯定会被某种实际类型替代,例如 Object、String、Integer 等具体类型。
      ? 是一种未知类型(represents an unknown type),表示的是实参,代表某种实际类型,但是类型未知,可能是任意类型,? 是比 Object 还要特殊的一种类型,<?> 可以用于方法参数、属性、局部变量、返回类型(通常建议使用更具体的类型)等,一般都是跟着 <> 一起使用。在使用时,如果无法确认类型,就是用 <?> 来表示,由于无法确定具体类型,所以都是用于接收,且不能直接实例化。
      所以 T 可以用于泛型类/泛型方法的声明,而 ? 则不行。另外,? 表示类型未知,不确定类型,因此无法直接实例化、无法直接写入数据。
    class Box<?> {} // error
    class Box<T> {} // ok
    class Box<? extends Number> {} // error
    class Box<T extends Number> {} // ok
    private <T> List<?> tttest(List<?> list) {...} // ok
    private <?> List<?> tttest(List<?> list) {...} // error
      多重继承方面的差别,此不赘述。
     

    5.3、带边界的通配符

      因为对于 List<?> 中的元素只能用 Object 来引用,在很多情况下不是很方便,例如希望都是 Number 及其子类,而不是只能使用 Object。此时,可以使用带边界的通配符来限制未知类型的范围。如 List<? extends Number> 说明 List 中可能包含的元素类型是 Number 及其子类,而 List<? super Number> 则说明 List 中包含的是 Number 及其父类(而 Number 的子类向上上转型为 Number )。
      当引入了上界之后,在使用的时候就可以使用上界类中定义的方法。比如访问 List<? extends Number> 的时候,就可以使用 Number 类的 intValue 等方法。List<Number> 比 List<?extends Number> 对类型限制更严格,因为前者只匹配类型 Number 的列表,而后者匹配类型 Number 的列表或其任何子类,例如 List<Number> 、List<Long>、List<Integer> 都是可以的。实际上,在编译器层面保证了只能读取不能写入,而且用 Number 类型接收,在这些限制的情况下,这些操作都是合理且安全的。
      关于带边界的通配符,Joshua Bloch在著名的《Effective Java》中提出了 PECS 助记符以帮助大家理解和记忆使用通配符类型的基本原则:Producer Extends, Consumer Super 。而 Naftalin 和 Wadler 称之为 Get and Put Principle 。即“读取时使用extends,写入时使用super”。 
    • <?extends T> 表示的是一个有上边界的通配符,不能写入,只能读取,使用 T (或者顶层父类 Object)作为读取的接收类型,一般用于读取(产生)数据,因此说 Producer Extends
      • 例如 List<?extends Number> ,可以读取,也即是向外提供 Number 元素,即 Producer 。
    • <?super T> 表示的是一个有下边界的通配符,不能读取,可以写入,仅能使用 T 及其子类(向上转型 T )进行写入,一般用于写入(接收)数据,因此说是 Consumer Super
      • 例如 List<?super Number>,可以写入,也即是消费 Number 元素,将 Long 、Integer 等记录起来,即 Consumer 。 
    [9]Guidelines for Wildcard Use 则称之为 in 和 out 原则。
    注:关于 <?extends T> 也有称为型变/协变,而 <?super T> 称为逆变
      下面对 PECS 进行具体分析。 

    5.3.1、extends

      以泛型 List<? extends Number> 为例,根据【4、类型系统】, 我们知道 ArrayList<Number> 、 ArrayList<Double> 、 ArrayList<Integer> 、ArrayList<Long> 都是子类型,因此,下面的语法是合法的:
    List<? extends Number> nums = new ArrayList<Number>();
    List<? extends Number> nums = new ArrayList<Integer>();
    List<? extends Number> nums = new ArrayList<Double>();
      下面从 get 和 set ,即读取和写入两个方面来看。
    5.3.1.1、读取
      当调用 nums.get(0) 时,从前面提到的例子中,我们知道返回值的类型涉及到以下几种情形:
    • 可以返回 Object 类型,Object 是所有类型的顶层父类,向上转型是安全合法的。
    • 可以返回 Number 类型,从 List<? extends Number> 可以知道,无论这个 List 实际上是存储哪种类型的,最终都会是 Number 或者 Number 的子类,向上转型为 Number 也是安全的。
    • 但是不可能返回 Long 、 Double 、Integer 等 Number 的子类型,因为 nums 实际上可能是 ArrayList<Integer>、ArrayList<Long> 等,我们根本不知道实际存储的具体是哪个类型,因此无法使用 Number 的子类型来接收。
      其实也好理解,<? extends T> 意味着都是 T 或者 T 的子类,当然也是属于 Object 的子类(或者本身),因此读取回来后用父类型 T 或者顶层父类 Object 来接收是安全合理的,编译器也知道存储的总是 T 或其子类,使用 T 类接收安全。综上,我们可以得出结论:List<? extends T> 可以用于读取,即 get ,且返回的类型是 T 或者 T 的父类对象,或者是顶层父类 Object ,但不能返回 T 的子类对象。称为 get 原则。
     
    5.3.1.2、写入
      当调用 nums.add() 时,我们该如何处理?还是按照相关的场景来看:
    • 写入 Object ?肯定不可以。很明显,任意一个 Object 不一定是 Number 类型,例如 Object 和 String 就不是。
    • 写入 Number ?不可以。nums 如果指向的是 ArrayList<Double> ,是不能随便添加一个 Number 类型的。
    • 写入 Integer、Long 等子类型?也不可以。同样, nums 如果指向的是 ArrayList<Double> ,不能随便添加一个 Integer、Long 类型的。
      我们知道,泛型 List<? extends T> 最终肯定是引用到某些具体的类型,可能是 T ,有可能是 T 的某些子类,但是编译器在检查类型是否符合要求时,无从得知到底是 T 或者 T 的哪些子类,也就无法进行检查,为了保证类型安全,编译器便禁止写入。综上,List<? extends T> 不适用于写入的场景。
      当然,无论如何,都可以写入 null ,即 nums.add(null) 是可以正常编译运行的,因为 null 可以转成任何类型,包括 T 及其所有子类。这里没有必要钻牛角尖。 

    5.3.2、super

      以泛型 List<? super Integer> 为例,根据【4、类型系统】, 我们知道 ArrayList<Number> 、 ArrayList<Integer> 、 ArrayList<Object> 都适用,因此,下面的语法是合法的:

    List<? super Integer> nums = new ArrayList<Integer>();
    List<? super Integer> nums = new ArrayList<Number>();
    List<? super Integer> nums = new ArrayList<Object>();
      下面从 get 和 set ,即读取和写入两个方面来看。
    5.3.2.1、读取
      例如,当调用 nums.get(0) 方法时,我们需要通过什么类型来接收?
    • 不能是 Integer ,因为实际上 nums 可能指向 ArrayList<Object> 、 ArrayList<Number>
    • 不能是 Number ,因为实际上 nums 可能指向 ArrayList<Object>
    • 所以只能是顶层父类 Object ,但是这样我们就无法得知具体的类型,基本失去了操作意义,很大程度上只能处理一些类型无关的操作。
      对于 List<?super T> ,编译器只知道可能存储的都是 T 、T 的子类(向上转型为 T )、T 的父类,但是编译器在检查类型是否符合要求时,并不清楚具体是哪种类型,也不可能用子类引用指向父类(例如不能使用 T 来接收),只能使用顶层父类 Object ,因此,从类型安全的角度来看, List<?super T> 不适用于读取数据的场景。
     
    5.3.2.2、写入
      这个时候可以写入的是 T 及其子类,编译器在检查类型是否符合要求时,会自动向上转型为 T ,而且实际上不管具体的 T 的哪个父类,T 及其子类都可以向上转型,因此,从类型安全的角度看,List<?super T> 可以安全地添加 T 及其子类,而无法添加 T 的不确定的任意父类对象。称为 put 原则。
     

    5.4、PECS原则总结

    • 如果要从集合中读取类型 T 的数据,并且不能写入,可以使用 ? extends 通配符 (Producer Extends)
    • 如果要从集合中写入类型 T 的数据,并且不需要读取,可以使用 ? super 通配符 (Consumer Super)
    • 如果既要写入又要读取,那么就不要使用任何通配符。
    注:其实并不局限于集合。
     

    5.5、多重继承绑定

      [11]Bounded Type Parameters 多边界限制/多重继承绑定 Multiple Bounds 的情况下,这么用:

    class D <T extends A & B & C>
      如果其中有一个是 class ,则需要放在最前面,否则会编译错误。 

    6、类型擦除

      Java 中的泛型基本上是在编译器这个层面来实现的,使用泛型时加上的类型参数,会被编译器在编译的时候去掉,在生成的 Java 字节码中是不包含类型信息的,这个过程就称为类型擦除 Type Erasure 。如在代码中定义的 List<Object> 和 List<String> 等类型,在编译之后都会变成 List 。JVM 看到的只是 List,而由泛型附加的类型信息对 JVM 来说是不可见的。Java 泛型只能用于编译期的静态类型检查,Java 编译器会在编译时尽可能地发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是 Java 泛型实现方式与 C++ 模板机制 实现方式之间的重要区别。
      根据官方文档 [6]Type Erasure ,为了实现泛型,Java编译器将类型擦除应用于:
    • 使用其边界(bounds)或者 Object(如果类型参数是未绑定的)来替换泛型类型中的所有类型参数。因此,生成的字节码只包含普通的类、接口和方法。
    • 如有必要,会自动插入类型强制转换以确保类型安全。
    • 生成桥接方法以确保在扩展泛型类型中的多态性。
      类型擦除确保不会为参数化类型创建新类;因此,泛型不会产生运行时开销。
      总的来说,类型擦除的基本过程也比较简单,首先就是找到用来替代类型参数的具体类,一般是 Object 这个顶层类,或者是指定的类型参数上界,同时去掉 类型声明 <> ,比如 T get() 方法声明就变成了 Object get() , List<String> 变成了 List 。其中还可能会根据需要生成一些桥接方法(bridge method),以及使用强制类型转换,以便确保泛型类型中的多态。例如下面的代码:

    class MyString implements Comparable<String> {
       public int compareTo(String str) {        
           return 0;    
       }
    }
      类型擦除后,就变成了 class MyString implements Comparable ,这样的话,因为没有实现 Comparable 的 compareTo(Object) 方法就会出现编译失败,此时,编译器会动态生成 int compareTo(Object) 的桥接方法以确保正常编译:

    class MyString implements Comparable<String> {
       public int compareTo(String str) {        
           return 0;    
       }
       public int compareTo(Object str) {        
           return compareTo((String) str);    
       }
    }

    6.1、类型擦除带来的问题

      类型擦除很大程度上是为了保证旧版本的兼容性而做出的一种妥协,另一方面,为了提前到编译期进行类型检查,使用泛型编程时会有一些限制,这也导致了很多与泛型相关的奇怪特性或者问题。可以参考 [15]Restrictions on Generics 中列出来的7个限制和其他的一些相关问题。
     
    • 1、不支持基本类型
    类型擦除后是 Object ,都是针对引用类型的,从这个角度讲是不适用于基本类型的。 
    • 2、无法利用类型参数直接创建实例,只能通过反射
    由于类型参数 T 只是一个标记,并非实际的类型,因此无法直接创建实例:

    T t = new T();
    类型擦除后用 Object ,没多大意义了。如果需要利用类型参数来创建实例的话,一般都是通过 Class 对象,使用反射来处理:

    public static <E> void append(List<E> list, Class<E> cls) throws Exception {
        E element = cls.newInstance();
        list.add(element);
    }
    然后这样调用 append 方法:

    List<String> ls = new ArrayList<>();
    append(ls, String.class);
    • 3、没有泛型静态变量
    如果允许的话,考虑以下场景:

    public class MobileDevice<T> {
        private static T os;
    
        // ...
    }
    os 究竟是什么类型 ?

    ///
    MobileDevice<Smartphone> phone = new MobileDevice<>();
    MobileDevice<Pager> pager = new MobileDevice<>();
    MobileDevice<TabletPC> pc = new MobileDevice<>();
    另外,静态变量是编译时就得知道类型并分配内存,而泛型则在编译期类型擦除,无法得知具体的类型信息。 
    • 4、无法对泛型类直接使用 instanceof 运算
    类型信息在编译期进行类型擦除后,运行时是没有的, ArrayList<String> 和 ArrayList<Integer> 在运行的类型是一样的。

    // RunTime Type Information,运行时类型信息
    private static <E> void rtti(List<E> list) {
        System.out.println(list instanceof ArrayList<String>);  // compile-time error
    }
    IDE 提示:Cannot perform instanceof check against parameterized type ArrayList<String>. Use the form ArrayList<?> instead since further generic type information will be erased at runtime。
    使用通配符形式,通过前面提到的类继承体系来处理:

    private static void rtti(List<?> list) {
        System.out.println(list instanceof ArrayList<?>);
    }
    • 5、不能使用参数化类型直接创建泛型数组
    数组可以接收子类数组,而在运行时可能出现类型转换异常 ArrayStoreException

    Object[] obj = new String[10];
    obj[0] = "1";
    obj[1] = 1; // ArrayStoreException
    泛型数组是不可以这样处理的。考虑如下案例:

    Object[] stringLists = new List<String>[];  // compiler error, but pretend it's allowed
    stringLists[0] = new ArrayList<String>();   // OK
    stringLists[1] = new ArrayList<Integer>();  // An ArrayStoreException should be thrown,
                                                // but the runtime can't detect it.
      如果允许参数化列表数组,由于类型擦除的原因,最终都是无差别的 ArrayList ,则前面的代码将无法引发所需的 ArrayStoreException 。
      另外根据 [17]The Fine Print ,这里提到的应该是不允许创建泛型类数组,除非它使用(未绑定的)通配符类型。可以声明元素类型为类型参数的数组类型 T[],但不能声明数组对象 List<String>[] 。这种限制对于避免以下情况是必要的:

    // Not really allowed.
    List<String>[] lsa = new List<String>[10];
    Object o = lsa;
    Object[] oa = (Object[]) o;
    List<Integer> li = new ArrayList<Integer>();
    li.add(new Integer(3));
    // Unsound, but passes run time store check
    oa[1] = li;
    
    // Run-time error: ClassCastException.
    String s = lsa[1].get(0);
      如果允许参数化类型的数组,类型擦除后 ArrayList<Integer> 赋值给 oa[1] 不会出现 ArrayStoreException ,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException ,这个示例在编译时不会出现任何未经检查的警告,但仍会在运行时失败。我们把类型安全作为泛型的主要设计目标。特别是,Java语言设计意在保证,如果使用javac-source 1.5及以上编译整个应用程序时没有出现未经检查的警告,那么它是类型安全的。
      当然,我们可以使用通配符数组。上面代码的以下变体放弃了使用元素类型为参数化的数组对象和数组类型。因此,我们必须显式强制转换才能从数组中获取字符串:

    // OK, array of unbounded wildcard type.
    List<?>[] lsa = new List<?>[10];
    Object o = lsa;
    Object[] oa = (Object[]) o;
    List<Integer> li = new ArrayList<Integer>();
    li.add(new Integer(3));
    // Correct.
    oa[1] = li;
    // Run time error, but cast is explicit.
    String s = (String) lsa[1].get(0);
      使用通配符数组,而还是使用参数化类型来接收也是会编译错误:

    // Error.
    List<String>[] lsa = new List<?>[10];
      类似地,直接创建类型参数数组对象也是会编译错误,这是由于运行时不存在类型变量,因此无法确定实际的数组类型。

    <T> T[] makeArray(T t) {
        return new T[100]; // Error.
    }
    处理这些限制的方法是使用运行时类型信息 java.lang.Class 类 [18]Class Literals as Runtime-Type Tokens 。泛型类 Class<T> , T 表示的是实际的类型,例如 Class<String> 表示 String.class ,我们可以通过调用 Class.newInstance() 方法来产生 T 的实例,前面提到的实例化 T 或者 T[] 都可以。 
    • 5.1、通过 java.lang.reflect.Array.newInstance(Class<?> componentType, int length) 来创建 T[] 数组,类型信息是通过 Class<?> 来获取的

    public class GenericArrayWithTypeToken<T> {
        private T[] array;
    
        @SuppressWarnings("unchecked")
        public GenericArrayWithTypeToken(Class<T> type, int sz) {
            array = (T[]) Array.newInstance(type, sz);
        }
    
        public void put(int index, T item) {
            array[index] = item;
        }
    
        public T get(int index) {
            return array[index];
        }
    
        // Expose the underlying representation:
        public T[] rep() {
            return array;
        }
    
        public static void main(String[] args) {
            GenericArrayWithTypeToken<Integer> gai = new GenericArrayWithTypeToken<>(Integer.class, 10);
            // This now works:
            Integer[] ia = gai.rep();
            ia[0] = 1;
        }
    }
    • 5.2、使用 Object[] 集合和强制类型转换来处理 T[]

    public class GenericsArray<T> {
        /// 直接使用 Object[]
        /// private Object[] array;
        private T[] array;
        
        @SuppressWarnings("unchecked")
        public GenericsArray(int size){
            array = (T[])new Object[size];
        }
        public void put(int index, T item) {
            array[index] = item;
        }
        /// @SuppressWarnings("unchecked")
        public T get(int index) {
            return (T)array[index];
        }
    }
    注:以上 2种方式 都可以在 JDK 源码的 ArrayList 种看到,其内部是用 Object[] 和强制类型转换来处理的,有兴趣的可以深入了解下。

    • 5.3、使用 ArrayList 来替换和模拟, ArrayList 内部其实还是使用了 Object[]
    参考《On Java 8》第20章 泛型 —— 泛型数组 
    • 6、泛型的类型参数不能用在 Java 异常处理的 catch 语句中,也不能创建和抛出泛型异常,因为异常处理是由 JVM 在运行时来进行的。由于类型信息被擦除,JVM 是无法区分两个异常类型 MyException<String> 和 MyException<Integer> 的。对于 JVM 来说,它们都是 MyException 类型的,也就无法执行与异常对应的 catch 语句。 
    • 7、无法通过类型参数进行重载,因为会被擦除成同样的原始类型
    如下类型擦除后方法签名一样,会产生编译错误。
    public class Example {
        public void print(Set<String> strSet) { }
        public void print(Set<Integer> intSet) { }
    }
    • 8、泛型类没有自己独有的 Class 对象。比如并不存在 ArrayList<String>.class 或者是 ArrayList<Integer>.class ,而只有 ArrayList.class ,下面这个结果是 true ,因为都是 java.util.ArrayList 。

    // 《On Java 8》 第20章 泛型的例子
    Class c1 = new ArrayList<String>().getClass();
    Class c2 = new ArrayList<Integer>().getClass();
    System.out.println(c1 == c2);
    • 9、桥接方法
      参考 [14]Effects of Type Erasure and Bridge Methods ,编译扩展参数化类或实现参数化接口的类或接口时,作为类型擦除过程的一部分,编译器可能需要创建一个称为桥接方法的合成方法。我们通常不需要担心桥接方法,但是如果一个方法出现在堆栈跟踪中,则可能会感到困惑。
      考虑如下的代码:

    public class Node<T> {
    
        public T data;
    
        public Node(T data) { this.data = data; }
    
        public void setData(T data) {
            System.out.println("Node.setData");
            this.data = data;
        }
    }
    
    public class MyNode extends Node<Integer> {
        public MyNode(Integer data) { super(data); }
        
        public void setData(Integer data) {
            System.out.println("MyNode.setData");
            super.setData(data);
        }
    }
    假如类型擦除后:

    public class Node {
    
        public Object data;
    
        public Node(Object data) { this.data = data; }
    
        public void setData(Object data) {
            System.out.println("Node.setData");
            this.data = data;
        }
    }
    
    public class MyNode extends Node {
    
        public MyNode(Integer data) { super(data); }
    
        public void setData(Integer data) {
            System.out.println("MyNode.setData");
            super.setData(data);
        }
    }
      类型擦除后,我们发现,需要重写的父类方法 setData(Object) 现在只有 setData(Integer) ,这个并不构成重写。为了解决这个问题并在类型擦除后保留泛型类型的多态性,Java 编译器生成一个桥方法来确保子类型按预期工作。对于 MyNode 类,编译器为 setData 生成以下桥接方法:

    class MyNode extends Node {
    
        // Bridge method generated by the compiler
        //
        public void setData(Object data) {
            setData((Integer) data);
        }
    
        public void setData(Integer data) {
            System.out.println("MyNode.setData");
            super.setData(data);
        }
    
        // ...
    }
    正如上面所示,桥方法在类型擦除后与 Node 类的 setData 方法具有相同的方法签名,它委托给原始的 setData 方法,这也是运行中会出现 ClassCastException 的原因。 
    • 10、静态变量同样是被泛型类的所有实例共享的,而且是已知类型的,非泛型类静态。
      对于声明为 MyClass<T> 的类,访问其中的静态变量的方法仍然是 Class.staticVariableName,例如 MyClass.myStaticVar ,不管是通过 new MyClass<String> 还是 new MyClass<Integer> 创建的对象,都是共享一个静态变量。
     

    7、泛型使用的一些实践建议

      在使用泛型的时候可以遵循一些基本的原则,从而避免一些常见的问题。
    • 在代码中避免泛型类和原始类型的混用。比如 List<String> 和 List 不应该共同使用。这样会产生一些编译器警告和潜在的运行时异常。当需要利用 JDK 5 之前开发的遗留代码,而不得不这么做时,也尽可能的隔离相关的代码。
    • 在使用带通配符的泛型类的时候,需要明确通配符所代表的一组类型的概念。由于具体的类型是未知的,很多操作是不允许的。
    • 泛型类最好不要同数组一块使用。你只能创建 new List<?>[10] 这样的数组,无法创建 new List[10] 这样的。这限制了数组的使用能力,而且会带来很多费解的问题。因此,当需要类似数组的功能时候,使用集合类即可。
    • 不要忽视编译器给出的警告信息,尽可能保证代码没有编译警告,确保类型安全。 

    8、其他

    9、参考

     
    其他参考:

  • 相关阅读:
    高效编写微信小程序
    故事怎么讲才有逼格?
    基于RESTful API 怎么设计用户权限控制?
    【开源访谈】腾讯贺嘉:从小程序谈起,开发者该如何跟进新技术?
    图标字体设计
    微信小程序即将上线,创业者机会在哪里?
    微信小程序开发学习资料
    PC 微信扫码登陆
    一张二维码同时集成微信、支付宝支付
    支付宝Wap支付你了解多少?
  • 原文地址:https://www.cnblogs.com/wpbxin/p/14618917.html
Copyright © 2011-2022 走看看