11
$\begingroup$

In java, you must explicitly cast in order to downcast a variable

public class Fruit{}  // parent class
public class Apple extends Fruit{}  // child class

public static void main(String args[]) {
    // An implicit upcast
    Fruit parent = new Apple();
    // An explicit downcast to Apple
    Apple child = (Apple)parent;
}

Is there any reason for this requirement, aside from the fact that java doesn't do any type inference?

Are there any "gotchas" with implementing automatic downcasting in a new language?

For instance:

Apple child = parent; // no cast required 
$\endgroup$
  • $\begingroup$ You are saying that, if the compiler can infer that the object being downcasted is always of the correct type you should be able to not explicitly express the downcast? But then depending on the compiler version and how much type inference it can do some programs may not be valid... bugs in the type inferencer could prevent valid programs from being written etc... it doesn't make sense to introduce some special case that depends on a lot of factors, it wouldn't be intuitive to the users if they'd have to keep adding or removing casts for no reason at every release. $\endgroup$ – Bakuriu Oct 18 '16 at 8:58
24
$\begingroup$

Upcasts always succeed.

Downcasts can result in a runtime error, when the object runtime type is not a subtype of the type used in the cast.

Since the second is a dangerous operation, most typed programming languages require the programmer to explicitly ask for it. Essentially, the programmer is telling the compiler "trust me, I know better -- this will be OK at runtime".

When type systems are concerned, upcasts put the burden of the proof on the compiler (which has to check it statically), downcasts put the burden of the proof on the programmer (which has to think hard about it).

One could argue that a properly designed programming language would forbid downcasts completely, or provide safe casts alternatives, e.g. returning an optional type Option<T>. Many widespread languages, though, chose the simpler and more pragmatic approach of simply returning T and raising an error otherwise.

In your specific example, the compiler could have been designed to deduce that parent is actually an Apple through a simple static analysis, and allow the implicit cast. However, in general the problem is undecidable, so we can't expect the compiler to perform too much magic.

$\endgroup$
  • 1
    $\begingroup$ Just for reference, an example of a language that downcasts to optionals is Rust, but that's because it does not have true inheritance, only a "any type". $\endgroup$ – Kroltan Oct 18 '16 at 12:15
3
$\begingroup$

Usually downcasting is what you do when the statically known knowledge the compiler has about the type of something is less specific than what you know (or at least hope).

In situations like your example, the object was created as an Apple and then that knowledge was thrown away by storing the refernce in a variable of type Fruit. Then you want to use the same refernce as an Apple again.

Since the information was only thrown away "locally", sure, the compiler could maintain the knowledge that parent is really an Apple, even though its declared type is Fruit.

But usually nobody does that. If you want to create an Apple and use it as an Apple, you store it in an Apple variable, not a Fruit one.

When you have a Fruit and want to use it as an Apple it usually means you obtained the Fruit through some means that generally could return any kind of Fruit, but in this case you know it was an Apple. Almost always you haven't just constructed it, you've been passed it by some other code.

An obvious example is if I have a parseFruit function that can turn strings like "apple", "orange", "lemon", etc, into the appropriate subclass; generally all we (and the compiler) can know about this function is that it returns some kind of Fruit, but if I call parseFruit("apple") then I know that's going to call an Apple and might want to use Apple methods, so I could downcast.

Again a sufficiently smart compiler could figure that out here by inlining the source code for parseFruit, since I'm calling it with a constant (unless it's in another module and we have separate compilation, like in Java). But you should easily be able to see how more complicated examples involving dynamic information could become more difficult (or even impossible!) for the compiler to verify.

In realistic code downcasts usually occur where the compiler couldn't verify that the downcast is safe using generic methods, and not in such simple cases as immediately following an upcast throwing away the same type information we're trying to get back by downcasting.

$\endgroup$
3
$\begingroup$

It's a matter of where do you want to draw the line. You can design a language that will detect validity of implicit downcast:

public static void main(String args[]) { 
    // An implicit upcast 
    Fruit parent = new Apple();
    // An implicit downcast to Apple 
    Apple child = parent; 
}

Now, let's extract a method:

public static void main(String args[]) { 
    // An implicit upcast 
    Fruit parent = new Apple();
    eat(parent);
}
public static void eat(Fruit parent) { 
    // An implicit downcast to Apple 
    Apple child = parent; 
}

We're still good. Static analysis is much harder but still possible.

But the problem pops up the very second somebody adds:

public static void causeTrouble() { 
    // An implicit upcast 
    Fruit trouble = new Orange();
    eat(trouble);
}

Now, where do you want to raise the error? This creates a dilemma, one can say that the problem is in Apple child = parent;, but this can be rebutted by "But it worked before". From the other hand, adding eat(trouble); caused the problem", but the whole point of polymorphism is to allow exactly that.

In this case, you can do some of the programmer's job, but you can't do it all the way. The further you take it before giving up, the harder it will be to explain what went wrong. So, it's better to halt as soon as possible, according to the principle of reporting errors early.

BTW, in Java the downcast you've described is not actually a downcast. This is a general cast, which can cast apples to oranges just as well. So, technically speaking @chi's idea is here already, there are no downcasts in Java, only "everycasts". It would make sense to design a specialized downcast operator which will throw compilation error when it's result type can't be found downstream from it's argument type. It would be a good idea to make "everycast" much more difficult to use in order to discourage programmers from using it without a very good reason. C++'s XXXXX_cast<type>(argument) syntax comes to mind.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.