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?

Screenshot 2024-09-24 at 3 41 23 PM

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

class Animal {
    public void sound() {
        System.out.println("moo");
    }
}

class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("woof");
    }
}

class Cat extends Animal {
    @Override
    public void sound() {
        System.out.println("meow");
    }
}

Animal dog = new Dog();
Animal cat = new Cat();
Animal animal = new Animal();

dog.sound();
cat.sound();
animal.sound();

woof
meow
moo

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();

Screenshot 2024-09-24 at 3 41 56 PM

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

Read this for more information

static: animal class, doesnt change dynamic: cat and dog and animal object, dynamic objects created at runtime

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

Screenshot 2024-09-24 at 3 40 55 PM

Popcorn Hacks

  • Define down-casting in your own words
  • add an example of down-casting to your previous polymorphism example


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

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));