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 对象没有存储实际类型,那它到底是怎么正确工作的呢?

4246 次点击
所在节点    程序员
34 条回复
zgqq
2019-09-11 02:07:12 +08:00
Class 包含了实际类型信息, 要知道调用方想要的类型只能通过 Class,Java 的泛型只是用来做类型检查,尽量在编译期提示错误,想一下如果没有泛型到处都是 Object 对象,很容易就 ClassCastException 了
amiwrong123
2019-09-11 09:32:46 +08:00
@zgqq
Class<T>类作为一个泛型类,它是怎么保存实际类型信息的呢?它就是要特殊一点是吗
napsterwu
2019-09-11 09:39:01 +08:00
任何一个对象都有 getClass 方法,调一下就知道了
bkmi
2019-09-11 09:47:33 +08:00
@amiwrong123 XXX.class ≠ Class<XXX>
一个是子类,不接收泛型参数,一个是基类,接收泛型参数
guyeu
2019-09-11 10:16:20 +08:00
Class 并不依靠泛型提供类型信息,它本身就是类型信息;
泛型擦除会擦除所有的动态泛型信息,但是会保留泛型类型声明里的类型信息,到运行时就是参数化类型了。
amiwrong123
2019-09-11 10:21:47 +08:00
@bkmi
等一下,我有点蒙,左边右边不都是 Class 对象吗,怎么还有子类和基类
amiwrong123
2019-09-11 10:23:52 +08:00
@guyeu
泛型本来就会擦除,那肯定是也不能依靠泛型提供类型信息了。

但“它本身就是类型信息”,这句我可能还没怎么理解==(虽然我知道 Class 对象的大概用法)
guyeu
2019-09-11 10:28:35 +08:00
@amiwrong123 #7 Java 中每个对象都会持有一个 Class 类的引用,这个 Class 类就是该对象的类型。Class 类本身就是用来描述类型的,当然不需要任何额外的信息
Aresxue
2019-09-11 10:32:14 +08:00
擦除是擦除到上界(比如你这里 House 的上界就是 Building ),不是直接啥都擦没了
icris
2019-09-11 10:39:00 +08:00
类型是类型,对象是对象,kind 是对象,给 kind 标成 Object 它一样保存类型信息。
简单的解决方案:Java 一开始没有范型。
bkmi
2019-09-11 10:47:42 +08:00
@amiwrong123 是我说错了,没有子类基类的关系,
XXX.class 是 Class<XXX> 的实例,
具体的类型信息存储在内部一堆成员变量上(包括 Nativie 部分),
这里的泛型只是用于编码和编译期间的检查,不会存储任何信息
Raymon111111
2019-09-11 10:48:25 +08:00
这个如果你深究 jvm 是怎么存 object 和 class 就很容易懂.

每一个 object 都有一个引用(指针)指向了平常说的方法区(perm 区 /metaspace)中一个类信息对象, 存着这个 object 对应类的元信息, 通过这个指针就可以拿到这个 object 本身对应类的信息, 反射什么的也是从这里拿东西的
amiwrong123
2019-09-11 11:00:03 +08:00
@icris
所以 private Class<T> kind;这句里面的 T 并不会让泛型代码存储到实际类型,这是因为擦除。

但 kind 指向的那个东西就是实际类型呗。。总感觉我是不是该好好研究一下 Class 才行啊。。
oneisall8955
2019-09-11 11:03:19 +08:00
1 楼正解,泛型是为了编码时候,编译就能发现可能存在的问题从而做出最严格安全检测而已,实际上存的都是 Object 类型。编译器总是按照最安全的地方去检测。如果调用反射,就会破除这种限制,从而出现类型转换异常可能性。昨天写了一个小小的 Demo,不是很对题,但可以看出一些问题 https://github.com/oneisall8955/learn/blob/master/src/main/java/com/oneisall/learn/java/advanced/genericity/ErasureTest.java
kifile
2019-09-11 11:04:18 +08:00
String.class != Integer.class,你传入的时候就是一个对象,本身就具备属性信息
amiwrong123
2019-09-11 11:05:52 +08:00
@Raymon111111
确实想研究一下了,Class 到底是怎么存储实际类型的。

还有就是,既然泛型会进行擦除,Class 的源码设计成了 Class<T>这样的泛型到底有什么用。
Raymon111111
2019-09-11 11:09:37 +08:00
@amiwrong123 java 里的泛型主要为了强类型编译期检查错误.

你把所有的东西都声明成 Object 类型也是可以跑通的. 但是很明显容易出错, 可能会把一个 Apple 赋值给 Banana
iffi
2019-09-11 11:16:08 +08:00
泛型定义不会被擦除,反射时可以动态获取泛型参数信息
momocraft
2019-09-11 11:19:59 +08:00
Class 不是对象

你能用 jawa 访问到的只是 Class (包括.java .class) 的一个 handle / representation
shily
2019-09-11 12:01:06 +08:00
@amiwrong123 我觉得你没有理解,类型和值的问题。
Class<T> 是类型;而 Building.class 是值,是具体的实现;例如:
void func(Class<? extends Building> clazz)
有如下的限定 Class<? extends Building> 用来接受 Building 及其子类。
在编译期间,func(Building.class) 和 func(House.class) 是合法的,他们的类型符合 Building 的任意子类,但 String.class 不行。


类型擦除是指,编译完成后,方法变成了
void func(Class clazz)
因为丢失了类型信息,可以传入 String.class 了。

你说的 『 Array.newInstance 传递过去的参数实际并未拥有 kind 所蕴含的类型信息』,是错误的。虽然类型被变量 clazz 的类型被擦除了,但是一个对象 String.class 的具体实现并不会丢失。值并没有改变呐大兄弟。

进而
String s = "ok Google";
Object o = s;

虽然说 o 对于 s 来说 『类型』被擦除了,但它依然是 String 类型,依然可以调用 String 相关的方法,依然可以转换回 String,依然可以反射到 String 类型。

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

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

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

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

© 2021 V2EX