Back to Week 8
Lab Session

Week 8: Lab — Polymorphism

Hands-on Programming Exercises — Ch. 11

Progress0 / 5 completed

1Polymorphism — Shape Area Calculator

Think about it: A graphics program needs to calculate the total area of all shapes on a canvas — circles, rectangles, and triangles. Instead of writing separate loops for each shape type, polymorphism lets you store all shapes in one array and call getArea() on each. The correct formula runs automatically based on the actual shape type.
Create a class GeometricObject with a field color (String), a constructor, and a method getArea() that returns 0.0. Create subclasses Circle (field: radius), Rectangle (fields: width, height), and Triangle (fields: base, height) that each override getArea() with the correct formula. In main, create a GeometricObject[] array holding one of each, loop through it to print each shape's color and area, then print the total area.
Expected OutputCircle (red): area = 78.53981633974483 Rectangle (blue): area = 24.0 Triangle (green): area = 10.0 Total area: 112.53981633974483
Your Code
Base class: class GeometricObject { String color; GeometricObject(String color){ this.color = color; } public double getArea(){ return 0.0; } }. Each subclass calls super(color) and overrides getArea(). In main: GeometricObject[] shapes = { new Circle(5.0,"red"), new Rectangle(4.0,6.0,"blue"), new Triangle(4.0,5.0,"green") }; then loop: for(GeometricObject s : shapes){ System.out.println(s.getColor() + ": area = " + s.getArea()); }. Sum areas in the same loop with total += s.getArea().

2Dynamic Binding — Greeting System

Think about it: A university system stores all people (students, professors, admins) in one list. When it calls greet() on each, it doesn't know at compile time which version to call — that's decided at runtime based on the actual type. This is dynamic binding: the JVM looks up the correct method version for the actual object, not the declared type.
Create a class Person with field name (String), a constructor, and a method greet() that prints "Hello, I'm [name]". Create subclass Student that overrides greet() to print "Hi! I'm student [name]". Create subclass Teacher that overrides greet() to print "Good day. I'm Professor [name]". In main, create a Person[] array with one of each type and call greet() on all — observe dynamic binding in action.
Expected OutputHello, I'm Ahmad Hi! I'm student Sara Good day. I'm Professor Khaled
Your Code
Person: class Person { String name; Person(String name){ this.name = name; } public void greet(){ System.out.println("Hello, I'm " + name); } }. Student: class Student extends Person { Student(String name){ super(name); } @Override public void greet(){ System.out.println("Hi! I'm student " + name); } }. Teacher follows the same pattern. In main: Person[] people = { new Person("Ahmad"), new Student("Sara"), new Teacher("Khaled") }; for(Person p : people) p.greet(); — the correct greet() is chosen at runtime based on actual type.

3Casting — Upcasting & Downcasting

Think about it: You can always treat a Dog as an Animal — that's safe upcasting. But to call a Dog-specific method like fetch(), you need to cast back down explicitly. If the object is actually a Cat, the cast throws a ClassCastException. This exercise makes you practice both directions of casting and the role of instanceof in safe downcasting.
Create a class Animal with field name (String), a constructor, and a method eat() that prints "[name] is eating". Create subclass Dog with a method fetch() that prints "[name] is fetching the ball". Create subclass Cat with a method purr() that prints "[name] is purring". In main: (1) upcast a Dog to Animal and call eat(), (2) safely downcast back to Dog using instanceof and call fetch(), (3) try to downcast a Cat to Dog — guard with instanceof to avoid the exception.
Expected OutputRex is eating Rex is fetching the ball Whiskers is eating Whiskers is not a Dog — cannot fetch!
Your Code
Upcasting: Animal a = new Dog("Rex"); — implicit, no cast needed. Call a.eat(). Downcasting: if (a instanceof Dog) { Dog d = (Dog) a; d.fetch(); }. For the Cat: Animal c = new Cat("Whiskers"); c.eat(); if (c instanceof Dog) { ((Dog) c).fetch(); } else { System.out.println(c.name + " is not a Dog \u2014 cannot fetch!"); }. Always use instanceof before downcasting to avoid ClassCastException.

4instanceof — Animal Type Classifier

Think about it: A veterinary clinic stores all patients in one Animal[] array. When processing each patient, the system needs to know the actual type to call the right treatment. The instanceof operator lets you safely identify the type at runtime and call type-specific methods — without risking a ClassCastException.
Reuse (or redefine) Animal, Dog, and Cat from the previous exercise, and add a new subclass Bird with a method chirp() that prints "[name] is chirping". In main, create an Animal[] array with mixed types. Loop through it: use instanceof to check the type, downcast, and call the type-specific method for each. Also print a count of each type at the end.
Expected OutputRex is fetching the ball Whiskers is purring Tweety is chirping Max is fetching the ball Luna is purring Dogs: 2, Cats: 2, Birds: 1
Your Code
Create array: Animal[] animals = { new Dog("Rex"), new Cat("Whiskers"), new Bird("Tweety"), new Dog("Max"), new Cat("Luna") };. Loop with counters: int dogs=0, cats=0, birds=0; for(Animal a : animals){ if(a instanceof Dog){ ((Dog)a).fetch(); dogs++; } else if(a instanceof Cat){ ((Cat)a).purr(); cats++; } else if(a instanceof Bird){ ((Bird)a).chirp(); birds++; } }. Then print counts.

5final Keyword — Bank Account

Think about it: A bank's core deposit/withdraw logic must never be tampered with by a subclass — making it final prevents any subclass from overriding it. Similarly, the penalty rate for overdraft should be a static final constant that nobody can change. This exercise practices using final to protect critical behavior and values.
Create a class BankAccount with a private field balance (double), a static final constant PENALTY_RATE = 0.05, and a constructor. Add a final method deposit(double amount) that adds to balance. Add a final method withdraw(double amount) that deducts from balance but applies a penalty (balance -= amount * PENALTY_RATE) if the result would go negative. Add getBalance(). Create a subclass SavingsAccount that adds a method addInterest(double rate) that increases balance by balance * rate — but cannot override deposit or withdraw. In main, demonstrate all operations.
Expected OutputBalance after deposit: 1000.0 Balance after withdraw 200: 800.0 Balance after overdraft attempt: 750.0 Balance after interest: 787.5
Your Code
Constant: public static final double PENALTY_RATE = 0.05;. Final method: public final void deposit(double amount){ balance += amount; }. Withdraw with penalty: public final void withdraw(double amount){ if(balance - amount >= 0) balance -= amount; else balance -= amount * PENALTY_RATE; }. SavingsAccount: class SavingsAccount extends BankAccount { SavingsAccount(double b){ super(b); } public void addInterest(double rate){ // need a protected setter or use deposit } }. Tip: make balance protected so SavingsAccount can access it for interest calculation.