• 《Java核心技术(卷1)》笔记:第8章 泛型程序设计


    1. (P 327)“菱形”语法:

      ArrayList<String> files = new ArrayList<>();
      // Java 9 扩展了菱形语法的使用范围,例如:现在可以对匿名子类使用菱形语法
      ArrayList<String> passwords = new ArrayList<>() {
          public String get(int n) {
              return super.get(n).replaceAll(".", "*");
          }
      }
      
    2. (P 328)定义泛型类:

      public class Pair<T, U> {
          ...
      }
      

      常见的做法是类型变量使用大写字母,而且很简短:

      • E表示集合的元素类型
      • KV分别表示表的的类型
      • TUS表示任意类型
    3. (P 330)定义泛型方法:类型变量放在修饰符的后面,并在返回类型的前面

      class ArrayAlg {
          public static <T> T getMiddle(T... a) {
              ...
          }
      }
      

      调用泛型方法:

      String middle = ArrayAlg.<String>getMiddle("John", "Q.", "Public");
      // 大多数情况下,可以省略类型参数
      String middle = ArrayAlg.getMiddle("John", "Q.", "Public");  // 编译器将参数的类型与泛型类型T进行匹配,推断出T一定是String
      
    4. (P 332)类型变量的限定

      T是限定类型(bounding type)的子类型:

      <T extends BoundingType>
      

      一个类型变量或通配符可以有多个限定,限定类型用“&”分隔,而逗号用来分隔类型变量

      <T extends BoundingType1 & BoundingType2>
      

      可以拥有多个接口超类型,但最多有一个限定可以是类。如果有一个类作为限定,它必须是限定列表中的第一个限定

    5. (P 333)类型擦除:无论何时定义一个泛型类型,都会自动提供一个相应的原始类型。这个原始类型的名字就是去掉类型参数后的泛型类型名。类型变量会被擦除,并替换为其限定类型(或者,对于无限定的变量则替换为Object)

    6. (P 334)为了提高效率,应该将标签接口(即没有方法的接口)放在限定列表的末尾

    7. (P 335)调用一个泛型方法时,编译器会擦除返回类型,并插入强制类型转换。当访问一个泛型字段时,也会插入强制类型转换。

      Pair<Employee> buddies = ...;
      Employee buddy = buddies.getFirst();
      // 编译器会做如下类似的处理
      Pair buddies = ...;	// 擦除类型参数,Pair中的所有泛型被替换为Object
      Employee buddy = (Employee) buddies.getFirst();	// 插入强制类型转换(方法原来的返回类型被擦除变成了Object)
      
    8. (P 335)桥方法:用来解决多态调用类型擦除的冲突

      方法的擦除会带来两个问题,考虑如下代码:

      class DateInterval extends Pair<LocalDate> {
          // (伪)重写父类中的方法
          // 之所以这里加个“伪”字,是因为父类的类型参数会被编译器擦除,变成Object,所以这里实际上是重载了父类中的方法,只是看起来像重写
          public void setSecond(LocalDate second) {
              ...
          }
          // 这个类中,除了上面的那个外,还存在一个从父类继承的方法
          public void setSecond(Object second);
      }
      

      这样在多态调用时会产生问题:

      DateInterval interval = ...;
      Pair<LocalDate> pair = interval;
      pair.setSecond(aDate);	// 这里调用的是哪个方法呢?类型擦除和多态发生了冲突
      			// 如果编译器什么都不做,将调用Pair.setSecond(Object),因为Pair中只存在这一个setSecond方法
      			// 而我们希望进行多态调用,即调用DateInterval.setSecond(LocalDate)
      

      为了解决这个问题,编译器会在子类中生成一个桥方法:

      class DateInterval extends Pair<LocalDate> {
          // (伪)重写父类中的方法
          public void setSecond(LocalDate second) {
              ...
          }
          // 编译器生成的桥方法,重写了父类的setSecond方法
          public void setSecond(Object second) {
              setSecond((LocalDate) second);	// 调用上面的那个setSecond方法
          }
      }
      

      另外,还有一个问题,考虑如下代码:

      class DateInterval extends Pair<LocalDate> {
          // (伪)重写父类中的方法
          public LocalDate getSecond() {
              ...
          }
          // 同理,编译器会生成桥方法,以便进行多态调用
          public Object getSecond() {
              return (LocalDate) getSecond();	// 这里调用的是哪个方法呢?方法重载时要求参数类型不同,但是这里两个getSecond方法都没有参数,似乎不合法
          }
      }
      

      程序员是不能这样编写Java代码的,但是在虚拟机中,会由参数类型返回类型共同指定一个方法。因此,编译器可以为两个仅返回类型不同的方法生成字节码,虚拟机能够正确地处理这种情况

    9. (P 337)对于Java泛型的转换,有如下几个事实:

      • 虚拟机中没有泛型,只有普通的类和方法
      • 所有的类型参数都会替换为它们的限定类型
      • 会合成桥方法来保持多态
      • 为保持类型安全性,必要时会插入强制类型转换
    10. (P 337)在泛型代码和遗留代码之间进行互操作时,编译器会发出一个警告,可以通过加注解@SuppressWarnings("unchecked")使之消失

      // 将泛型对象赋给原始类型对象
      Dictionary<Integer, Component> labelTable = ...;
      @SuppressWarnings("unchecked")	// 抑制编译器的警告
      slider.setLabelTabel(labelTable);	// warning
      
      // 将原始类型对象赋给泛型对象
      @SuppressWarnings("unchecked")	// 抑制编译器的警告
      Dictionary<Integer, Component> labelTable = slider.getLabelTable();	// warning
      
    11. (P 338)限制与局限性:

      • 不能用基本类型实例化类型参数

        Pair<double> pair = ...; // 不合法,double是基本类型
        
      • 运行时类型查询只适用于原始类型

        if (a instanceof Pair<String>)		// 错误
        if (a instanceof Pair<T>)		// 错误
        if (a instanceof Pair)			// 正确
        
        Pair<String> pair = (Pair<String>) a;	// 错误
        

        getClass方法总是返回原始类型

        Pair<String> stringPair = ...;
        Pair<Employee> employeePair = ...;
        if (stringPair.getClass() == employeePair.getClass()) // 比较结果为true,两个getClass调用都返回Pair.class
        
      • 不能创建参数化类型的数组(可以声明,但不能创建)

        var table = new Pair<String>[10];	// 错误
        var table = (Pair<String>[]) new Pair<?>[10]; // 可以,但是结果将是不安全的
        

        如果需要收集参数化类型对象,简单地使用ArrayList更安全、有效

        var table = new ArrayList<Pair<String>>();	// 合法
        
      • Varargs警告:向参数个数可变的方法传递一个泛型类型的实例,编译器会发出一个警告,可以使用@SuppressWarnings("unchecked")或者@SafeVarargs注解来抑制这个警告

        @SafeVarargs
        public static <T> void addAll(Collection<T> coll, T... ts)	// 调用这个方法时,虚拟机必须要创建T类型的数组ts
        								// 这违反了前面的规则,但此时编译器只会发出一个警告
        
        • 对于任何只需要读取参数数组元素的方法,都可以使用@SafeVarargs注解
        • @SafeVarargs只能用于声明为staticfinalprivate的构造器和方法。
      • 不能实例化类型变量

        public Pair() {
            first = new T();	// 错误
            second = new T();	// 错误
        }
        

        Java 8之后,最好的解决办法:让调用者提供一个构造器表达式

        public static <T> Pair<T> makePair(Supplier<T> constr) {
            return new Pair<>(constr.get(), constr.get());
        }
        
        Pair<String> p = Pair.makePair(String::new);
        

        传统的解决方法:通过反射调用Constructor.newInstance方法来构造泛型对象

        first = T.class.getConstructor().newInstance();	// 错误,T被擦除为Object
        
        public static <T> Pair<T> makePair(Class<T> cl) {
            try {
                return new Pair<>(cl.getConstructor().newInstance(), cl.getConstructor().newInstance());
            } catch (Exception e) {
                return null;
            }
        }
        
        Pair<String> p = Pair.makePair(String.class);
        
      • 不能构造泛型数组

        public static <T extends Comparable> T[] minmax(T... a) {
            T[] mm = new T[2];	// 错误
            ...
        }
        
      • 泛型类的静态上下文中类型变量无效:不能在静态字段或方法中引用类型变量

        public class Singleton<T> {
            private static T singleInstance;	// 错误
            public static T getSingleInstance() {	// 错误
                ...
            }
        }
        
      • 不能抛出或捕获泛型类的实例

        public class Problem<T> extends Exception { ... }	// 错误,泛型类不能扩展Throwable
        try { ... } catch (T e) { ... }			// 错误,catch子句中不能使用类型变量
        
      • 可以取消对检查型异常的检查

        通过使用泛型类、擦除和@SuppressWarnings注解,我们就能消除Java类型系统的部分基本限制(详见P 343 ~ P 345)

      • 注意擦除后的冲突:例如在类中增加一个equals方法就可能和从Object中继承的equals方法冲突

        倘若两个接口类型是同一接口的不同参数化,一个类或类型变量就不能同时作为这两个接口类型的子类

        class Employee implements Comparable { ... }
        class Manager extends Employee implements Comparable { ... } // 错误
        
    12. (P 346)具有继承关系的类如果作为泛型类的类型参数,则这些泛型类之间没有继承关系(通配符类型可以解决这个问题),例如EmployeeManager具有继承关系,但是Pair<Employee>Pair<Manager>之间没有继承关系。注意:数组类型Employee[]Manager[]之间具有继承关系

    13. (P 347)总是可以将参数化类型转换为一个原始类型

      var managerBuddies = new Pair<Manager>(...);
      Pair rawBuddies = managerBuddies;	// 合法
      
    14. (P 347)泛型类可以扩展或实现其他的泛型类。如:ArrayList<T>实现了List<T>接口,这意味着ArrayList<Manager>实现了List<Manager>接口

    15. (P 348)通配符:在通配符类型中,允许类型参数发生变化

      Pair<? extends Employee>	// 表示任何泛型Pair类型,它的类型参数是Employee的子类
      				// 如Pair<Manager>是Pair<? extends Employee>的子类
      

      其中的方法如下:

      ? extends Employee getFirst()		// 合法,可以将返回值赋给一个Employee
      void setFirst(? extends Employee)	// 这样不可能调用这个方法,它拒绝传递任何特定的类型
      
    16. (P 349)超类型限定:? super Manager,这个通配符限制为Manager的所有超类型

      void setFirst(? super Manager)	// 合法,可以向方法传递一个Manager对象,或者其子类型的对象
      ? super Manager getFirst()	// 不能调用这个方法,它无法确定返回值的类型,只能赋给Object
      
    17. (P 350)直观地讲,带有超类型限定的通配符允许你写入一个泛型对象,而带有子类型限定的通配符允许你读取一个泛型对象

    18. (P 351)无限定通配符:在编写不需要实际类型的方法时很有用,可读性更好

      ? getFirst()		// 返回值只能赋给Object
      void setFirst(?)	// 不能被调用,甚至不能传递Object(原始的Pair类型可以,这是Pair<T>和Pair主要的不同),可以传递null
      
    19. (P 352)不能在编写代码中使用“?”作为一种类型,必须保存?类型的变量时,可以通过编写辅助方法(泛型方法)解决

    20. (P 353)通配符捕获只有在非常限定的情况下才是合法的,编译器必须能够保证通配符表示单个确定的类型

    21. (P 356)可以使用java.lang.reflect包中的接口Type表述泛型类型的声明,其包含以下子类:

      • Class类,描述具体类型
      • TypeVariable接口,描述类型变量
      • WildcardType接口,描述通配符
      • ParameterizedType接口,描述泛型类或接口类型
      • GenericArrayType接口,描述泛型数组
  • 相关阅读:
    一秒 解决 ERROR 1044 (42000): Access denied for user ''@'localhost' to database 'mysql 问题
    30分钟让你学会 Spring事务管理属性
    判断是否是回文
    Linux自动获取IP地址 重启不会消失
    .The server quit without updating PID file (/var/lib/mysql/pc.pid).
    线程的状态
    linux vim基本操作
    C++ 函数重载和参数的缺省值
    C++ 类中的3种访问权限和继承方式
    C++ 内存管理
  • 原文地址:https://www.cnblogs.com/qinjinyu/p/13209188.html
Copyright © 2020-2023  润新知