Java 11 Developer Certification - Polymorphism - Casting

February 24, 2021

What we are covering in this lesson

  1. Casting Object vs Reference
  2. Polymorphism Casting
  3. DownCasting
  4. UpCasting

Casting Object vs Reference

As we know, Java is a strongly typed language. Simply put, when we declare a variable of a particular type, we cannot change that declared type later on. It is though possible to assign an object of different type to the declared variable, but that too has some restrictions which we will be looking at in this section.

Below are some examples which are perfectly valid.

// String literal assigned to an Object reference
Object o1 = "Hi, Hello";

// StringBuilder object assigned to an object reference
Object o2 = new StringBuilder("Hi, Helloooo");

// int 100 will get boxed to Integer wrapped and assigned to Object Reference
Object o3 = 100;

// Invalid 
Integer o3 = 150;

The last line is invalid and will throw a compiler error as we cannot change the actual type of reference variable, o3 in our case.

Also, the compiler does type checking at compile time, and we cannot assign an object that is not a derivative of the declared type to the variable, e.g., Thread t = "Hi"; is incorrect since there is nothing in common between the types (Thread and String).

Another good example is String str = new StringBuilder("Hi");. Both the String and StringBuilder are subclasses of CharSequence but StringBuilder is not derived from String so this is also invalid.

In the below example, s1 seems to be valid short since 15 (5 + 10) fits within a short’s range, but it wont work and the compiler will throw error.

short s = 5;
short s1 = s + 10;  //Invalid

Polymorphism Casting

The main thing to remember is that polymorphism occurs at runtime, and NOT compile time. Hence sometimes we need the ability to force an object into a different form of itself, to produce the results we want.

We need the code to compile and so we need a way to inform the compiler to ignore some of its type-checking rules. This is done with the help of casting.

We can use casting

  • In assignments
  • In expressions
  • In passing objects to method calls.

There are two types of casting

  • Downcasting is casting to a more specific type from the defined type. The downcast must meed the isA criteria.
  • Upcasting is casting to a more generic type.

We will be looking at casting and interfaces in detail later.

Downcasting

Below is a sample code for Downcasting example.

We have got a Animal class with a single method printAnimal. Two other classes, Dog and Cat, both extends Animal class and each have their own single method. Below that we have the main class DowncastExamples which has got 3 overloaded methods for testAnimal, one for each of the classes defined above. We also have 2 specifically typed methods which print dog or cat.

package maxCode.online.polymorphism;

//Animal is our base class
class Animal {
	public void printAnimal() {
		System.out.println("I am an animal");
	}
}

// Dog is a subclass of Animal
class Dog extends Animal {
	public void printDog() {
		System.out.println("I am a dog");
	}
}

// Cat is also a subclass of Animal
class Cat extends Animal {
	public void printCat() {
		System.out.println("I am a cat");
	}
}

public class DowncastExamples {
	public static void main(String[] args) {
		DowncastExamples dex = new DowncastExamples();

		// We create instances of Dog and Cat but assign them to
		// variables of type Animal
		Animal genericDog = new Dog();
		Animal genericCat = new Cat();

		// We cast now:
		dex.testDog((Dog) genericDog);
		dex.testCat((Cat) genericCat);

		// Try the overloaded methods with
		// specifically typed variables.
		dex.testAnimal((Dog) genericDog);
		dex.testAnimal((Cat) genericCat);

		// Try the overloaded methods with
		// generically typed variables.
		dex.testAnimal(genericDog);
		dex.testAnimal(genericCat);
	}

	// Three Overloaded methods
	public void testAnimal(Animal animal) {
		System.out.println("Executing testAnimal with Animal type");
		animal.printAnimal();
	}

	public void testAnimal(Cat cat) {
		System.out.println("Executing testAnimal with Cat type");
		cat.printAnimal();
	}

	public void testAnimal(Dog dog) {
		System.out.println("Executing testAnimal with Dog type");
		dog.printAnimal();
	}

	// Specifically Typed method calls
	public void testDog(Dog dog) {
		dog.printDog();
	}

	public void testCat(Cat cat) {
		cat.printCat();
	}
}

Output

I am a dog
I am a cat
Executing testAnimal with Dog type
I am an animal
Executing testAnimal with Cat type
I am an animal
Executing testAnimal with Animal type
I am an animal
Executing testAnimal with Animal type
I am an animal

The methods execute using the reference types we assigned the objects to. When we assigned a Dog object to a Dog reference, the call to the tests-Animal method used the overloaded method with a Dog parameter. And when we assigned a Dog object to an Animal reference, the call to the test-Animal method used the overloaded method with an Animal parameter.

Now if we modify the casts in such a way that the classes no longer are compatible for casting, the code will NOT throw any compiler error, but will give a runtime ClassCastException.

The below code throws ClassCastException.

// Runtime error due to invalid casting
dex.testDog((Dog) genericCat);
dex.testCat((Cat) genericDog);

dex.testAnimal((Dog) genericCat);
dex.testAnimal((Cat) genericDog);

If we do something like below, we do get a compiler error. So it’s a compiler error to assign an object or a reference type where there’s no relationship. But it’s not a compiler error to pass an object to a parameter, casting it incorrectly to the wrong type.

//Compiler error
Dog specificDog = new Cat();
Cat specificCat = new Dog();

Lets look at casting in expressions. We add a simple method in Dog class.

public void getDogString() {
	return "dog";
}

// Below code in main method
if (((Dog)genericDog).getDogString().equals("dog")) {
	System.out.println("Matched!");
}

Now if we downcast the genericDog class to Dog class, and wraps it to get access to the getDogString method in the Dog class. If we run it, we get the output “Matched!”

Finally, downcasting used on a return type from a method. Lets add another method methodDowncast for that.

public Object methodDowncast(Object o) {
	return o;
}

// Below code in main method
Cat c = (Cat) dex.methodDowncast(genericCat);
c.printCat();

The output would be “I am a cat”. That would be all for downcasting. Now lets look at upcasting.

Upcasting

We have a Tree base class, an instance variable and a getTreeString method. DeciduousTree and FruitTree extend Tree class, and have their specific type of tree. Both override the getTreeString method.

The main class UpcastExamples, we create specific tree instances, then doing an upcast of a DeciduousTree to its parent class, another upcast while printing mapleTree type, and final upcast at the end to pass object as a parameter. The upcast of appleTree to Tree type is actually redundant as it would have anyway happened automatically. The object pass is ultimately a fruit tree and executing its get tree string method executes the fruit tree’s method because that method is overridden.

package maxCode.online.polymorphism;

//Tree is the base class
class Tree {
	String type = "unknown";

	String getTreeString() {
		return "Tree";
	}
}

// DeciduousTree is a subclass of Tree
class DeciduousTree extends Tree {
	String type = "deciduous";

	String getTreeString() {
		return "Leafy Tree";
	}
}

// FruitTree is a subclass of Tree
class FruitTree extends Tree {
	String type = "fruit";

	String getTreeString() {
		return "Fruit Tree";
	}
}

public class UpcastExamples {
	public static void main(String[] args) {
		UpcastExamples upex = new UpcastExamples();

		// Create two specific trees
		DeciduousTree mapleTree = new DeciduousTree();
		FruitTree appleTree = new FruitTree();

		// we upcast deciduousTree to its parent class
		Tree genericTree = (Tree) mapleTree;

		// Print mapleTree's type
		System.out.println("Tree type = " + mapleTree.type);

		// Let's upcast to use the generic Tree's type..
		System.out.println("Tree type = " + ((Tree) mapleTree).type);

		// Upcasting to pass object as a parameter
		upex.printTreeType((Tree) appleTree);
	}

	public void printTreeType(Tree tree) {
		System.out.println("Tree type = " + tree.getTreeString());
	}
}

Output

Tree type = deciduous
Tree type = unknown
Tree type = Fruit Tree

So the important thing to remember here is that casting can be used to retype your object dynamically. It allows the compiler to relax its type reference rules. But if done incorrectly, it can cause runtime errors.

That would be all for this section. In the next one, we will look at some of the special concepts of casting and generics. See you there!