• 【阅读笔记】Java核心技术卷一 #6.Chapter8


    8 泛型程序设计

    8.1 为什么要使用泛型程序设计

    • 类型参数(type parameters)(ETS...)
    • 通配符类型(wildcard type)(?

    注意这两者用法用处并不同。

    8.2 定义简单泛型类

    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 void setFirst(T newValue) { first = newValue; }
        public void setSecond(T newValue) { second = newValue; }
    }
    

    8.3 泛型方法

    类型变量放在修饰符的后面,返回类型的前面。
    泛型方法可以定义在普通类中,也可以定义在泛型类中。
    当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型。

    8.4 类型变量的限定

    可以通过对类型变量 T 设置限定(bound):

    public static <T extends Comparable> T min(T[] a) {. . .}
    
    • 关键词是extends,T 和限定类型可以是类,也可以是接口。
    • 一个类型变量或通配符可以有多个限定,例如:T extends Comparable & Serializable;限定类型用“&”分隔,而逗号用来分隔类型变量。
    • 限定中至多有一个类,且必须是限定列表中的第一个。

    8.5 泛型代码和虚拟机

    虚拟机没有泛型类型对象 —— 所有对象都属于普通类。

    8.5.1 类型擦除

    无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。原始类型的名字就是删去类型参数后的泛型类型名。擦除(erased)类型变量,并替换为限定类型。

    原始类型用第一个限定的类型变量来替换,如果没有给定限定就用 Object 替换。为了提高效率,应该将标记接口(即没有方法的接口)放在边界列表的末尾。

    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; }
    
        public void setFirst(Object newValue) { first = newValue; }
        public void setSecond(Object newValue) { second = newValue; }
    }
    

    8.5.2 翻译泛型表达式

    当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。

    8.5.3 翻译泛型方法

    如果类型擦除与多态发生了冲突,例如:

    public class Datelnterval extends Pair<LocalDate> {
        @Override
        public void setSecond(LocalDate second) {
            if (second.compareTo(getFirst()) >= 0)
                super.setSecond(second);
        }
        ...
    }
    

    类型擦除之后,成为:

    class DateInterval extends Pair {
        public void setSecond(LocalDate second) { . . . }
        . . .
    }
    

    但是 Pair 类型擦除之后的方法是

    public void setSecond(Object second);
    

    覆盖时方法参数类型不一致,这与多态产生了冲突。因此编译器会为 Datelnterval 生成桥方法(bridge method):

    public void setSecond(Object second) { setSecond((LocalDate) second); }
    

    如果为 get 方法生成桥方法,于是在 Datelnterval 类中,有两个 getSecond 方法:

    LocalDate getSecond() // defined in DateInterval 中定义的
    Object getSecond() // 桥方法,覆盖 Pair 中的方法,调用第一个方法
    

    方法签名(不包含返回值)相同的两个方法是不合法的。
    但是在虚拟机中,用参数类型和返回类型确定一个方法。因此,编译器可能产生两个仅返回类型不同的方法字节码,虚拟机能够正确地处理这一情况。

    第五章提到的协变返回类型也使用了桥方法

    总之,需要记住有关 Java 泛型转换的事实:

    • 虚拟机中没有泛型,只有普通的类和方法。
    • 所有的类型参数都用它们的限定类型替换。
    • 桥方法被合成来保持多态。
    • 为保持类型安全性,必要时插人强制类型转换。

    8.5.4 调用遗留代码(略)

    8.6 约束与局限性

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

    没有Pair<double>,只有Pair<Double>。因为类型擦除之后,Pair 会有 Object 域,而 Object 不能存储 double。

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

    如:

    if (a instanceof Pair<String>) // Error
    if (a instanceof Pair<T>) // Error
    Pair<String> p = (Pair<String>) a; // Warning--can only test that a is a Pair
    

    其实只能测试 a 是否是一个 Pair。为提醒这一风险,试图查询一个对象是否属于某个泛型类型时,倘若使用 instanceof 会得到一个编译器错误,如果使用强制类型转换会得到一个警告。同样,getClass方法总是返回原始类型。因为虚拟机中的对象总有一个特定的非泛型类型。

    8.6.3 不能创建参数化类型的数组

    如:

    Pair<String>[] table = new Pair<String>[10]; // Error
    Object[] objarray = table;
    objarray[0] = "Hello"; // Error--component type is Pair
    objarray[0] = new Pair<Employee>(); // 可以通过数组存储检査
    

    因此不允许创建(new)参数化类型的数组。但是声明是合法的,可以使用类型转换来初始化。

    Pair<String>[] table = (Pair<String>[]) new Pair<?>[10];
    

    当然这样不安全。只有一种安全而有效的方法:ArrayList<Pair<String>>

    8.6.4 Varargs 警告

    参数个数可变的方法:

    public static <T> void addAll(Collection<T> coll, T... ts)
    {
        for (t : ts) coll.add(t);
    }
    // 如下调用:
    Collection<Pair<String>> table = . . .;
    Pair<String> pair1 = . . .;
    Pair<String> pair2 = . . .;
    addAll(table, pair1, pair2);
    

    为了调用这个方法虚拟机会创建Pair<String>的数组。不过此时只会得到一个警告,而不是错误。消除这个警告可以用注解@SuppressWarnings("unchecked")@SafeVarargs

    8.6.5 不能实例化类型变置

    不能使用new T(...)T.class这样的表达式。

    想要创建实例可以调用者提供一个构造器表达式或提供class(然后使用Class.newlnstance方法)。

    // 1.构造器表达式
    Pair<String> p = Pair.makePair(String::new);
    public static <T> Pair<T> makePair(Supplier<T> constr)
    {
        return new Pair<>(constr.get(), constr.get());
    }
    // 2. 提供class
    Pair<String> p = Pair.makePair(String.class);
    public static <T> Pair<T> makePair(Class<T> cl)
    {
        try { return new Pair<>(cl.newInstance(), cl.newInstance()); }
        catch (Exception ex) { return null; }
    }
    

    8.6.6 不能构造泛型数组

    不能new T[...]

    • 构造器表达式或者反射,详见P324
    • ArrayList.toArray的例子,详见P324

    8.6.7 泛型类的静态上下文中类型变量无效

    不能在静态域或方法中引用类型变量。

    public class Singleton<T> {
        private static T singleInstance; // Error
        public static T getSingleInstance() // Error
        {
            if (singleInstance == null) construct new instance of T
            return singleInstance;
        }
    }
    

    声明Singleton<Random>Singleton<JFileChooser>,然而类型擦除后只剩 Singleton 类,它只有一个 singleInstance 域。

    8.6.8 不能抛出或捕获泛型类的实例

    既不能抛出也不能捕获泛型类对象。实际上,甚至泛型类扩展 Throwable 都是不合法的。

    public class Problem<T> extends Exception { /* . . . */ } // Error--can't extend Throwable
    

    catch 子句中不能使用类型变量:

    public static <T extends Throwable> void doWork(Class<T> t) {
        try {
            do work
        }
        catch (T e) { // Error--can't catch type variable
            Logger.global.info(...)
        }
    }
    

    不过,在异常规范中使用类型变量是允许的。

    public static <T extends Throwable> void doWork(T t) throws T { // OK
        try {
            do work
        }
        catch (Throwable realCause) {
            t.initCause(realCause);
            throw t;
        }
    }
    

    8.6.9 可以消除对受查异常的检查

    8.6.10 注意擦除后的冲突

    8.7 泛型类型的继承规则

    • 无论 S 与 T 有什么联系,通常,Pair<S>Pair<T>没有什么联系。

    • 永远可以将参数化类型转换为一个原始类型。但是会有风险,如:

    Pair<Manager> managerBuddies = new Pair<>(ceo, cfo);
    Pair rawBuddies = managerBuddies; // OK
    rawBuddies.setFirst(new File(". . .")); // only a compile-time warning
    Manager one = rawBuddies.getFirst();    // 抛出 ClassCastException 异常 
    
    • 泛型类可以扩展或实现其他的泛型类。就这一点而言,与普通的类没有什么区别。
      例如,ArrayList<T>类实现List<T>接口。这意味着,一个ArrayList<Manager>可以被转换为一个List<Manager>。但是如前面所见,一个ArrayList<Manager>不是一个ArrayList<Employee>List<Employee>

    8.8 通配符类型

    8.8.1 通配符概念

    通配符类型中,允许类型参数变化。例如:

    Pair<? extends Employee>
    

    它表示类型参数是 Employee 的子类任何泛型 Pair 类型。

    Pair<Employee>Pair<Manager>都是Pair<? extends Employee>的子类型。

    使用通配符会通过Pair<? extends Employee>的引用不会破坏Pair<Manager>

    Pair<Manager> managerBuddies = new Pair<>(ceo, cfo);
    Pair<? extends Employee> wildcardBuddies = managerBuddies; // OK
    wildcardBuddies.setFirst(lowlyEmployee); // compile-time error
    

    因为,Pair<? extends Employee>的方法看起来是这样的:

    ? extends Employee getFirst()
    void setFirst(? extends Employee)
    

    这样将不可能调用 setFirst 方法。编译器只知道需要某个 Employee 的子类型,但不知道
    具体是什么类型。它拒绝传递任何特定的类型。毕竟?不能用来匹配。
    使用 getFirst 就不存在这个问题:将 getFirst 的返回值赋给一个 Employee 的引用完全合法。

    这就是引入有限定的通配符的关键之处:现在已经有办法区分安全的访问器方法和不安全的更改器方法了。

    8.8.2 通配符的超类型限定

    通配符限定类型变量限定十分类似,但是它还可以指定一个超类型限定(supertype bound)。

    例如,Pair<? super Manager>有方法:

    void setFirst(? super Manager)
    ? super Manager getFirst()
    

    编译器无法知道 setFirst 方法的具体类型,因此调用这个方法时不能接受类型为 Employee 或 Object 的参数。只能传递Manager 类型的对象,或者某个子类型对象。
    另外,如果调用 getFirst,不能保证返回对象的类型,只能把它赋给一个 Object。

    直观地讲,带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取。

    8.8.3 无限定通配符

    如:Pair<?>

    ? getFirst()
    void setFirst(?)
    

    getFirst 的返回值只能赋给一个 Object 。setFirst 方法不能被调用,甚至传入 Object 对象(但是可以调用setFirst(null))。但是原始类型 Pair 可以用任意 Object 对象传入其 setFirst 方法。

    • 用处在于一些简单操作的可读性更强。如检测一个 pair 是否包含一个 null 引用:
    public static boolean hasNulls(Pair<?> p)
    {
        return p.getFirst() == null || p.getSecond() == null;
    }
    

    8.8.4 通配符捕获

    public static void swap(Pair<?> p) { 
        swapHelper(p);
    }
    
    public static <T> void swapHelper(Pair<T> p) {
        T t = p.getFirst();
        p.setFirst(p.getSecond());
        p.setSecond(t);
    }
    

    注意:swapHelper 是一个泛型方法,而 swap 不是,它具有固定的Pair<?>类型的参数。
    在这种情况下,swapHelper 方法的参数 T 捕获通配符。它不知道通配符表示哪种类型,但是是一个确定的类型。

    8.9 反射和泛型

    Class 类是泛型的。例如,String.class实际上是一个Class<String>类的对象(事实上,是唯一的对象)。

    被擦除的类仍然保留了一些关于它们的泛型起源的信息:
    public static Comparable min(Comparable[] a)
    是由泛型方法擦除的:
    public static <T extends Comparable<? super T>> T min(T[] a)
    可以使用反射 API 来确定:

    • 这个泛型方法有一个叫做 T 的类型参数。
    • 这个类型参数有一个子类型限定,其自身又是一个泛型类型。// Comparable<? super T>
    • 这个限定类型有一个通配符参数。// ? super T
    • 这个通配符参数有一个超类型限定。
    • 这个泛型方法有一个泛型数组参数。// T[] a

    换句话说,你可以重建关于泛型类和方法的所有信息,就如他们的实现者声明的那样。但是你不会知道特定的对象或方法调用是如何处理类型参数的。

    为了表达泛型类型声明,使用java.lang.reflect包中提供的接口Type。这个接口包含下列子类型:

    • Class类,描述具体类型。
    • Type Variable接口,描述类型变量(如T extends Comparable<? super T>)。
    • WildcardType接口,描述通配符(如?super T)。
    • ParameterizedType接口,描述泛型类或接口类型(如Comparable<? super T>)。
    • GenericArrayType接口,描述泛型数组(如T[])。

    说实话,通配符及后面部分看的云里雾里的。。。。。

  • 相关阅读:
    实现 js 数据类型的判断函数type
    函数防抖 debounce
    验证对象属性是否存在
    解析字串
    完成将 toChineseNum, 可以将数字转换成中文大写的表示,处理到万级别,例如 toChineseNum(12345),返回 一万二千三百四十五
    前端开发仓库
    javascript功能插件大集合,写前端的亲们记得收藏
    Memcached存储命令
    Memcached存储命令
    Memcached 简介、安装和基本使用
  • 原文地址:https://www.cnblogs.com/caophoenix/p/12557006.html
Copyright © 2020-2023  润新知