Polymorphism
Learning Targets:
- What is Polymorphism
- Difference between Static and Dynamic Data Types
- Compile Time vs Run time
- “A reference variable is polymorphic when it can refer to objects of different classes in the code”
- “A method is polymorphic when it is overriden in at least one subclass”
- “Polymorphism is the act of executing an overriden
non-static
method from the correct class at runtime based on the actual object type”
public class Shape {
protected String name;
private int length;
private int width;
// Parameterized constructor
public Shape(String name, int length, int width) {
this.name = name;
this.length = length;
this.width = width;
}
// Method to calculate the area
public double calc_area() {
return this.length * this.width;
}
}
public class Triangle extends Shape {
private int side1;
private int side2;
private int side3;
// Constructor that takes a name and three side lengths
public Triangle(String name, int s1, int s2, int s3) {
super(name, 0, 0); // Call to Shape constructor to set the name
this.name = "triangle";
this.side1 = s1;
this.side2 = s2;
this.side3 = s3;
}
@Override
public double calc_area() {
double s = (side1 + side2 + side3) / 2.0; // Semi-perimeter
return Math.sqrt(s * (s - side1) * (s - side2) * (s - side3));
}
}
//creates a "Shape" class using a "Triangle" constructor
Shape triangle = new Triangle("Equilateral",1,1,1);
// Therefore the calc_area is overriden
System.out.println(triangle.calc_area());
How Does This Work?
Let’s start with this line:
Shape myObject = new Triangle();
It may seem like you are magically creating a Shape Object with the Triangle, but you are not. Instead you actually are creating a Triangle Object. The difference is that the Shape variable is referencing the Shape parts of the Triangle.
- “A reference variable can store a refernece to its declared class or any subclass of its declared class”
This also means that if the data type is the SuperClass and you try to call a method that doesn’t exist in the SuperClass, it will return an error. But you can cast the variable to the SubClass because the refrence is the SubClass. After casting if you call a method that is only in the subclass then it won’t return an error.
Next running methods:
myObject.calc_area() == Triangle.calc_area();
When you run a method that the Shape has, it starts at the referenced object and checks if there is an override, if not it moves up the ancestry chain until it either finds an override or finds the orginal method.
Popcorn Hacks
- Create an example of Polymorphism in your own project.
If you want some more information and examples of Polymorphism see the examples further down
abstract class Fruit {
abstract String getDescription();
}
class Apple extends Fruit {
@Override
String getDescription() {
return "This is an apple.";
}
}
class Banana extends Fruit {
@Override
String getDescription() {
return "This is a banana.";
}
}
public class Main {
public static void printDescription(Fruit fruit) {
System.out.println(fruit.getDescription());
}
public static void main(String[] args) {
Fruit apple = new Apple();
Fruit banana = new Banana();
printDescription(apple);
printDescription(banana);
}
}
Static vs Dynamic Types
static types: static types is simply the type.
dynamic types: dynamic type is the type of the value during runtime
class Shape {
String getName(){
return "Shape"
}
}
class Square extends Shape{
@Override
String getName(){
return "Square"
}
}
Shape myShape = new Square();
Shape is a static type, but at runtime myShape
is referencing Sqaure, so my myShape
is the dynamic type of Square.
Popcorn Hacks
Using your previous polymorphism example, explain which parts are the static types and which are the dynamic types In this example of polymorphism, we can see the difference between static and dynamic types. The static types are set at compile time and refer to how the variables are declared; both apple and banana are declared as type Fruit. The dynamic types, however, are determined at runtime and refer to the actual objects created: apple is an instance of Apple, and banana is an instance of Banana. When the printDescription method is called, it uses the dynamic type to execute fruit.getDescription(), allowing the same method to behave differently based on the actual object. This shows how method overriding and dynamic dispatch make the code flexible and reusable.
Read this for more information
Compile-Time vs Run-Time methods
Compile time is when you are writing your code. Once you have written your code it is compiled into something the computer can run.
Run-time is when you are actually running the code. This is after the compiler is complete and when the code starts exectuting.
There are some differences between Compile-Time and Run-Time. The case we will be going over breifly is the difference in methods. When you create a Shape with a Square you can’t run the methods built solely into the square, it must be methods from the shape first. But why?
This is the difference between Compile-Time and Run-Time
During Runtime: When you are creating a dynamic reference of a different with a type than the static type, it searches the superclass for the attributes of the static type, then overrides any that the child has overriden. But it doesn’t include the methods from the child.
So basically even if you have methods that exist on the referenced object, in run-time those methods may be ignored because the static type doesn’t include them.
If you want to run a method that is only in the child class then you can use down-casting.
class Shape {
String getName(){
return "Shape";
}
}
class Square extends Shape{
@Override
String getName(){
return "Square";
}
int getSides(){
return 4;
}
}
Shape myShape = new Square(); //this does not have access to the Sqaure methods despite referencing a sqaure
Square mySquare = (Square)myShape; //down-cast the Shape to a Sqaure to run the Sqaure specific methods
System.out.println(mySquare.getSides());//after down-casting you can now run the Square methods
Popcorn Hacks
- Define down-casting in your own words
Down-casting involves converting a reference of a superclass type to a subclass type. This is commonly done to access specific methods or properties that the subclass provides, which are not available in the superclass.
- add an example of down-casting to your previous polymorphism example
abstract class Fruit {
abstract String getDescription();
}
class Apple extends Fruit {
@Override
String getDescription() {
return "This is an apple.";
}
void appleSpecificMethod() {
System.out.println("Apple-specific method called.");
}
}
class Banana extends Fruit {
@Override
String getDescription() {
return "This is a banana.";
}
}
public class Main {
public static void printDescription(Fruit fruit) {
System.out.println(fruit.getDescription());
}
public static void main(String[] args) {
Fruit apple = new Apple();
Fruit banana = new Banana();
printDescription(apple);
printDescription(banana);
// Down-casting
if (apple instanceof Apple) {
Apple myApple = (Apple) apple; // Down-casting to Apple
myApple.appleSpecificMethod(); // Now we can call the Apple-specific method
}
}
}
Examples of Polymorphism and the effects
Here are some examples of Polymorphism.
1- Executing the overriden method from the referenced subclass in the datatype of the superclass.
class Water {
public String typeOfWater(){
return "water";
}
}
class Lake extends Water {
@Override
public String typeOfWater(){
return "lake";
}
}
//creates a "Water" class using a "Lake" constructor
Water lake = new Lake();
// Therefore the typeOfWater method is overriden
System.out.println(lake.typeOfWater());
2- You can pass a subclass into an argument that is asking for the parent class.
class Shape{
public int getSize(){
return 1;
}
}
class Square extends Shape{
@Override
public int getSize(){
return 2;
}
}
int getSizePlusOne(Shape s){ //argument of class "Shape"
return s.getSize() +1;
}
Shape myShape = new Shape();
//passes through a "Shape"
System.out.println(getSizePlusOne(myShape));
Square mySquare = new Square();
//passes through a "Square" as a "Shape" with the square's "getSize()" method
System.out.println(getSizePlusOne(mySquare));
3- You can cast from the superclass to the subclass (down-casting). The superclass must be referencing the subclass.
class Electronic{
public void playSound(){
System.out.println("Beep boop");
}
}
class Computer extends Electronic{
@Override
public void playSound(){
System.out.println("Click clack");
}
}
//creates a "Electronic" class using a "Computer" constructor
Electronic device = new Computer();
//casts the "Electronic" to a "Computer"
Computer computer = (Computer)device;
computer.playSound();
class Electronic{
public void playSound(){
System.out.println("Beep boop");
}
}
class Computer extends Electronic{
@Override
public void playSound(){
System.out.println("Click clack");
}
}
Electronic device = new Electronic(); //creates a "Electronic" class using a "Electronic" constructor
Computer computer = (Computer)device; //cannot cast the "Electronic" to a "Computer"
4- Down-casting doesn’t mean changing the reference to the obejct, so polymorphism will still apply
class Furniture{
String getName(){
return "Furniture";
}
}
class Table extends Furniture{
@Override
String getName(){
return "Table";
}
}
class CoffeeTable extends Table{
@Override
String getName(){
return super.getName() + " Coffee";
}
}
Furniture myTable = new CoffeeTable();
//runs "CoffeeTable" method
System.out.println(myTable.getName());
Table myOtherTable = (Table)myTable;
//method exectuted doesn't change despite casting-down
System.out.println(myOtherTable.getName());
5- you can cast upward and polymorphic behaviour will apply.
class Sport{
public int numberOfPlayers(){
return (int)Double.NaN; //ends up being 0 but whatever
}
public String name(){
return "Sport";
}
}
class Soccer extends Sport{
@Override
public int numberOfPlayers(){
return 11;
}
@Override
public String name(){
return "Soccer";
}
}
//creates a a soccer object
Soccer mySoccer = new Soccer();
Sport mySoccerSport = (Sport)mySoccer;
System.out.println(mySoccerSport.numberOfPlayers());
Polymorphism with the Object class
- see the lesson 9.7 about Object Superclass
Polymorphism also applies with the Object Superclass.
Techically any class or object is an Object
.
class Fruit{
@Override
public String toString(){
return "I'm a Fruit!";
}
}
class Banana extends Fruit{
@Override
public boolean equals(Object obj){ //overrides the equals
return true;
}
}
Object banana = new Banana();
System.out.println(banana.toString());
//if ".equals()" was not overriden from the "Object" this should always return false
System.out.println(banana.equals(null));