【轻实验】Java 编程坑点之原生类型处理
1实验 223查看
沐风
沐风
L1296
2022-06-28 10:00
启动环境

图片描述 蓝桥云课近期上线了 轻实验功能,支持在社区发布技术类帖子的时候绑定实验环境。读者可以在阅读技术内容的时候,打开实验环境练习。

原生类型的处理

我们先来看一段代码,这段代码如下所示,类名为 Pair 的类表示一对类型相似的对象。它使用了 JDK5 的特性,包括泛型、自动拆装箱、可变长参数和增强 for 循环。

import java.util.*; 
public class Pair<T> {   
    private final T first;    
    private final T second;     
    public Pair(T first, T second) {        
        this.first = first;        
        this.second = second;    
    }     
    public T first() {        
        return first;    
    }    
    public T second() {       
        return second;    
    }    
    public List<String> stringList() {       
       return Arrays.asList(String.valueOf(first),String.valueOf(second));    
    }    
    public static void main(String[] args) {        
         Pair p = new Pair<Object> (2022, "lanqiao"); 
        System.out.println(p.first() + " " + p.second());
        for (String s : p.stringList())
            System.out.print(s + " ");    
    } 
}
copy

这段程序看上去似乎相当简单。在 main 方法中创建了一个 Pair 对象,其中第一个元素是一个值为 2022 的 Integer 对象,第二个元素是一个字符串 “lanqiao”,然后这段程序将输出这个对象的第一个和第二个元素,并用一个空格隔开。之后循环迭代这两个元素的字符串集合,,并且再次输出,所以理论上这段程序会输出两次 2022 lanqiao。但是,它根本不能通过编译。更糟的是,编译器的错误消息更是另人困惑:

Pair.java:26: incompatible types;
found: Object, required: String
            for (String s : p.stringList())
                               ^
copy

如果 Pair 类的 stringList 方法是声明返回List的话,那么这个错误消息还是可以明白的,但是事实是它返回的是List。那这究竟是怎么回事呢?

这个十分奇怪的现象是因为程序使用了原生类型(raw type)而引起的。一个原生类型就是一个没有任何类型参数的泛型类或泛型接口的名字。例如,List 是一个泛型接口,List 是一个参数化的类型,而 List 就是一个原生类型。在我们的程序中,唯一用到原生类型的地方就是在 main 方法中对局部变量 p 的声明:

Pair p = new Pair<Object> (2022, "lanqiao"); 
copy

一个原生类型很像其对应的参数化类型,但是它的所有实例成员都要被替换掉,而替换物就是这些实例成员被擦除掉对应部分之后剩下的东西。具体地说,在一个实例方法声明中出现的每个参数化的类型都要被其对应的原生部分所取代。我们程序中的变量 p 是属于原生类型 Pair 的,所以它的所有实例方法都要执行这种擦除。这也包括声明返回 List 的方法 stringList,编译器会将这个方法解释为返回原生类型 List。

当 List实现了参数化类型 Iterable时,List 也实现了原生类型 Iterable。Iterable有一个 iterator 方法返回参数化类型 Iterator,相应地,Iterable 也有一个 iterator 方法返回原生类型 Iterator。当 Iterator的 next方法返回 String 时,Iterator 的 next 方法返回 Object。因此,循环迭代 p.stringList()需要一个Object类型的循环变量,这就解释了编译器的那个奇怪的错误消息的由来。这种现象令人想不通的原因在于参数化类型 List虽然是方法 stringList 的返回类型,但它与 Pair 的类型参数没有关系,事实上最后它被擦除了,这过程称为泛型擦除。

你可以尝试通过将循环变量类型从 String 改成 Object 这一做法来解决这个问题:

//   此方式不推荐,不应使用此方式解决
    for (Object s : p.stringList())
        System.out.print(s + " "); 
copy

这样确实令程序输出了满意的结果,但是它并没有真正解决这个问题。你会失去泛型带来的所有优点,并且如果该循环在 s 上调用了任何 String 方法,那么程序甚至不能通过编译。正确解决这个问题的方法是为局部变量 p 提供一个合适的参数化的声明:

    Pair<Object> p = new Pair<Object>(23, "skidoo"); 
copy

以下是要点强调:原生类型 List 和参数化类 型List是不一样的。如果使用了原生类型,编译器不会知道在 list 允许接受的元素类型上是否有任何限制,它会允许你添加任何类型的元素到 list 中。这不是类型安全的:如果你添加了一个错误类型的对象,那么在程序接下来的执行中的某个时刻,你会得到一个 ClassCastException 异常。如果使用了参数化类型 List,编译器便会明白这个 list 可以包含任何类型的元素,所以你添加任何对象都是安全的。

还有第三种与以上两种类型密切相关的类型:List是一种特殊的参数化类型,被称为通配符类型(wildcard type)。像原生类型 List 一样,编译器也不会知道它接受哪种类型的元素,但是因为 List是一个参数化类型,从语言上来说需要更强的类型检查。为了避免出现 ClassCastException 异常,编译器不允许你添加除 null 以外的任何元素到一个类型为 List<?>的 list 中。

原生类型是为兼容 5.0 版以前的已有代码而设计的,因为它们不能使用泛型。5.0 版中的许多核心库类,如 Collections,已经利用泛型做了改变,但是使用这些类的已有程序的行为仍然与在以前的版本上运行一样。这些原生类型及其成员的行为被设计成可以镜像映射到 5.0 之前的 Java 语言上,从而保持了兼容性。

这个程序的真正问题在于编程者没有决定究竟使用哪种 Java 版本。尽管程序中大部分使用了泛型,而变量 p 却被声明成原生类型。为了避免被编译错误所迷惑,请避免在打算用 5.0 或更新的版本来运行的代码中编写原生类型。如果一个已有的库方法返回了一个原生类型,那么请将它的结果存储在一个恰当的参数化类型的变量中。然而,最好的办法还是尽量将该库升级到使用泛型的版本上。虽然Java提供了原生类型和参数化类型间的良好的互用性,但是原生类型的局限性会妨碍泛型的使用。

实际上,这种问题在用 getAnnotation 方法在运行期读取 Class 的注解(annotations)的情况下也会发生,该方法是在5.0版中新添加到Class类中的。每次调用 getAnnotation 方法时都会涉及到两个 Class 对象:一个是在其上调用该方法的对象,另一个是作为传递参数指出需要哪个类的注解的对象。在一个典型的调用中,前者是通过反射获得的,而后者是一个类名称字面常量,如下例所示:

Author a = Class.forName(name).getAnnotation(Author.class);
copy

你不必把 getAnnotation 的返回值转型为 Author。以下两种机制保证了这种做法可以正常工作:(1)getAnnotation 方法是泛型的。它是通过它的参数类型来确定返回类型的。具体地说,它接受一个 Class类型的参数,返回一个T类型的值。(2)类名称字面常量提供了泛型信息。例如,Author.class 的类型是 Class。类名称字面常量可以传递运行时和编译时的类型信息。以这种方式使用的类名称字面常量被称作类型符号(type token)。

与类名称字面常量不同的是,通过反射获得 Class 对象不能提供完整的泛型类型信息:Class.forName 的返回类型是通配类型 Class<?>。在调用 getAnnotation 方法的表达式中,使用的是通配类型而不是原生类型 Class,这一点很重要。如果你采用了原生类型,返回的注解具有的就是编译期的 Annotation 类型而不是通过类名称字面常量指示的类型了。下面的程序片断错误地使用了原生类型,和最初的程序一样不能通过编译,其原因也一样:

Class c = Class.forName(name);         
Author a = c.getAnnotation(Author.class);    
copy

总之,原生类型的成员被擦掉,是为了模拟泛型被添加到语言中之前的那些类型的行为。如果你将原生类型和参数化类型混合使用,那么便无法获得使用泛型的所有好处,而且有可能产生让你困惑的编译错误。另外,原生类型和以 Object 为类型参数的参数化类型也不相同。最后,如果你想重构现有的代码以利用泛型的优点,那么最好的方法是一次只重构一个 API,并且保证新的代码中绝不使用原生类型。

还等什么呢,赶紧动动你的小手来验证一下吧!

#轻实验
你的回复