Ignat Insarov
Ignat Insarov

Reputation: 4832

Cast super class instance returned bya super class method to a subclass instance

Consider:

public class Super
{
    Integer state = 0;

    Super f()
    {
        Super x = new Super();
        x.state = this.state + 1;
        return x;
    }
}

public class Sub extends Super
{
    Sub f()
    {
        return (Sub) super.f();
    }
}

public class Main
{
    public static void main (String[] args)
    {
        Sub s = new Sub();
        Sub t = s.f();
    }
}

This results in a crash:

Exception in thread "main" java.lang.ClassCastException: Super cannot be cast to Sub
    at Sub.f(Sub.java:5)
    at Main.main(Main.java:6)

And yet, an instance of Sub is not in a smallest feature distinct from that of Super.

In a more realistic example, Sub would have exactly the same properties as Super (which is what makes me believe they can be safely cast both ways), but redefine some of its methods.

Is it possible to make this code work, without duplicating the definition of f?

Upvotes: 0

Views: 100

Answers (3)

Chetan Kinger
Chetan Kinger

Reputation: 15212

Let's address the first part of the question :

Is it possible to make this code work

In it's current state, the code cannot work because this is against the language specification as defined in Section 5.5.3 of the Java Language Specification which states that :

If T is a class type, then R must be either the same class (§4.3.4) as T or a subclass of T, or a run-time exception is thrown

In the example provided in the question, (return (Sub) super.f(); ), T is Sub and R is Super.

Let's now address the second part of the question :

without duplicating the definition of f

One way to achieve this is to move the code that is common between super and Sub to a separate method :

public class Super {
    Integer state = 0;

    Super f() {
        Super x = new Super();
        incrementState(x);
        return x;
    }

    protected void incrementState(Super x) {
        x.state = this.state + 1;
    }
}

public class Sub extends Super {
    Sub f() {
        Sub s = new Sub();
        incrementState(s);
        return s;
    }
}

Upvotes: 1

Ambro-r
Ambro-r

Reputation: 929

So the short answer is that yes, you can downcast. Provided that the actual object type at runtime is the target type you are down-casting too.

When your example runs return (Sub) super.f(); it'll fail with a java.lang.ClassCastException as in this instance the actual object is not a Sub, it is a Super. You can see this by modifying your method a bit as follows:

Sub f() {
    Super sup = super.f();
    if(sup instanceof  Sub) {
        System.out.println("Instance of Sub");
    } else {
        System.out.println("Not an instance of Sub, it is a : " +  sup.getClass());
    }
    return (Sub) sup;
}

The reason here is because in Super f() your instantiating a pure Super object and returning it, hence the actual object type is Super and since a Superis not a Sub so we cannot cast it to a Sub.

Here is an example:

public class Animal {

  private String type;
  private int age;

  public Animal(String type, int age) {
    this.type = type;
    this.age = age;
  }

  public int getAge() {
    return age;
  }

}

public class Cat extends Animal {
  public Cat(String type, int age) {
    super(type, age);
  }
}
public class Dog extends Animal {
  public Dog(String type, int age) {
    super(type, age);
  }
}

public class Vet {

  public static void printHumanYears(Animal animal) {
    if(animal instanceof Dog) {
        Dog dog = (Dog) animal;
        System.out.println("Dogs age : " + dog.getAge() * 7);
    } else if(animal instanceof Cat) {
        Cat cat = (Cat) animal;
        System.out.println("Cat age : " + cat.getAge() * 5);
    } else {
        System.out.println("Not sure how to calculate age for a " + animal.getClass());
    }
  }

  public static void main (String [] args) {
    Dog dog = new Dog("Collie", 10);
    Cat cat = new Cat("Tabby", 20);
    Animal animal = new Animal("Animal", 20);
    printHumanYears(dog);
    printHumanYears(cat);
    printHumanYears(animal);
  }

}

When the method printHumanYears(Animal animal) is invoked, it is passed an Animal Object, we then check what this Animal object is an instance of and then downcast accordingly. The reason we would not get a ClassCastException is because the target object (either Dog or Cat) was create at run time. When we pass an Animal it can't downcast to either a Dog or Cat because it's neither of these when it was initially instantiated.

Good to remember:

  • Casting does not change the actual object type.
  • Casting only changes the reference type.
  • Upcasting is always safe and never fails.
  • Downcasting is not safe and can throw a ClassCastException, so always a good idea to use instanceof to check the the target type is what you are trying to cast too.

Upvotes: 1

josejuan
josejuan

Reputation: 9566

Yes, you can, but your f operation should be protected inside class hierarchy.

static class Super implements Cloneable {
    int state = 0;

    // inspection
    public int getState() {
        return state;
    }

    // default behavior
    public void doSomething() {
        state += 1;
    }

    // unsafe, should be protected
    public <S extends Super> S f() throws CloneNotSupportedException {
        // unchecked cast !!!
        S s = (S) this.clone();
        s.doSomething();
        return s;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

static class Sub extends Super {
    // override behavior
    @Override
    public void doSomething() {
        super.doSomething();
        super.doSomething();
    }
}

public static void main(String[] args) throws Exception {
    Sub s = new Sub();
    System.out.printf("%d => %d%n", s.getState(), s.f().getState());
}

With output:

0 => 2

Here, f return not Super but any S extending Super, now f will be closed under the group operations (each superclass operations). But must be protected since that cast is unchecked at compile time. Eg.

Sub s = new Sub();
Sub t = s.f();  // ok
Sub2 u = s.f(); // java.lang.ClassCastException !!!!

That is why f operation should be protected and not publicly exposed.

Upvotes: 1

Related Questions