Part II: Casting Objects

This is a short supplement to the article Why Use Object-Oriented Technologies?. It is designed to address the issue of why:

Shape shape = new Circle();

is a valid assignment.

Converting and Casting Between Primitive Datatypes

The Java language supports both primitive datatypes and reference datatypes (objects and arrays). Primitive datatypes are not defined in terms of other datatypes, and there are eight primitive datatypes of different precisions in Java. What happens when the value of a primitive datatype is assigned to a variable of a different primitive datatype? Let's say there are two numeric datatypes, short and int. An integer datatype is larger than a short and let's attempt the following assignment:

short s = 5;
int i = s;

Is this valid, or not? It's valid because in assigning a datatype with less precision to a datatype with more precision, we don't lose any information. What if we reverse the process?

int i = 5;
short s = i;

In trying to shoehorn a larger precision datatype into a smaller precision datatype, we could potentially lose information, and the above code won't compile in Java. The only way to circumvent this at compile time is to use a cast operator in the following way:

int i = 5;
short s = (short)i;

Now this will compile, but the results will be unpredictable since you may lose data in the conversion. This seems like common sense to most people.

Converting and Casting Between Object Types

In the article Why Use Object-Oriented Technologies?, we were building a graphical tool that manipulated different shapes. For the object-oriented solution, we had created a superclass called Shape, with a number of subclasses (e.g. Circle, Square, Triangle, etc).

These classes can be treated as types; you can create instances and store their references in variables, e.g.:

Shape shape = new Shape();
Circle circle = new Circle();

However, that's not the end of the story when you use inheritance. You could also do something like this:

Shape shape = new Circle();

It turns out a variable can refer to an object of its own type, or any of its subclasses. The Circle object returned by the new operator is implicitly cast (this is known as "upcasting") to its superclass type before the assignment is done.

If you haven't seen this before, review what we discussed about converting primitive types, look at the above object assignment again and ask if this seems strange.

This should appear to be wrong initially, because a subclass contains everything that it's superclass has, plus more. This implies that a superclass is "smaller" than its subclass, since it has less functionality. Well, if assigning a larger datatype to a smaller datatype is dangerous because we lose data, why is the above assignment valid?

How Upcasting Works

Upcasting is a fundamental concept which is critical to building maintainable OO applications.

Assuming an object assignment can be treated the same way as a primitive assignment is guaranteed to cause confusion. What are the real questions we should be asking?

I like to view the problem this way.

First, look at the right hand side of the assignment. When you say: "new anything", e.g. new Circle(), the internal representation created is a Circle and will remain a Circle until that object is destroyed.

What about on the left hand side? The type on the left is like a surface contract; whatever methods are available for that type, the internal representation must be able to fulfill that surface contract.

Shape shape = new Shape();

Question: can the internal representation fulfill the assigned type's contract? I would hope so, it's the same type.

Shape shape = new Circle();

How about this one?

The answer is yes, because a subclass has everything its superclass has, plus more. So all subclasses can fulfill the contract of a superclass, since they invariably have more functionality than their superclass. This is safe.

Circle circle = new Shape();

How about this one?

Here, you're asking the superclass' internal representation to fulfill the contract of a subclass, but the subclass may have methods the superclass doesn't have, so this is not safe.

In summary:

  1. Don't think of object assignment the same way you think of primitive assignment. Size doesn't matter when doing object assignment.

  2. The real question is: what is the internal representation and can that internal representation fulfill the surface contract? If so, the assignment is safe.

Why is this Important?

Without this understanding, the best you can do is build two endpoints that talk to each other. When you understand how this concept works, you can now build an endpoint that can talk to some future endpoint that does not yet exist. This helps lessen the chances of one development group being at a standstill until something is complete. You can quickly scaffold things together and have them working even if the details aren't filled in.