Verified for the 2025 AP Computer Science A exam•Last 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.
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:
In an inheritance hierarchy, a key concept is the "is-a" relationship:
For example, if Dog extends Animal
, then:
This relationship is the foundation for how reference variables work in inheritance hierarchies.
One of the most important rules in Java is:
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 AnimalThis capability is fundamental to polymorphism and flexible code design.
When you have a superclass reference pointing to a subclass object, there's an important rule to remember:
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 classeat()
due to method overridingmyPet.bark()
would cause a compilation error because the Animal class doesn't have a bark()
methodPolymorphism (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:
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:
shape1
and shape2
are Shape referencesdraw()
is called, Java uses the appropriate version based on the actual object typeSuperclass 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.
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.
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());
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(); }
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"
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:
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(); }
// 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); } }
// 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); } }
Trying to call subclass-specific methods without casting:
Animal myPet = new Dog(); myPet.bark(); // Error: Animal doesn't have bark()
Unsafe casting:
Animal myPet = new Cat(); Dog myDog = (Dog) myPet; // Runtime error: ClassCastException
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()
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.