Fiveable
Fiveable
AP Computer Science A
Find gaps with guided practice
Guided practice grid visualization
Table of Contents

💻ap computer science a review

9.5 Creating References Using Inheritance Hierarchies

Verified for the 2025 AP Computer Science A examLast Updated on June 18, 2024

When working with inheritance in Java, one of the most powerful features is the ability to use reference variables in flexible ways. Have you ever wondered how a single variable could refer to different types of objects at different times? This guide explores how reference variables work in inheritance hierarchies, how they enable polymorphism, and how they make your code more flexible and reusable. Understanding these concepts will help you write more elegant, maintainable code that can work with entire families of related classes.

Understanding Reference Variables

A reference variable in Java is a variable that "points to" or "refers to" an object. The variable itself doesn't contain the object's data, but rather the address where the object is stored in memory.

Every reference variable has a declared type, which determines:

  • Which methods and fields you can access through that variable
  • What kinds of objects can be assigned to that variable

The "is-a" Relationship

In an inheritance hierarchy, a key concept is the "is-a" relationship:

  • When class S extends class T, we say that S "is-a" T
  • This means every instance of S is also an instance of T
  • S inherits all the public and protected features of T
  • S can be treated as a T when needed

For example, if Dog extends Animal, then:

  • A Dog "is-an" Animal
  • Every Dog has all the characteristics of an Animal
  • Anywhere an Animal is expected, a Dog can be used

This relationship is the foundation for how reference variables work in inheritance hierarchies.

Assigning Subclass Objects to Superclass References

One of the most important rules in Java is:

  • If S is a subclass of T, then a reference variable of type T can refer to an object of type S

This means we can write code like:

Animal myPet = new Dog();  // Dog is-an Animal, so this works

In this example:

  • myPet is a reference variable of type Animal
  • It's referring to an object of type Dog
  • This is valid because a Dog is-an Animal

This capability is fundamental to polymorphism and flexible code design.

Accessing Members Through Superclass References

When you have a superclass reference pointing to a subclass object, there's an important rule to remember:

  • You can only access methods and fields that are defined in the reference variable's declared type (or its superclasses)

For example:

public class Animal {
    public void eat() {
        System.out.println("The animal is eating");
    }
}

public class Dog extends Animal {
    public void bark() {
        System.out.println("Woof!");
    }
    
    @Override
    public void eat() {
        System.out.println("The dog is eating a bone");
    }
}

// In another class:
Animal myPet = new Dog();
myPet.eat();     // Works! eat() is defined in Animal
// myPet.bark();    // Error! bark() is not defined in Animal

In this example:

  • myPet.eat() works because eat() is a method in the Animal class
  • The actual method called is Dog's version of eat() due to method overriding
  • myPet.bark() would cause a compilation error because the Animal class doesn't have a bark() method

Polymorphism Through Inheritance

Polymorphism (meaning "many forms") is a powerful concept that allows methods to behave differently based on the object they're acting upon. In Java, this is achieved through method overriding and the use of superclass references.

When a method is called on a reference variable:

  • Java uses the actual object type (not the reference type) to determine which method implementation to execute
  • This happens at runtime, which is why it's called "runtime polymorphism"

Example:

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

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

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

// In another class:
Shape shape1 = new Circle();
Shape shape2 = new Rectangle();

shape1.draw();  // Outputs: "Drawing a circle"
shape2.draw();  // Outputs: "Drawing a rectangle"

In this example:

  • Both shape1 and shape2 are Shape references
  • The actual objects are Circle and Rectangle
  • When draw() is called, Java uses the appropriate version based on the actual object type

Reference Variables in Method Parameters

Superclass reference variables are particularly useful in method parameters:

public void displayAnimal(Animal animal) {
    System.out.println("This animal says:");
    animal.makeSound();
}

// This method can be called with any subclass of Animal:
Dog myDog = new Dog();
Cat myCat = new Cat();

displayAnimal(myDog);  // Works because Dog is-an Animal
displayAnimal(myCat);  // Works because Cat is-an Animal

This makes the displayAnimal method flexible - it can work with any current or future Animal subclass.

Reference Variables in Arrays

You can create arrays of superclass references that can store objects of any subclass:

// Create an array of Animal references
Animal[] pets = new Animal[3];

// Fill the array with different subclass objects
pets[0] = new Dog();
pets[1] = new Cat();
pets[2] = new Bird();

// Process all animals the same way
for (Animal pet : pets) {
    pet.eat();
    pet.makeSound();
    // Each call uses the appropriate subclass method
}

This is a powerful way to group related objects and process them uniformly.

Formal Method Parameters

According to the College Board, declaring references of type T when S is a subclass of T is useful in formal method parameters.

This allows methods to accept any subtype of the parameter type:

public void feedAnimal(Animal animal) {
    // This method can accept any Animal subclass
    animal.eat();
}

// Can be called with:
feedAnimal(new Dog());
feedAnimal(new Cat());
feedAnimal(new Horse());

Arrays with Supertype References

The College Board also notes that declaring references of type T when S is a subclass of T is useful in arrays.

Here's how you might use this with the ArrayList class:

ArrayList<Shape> shapes = new ArrayList<Shape>();

// Can add any subclass of Shape
shapes.add(new Circle());
shapes.add(new Rectangle());
shapes.add(new Triangle());

// Process all shapes uniformly
for (Shape s : shapes) {
    s.draw();
    s.calculateArea();
}

Using Both Types of References Together

You can combine both superclass and subclass references to the same object:

Dog actualDog = new Dog();  // Dog reference to Dog object
Animal dogRef = actualDog;  // Animal reference to the same Dog object

actualDog.bark();  // Can call Dog-specific methods
dogRef.eat();      // Can call methods from Animal

// Both references point to the same object
actualDog.setName("Rex");
System.out.println(dogRef.getName());  // Prints "Rex"

Type Casting with Inheritance

Sometimes you need to convert between reference types:

Animal myPet = new Dog();
// myPet.bark();  // Error: Animal doesn't have bark()

// To call Dog-specific methods, we need to cast:
Dog myDog = (Dog) myPet;
myDog.bark();  // Now this works!

// Short form:
((Dog) myPet).bark();  // Cast and call in one line

Casting should be used carefully:

  • Casting doesn't change the object
  • It just changes how Java views the object
  • Casting to an incompatible type causes a ClassCastException

The instanceof Operator

To check if casting is safe, use the instanceof operator:

Animal unknownPet = getRandomPet();  // Could be any Animal subclass

if (unknownPet instanceof Dog) {
    Dog dog = (Dog) unknownPet;
    dog.bark();
} else if (unknownPet instanceof Cat) {
    Cat cat = (Cat) unknownPet;
    cat.meow();
}

Practical Examples

Example 1: Shape Hierarchy

// Base class
public class Shape {
    private String color;
    
    public Shape(String color) {
        this.color = color;
    }
    
    public double getArea() {
        return 0.0;  // Default implementation
    }
    
    public String getColor() {
        return color;
    }
    
    public void display() {
        System.out.println("This is a " + color + " shape");
    }
}

// Subclasses
public class Circle extends Shape {
    private double radius;
    
    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }
    
    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }
    
    @Override
    public void display() {
        System.out.println("This is a " + getColor() + " circle with radius " + radius);
    }
}

public class Rectangle extends Shape {
    private double width;
    private double height;
    
    public Rectangle(String color, double width, double height) {
        super(color);
        this.width = width;
        this.height = height;
    }
    
    @Override
    public double getArea() {
        return width * height;
    }
    
    @Override
    public void display() {
        System.out.println("This is a " + getColor() + " rectangle with width " + 
                          width + " and height " + height);
    }
}

// Using the classes with reference variables
public class ShapeProcessor {
    public static void printShapeInfo(Shape shape) {
        System.out.println("Shape color: " + shape.getColor());
        System.out.println("Shape area: " + shape.getArea());
        shape.display();
        System.out.println();
    }
    
    public static void main(String[] args) {
        Shape shape1 = new Circle("red", 5.0);
        Shape shape2 = new Rectangle("blue", 4.0, 6.0);
        
        printShapeInfo(shape1);  // Works with Circle
        printShapeInfo(shape2);  // Works with Rectangle
        
        // Array of shapes
        Shape[] shapes = new Shape[3];
        shapes[0] = new Circle("green", 3.0);
        shapes[1] = new Rectangle("yellow", 2.0, 8.0);
        shapes[2] = new Shape("purple");
        
        // Process all shapes
        double totalArea = 0;
        for (Shape s : shapes) {
            totalArea += s.getArea();
            s.display();
        }
        
        System.out.println("Total area: " + totalArea);
    }
}

Example 2: Employee Payroll System

// Base class
public class Employee {
    private String name;
    private String id;
    
    public Employee(String name, String id) {
        this.name = name;
        this.id = id;
    }
    
    public double calculatePay() {
        return 0.0;  // Default implementation
    }
    
    public String getName() {
        return name;
    }
    
    public String getId() {
        return id;
    }
}

// Subclasses
public class HourlyEmployee extends Employee {
    private double hourlyRate;
    private double hoursWorked;
    
    public HourlyEmployee(String name, String id, double hourlyRate) {
        super(name, id);
        this.hourlyRate = hourlyRate;
    }
    
    public void setHoursWorked(double hours) {
        this.hoursWorked = hours;
    }
    
    @Override
    public double calculatePay() {
        return hourlyRate * hoursWorked;
    }
}

public class SalariedEmployee extends Employee {
    private double annualSalary;
    
    public SalariedEmployee(String name, String id, double annualSalary) {
        super(name, id);
        this.annualSalary = annualSalary;
    }
    
    @Override
    public double calculatePay() {
        return annualSalary / 26;  // Bi-weekly pay
    }
}

// Using the hierarchy
public class PayrollSystem {
    public static void processPay(Employee employee) {
        System.out.println("Processing payment for: " + employee.getName());
        System.out.println("ID: " + employee.getId());
        System.out.println("Payment amount: $" + employee.calculatePay());
        System.out.println();
    }
    
    public static void main(String[] args) {
        HourlyEmployee hourly = new HourlyEmployee("John Smith", "H001", 20.0);
        hourly.setHoursWorked(40);
        
        SalariedEmployee salaried = new SalariedEmployee("Jane Doe", "S001", 52000.0);
        
        // Process different types of employees
        processPay(hourly);
        processPay(salaried);
        
        // Create an array of employees
        Employee[] staff = new Employee[4];
        staff[0] = hourly;
        staff[1] = salaried;
        staff[2] = new HourlyEmployee("Bob Johnson", "H002", 15.0);
        ((HourlyEmployee)staff[2]).setHoursWorked(20);
        staff[3] = new SalariedEmployee("Alice Brown", "S002", 65000.0);
        
        // Process all employees
        double totalPayroll = 0;
        for (Employee emp : staff) {
            totalPayroll += emp.calculatePay();
        }
        
        System.out.println("Total payroll: $" + totalPayroll);
    }
}

Common Mistakes and Best Practices

Common Mistakes

  1. Trying to call subclass-specific methods without casting:

    Animal myPet = new Dog();
    myPet.bark();  // Error: Animal doesn't have bark()
    
  2. Unsafe casting:

    Animal myPet = new Cat();
    Dog myDog = (Dog) myPet;  // Runtime error: ClassCastException
    
  3. Forgetting that arrays with superclass references still need casting for subclass methods:

    Animal[] pets = new Animal[3];
    pets[0] = new Dog();
    pets[0].bark();  // Error: Animal doesn't have bark()
    

Best Practices

  1. Use superclass references for maximum flexibility in parameters and return types
  2. Always check with instanceof before casting
  3. Design class hierarchies with common behavior in the superclass
  4. Override methods appropriately to get polymorphic behavior
  5. Use arrays or collections of superclass references to group related objects

Summary

Reference variables in inheritance hierarchies are a powerful feature of Java. By allowing a superclass reference to refer to a subclass object, Java enables polymorphism and flexible code design. Remember that if S is a subclass of T, then a reference of type T can be used to refer to an object of type S. This principle is especially useful in method parameters and arrays, allowing methods and data structures to work with entire families of related classes. By mastering these concepts, you'll be able to write more flexible, extensible, and maintainable code.

Key Terms to Review (5)

Hierarchy Tree: A hierarchy tree is a graphical representation that illustrates the hierarchical relationship between different elements or entities. It organizes them into levels or layers based on their parent-child relationships.
Inheritance: Inheritance is a concept in object-oriented programming where a class inherits the properties and behaviors of another class. It allows for code reuse and promotes the creation of hierarchical relationships between classes.
Key Term: Object: An object is an instance of a class that encapsulates data and behavior. It represents a real-world entity or concept in the program.
Polymorphism: Polymorphism refers to the ability of objects to take on multiple forms or have multiple types. In programming, it allows different objects to be treated as instances of a common superclass, enabling flexibility and extensibility.
Type Diagram: A type diagram is a visual representation that shows the relationships between different types of objects in a program. It helps programmers understand how classes and interfaces are related to each other.