Unit 9

9.1 Creating Superclasses and Subclasses

Goals

  • Create an inheritance relationship from a subclass to the superclass.

  • Write program code to define a new type by creating superclasses and subclasses.

Inheritance Keywords

Click Here

Key concepts and keywords for Java Inheritance:

Core Concepts:

  1. Inheritance: The mechanism in Java where one class can inherit fields and methods from another class.

  2. Superclass (Parent Class): The class being inherited from.

  3. Subclass (Child Class): The class that inherits from the superclass.

  4. extends keyword: Used to indicate that a class is inheriting from another class.

  5. Overriding: Redefining a method in the subclass to provide a specific implementation. @Override annotation: Used to indicate that a method is overriding a method in the superclass.

  6. super keyword: Used to refer to the superclass’s fields and methods, including calling the superclass constructor. super(): Calls the superclass’s constructor.

  7. Method Overloading: Having multiple methods in the same class with the same name but different parameter types.

  8. Method Overriding: A subclass provides its own implementation of a method already defined in its superclass.

  9. Access Modifiers: Controls access to inherited methods and fields (e.g., public, protected, private).

  10. Polymorphism: The ability to treat an object of a subclass as if it were an object of the superclass. It is usually used in conjunction with method overriding.

  11. Upcasting: Assigning an object of a subclass to a variable of the superclass type.

  12. Downcasting: Casting a superclass reference back to a subclass type (can lead to ClassCastException).

  13. Abstract Classes: Classes that cannot be instantiated and may contain abstract methods (methods without a body) that must be implemented by subclasses. It may contain abstract methods (methods without a body) as well as non-abstract methods (methods with implementations). The purpose of an abstract class is to provide a base for other classes to inherit from. It is often used to define a common interface or shared functionality for subclasses while enforcing a certain structure.

    • Key points about abstract classes:

      • It can have both abstract (without implementation) and non-abstract methods (with implementation).

      • It can have instance variables (fields) and constructors.

      • It is declared using the abstract keyword.

  14. Abstract Methods: Methods in an abstract class that do not have a body and must be implemented in concrete subclasses. An abstract method is a method that is declared without a body in an abstract class (or an interface). Abstract methods define a signature (name, parameters, and return type) but do not provide the actual implementation. Any subclass of an abstract class is required to implement all of its abstract methods unless the subclass is also abstract.

    • Key points about abstract methods:

      • It cannot have a body (no implementation).

      • It must be implemented by a concrete (non-abstract) subclass.

      • It is declared using the abstract keyword.

  15. instanceof Operator: Checks whether an object is an instance of a particular class or subclass.

  16. Constructors in Inheritance: Subclasses may call the constructor of the superclass using super(), and can also have their own constructors.

  17. this keyword: Refers to the current object in the subclass.

Key Terms:

  1. Single Inheritance: A subclass inherits from only one superclass.

  2. Multilevel Inheritance: A class can inherit from a subclass, which itself inherits from another superclass.

  3. Hierarchical Inheritance: Multiple subclasses inherit from a single superclass.

  4. Interface: Not strictly inheritance, but allows a class to inherit behavior (methods) without implementing it.

  5. Concrete Class: A class that can be instantiated and does not contain abstract methods.

The keywords and concepts form the foundation of inheritance in Java. In Object-Oriented Programming (OOP), understanding how objects and classes interact is vital to successful programming.



AP CSA Project: Designing with Inheritance

Click Here

Project Title: Build a Simulation Using Inheritance

Project Overview:

You will design and build a Java program that models a real-world system using inheritance and polymorphism.

Your program must include:

  • A superclass that defines shared behavior

  • At least 2–3 subclasses with specialized behavior

  • Use of method overriding

  • Use of constructors with super()

  • A collection (ArrayList) using polymorphism

  • At least one use of casting or instanceof

  • Clear demonstration of dynamic method behavior


Project Theme Options:

Students choose ONE:

1. Animal Ecosystem

  • Superclass: Animal

  • Subclasses: Dog, Bird, Fish, etc.

  • Behaviors: move(), eat(), makeSound()

2. Vehicle System

  • Superclass: Vehicle

  • Subclasses: Car, Truck, ElectricCar

  • Behaviors: drive(), fuelType()

3. Game Characters

  • Superclass: Character

  • Subclasses: Warrior, Mage, Archer

  • Behaviors: attack(), defend()

4. School System

  • Superclass: Person

  • Subclasses: Admin, Counselor, Teacher, Student

  • Behaviors: performRole()

5. Inter-disciplinary Project

  • Superclass: ?

  • Subclasses: ?, ?, ?

  • Behaviors: ?()


Learning Objectives:

Students will be able to:

Core Inheritance Skills (9.1)

  • Create a superclass and subclass using extends

  • Design a class hierarchy with shared and unique behaviors

Constructors (9.2)

  • Write subclass constructors that call super()

  • Properly initialize inherited and new instance variables

Method Overriding (9.3)

  • Override superclass methods correctly using @Override

  • Explain how and why overridden methods behave differently

super Keyword (9.4)

  • Use super() to call constructors

  • Use super.method() when extending behavior

References & Casting (9.5)

  • Use superclass references for subclass objects

  • Demonstrate casting or instanceof safely

Polymorphism (9.6)

  • Use an ArrayList<Superclass> to store multiple object types

  • Demonstrate dynamic method calls

Object Class (9.7)

  • Override toString() for meaningful output


Minimum Program Requirements

Your program MUST include:

1. Superclass

  • At least 2–3 instance variables

  • At least 2 methods

  • A constructor

2. Subclasses (2–3 minimum)

  • Add at least 1 unique variable

  • Override at least 1 method

  • Use super() in constructor

3. Polymorphism

  • Store multiple subclass objects

  • Loop through and call overridden methods

ArrayList<Superclass> list = new ArrayList<>();

4. Casting / instanceof

  • Demonstrate accessing subclass-specific behavior

5. Output

  • Program must clearly show:

    • Different behaviors from same method call

    • Meaningful printed results


Submit your finished code as a .java file. Provide appropriate comments within your program. Use Java Time API to display Local Date Time. Submit one document for your test case(s) showing that program works as intended (pdf).

Deliverables:

  1. Java Code

  2. Class Diagram

  3. Written Explanation

    • What is your superclass and why?

    • How does inheritance reduce repetition?

    • Where is polymorphism happening?

    • What method did you override and why?


10-Point Rubric

1. Class Design (2 pts)

  • (2) Clear superclass + logical subclasses with shared structure with comments

  • (1) Basic inheritance present but weak design

  • (0) No meaningful inheritance


2. Constructors & super() (2 pts)

  • (2) Correct use of constructors and super() with comments

  • (1) Partial or incorrect use

  • (0) Missing or incorrect


3. Method Overriding (2 pts)

  • (2) Correct overriding with different behaviorwith comments

  • (1) Attempted but incorrect or unclear

  • (0) No overriding


4. Polymorphism with ArrayList (2 pts)

  • (2) Uses ArrayList<Superclass> and demonstrates dynamic method calls with comments

  • (1) Partial use or unclear understanding

  • (0) Not present


5. Casting / Advanced Behavior (1 pt)

  • (1) Correct use of instanceof or casting with comments

  • (0) Missing or incorrect


6. Output & Program Functionality (1 pt)

  • (1) Output clearly demonstrates inheritance + polymorphism concepts with comments

  • (0) Output unclear or broken


Extension

  • Abstract classes

  • More complex hierarchies (multilevel)



In Java, there are four different types of inheritance variations that you can demonstrate:

  • single inheritance

  • multiple inheritance (through interfaces)

  • multilevel inheritance

  • hierarchical inheritance.

Here are examples that illustrates each of these variations:

Single Inheritance:

Single inheritance is a type of inheritance where a class extends only one superclass. In this example, we’ll create a base class called Animal and a derived class called Dog, which inherits from Animal.

// Single Inheritance
class Animal {
    void sound() {
        System.out.println("Animals make different sounds.");
    }
}

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

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.sound(); // Output: Dog barks.
    }
}

Multiple Inheritance (through interfaces):

In Java, interfaces provide a way to achieve multiple inheritance of behavior without the problems caused by inheriting from multiple classes. Java does not allow a class to extend more than one superclass because of the diamond problem, where two parent classes might define the same method and create ambiguity. Instead, interfaces act as contracts that specify what methods a class must implement, without providing the full implementation. This allows a class to adopt multiple abilities safely. In this example, the interfaces Swim and Fly define behaviors, and the Bird class implements both, meaning it agrees to provide implementations for swim() and fly(). This demonstrates how a single class can take on multiple roles—something that would not be possible using class inheritance alone—while avoiding conflicts and maintaining clear, organized code.

// Multiple Inheritance (through interfaces)
interface Swim {
    void swim();
}

interface Fly {
    void fly();
}

class Bird implements Swim, Fly {
    @Override
    public void swim() {
        System.out.println("Bird can swim.");
    }

    public void fly() {
        System.out.println("Bird can fly.");
    }
}

public class Main {
    public static void main(String[] args) {
        Bird bird = new Bird();
        bird.swim(); // Output: Bird can swim.
        bird.fly();  // Output: Bird can fly.
    }
}

Multilevel Inheritance:

In multilevel inheritance, a class extends another class, and that class further extends another class. In this example, we’ll create a base class Vehicle, a derived class Car that inherits from Vehicle, and another class Sedan that inherits from Car.

// Multilevel Inheritance
class Vehicle {
    void drive() {
        System.out.println("Vehicles can be driven.");
    }
}

class Car extends Vehicle {
    @Override
    void honk() {
        System.out.println("Car honks.");
    }
}

class Sedan extends Car {
    @Override
    void fuelType() {
        System.out.println("Sedan uses petrol for fuel.");
    }
}

public class Main {
    public static void main(String[] args) {
        Sedan sedan = new Sedan();
        sedan.drive();    // Output: Vehicles can be driven.
        sedan.honk();     // Output: Car honks.
        sedan.fuelType(); // Output: Sedan uses petrol for fuel.
    }
}

Hierarchical Inheritance:

Hierarchical inheritance is a type of inheritance where multiple classes inherit from a single base class. In this example, we’ll create a base class Shape and two derived classes Circle and Square, which both inherit from Shape.

// Hierarchical Inheritance
class Shape {
    void draw() {
        System.out.println("Shapes can be drawn.");
    }
}

class Circle extends Shape {
    @Override
    void draw() {
        System.out.println("Circle can be drawn.");
    }
}

class Square extends Shape {
    @Override
    void draw() {
        System.out.println("Square can be drawn.");
    }
}

public class Main {
    public static void main(String[] args) {
        Circle circle = new Circle();
        Square square = new Square();

        circle.draw(); // Output: Circle can be drawn.
        square.draw(); // Output: Square can be drawn.
    }
}

The three Java programs that demonstrate three different types of inheritance: single inheritance, multiple inheritance (achieved through interfaces), and hierarchical inheritance. Each program will include an ArrayList to showcase different aspects of inheritance.

Single Inheritance: In single inheritance, a class extends only one superclass.

import java.util.ArrayList;

class Animal {
    void sound() {
        System.out.println("Animal makes a sound");
    }
}

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

public class Main {
    public static void main(String[] args) {
        ArrayList<Animal> animals = new ArrayList<>();
        animals.add(new Animal());
        animals.add(new Dog());

        for (Animal animal : animals) {
            animal.sound();
        }
    }
}

Multiple Inheritance (Using Interfaces): In Java, multiple inheritance can be achieved through interfaces. A class can implement multiple interfaces.

import java.util.ArrayList;

interface Flyable {
    void fly();
}

interface Swimmable {
    void swim();
}

class Bird implements Flyable {
    @Override
    public void fly() {
        System.out.println("Bird is flying");
    }
}

class Fish implements Swimmable {
    @Override
    public void swim() {
        System.out.println("Fish is swimming");
    }
}

public class Main {
    public static void main(String[] args) {
        ArrayList<Object> entities = new ArrayList<>();
        entities.add(new Bird());
        entities.add(new Fish());

        for (Object entity : entities) {
            if (entity instanceof Flyable) {
                ((Flyable) entity).fly();
            } else if (entity instanceof Swimmable) {
                ((Swimmable) entity).swim();
            }
        }
    }
}

Hierarchical Inheritance: In hierarchical inheritance, a single superclass is extended by multiple subclasses.

import java.util.ArrayList;

class Shape {
    void draw() {
        System.out.println("Drawing a shape");
    }
}

class Circle extends Shape {
    @Override
    void draw() {
        System.out.println("Drawing a circle");
    }
}

class Rectangle extends Shape {
    @Override
    void draw() {
        System.out.println("Drawing a rectangle");
    }
}

public class Main {
    public static void main(String[] args) {
        ArrayList<Shape> shapes = new ArrayList<>();
        shapes.add(new Circle());
        shapes.add(new Rectangle());

        for (Shape shape : shapes) {
            shape.draw();
        }
    }
}

9.2 Writing Constructors for Subclasses

When creating subclasses in Java, constructors are used to initialize the subclass’s specific attributes and also to ensure that the superclass’s state is properly initialized. To write constructors for subclasses, you can use the super() keyword to invoke the superclass’s constructor and provide any additional initialization specific to the subclass.

Protected vs Private Keywords

Click Here

In Java, the protected and private keywords are access modifiers that control the visibility (or accessibility) of classes, methods, and variables. Below is a comparison between the two:


private:

  • Access Level: Most restrictive.

  • Accessible From:

    • Only within the same class.

  • Not Accessible From:

    • Subclasses (even if they are in the same package).

    • Other classes in the same package.

    • Any external class.

Use Case: Use private when you want to encapsulate your data or methods so that they cannot be accessed or modified from outside the class.

public class Example {
    private int data;

    private void display() {
        System.out.println("This is private.");
    }
}

protected:

  • Access Level: Less restrictive than private, more restrictive than public.

  • Accessible From:

    • The same class.

    • Subclasses (even in different packages).

    • Other classes in the same package.

  • Not Accessible From:

    • Non-subclass classes in different packages.

Use Case: Use protected when you want to allow subclasses to access or override certain methods or fields, but still keep access restricted from unrelated classes.

public class Example {
    protected int data;

    protected void display() {
        System.out.println("This is protected.");
    }
}

Summary Table:

Modifier

Same Class

Same Package

Subclass (diff package)

Other Classes

private

protected


Here’s an example that demonstrates how to write constructors for a subclass:

public class Animal {
    protected String name;

    public Animal(String name) {
        this.name = name;
    }

    public void speak() {
        System.out.println("Animal speaks");
    }
}

public class Dog extends Animal {
    private String breed;

    public Dog(String name, String breed) {
        super(name); // Invoking superclass constructor
        this.breed = breed;
    }

    public void bark() {
        System.out.println("Dog barks");
    }

    @Override
    public void speak() {
        System.out.println("Dog speaks");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog("Buddy", "Labrador");
        System.out.println("Name: " + dog.name);
        System.out.println("Breed: " + dog.breed);
        dog.speak();
        dog.bark();
    }
}

In this example, we have a superclass Animal and a subclass Dog. The Animal class has a constructor that takes a name parameter and initializes the name attribute. It also has a speak() method.

The Dog class extends the Animal class and adds a new attribute breed. The Dog class has a constructor that takes both name and breed parameters. It invokes the superclass constructor using super(name) to initialize the name attribute inherited from Animal. It also initializes the breed attribute. The Dog class overrides the speak() method from the Animal class and adds a new method bark().

In the Main class, we create an instance of Dog and pass values to the constructor for name and breed. We then access the name and breed attributes of the Dog object and call the speak() and bark() methods.

When you run the program, it will output:

Name: Buddy
Breed: Labrador
Dog speaks
Dog barks

This demonstrates how to write constructors for a subclass in Java, ensuring proper initialization of both superclass and subclass attributes.

9.3 Overriding Methods

Goals

  • Create an inheritance relationship from a subclass to the superclass.

  • Incorporate method overriding in a subclass.

Concepts you need to understand:

  • When can you override?

  • How do you override?

  • What happens when you override?

Recall, a void method does not return a value. It performs an action (like printing something or modifying an instance variable), but does not give anything back to the ‘call’. When a class (subclass) extends another class (superclass), it inherits its methods and instance variables. A subclass can override a void method of the superclass which means the subclass provides its own version of the method.

Example:

public class Animal {
    public void speak() {
        System.out.println("Animal speaks");
    }
}

public class Dog extends Animal {
    public void speak() {
        System.out.println("Bark");
    }
}

Animal a = new Dog();
a.speak(); // Output: Bark

Even though a is declared as an Animal, the actual object is a Dog, so the Dog’s speak() method is used. This is called dynamic method or polymorphism.

Note:

Understanding how void methods behave with inheritance is key to:

  • Using polymorphism correctly

  • Designing flexible, reusable code

  • Knowing which version of a method is called at runtime


Here’s an example that demonstrates method overriding in Java:

public class Animal {
    public void makeSound() {
        System.out.println("Animal makes a sound");
    }
}

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

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

public class Main {
    public static void main(String[] args) {
        Animal animal1 = new Animal();
        animal1.makeSound();

        Animal animal2 = new Cat();
        animal2.makeSound();

        Animal animal3 = new Dog();
        animal3.makeSound();
    }
}

In this example, we have a superclass Animal with a method makeSound(). The Cat and Dog classes are subclasses of Animal and they override the makeSound() method.

The Cat class overrides the makeSound() method and provides its own implementation to print “Cat meows”.

The Dog class also overrides the makeSound() method and provides its own implementation to print “Dog barks”.

In the Main class, we create instances of Animal, Cat, and Dog. We assign the Cat and Dog objects to Animal references. When we call the makeSound() method on each object, the overridden version of the method in the respective subclass is invoked.

When you run the program, it will output:

Animal makes a sound
Cat meows
Dog barks

Sample Question

public class Shape {
    public void printType() {
        System.out.println("Shape");
    }
}

public class Circle extends Shape {
    public void printType() {
        System.out.println("Circle");
    }
}

Shape s = new Circle();
s.printType();

What does s print?

Check Here

Circle

Even though s is declared as a Shape, it refers to a Circle object, so the Circle’s version of printType() runs.

9.4 super Keyword

In Java, the super keyword is used to refer to the superclass, allowing you to access its members (fields and methods) from within a subclass. It is particularly useful when you want to distinguish between the superclass’s members and the subclass’s members that have the same name. The super keyword can be used to invoke the superclass’s constructor, access its instance variables or methods, and invoke its overridden methods.

Vocabulary

  1. super keyword – A reference to the parent (superclass) of the current object.

  2. Superclass – The class being extended or inherited from.

  3. Subclass – The class that extends the superclass.

  4. Constructor – A special method used to create objects.

  5. Method overriding – When a subclass provides a specific implementation of a method that is already defined in its superclass.


Main Concepts of super

  1. super is used in two main ways:

    • To call a superclass constructor

    • To call a superclass method


Notes:

  • If a superclass has a no-argument constructor, the compiler automatically inserts super() in the subclass constructor.

  • If the superclass only has constructors with parameters, then the subclass must explicitly call super(parameters).


Here’s an example that demonstrates the usage of the super keyword in Java:

public class Animal {
    protected String name;

    public Animal(String name) {
        this.name = name;
    }

    public void sleep() {
        System.out.println("Animal sleeps");
    }
}

public class Dog extends Animal {
    private String breed;

    public Dog(String name, String breed) {
        super(name); // Invoking superclass constructor
        this.breed = breed;
    }

    public void sleep() {
        super.sleep(); // Invoking superclass method
        System.out.println("Dog sleeps");
    }

    public void displayDetails() {
        System.out.println("Name: " + super.name); // Accessing superclass variable
        System.out.println("Breed: " + breed);
    }
}

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog("Buddy", "Labrador");
        dog.sleep(); // Invoking overridden method
        dog.displayDetails(); // Accessing superclass variable
    }
}

In this example, we have a superclass Animal with a constructor that takes a name parameter and initializes the name attribute. It also has a sleep() method.

The Dog class extends the Animal class and adds a new attribute breed. The Dog class has a constructor that takes both name and breed parameters. It invokes the superclass constructor using super(name) to initialize the name attribute inherited from Animal. It also initializes the breed attribute. The Dog class overrides the sleep() method from the Animal class and adds a new method displayDetails().

In the sleep() method of the Dog class, we invoke the superclass’s sleep() method using super.sleep() to execute the superclass’s behavior before adding the dog-specific behavior.

In the displayDetails() method, we access the name attribute of the superclass using super.name to distinguish it from the name attribute of the subclass.

In the Main class, we create an instance of Dog and pass values to the constructor for name and breed. We then call the sleep() method, which invokes the overridden method, and the displayDetails() method, which accesses the superclass variable.

When you run the program, it will output:

Animal sleeps
Dog sleeps
Name: Buddy
Breed: Labrador

9.5 Creating References Using Inheritance Hierarchies

What Does “Creating References Using Inheritance Hierarchies” Mean?

In Java, you can create references using inheritance hierarchies to refer to objects of both the superclass and its subclasses. This allows you to treat objects of different classes in the hierarchy as interchangeable, providing flexibility and polymorphic behavior.

Key Concept: You can use a superclass reference variable to refer to an object of a subclass. This is legal because the subclass inherits from the superclass and is guaranteed to have at least the same structure and behavior.

Animal a = new Dog();  // Legal! Dog is an Animal

This allows for flexibility, abstraction, and polymorphism in code.


Let’s look at this hierarchy Example called Animals:

Superclass:

public class Animal {
    public void speak() {
        System.out.println("Some generic animal sound");
    }
}

Subclass:

public class Dog extends Animal {
    public void speak() {
        System.out.println("Woof!");
    }
}

Another Subclass:

public class Cat extends Animal {
    public void speak() {
        System.out.println("Meow!");
    }
}

Now try this:

Animal myPet = new Dog();
myPet.speak();  // Output: Woof!

Note: Even though myPet is declared as type Animal, because it was created as a Dog, the Dog’s version of speak() is called. This is dynamic method—a key concept in polymorphism.


Here is another example creating references using inheritance hierarchies:

public class Animal {
    protected String name;

    public Animal(String name) {
        this.name = name;
    }

    public void makeSound() {
        System.out.println("Animal makes a sound");
    }
}

public class Dog extends Animal {
    private String breed;

    public Dog(String name, String breed) {
        super(name); // Invoking superclass constructor
        this.breed = breed;
    }

    public void makeSound() {
        System.out.println("Dog barks");
    }

    public void fetch() {
        System.out.println("Dog fetches");
    }
}

public class Cat extends Animal {
    private String color;

    public Cat(String name, String color) {
        super(name); // Invoking superclass constructor
        this.color = color;
    }

    public void makeSound() {
        System.out.println("Cat meows");
    }

    public void scratch() {
        System.out.println("Cat scratches");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal animal1 = new Animal("Generic Animal");
        Animal animal2 = new Dog("Buddy", "Labrador");
        Animal animal3 = new Cat("Kitty", "White");

        animal1.makeSound();
        animal2.makeSound();
        animal3.makeSound();

        // animal2.fetch(); // Error: fetch() is not defined in the Animal class
        // animal3.scratch(); // Error: scratch() is not defined in the Animal class

        Dog dog = (Dog) animal2; // Casting the reference to Dog type
        dog.fetch();

        Cat cat = (Cat) animal3; // Casting the reference to Cat type
        cat.scratch();
    }
}

In the above example, we have a superclass Animal with a constructor that takes a name parameter and initializes the name attribute. It also has a makeSound() method.

The Dog class extends the Animal class and adds a new attribute breed. It has a constructor that takes both name and breed parameters. It overrides the makeSound() method and adds a new method fetch().

The Cat class also extends the Animal class and adds a new attribute color. It has a constructor that takes both name and color parameters. It overrides the makeSound() method and adds a new method scratch().

In the Main class, we create references of type Animal and assign them objects of Animal, Dog, and Cat. This demonstrates polymorphism, where objects of different types in the inheritance hierarchy can be referred to by a common superclass reference.

We call the makeSound() method on each reference, which invokes the overridden method based on the actual type of the object.

We cannot directly call the fetch() method on the Animal references because it is not defined in the Animal class. To access the fetch() method, we can cast the Animal reference to Dog type, as demonstrated with Dog dog = (Dog) animal2, and then we can call dog.fetch().

Similarly, we can cast the Animal reference to Cat type, as demonstrated with Cat cat = (Cat) animal3, and then we can call cat.scratch().

When you run the program, it will output:

Animal makes a sound
Dog barks
Cat meows
Dog fetches
Cat scratches

9.6 Polymorphism

Polymorphism in Java refers to the ability of objects of different classes in an inheritance hierarchy to be treated as objects of their common superclass. This allows for code reuse, flexibility, and the ability to invoke overridden methods based on the actual type of the object. Here are multiple examples that demonstrate polymorphism in Java:

  • Polymorphic Method Invocation:

public class Animal {
    public void makeSound() {
        System.out.println("Animal makes a sound");
    }
}

public class Dog extends Animal {
    public void makeSound() {
        System.out.println("Dog barks");
    }
}

public class Cat extends Animal {
    public void makeSound() {
        System.out.println("Cat meows");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal animal1 = new Animal();
        Animal animal2 = new Dog();
        Animal animal3 = new Cat();

        animal1.makeSound();
        animal2.makeSound();
        animal3.makeSound();
    }
}

Output

Animal makes a sound
Dog barks
Cat meows

In this example, we have a superclass Animal with a makeSound() method. The Dog and Cat classes are subclasses of Animal and override the makeSound() method. In the Main class, we create objects of type Animal, Dog, and Cat, and call the makeSound() method on each object. The actual implementation of the method invoked depends on the type of the object, demonstrating polymorphic method invocation.

Polymorphic Assignments:

public class Shape {
    public void draw() {
        System.out.println("Drawing a shape");
    }
}

public class Circle extends Shape {
    public void draw() {
        System.out.println("Drawing a circle");
    }
}

public class Rectangle extends Shape {
    public void draw() {
        System.out.println("Drawing a rectangle");
    }
}

public class Main {
    public static void main(String[] args) {
        Shape shape1 = new Circle();
        Shape shape2 = new Rectangle();

        shape1.draw();
        shape2.draw();
    }
}

Output

Drawing a circle
Drawing a rectangle

In this example, we have a superclass Shape with a draw() method. The Circle and Rectangle classes are subclasses of Shape and override the draw() method. In the Main class, we create objects of type Circle and Rectangle, but assign them to references of type Shape. We then call the draw() method on each reference, and the actual implementation of the method invoked is based on the type of the object referred to, demonstrating polymorphic assignments.

Polymorphic Parameter Usage:

public class Animal {
    public void makeSound() {
        System.out.println("Animal makes a sound");
    }
}

public class Dog extends Animal {
    public void makeSound() {
        System.out.println("Dog barks");
    }
}

public class Cat extends Animal {
    public void makeSound() {
        System.out.println("Cat meows");
    }
}

public class AnimalShelter {
    public void makeAnimalsSpeak(Animal[] animals) {
        for (Animal animal : animals) {
            animal.makeSound();
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Animal[] animals = { new Dog(), new Cat() };
        AnimalShelter shelter = new AnimalShelter();
        shelter.makeAnimalsSpeak(animals);
    }
}

Output

Dog barks
Cat meows

In this example, we have a superclass Animal with a makeSound() method. The Dog and Cat classes are subclasses of Animal and override the makeSound() method. The AnimalShelter class has a makeAnimalsSpeak() method that takes an array of Animal objects and calls the makeSound() method on each object. In the Main class, we create an array of Animal objects with a Dog and a Cat, and pass it to the makeAnimalsSpeak() method. The overridden makeSound() method of each object is invoked based on its actual type, demonstrating polymorphic parameter usage.

These examples illustrate different aspects of polymorphism in Java, including polymorphic method invocation, polymorphic assignments, and polymorphic parameter usage. Polymorphism allows for flexible and extensible code by leveraging the inheritance hierarchy and the ability to treat objects of different classes as objects of their common superclass.

9.7 Object Superclass

In Java, the Object class is the root superclass for all other classes. Every class in Java implicitly inherits from the Object class, either directly or through a chain of class inheritance. The Object class provides some common methods and functionality that are inherited by all classes. Here are several examples of the Object superclass in Java:

toString() Method:

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
}

public class Main {
    public static void main(String[] args) {
        Person person = new Person("John Doe", 25);
        System.out.println(person.toString());
    }
}

Output

Person [name=John Doe, age=25]

In this example, the Person class overrides the toString() method inherited from the Object class. The overridden toString() method returns a string representation of the Person object. When we call person.toString() or implicitly use System.out.println(person), it invokes the overridden toString() method to provide a meaningful string representation of the object.

equals() Method:

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        Person person = (Person) obj;
        return age == person.age && Objects.equals(name, person.name);
    }
}

public class Main {
    public static void main(String[] args) {
        Person person1 = new Person("John Doe", 25);
        Person person2 = new Person("John Doe", 25);
        Person person3 = new Person("Jane Smith", 30);

        System.out.println(person1.equals(person2)); // true
        System.out.println(person1.equals(person3)); // false
    }
}

Output:

true
false

In this example, the Person class overrides the equals() method inherited from the Object class. The overridden equals() method compares two Person objects based on their name and age attributes. When we call person1.equals(person2), it invokes the overridden equals() method to check if the two objects are equal based on their attributes.

hashCode() Method:

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

public class Main {
    public static void main(String[] args) {
        Person person = new Person("John Doe", 25);
        System.out.println(person.hashCode());
    }
}

Output

964396650

In this example, the Person class overrides the hashCode() method inherited from the Object class. The overridden hashCode() method calculates the hash code of the Person object based on its name and age attributes. When we call person.hashCode(), it invokes the overridden hashCode() method to retrieve the hash code value of the object.