Java 泛型擦除与补偿的迷惑

2019-09-11 00:09:13 +08:00
 amiwrong123

例子均来自 java 编程思想:

//: generics/ArrayMaker.java
import java.lang.reflect.*;
import java.util.*;

public class ArrayMaker<T> {
    private Class<T> kind;
    public ArrayMaker(Class<T> kind) { this.kind = kind; }
    @SuppressWarnings("unchecked")
    T[] create(int size) {
        return (T[])Array.newInstance(kind, size);
    }
    public static void main(String[] args) {
        ArrayMaker<String> stringMaker =
                new ArrayMaker<String>(String.class);
        String[] stringArray = stringMaker.create(9);
        System.out.println(Arrays.toString(stringArray));
    }
} /* Output:
[null, null, null, null, null, null, null, null, null]
*///:~

作者刚在这个例子下说:即使 kind 被存储成了 Class<t>,擦除也意味着它实际上将存储为 Class,没有任何参数。Array.newInstance 传递过去的参数实际并未拥有 kind 所蕴含的类型信息。 我觉得作者说的有道理,确实 Class<t>的 T 作为类型参数不会被实际存储,但看完我有点担心对 Class 对象的使用了,因为 Class 对象并没有存储实际类型。我甚至开始怀疑以前 Class 对象的用法,它都没有存储实际类型,那它到底是怎么 newInstance 的呢?怎么还能产生正确的结果呢?</t></t>

//: generics/ClassTypeCapture.java

class Building {}
class House extends Building {}

public class ClassTypeCapture<T> {
    Class<T> kind;
    public ClassTypeCapture(Class<T> kind) {
        this.kind = kind;
    }
    public boolean f(Object arg) {
        return kind.isInstance(arg);
    }   
    public static void main(String[] args) {
        ClassTypeCapture<Building> ctt1 =
            new ClassTypeCapture<Building>(Building.class);
        System.out.println(ctt1.f(new Building()));
        System.out.println(ctt1.f(new House()));
        ClassTypeCapture<House> ctt2 =
            new ClassTypeCapture<House>(House.class);
        System.out.println(ctt2.f(new Building()));
        System.out.println(ctt2.f(new House()));
    }
} /* Output:
true
true
false
true
*///:~

这里作者说:如果引入类型标签,就可以转而使用动态的 isInstance 了。总之他意思是用 Class 对象可以对擦除进行补偿。

但这个例子中居然可以使用 Class 对象的 isInstance 方法,而且可以正确的返回值。那他前面强调的“即使 kind 被存储成了 Class<t>,擦除也意味着它实际上将存储为 Class”的这个担心点也不复存在了吗?</t>

总之,问题就是:1 是否 Class 对象不会存储实际类型呢? 2 如果 Class 对象没有存储实际类型,那它到底是怎么正确工作的呢?

4259 次点击
所在节点    程序员
34 条回复
mxalbert1996
2019-09-11 12:12:14 +08:00
不是很能理解楼主为什么会这么想,如果类型擦除能把 Class 对象本身代表的类型信息都擦掉的话那你说 Class 对象的 getName()方法返回什么?难道全部返回"java.lang.Object"么?
amiwrong123
2019-09-11 14:10:20 +08:00
@shily
感谢回答,感觉有点弄清楚了。其实就是,虽然引用 Class<T>的类型参数被擦除了,但其 Class 对象还是没有改变。

专门去看了 Class 的源码,大概有这几个函数用了 T:
1. public T newInstance()
2. public TypeVariable<Class<T>>[] getTypeParameters()
3. public native Class<? super T> getSuperclass()
4. public Constructor<T> getConstructor(Class<?>... parameterTypes)
5. public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)

以最常用的 newInstance 为例,以泛型的知识来理解,这里也应该是: 调用 newInstance 返回的是一个 Object,然后隐式地在这里加了一句强制类型转换呗。
leafin
2019-09-11 14:43:51 +08:00
其实这个问题在 1 楼就已经回答了。
在运行时关于类型的信息都是保存在 Class 里面的,这个时候没有泛型的事。
泛型是用来在编译期防止不恰当的访问的,编译完成后,泛型信息就被擦除了。
希望你能明白编译期和运行时的区别。
yumeko
2019-09-11 16:57:07 +08:00
Java 的类型检查包括静态的和动态的检查。
静态检查由 Java 编译器进行。动态检查由 Java 虚拟机进行。
动态检查可以防止错误的 type cast 造成程序产生 undefined behavior。
当然动态检查的结果是,错误的 type cast 会让程序产生 Error 而退出执行。
范型虽然会擦除类型,但是如果代码里没有 type cast,那么执行就是安全的。
T 是一个类型参数,你不能用一个类型参数来产生类型实例。
类型参数只是给编译器确定代码里一个范型类的具体类型是什么。
所以范型类内部无法使用 T 来确定需要构造什么类型。
但是 Java Byte Code 在具体的范型类实现上又会保存类型信息,并且可以通过反射 API 获取。
例如说 static List<String> getList() {...} 的函数签名返回类型是 java.util.List<java.lang.Object>,
但是范型信息会保存 java.util.List<java.lang.String>。
这样可以使用反射来对某一个范型类型的特化类型生成一个具有特化参数实例的安全的范型实例。
pursuer
2019-09-11 17:42:17 +08:00
附言的理解不对啊,Java 的泛型只是用于编译器检查的,对实际生成的代码是没有影响的
amiwrong123
2019-09-11 17:50:40 +08:00
@pursuer
有影响的,泛型除了检查外,在明确指定了泛型的具体类型后( ArrayMaker<String> stringMaker = new ArrayMaker<String>(String.class);),返回 T 类型的方法会隐式地加一句强制类型转换,这一点我刚才通过 javap 看 java 汇编看出来了的。
pursuer
2019-09-11 17:53:41 +08:00
@pursuer 不过好像这么理解也没错,kind 是一个 Class,运行期要用 isAssignabelFrom getName 之类的方法才能判断类型 kind 代表的类型
amiwrong123
2019-09-11 17:54:42 +08:00
@pursuer
准确的说,泛型代码的字节码没有变化。但是在主函数的调用 create 方法的地方,会隐式地加一句强制类型转换。
pursuer
2019-09-11 17:57:55 +08:00
@amiwrong123 这样,我之前一直以为泛型对代码生成是没有影响的,感觉一般泛型 xxx<xxx>最后都会变成 xxx<Object>
amiwrong123
2019-09-12 11:15:49 +08:00
@zgqq
@napsterwu
@bkmi
@guyeu
@Raymon111111
@shily
@leafin
各位大佬,能否帮忙看一下附言 2 关于泛型数组的我的理解对吗?
guyeu
2019-09-12 16:30:22 +08:00
声明的一个泛型类型是有上界和下界的,泛型擦除会把它的类型擦除到它的上界,默认情况下就是 Object 了。
你可以把泛型理解为编码期间用于提示类型的一种语法糖(当然这有点片面),返回一个泛型数组的方法比如你这个 ArrayMaker.create,它的类型在你 new 出这个 ArrayMaker 实例的时候就给它指定了。

另外,我使用的 jdk11,不可以进行类似的强转:
```java
Integer[] array = (Integer[]) new Object[] {};
```
这是因为`new Object[] {}`的类型是`Object[]`,而 java 是不支持向下的强制类型转换的。
Array.newInstance 是一个 native 的方法,它返回的就是一个指定类型的数组,而你的字节码里的强制类型转换是你的代码里原本就存在的,并不是编译器帮你加的。
amiwrong123
2019-09-12 17:21:51 +08:00
@guyeu
我才发现,如果是 java12,所以这样 Integer[] b = (Integer[]) new Object[] {};居然是能通过编译的。然后试了 java8,也是不可以的。


“而你的字节码里的强制类型转换是你的代码里原本就存在的,并不是编译器帮你加的。”(代码里也没有写(String[])的啊),难道你意思就是,因为 String[] stringArray = stringMaker.create(9);左边是 String[],所以就有了强制转换呗。但是,我觉得强制转换是需要自己写出来的啊,像这样 String[] stringArray = (String[])stringMaker.create(9);。毕竟 create 方法返回的是 Object[],从 // Method create:(I)[Ljava/lang/Object;也能看出来。

而且根据 java8 的测试,就算写了也是编译通不过。那这里 String[] stringArray = stringMaker.create(9);到底是怎么执行成功的呢。

有点笨,见谅哈==
guyeu
2019-09-12 18:39:23 +08:00
@amiwrong123 #32
你的这个实例的泛型参数是 String,然后`return (T[])Array.newInstance(kind, size);`这句话进行了显式的强制类型转换,所以我说代码里本来就有强制类型转换。

`Array.newInstance`和`new Object[] {}`是不一样的,Array.newInstance 调用 native 方法生成了一个指定类型的对象数组,即使方法签名的返回值是 Object,那只是一个符号,实际的对象的类型是指定类型的数组,而`new Object[] {}`创建了一个 Object 类型的数组。

而像你那样的强制类型转换编译是可以通过的,因为编译器并不负责保证你的显式强制类型转换的类型安全,但是在运行时会抛 ClassCastException。
guyeu
2019-09-12 18:42:44 +08:00
@pursuer #29
@amiwrong123 #32
说泛型对字节码没有影响是片面的,泛型的上界会影响泛型变量的类型,类 /接口声明时的泛型参数会被记录,可以在运行时通过反射 API 获取。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/599802

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX