Based on this oracle doc example I try create own example to test it. Of course referenced example work as suggested. But when I try base on Class.cast method I have a problem:

My code is

CL3 extends CL2 extends CL1 extends CL0 extends 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()));
        }
    }

and the main function code is:

    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());
    }

the output is:

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    : [email protected]
before class  : class test.code.hierarchy.read.CL3
before x      : 0
before getX() : 3

clazz       : class test.code.hierarchy.read.CL3
cast        : [email protected]
cast.x      : 0
cast.getX() : 3
#
clazz       : class test.code.hierarchy.read.CL2
cast        : [email protected]
cast.x      : 0
cast.getX() : 3
#
clazz       : class test.code.hierarchy.read.CL1
cast        : [email protected]
cast.x      : 0
cast.getX() : 3
#
clazz       : class test.code.hierarchy.read.CL0
cast        : [email protected]
cast.x      : 0
cast.getX() : 3
#
0/0/0/0

And the question is - why when we use Class.cast (called by getPath().getClazzPath()) method we have different results than produced by cast operator when x field is accessed directly? As we see output from getClazzPath method ('clazz : ') return proper types CL3 -> CL2 -> CL1 -> CL0 but x always referenced to 0.

I discovered that this is connected with T type in getClazzPath method - but I don't know how explain it correctly.

If here is any expert who can explain why in my case the behavior is different between cast operator and Class.cast?

2 Answers

2
Holger On

A type cast does not change the type of an object. It only changes the compile-time type of a reference to it, with a runtime check regarding the validity when needed.

Since the cast does not change the type of the object, it never changes the outcome of an invocation of an overridable method like your getX(), which will always invoke the most specific method. Likewise, when appending the object to a String, the cast has no effect, as the result will be the invocation of the overridable method toString().

When it comes to fields or private methods, the compile-time type of a reference can have an impact on which field or method is accessed, however, since the knowledge about a type is limited for a type parameter of a generic method, you can not expect an impact for a type only the caller knows.

So when you replace

@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()));
}

with

@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()));
}

the outcome doesn’t change. There is no difference between an ordinary type cast (T)… and clazz.cast(…) when clazz is a Class<T>, both have the effect of changing the compile-time type of the reference to T¹.

But what is T? The method doesn’t know, all it knows, is that it is assignable to CL0, due to the declaration <T extends CL0>, and hence, allows to access members of CL0, including the field CL0.x.

You may consider it a language design mistake to allow accessing non-overridable members like fields of CL0 through a reference of type T, despite T could be a subclass declaring its own field with the same name. In fact, for private members, the compiler would generate an error when accessing them via a reference of type T, even when private members of CL0 are accessible.


To demonstrate further that there is no difference between an ordinary type cast and Clazz.cast, you may change

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);

of your main method to

System.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);

and the outcome also is the same. There is no difference between these approaches, all that matters, is that at one place, you’re casting to concrete types CL3, CL2, CL1, CL0, or Base, at the other, you’re casting to a type parameter T.


¹ There is the difference that Class.cast will check the validity at runtime, unlike the unchecked cast, but since they all are valid in this example, the outcome doesn’t change and we can focus on the impact on the member selection.

0
ggolebio On

Thank you Holger for explanation. Make the history shorter. I can't base in my example on fact that fields are no polymorphic because fields references are resolved in compile time.

In main method the code:

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);

generate output:

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

because in compile time e.g. '((CL1) cl3).x)' is change to 'CL1.x'.

The console output from method:

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

will always print for 'cast.x : 0' because in compile time compiler always resolved it to 'CL0.x'. In that place there is no matter what clazz is reported in runtime, because in compile time CL0.x will be always used. It is logical because we can not ensure that childs of CL0 have x field.

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

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