《Java 核心技术卷1 第10版》学习笔记 ------ 泛型【进阶】

本文深入探讨Java泛型的实现原理,包括类型擦除、泛型方法的翻译、通配符类型的应用以及泛型与虚拟机的交互。通过实例解释类型擦除如何在编译期间将泛型转换为非泛型,以及如何通过桥方法保持多态性。此外,介绍了通配符类型如"? extends"和"? super"的用途,用于在读写操作中提供更灵活的类型约束。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这部分主要是结合 Java 虚拟机实现泛型的原理进一步研究如何更好的使用泛型。

8.5 泛型代码和虚拟机

虚拟机没有泛型类型对象---所有对象都属于普通类。所以编译器在编译的时候会进行类型擦除操作。

8.5.1 类型擦除

1. 什么是类型擦除?

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

擦除规则:原始类型用第一个限定的类型变量来替换, 如果没有给定限定就用 Object 替换。

例如 Pair<T> 的原始类型如下:

public class Pair
{
    private Object first;
    private Object second;

    public Pair(Object first, Object second)
    {
        this,first = first;
        this.second = second;
    }

    public Object getFirstO { return first; }
    public Object getSecondO { return second; }
    public void setFirst(Object newValue) { first = newValue; }
    public void setSecond(Object newValue) { second = newValue; }
}

// 因为 T 是无限定的变量,所以用 Object 代替

结果是一个普通的类, 就好像泛型引人 Java 语言之前已经实现的那样。

多个限定类型例子:

// 泛型类【多个限定修饰符 Comparable 和 Serializable】
public class Interval <T extends Comparable & Serializable〉implements Serializable
{
    private T lower;
    private T upper;
    public Interval (T first, T second)
    {
        if (first.compareTo(second) <= 0) { lower = first; upper = second; }
        else { lower = second; upper = first; }
    }
}

// 类型擦除后【多个限定符,选择第一个限定符 Comparable 】
public class Interval implements Serializable
{
    private Comparable lower;
    private Comparable upper;
    public Interval (Coiparable first, Coiparable second) { . . . }
}

注释:读者可能想要知道切换限定: class Interval<T extends Serializable & Comparable>会发生什么。如果这样做, 原始类型用 Serializable 替换 T, 而编译器在必要时要向Comparable 插入强制类型转换。为了提高效率,应该将标签(tagging) 接口(即没有方法的接口)放在边界列表的末尾。

8.5.2 翻译泛型表达式

当程序调用泛型方法时,如果擦除返回类型, 编译器插入强制类型转换。例如,下面这个语句序列

Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();

擦除 getFirst 的返回类型后将返回 Object 类型。编译器自动插人 Employee 的强制类型转换。
也就是说,编译器把这个方法调用翻译为两条虚拟机指令:

  • 对原始方法 Pair.getFirst 的调用。
  • 将返回的 Object 类型强制转换为 Employee 类型。

8.5.3 翻译泛型方法

类型擦除也会出现在泛型方法中。程序员通常认为下述的泛型方法

public static <T extends Comparable〉T min(T[] a)

是一个完整的方法族,而擦除类型之后,只剩下一个方法:

// 擦除泛型之后
public static Comparable min(Comparable[] a)

注意,类型参数 T 已经被擦除了, 只留下了限定类型 Comparable。

方法的擦除带来了两个复杂问题。看一看下面这个示例:

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

上面代码的意思是,一个日期区间是一对 LocalDate 对象,并且需要覆盖这个方法来确保第二个值永远不小于第一个值。这个类擦除后变成:

class DataInterval extends Pair // after erasure
{
    public void setSecond(LocalDate second){...}
}

令人感到奇怪的是,存在另一个从 Pair 继承的 setSecond 方法,即

// Pair 中存在的 setSecond 方法
public void setSecond(Object second)

这显然是一个不同的方法,因为它有一个不同类型的参数 Object, 而不是 LocalDate。然而,不应该不一样。考虑下面的语句序列:

Datelnterval interval = new Datelnterval(. . .);
Pair<Loca1Date> pair = interval; // OK assignment to superclass
pair.setSecond(aDate);

这里, 希望对 setSecond 的调用具有多态性, 并调用最合适的那个方法。由于 pair 引用 Datelnterval 对象,所以应该调用 Datelnterval.setSecond。问题在于类型擦除与多态发生了冲突。要解决这个问题, 就需要编译器在 Datelnterval 类中生成一个桥方法(bridge method):

// 编译器重写了 Pair 的 setSecond(Object second)
@Override
public void setSecond(Object second) { 
    setSecond( (LocalDate) second );    // 调用擦除后的 setSecond( (LocalDate) second)【桥方法】
}

要想了解它的工作过程,请仔细地跟踪下列语句的执行:

pair.setSecond(aDate);

变量 pair 已经声明为类型 Pair<LocalDate>, 并且这个类型只有一个简单的方法叫 setSecond, 即 setSecond(Object second) 虚拟机用 pair 引用的对象调用这个方法。这个对象是 Datelnterval 类型的, 因而将会调用 Datelnterval.setSecond(Object)方法。这个方法是合成的
桥方法。它调用 Datelnterval.setSecond(LocalDate), 这正是我们所期望的操作效果。

桥方法可能会变得十分奇怪。假设 Datelnterval 方法也覆盖了 getSecond 方法:

class Datelnterval extends Pair<LocalDate>
{
    public LocalDate getSecond() { 
        return (Date) super.getSecond().clone(); 
    }
}

// 在 Datelnterval 类中,有两个 getSecond 方法:
LocalDate getSecond() // defined in Datelnterval
Object getSecond() // overrides the method defined in Pair to call the first method

不能这样编写 Java 代码(在这里,具有相同参数类型的两个方法是不合法的)。它们都没有参数。但是,在虚拟机中,用参数类型和返回类型确定一个方法。因此, 编译器可能产生两个仅返回类型不同的方法字节码,虚拟机能够正确地处理这一情况。

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

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

8.5.4 调用遗留代码

设计 Java 泛型类型时,主要目标是允许泛型代码和遗留代码之间能够互操作

8.6 约束与局限

限于时间及该部分实际情况下遇到这么细小的问题机会比较少,不详细写,更多参考书本。PS:其实只要理解了上面提到的类型擦除过程就很容易理解下面列出的这些约束。

1. 不能用基本数据类型实例化参数类,应使用基本类型对应的包装类对象

 【错误:Pair<double>、Pair<int>;正确:Pair<Double>、Pair<Int>】

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

   if(a instanceof pair<String>) // Error

   if(a instanceof pair<T>) // Error

3. 不能创建参数化类型数组

   Pair<String>[] table = new Pair<String>[10] //Error

4. varargs警告

5. 不能实例化类型变量

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

6. 不能构造泛型数组

   public static <T extends Comparable〉T[] minmax(T[] a) { T[] mm = new T[2]; . . . } // Error

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

   private static T singlelnstance; // Error

   public static T getSinglelnstance(){ ... } // Error

8. 不能抛出或者捕获泛型类的实例

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

9. 可以消除对受查异常的检查

10. 注意擦除后的冲突

8.7 泛型类型的继承规则

略,详情看书

8.8 通配符类型

8.8.1. 通配符的概念(?)

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

Pair<? extends Employee>

表示任何泛型 Pair 类型, 它的类型参数是 Employee 的子类, 如 Pair<Manager>, 但不是 Pair<String>。

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

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

这可能不会引起破坏。对 setFirst 的调用有一个类型错误。要了解其中的缘由,请仔细看一看类型 Pair<? extends Employee>。其方法似乎是这样的:

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

这样将不可能调用 setFirst 方法。编译器只知道需要某个 Employee 的子类型,但不知道具体是什么类型。它拒绝传递任何特定的类型。毕竟?不能用来匹配

使用 getFirst 就不存在这个问题: 将 getFirst 的返回值赋给一个 Employee 的引用完全合法。这就是引人有限定的通配符的关键之处。现在已经有办法区分安全的访问器方法不安全的更改器方法了。

8.8.2. 通配符的超类限定(super 关键字)

通配符限定与类型变量限定十分类似,但是,还有一个附加的能力,即可以指定一个超类型限定 (supertypebound), 如下所亦:

? super Manager

这个通配符限制为 Manager 的所有超类型。带有超类型限定的通配符的行为与 8.8.1 节介绍的相反。可以为 set 方法提供参数, 但不能使用get返回值。例如, Pair<? super Manager> 有方法

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

这不是真正的 Java 语法,但是可以看出编译器知道什么。编译器无法知道 setFirst 方法的具体类型, 因此调用这个方法时不能接受类型为 Employee 或 Object 的参数。 只能传递 Manager 类型的对象,或者某个子类型(如 Executive 总经理, Manager 的子类) 对象。另外, 如果调用 getFirst, 不能保证返回对象的类型。只能把它赋给一个 Object。

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

8.8.3. 无限定通配符

Pair<?> 初看起来,这好像与原始的 Pair 类型一样。实际上, 有很大的不同。类型 Pair<?> 有以下方法:

? getFirst()
void setFirst(?)

getFirst 的返回值只能赋给一个 Object。setFirst 方法不能被调用, 甚至不能用 Object 调用。Pair<?> 和 Pair 本质的不同在于: 可以用任意 Object 对象调用原始 Pair 类的 setObject 方法。

8.8.4. 通配符捕获

详情参考书。

8.9 反射和泛型【待完善】

8.9.1 泛型 Class 类

 

8.9.2 使用 Class 参数进行类型匹配

8.9.3 虚拟机中的泛型类型信息

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值