Java揭秘之谜-Class.cast与Cast运算符

时间:2019-05-09 12:23:14

标签: java types casting jvm

基于此oracle doc example 我尝试创建自己的示例进行测试。当然,建议的参考示例工作。但是当我尝试基于Class.cast方法时,我遇到了一个问题:

我的代码是

CL3扩展CL2扩展CL1扩展CL0扩展Base。

    public class CL3 extends CL2 {
        int x = 3;
        public int getX() { return x; }
    }

    public class CL2 extends CL1 {
        int x = 2;
        public int getX() { return x; }
    }

    public class CL1 extends CL0 {
        int x = 1;
        public int getX() { return x; }
    }

    public class CL0 extends Base {
        int x = 0;
        public int getX() { return x; }

        public String getPath() {
            System.out.println("before obj    : " + getClass().cast(this));
            System.out.println("before class  : " + getClass());
            System.out.println("before x      : " + getClass().cast(this).x);
            System.out.println("before getX() : " + getClass().cast(this).getX());
            System.out.println();
            return getClazzPath(getClass());
        }
    }

    public abstract class Base {
        int x = -1;
        abstract int getX();

        @SuppressWarnings("unchecked")
        public <T extends CL0> String getClazzPath(Class<T> clazz) {
            System.out.println("clazz       : " + clazz);
            System.out.println("cast        : " + clazz.cast(this));
            System.out.println("cast.x      : " + clazz.cast(this).x);
            System.out.println("cast.getX() : " + clazz.cast(this).getX());
            System.out.println("#");
            return clazz.cast(this).x + (clazz == CL0.class ? "" : "/" + getClazzPath((Class<T>) clazz.getSuperclass()));
        }
    }

,主要功能代码为:

    public static void main(String[] args) {
        CL3 cl3 = new CL3();

        System.out.println("cl3.getX()=" + cl3.getX());
        System.out.println("((CL2)cl3).getX()=" + ((CL2) cl3).getX());
        System.out.println("((CL1)cl3).getX()=" + ((CL1) cl3).getX());
        System.out.println("((CL0)cl3).getX()=" + ((CL0) cl3).getX());
        System.out.println("((IdentyfiedScope)cl3).getX()=" + ((Base) cl3).getX());

        System.out.println();
        System.out.println("cl3.x=" + cl3.x);
        System.out.println("((CL2)cl3).x=" + ((CL2) cl3).x);
        System.out.println("((CL1)cl3).x=" + ((CL1) cl3).x);
        System.out.println("((CL0)cl3).x=" + ((CL0) cl3).x);
        System.out.println("((IdentyfiedScope)cl3).x=" + ((Base) cl3).x);

        System.out.println();
        System.out.println(cl3.getPath());
    }

输出为:

cl3.getX()=3
((CL2)cl3).getX()=3
((CL1)cl3).getX()=3
((CL0)cl3).getX()=3
((IdentyfiedScope)cl3).getX()=3

cl3.x=3
((CL2)cl3).x=2
((CL1)cl3).x=1
((CL0)cl3).x=0
((IdentyfiedScope)cl3).x=-1

before obj    : test.code.hierarchy.read.CL3@70dea4e
before class  : class test.code.hierarchy.read.CL3
before x      : 0
before getX() : 3

clazz       : class test.code.hierarchy.read.CL3
cast        : test.code.hierarchy.read.CL3@70dea4e
cast.x      : 0
cast.getX() : 3
#
clazz       : class test.code.hierarchy.read.CL2
cast        : test.code.hierarchy.read.CL3@70dea4e
cast.x      : 0
cast.getX() : 3
#
clazz       : class test.code.hierarchy.read.CL1
cast        : test.code.hierarchy.read.CL3@70dea4e
cast.x      : 0
cast.getX() : 3
#
clazz       : class test.code.hierarchy.read.CL0
cast        : test.code.hierarchy.read.CL3@70dea4e
cast.x      : 0
cast.getX() : 3
#
0/0/0/0

问题是-为什么当我们使用Class.cast(由getPath()。getClazzPath()方法调用)时,当直接访问x字段时,结果与强制转换运算符产生的结果不同吗?如我们所见,getClazzPath方法('clazz:')的输出返回正确的类型CL3-> CL2-> CL1-> CL0,但x始终引用为0。

我发现它与getClazzPath方法中的T类型有关-但我不知道如何正确解释它。

如果有任何专家可以解释为什么在我的情况下强制转换运算符和Class.cast之间的行为不同?

2 个答案:

答案 0 :(得分:2)

类型转换不会更改对象的类型。它仅更改对其引用的编译时类型,并在需要时进行有关有效性的运行时检查。

由于强制转换不会更改对象的类型,因此它永远不会更改调用getX()之类的可覆盖方法的结果,该方法将始终调用最特定的方法。同样,将对象附加到String时,强制转换无效,因为结果将是可覆盖方法toString()的调用。

涉及字段或private方法时,引用的编译时类型可能会影响访问哪个字段或方法,但是,因为有关类型的知识仅限于类型参数通用方法的使用,您不能期望仅调用者知道的类型会受到影响。

所以当您更换

@SuppressWarnings("unchecked")
public <T extends CL0> String getClazzPath(Class<T> clazz) {
    System.out.println("clazz       : " + clazz);
    System.out.println("cast        : " + clazz.cast(this));
    System.out.println("cast.x      : " + clazz.cast(this).x);
    System.out.println("cast.getX() : " + clazz.cast(this).getX());
    System.out.println("#");
    return clazz.cast(this).x + (clazz == CL0.class ? "" : "/" + getClazzPath((Class<T>) clazz.getSuperclass()));
}

使用

@SuppressWarnings("unchecked")
public <T extends CL0> String getClazzPath(Class<T> clazz) {
    System.out.println("clazz       : " + clazz);
    System.out.println("cast        : " + (T)this);
    System.out.println("cast.x      : " + ((T)this).x);
    System.out.println("cast.getX() : " + ((T)this).getX());
    System.out.println("#");
    return clazz.cast(this).x + (clazz == CL0.class ? "" : "/" + getClazzPath((Class<T>) clazz.getSuperclass()));
}

结果没有改变。当(T)…clazz.cast(…)时,普通类型强制转换clazzClass<T>之间没有区别,两者都具有将引用的编译时类型更改为{ {1}} ¹

但是T是什么?该方法不知道,由于声明T,它是否可分配给CL0,因此,它允许访问<T extends CL0>的成员,包括字段CL0

您可能会认为这是一种语言设计错误,尽管CL0.x可能是声明其自身的子类,但允许通过类型CL0的引用访问T的字段之类的不可覆盖的成员具有相同名称的字段。实际上,对于T成员,即使private的{​​{1}}成员可以访问,编译器通过类型为T的引用访问它们时,也会产生错误。


为进一步证明普通类型转换和private之间没有区别,可以更改

CL0
您的Clazz.cast方法的

System.out.println("cl3.getX()=" + cl3.getX());
System.out.println("((CL2)cl3).getX()=" + ((CL2) cl3).getX());
System.out.println("((CL1)cl3).getX()=" + ((CL1) cl3).getX());
System.out.println("((CL0)cl3).getX()=" + ((CL0) cl3).getX());
System.out.println("((IdentyfiedScope)cl3).getX()=" + ((Base) cl3).getX());

System.out.println();
System.out.println("cl3.x=" + cl3.x);
System.out.println("((CL2)cl3).x=" + ((CL2) cl3).x);
System.out.println("((CL1)cl3).x=" + ((CL1) cl3).x);
System.out.println("((CL0)cl3).x=" + ((CL0) cl3).x);
System.out.println("((IdentyfiedScope)cl3).x=" + ((Base) cl3).x);

,结果也一样。这两种方法之间没有什么区别,重要的是,在一个地方,您要转换为具体类型mainSystem.out.println("cl3.getX()=" + cl3.getX()); System.out.println("((CL2)cl3).getX()=" + CL2.class.cast(cl3).getX()); System.out.println("((CL1)cl3).getX()=" + CL1.class.cast(cl3).getX()); System.out.println("((CL0)cl3).getX()=" + CL0.class.cast(cl3).getX()); System.out.println("((IdentyfiedScope)cl3).getX()=" + Base.class.cast(cl3).getX()); System.out.println(); System.out.println("cl3.x=" + cl3.x); System.out.println("((CL2)cl3).x=" + CL2.class.cast(cl3).x); System.out.println("((CL1)cl3).x=" + CL1.class.cast(cl3).x); System.out.println("((CL0)cl3).x=" + CL0.class.cast(cl3).x); System.out.println("((IdentyfiedScope)cl3).x=" + Base.class.cast(cl3).x); CL3CL2CL1,另一方面,您正在转换为类型参数CL0


¹的区别在于Base将在运行时检查有效性,这与未检查的强制转换不同,但是由于它们在此示例中都是有效的,因此结果不会更改,并且我们可以专注于对成员选择的影响。

答案 1 :(得分:0)

谢谢Holger的解释。缩短历史记录。在我的示例中,我不能基于字段不是多态的事实,因为字段引用是在编译时解决的。

在主要方法中,代码:

System.out.println("cl3.x=" + cl3.x);
System.out.println("((CL2)cl3).x=" + ((CL2) cl3).x);
System.out.println("((CL1)cl3).x=" + ((CL1) cl3).x);
System.out.println("((CL0)cl3).x=" + ((CL0) cl3).x);
System.out.println("((IdentyfiedScope)cl3).x=" + ((Base) cl3).x);

生成输出:

cl3.x=3
((CL2)cl3).x=2
((CL1)cl3).x=1
((CL0)cl3).x=0
((IdentyfiedScope)cl3).x=-1

因为在编译时,例如'(((CL1)cl3).x)'更改为'CL1.x'。

方法的控制台输出:

public <T extends CL0> String getClazzPath(Class<T> clazz) {
    System.out.println("clazz       : " + clazz);
    System.out.println("cast.x      : " + clazz.cast(this).x);
}

将始终为'cast.x:0'打印,因为在编译时,编译器始终将其解析为'CL0.x'。在那个地方,无论在运行时报告什么混乱,因为在编译时将始终使用CL0.x。这是合乎逻辑的,因为我们不能确保CL0的子级具有x字段。

clazz       : class test.code.hierarchy.read.CL3
cast.x      : 0

clazz       : class test.code.hierarchy.read.CL2
cast.x      : 0