《JAVA核心技术 卷I》第八章 - 泛型程序设计

第八章 - 泛型程序设计

目录

1. 为什么要使用泛型

  • 泛型程序设计意味着编写的代码可以对多种不同类型的对象重用

  • 泛型能提高代码的安全性和可读性

    以ArrayList类为例,在泛型之前ArrayList类只维护一个Object引用的数组,这样的类,每当获取一个值都必须进行强制转换,而且没有错误检查

    ArrayList files = new ArrayList();
    String filename = (String)files.get(0);
    files.add(new File("..."));	//不会在编译阶段报错,直到运行阶段调用并强制转换为String才可能会报错
    

    类型参数(type parameter)可以用来指示元素的类型,这使得代码具有更好的可读性和安全性

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

    编译器也可以充分利用这个类型信息调用get的时候,不需要进行强制类型转换。编译器知道返回值类型为String,而不是Object

    String filename = files.get(0);	//不用强制转换
    

    编译器还知道ArrayList<String>的add方法有一个类型为String的参数,这比有一个Object类型的参数要安全得多。现在编译器可以检查,防止插入错误类型的对象

    files.add(new File("..."));	//无法通过编译
    

2. 泛型类和泛型方法

2.1 泛型类

  • 泛型类就是有一个或多个类型变量的类,以Pair类为例

    public class Pair<T>
    {
        private T first;
        private T second;
        
        public Pair() {first = null; second = null;}
        public Pair(T first, T second){this.first = first; this.second = second;}
        
        public T getFirst() {return first;}
        public T getSecond() {return second;}
    }
    
    //泛型类可以有多个类型变量,比如public class Pair<T,U> {...}。类型变量在对整个类定义中用于指定方法的返回类型以及字段和局部变量的类型
    //常见的做法是类型变量使用大写字母。Java库使用变量E表示集合的元素类型,K和V分别表示表的键和值的类型。T,U,S表示"任意类型"
    

2.2 泛型方法

  • 可以定义一个带有类型参数的方法,泛型方法不一定要定义在泛型类中

    class ArrayAlg
    {
        public static <T> T getMiddle(T ... a){
            return a[a.length/2];
        }
    }
    
    //大多数情况,可以省略类型参数直接调用泛型方法
    String middle = ArrayAlg.getMiddle("John","Q.","Public");
    //在极少数情况下,这可能会导致编译器无法从多个泛型方法中选择合适的方法,此时可以选择补上泛型或调整参数
    

2.3 类型变量的限定

  • 有时候,类或方法需要对类型变量加以约束,比如某个方法限制泛型T只能是实现了Comparable接口的类。可以通过对类型变量T设置一个限定(bound)来实现这一点

    //T和限定类型可以是类,也可以是接口。
    public static <T extends Comparable> T min(T[] a){...};
    
    //一个类型变量或通配符可以有多个限定,限定类型用"&"分隔,而逗号用来分隔类型变量
    public static <T extends Comparable & Serializable,U> T min(T[] a){...};
    

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

3. 泛型的实现原理

无论何时定义一个泛型类型,都会自动提供一个相应的原始类型(raw type),这个原始类型的名字就是去掉类型参数后的泛型类型名。类型变量会被擦除(erased),并替换为其限定类型。例如,上面的Pair的原始类型就如下所示:

public class Pair
{
    private Object first;
    private Object second;
    
    public Pair() {first = null; second = null;}
    public Pair(Object first, Object second){this.first = first; this.second = second;}
    
    public Object getFirst() {return first;}
    public Object getSecond() {return second;}
}

//如果有限定类型,那么原始类型会用第一个限定来替换类型变量。如果上面的例子写的是<T extends Comparable & Serializable>那么所有Object都会被替换为Compareable
//如果把上述顺序调转一下<T extends Serializable & Comparable>那么就会把所有Object替换为Serializable,而编译器会在必要时向Comparable插入强制类型转换。为了提高效率,应该将标签接口(没有方法的接口)放在限定列表的末尾

编写一个泛型方法调用时,如果擦除了返回类型,编译器会插入强制类型转换。例如:

Pair<Employee> buddles = ...;
Employee buddy = buddies.getFirst();
//getFirst擦除类型后的返回类型是Object。编译器自动插入转换到Employee的强制类型转换。

3.1 桥方法

  • 重写中的桥方法

桥方法早在使用重写的时候就已经出现过:重写方法时,当一个方法覆盖另一个方法,可以指定一个更严格的返回类型(如返回子类),以clone方法举例

public class Employee implements Cloneable
{
    public Employee clone() {...};
}

表面上Employee clone()覆盖了Object clone()。此时,Employee内实际上有两个clone()方法,一个是Employee clone(),另一个是Object clone()。在调用Employee内重写的clone方法的时候,编译器会按照重写规则先调用Object clone()方法,然后通过桥方法再调用Employee clone()

//自动生成的桥方法
public Object clone() {clone((Employee) e);}
  • 类型擦除中的桥方法

类型擦除也会出现在泛型方法中,一般来说擦除的方式和类的没什么区别。可是当涉及到子父类之间方法的重写的时候,事情就变得复杂起来,举例:

//擦除前
class DataInterval extends Pair<LocalDate>
{
    public void getSecond(LocalDate second){...};
}

//擦除后
class DataInterval extends Pair
{
    public void getSecond(LocalDate second){...};
}

//按理来说,DataInterval中只会存在一个LocalDate类型的getSecond用于重写Pair中的getSecond。但由于类型擦除的存在,DataInterval还继承了Pair内Object类型的getSecond
//此时如果我们如下操作,就会出现歧义
DataInterval interval = new DateInterval();
Pair<LocalDate> pair = interval;
pair.setSecond(aDate);

//pair是一个Pair<LocalDate>类型的对象,根据擦除原理,它会变为Pair类型,其内的所有方法也都变为Object类型。现在让被擦除后的pair去调用setSecond,根据多态性,他会去搜索interval中有没有重写过Object类型的setSecond。很不幸的是,interval中刚好有。按照原定的设想,pair.setSecond应该调用LocalDate类型的setSecond,而不是Object类型的setSecond
//为了解决这一问题,桥方法出现了
public void setSecond(Object second) {setSecond((LocalDate) second);}
  • 对于Java泛型的转换,记住以下几点
    • 虚拟机中没有泛型,只有普通的类和方法
    • 所有的类型参数都会替换为它们的限定类型
    • 会合成桥方法来保持多态
    • 为保持类型安全性,必要时会插入强制类型转换

4. 泛型的限制与局限性

  1. 不能使用基本类型实例化类型参数

    没有Pair<double>,只有Pair<Double>。擦除造成了这一问题,因为擦除后Pair类含有Object类型的字段,而Object不能储存double值

  2. 运行时类型查询只适用于原始类型

    由于擦除,所有的类型查询只产生原始类型,比如Pair<...>在类型查询时永远统一为Pair

    if(a instanceof Pair<String>);
    Pair<String> p = (Pair<String>) a;
    //这些实际上都只是在测试a是不是一个Pair,根本检测不到特定的泛型类型
    //同理getClass方法也总是返回原始类型
    Pair<String> stringPair = ...;
    Pair<Employee> employePair = ...;
    if(stringPair,getClass() == employePair.getClass());	//返回true
    //两者的具体值都是Pair.class
    
  3. 不能创建参数化类型的数组

    由于擦除,不能创建特定泛型类型的数组

    Pair<String> table = new Pair<>[10];	//错误
    //擦除之后,数组的类型就是Pair[],虽然可以阻止不属于Pair类型的元素进入,但是对于诸如Pair<Employee>这样的元素,就无法阻止。所以Java从根源上就阻止了特定泛型类型数组的创建
    //但是仍然可以声明Pair<String>[]这样的变量,只是不允许初始化此类数组而已
    

    如果需要收集参数化类型对象,使用ArrayList:ArrayList<Pair>更安全有效

  4. Varargs警告

    和上面一样,既然不支持创建参数化类型的数组,那么对于可变参数也不行

    public static <T> void addAll(Collection<T> coll, T ... ts){
        for(T t : ts) coll.add(t);
    }
    //实际上ts就是一个数组,如果有如下调用
    Collection<Pair<String>> table = ...;
    Pair<String> pair1 = ...;
    Pair<String> pair2 = ...;
    addAll(table,pair1,pair2);
    //虚拟机就会不得不创建Pair<String>数组,这就违反了3.中的规则
    

    虽然理论上这不会产生错误,而只是得到一个警告。但还是不要使用这种方法比较好

  5. 不能实例化类型变量

    //不能在类似new T(...)的表达式中使用类型变量
    public Pair() {first = new T(); second = new T();}	//错误
    

    擦除将会把T变成Object,你一定不希望调用new Object()。解决方法有两种

    1. Java8之后的方法引用

      //创建makePair方法接收一个Supplier<T>
      //Supplier<T>是一个函数式接口,表示一个无参数且返回类型为T的函数
      Pair<String> p = Pair.makePair(String::new); //指定创建String类型对象
      
      public static <T> Pair<T> makePair(Supplier<T> constr){
          return new Pair<>(constr.get(),constr.get());
      }
      
    2. 反射

      //反射的情况会复杂一些,因为常规的实例创建方法不起作用
      first = T.class.newInstance();
      //表达式T.class时不合法的,毕竟擦除之后会变为Object.class
      
      
      //正确的方法
      Pair<String> p = Pair.makePair(String.class);
      
      public static <T> Pair<T> makePair(Class<T> cl){
          try{
              return new Pair<>(cl.getConstructor().newInstance(),cl.getConstructor().newInstance());
          }
          catch (Exception e) {return null;}
      }
      
  6. 不能构造泛型数组

    和5.差不多,类型会被擦除,会带来潜在的风险。解决方法也和5.类似

    1.方法引用

    String[] names = ArrayAlg.minmax(String[]::new,"Tom","Dick","Harry");
    
    public static <T extends Comparable> T[] minmax(InFunction<T[]> constr, T...a){
        T[] result = constr.apply(2);
        //...
    }
    

    2.反射

    public static <T extends Comparable> T[] minmax(T...a){
        T[] result = (T[])Array.newInstance(a.getClass().getComponentType(),2);
        //...
    }
    
  7. 泛型类的静态上下文中类型变量无效

    不能在静态字段或方法中引用类型变量,擦除之后只存在一种Singleton,会造成冲突

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

    public static <T extends Throwable> void doWork(Class<T> t)
    {
        try
        {
            //...
        }
        catch (T e)	//错误
        {
            Logger.global.info(...);
        }
    }
    
  9. 额外的限制

    擦除可能会带来不少潜在的冲突,对于方法更是如此,如下列例子

    class Pair<T>{
        private T first;
        private T second;
    
        public Pair() {first = null; second = null;}
    
        public boolean equals(T value){...} //ERROR
    }
    //看似此处equals方法和Object内的equals方法不同,但是实际上擦除后还是和Object内的equals方法重复了。即便以重写的角度来看也是不允许的,会直接报错
    

    泛型规范说明引用了一个原则:"为了支持擦除转换,要施加一个限制:倘若两个接口类型是同一个接口的不同参数化,一个类或类型变量就不能同时作为这两个接口类型的子类"

    class Employee implements Comparable<Employee> {...};
    class Manager extends Employee implements Comparable<Manager> {...}; //ERROR
    //Manager会实现Comparable<Employee>和Comparable<Manager>,这是同一接口的不同参数化
    //背后的具体原因可能与桥方法的合成有关
    

5. 通配符类型

  • 在通配符类型中,允许类型参数发生变化。

  • 通配符的优点主要有两点:

    1. 提高泛用性:允许方法或类在一定范围内适配更多类型的泛型
    //printBuddies允许Employee及其子类使用
    public static void main(String[] args) {
    
            Manager ceo = new Manager();
            Manager cfo = new Manager();
            Employee lowlyEmployee = new Employee();
        	Pair<Employee> pair2 = new Pair<>(ceo,cfo);
            Pair<Employee> pair1 = new Pair<>(lowlyEmployee,lowlyEmployee);
    
            printBuddies(pair1);
    		printBuddies(pair2);
        }
    
        public static void printBuddies(Pair<? extends Employee> p){
            Employee first = p.getFirst();
            Employee second = p.getSecond();
            System.out.println(first + " " + second);
        }
    
    1. 提高安全性:可以区分安全的访问器方法和不安全的更改器方法,通过指定通配符限定读取/写入操作
    Pair<? extends Employee>
    //表示任何泛型Pair类型,它的类型参数是Employee的子类(不包括Employee)
        
    Pair<Manager> managerBuddies = new Pair<Manager>(ceo,cfo);
    Pair<? extends Employee> wildcardBuddies = managerBuddies;
    wildcardBuddies.setFirst(lowlyEmployee);	//错误,不允许set
    wildcardBuddies.setFirst(ceo2);	//错误,不允许set
    

    一般将<? extends [类型]> 称作子类型限定通配符,这种类型的通配符,只允许读取(get)不允许更改(set);与之对应的是<? super [类型]> 称作 超类型限定通配符,与上面的相反,它只允许更改(set)不能准确读取(get)

    之所以能够这样限定,可以倒推得知。对于子类型限定通配符,如果要读取,只需要设置一个[类型]的变量,就可以接收任意子类的值,不会造成安全性问题。如果要更改,编译器就不知道设置什么变量好,如果设置的变量等级过高,就会出错。对于超类型限定通配符,如果要读取,那么最后只能走Object变量。如果要更改,只需要设置一个[类型]的变量,[类型]的变量足够具体,不会造成安全性问题。

    <关于超类型限定通配符的更深入用法,参看书P350~P351>

  • 可以使用根本无限定的通配符,如Pair<?>,它和原始Pair类型有很大不同。Pair<?>类似于Pair<? extends [类型]>和Pair<? super [类型]>的结合体,它既无法准确读取,也不允许更改。仅仅允许set(null),或返回Object类型的值。这种类型很脆弱,无限定通配符一般只被用来检测null

6. 反射

《Java核心技术 卷I》的反射基础讲的不是很详细,可以参看尚硅谷的笔记

7. 反射与泛型

即便泛型在运行时会被擦除,反射依旧能获得一个类或方法的泛型信息。为了表述泛型类型声明,可以使用反射包中的接口Type,这个接口包含以下子类型:

  • Class类,描述具体类型

  • Typevariable接口,描述类型变量(如 T)。确定这个泛型方法有一个名为T的类型参数

  • WildcardType接口,描述通配符(如 ? super T)。确定这个限定类型有一个通配符参数,判断通配符是否有超类型限定

  • ParameterizedType接口,描述泛型类或接口类型(如Comparable<? super T>)。确定这个这个类型参数有一个子类型限定,而自身又是一个泛型类型

  • GenericArrayType接口,描述泛型数组(如T[])。确定泛型方法有一个泛型数组参数

  • 关于"类型字面量"的内容,参考书P359-363

上一篇:codeforces educational round #111


下一篇:mac 下用 brew 安装mongodb