Verified for the 2025 AP Computer Science A exam•Citation:
When you create an object in Java, a constructor is called to set up the initial state of that object. In inheritance relationships, constructors play a special role because both the superclass and subclass need to be properly initialized. This guide explores how constructors work in inheritance, how to call the superclass constructor from a subclass, and the rules that govern this process. Understanding these concepts will help you create robust class hierarchies where objects are correctly initialized at all levels.
Before diving into the details, let's clarify some important points about constructors in inheritance:
When you create an object of a subclass, a constructor chain occurs:
The super
keyword serves a special purpose in constructors. When used with parentheses super()
, it calls a constructor from the immediate superclass.
Some important rules about using super()
in constructors:
super(arg1, arg2, ...)
Let's look at a basic example of a superclass and subclass with constructors:
// Superclass public class Person { private String name; private int age; // Constructor public Person(String name, int age) { this.name = name; this.age = age; } // Getters and other methods... } // Subclass public class Student extends Person { private String studentId; // Constructor that calls the superclass constructor public Student(String name, int age, String studentId) { super(name, age); // Call to superclass constructor this.studentId = studentId; } // Additional methods... }
In this example:
Student
constructor takes parameters for all necessary valuesPerson
constructor using super(name, age)
to initialize the inherited instance variablesstudentId
When calling the superclass constructor, you need to provide appropriate values for its parameters. These values can come from:
The actual parameters passed in the call to the superclass constructor provide the values that the superclass constructor uses to initialize the object's instance variables. This is how the inherited instance variables get their initial values.
public class Employee extends Person { private String employeeId; private double salary; public Employee(String name, int age, String employeeId, double salary) { // Pass name and age to the Person constructor super(name, age); // Initialize Employee-specific variables this.employeeId = employeeId; this.salary = salary; } }
If you don't explicitly call a superclass constructor using super()
, Java automatically inserts a call to the superclass's no-argument constructor as the first statement in your constructor:
// This code: public Student(String studentId) { this.studentId = studentId; } // Is equivalent to: public Student(String studentId) { super(); // Implicitly added by Java this.studentId = studentId; }
This means the superclass must have a no-argument constructor available. If the superclass doesn't have a no-argument constructor, you'll get a compilation error unless you explicitly call a different superclass constructor.
A superclass might have multiple constructors, and you can call any of them using super()
with the appropriate arguments:
public class Vehicle { private String make; private String model; private int year; // Constructor with all parameters public Vehicle(String make, String model, int year) { this.make = make; this.model = model; this.year = year; } // Constructor with make and model only (default year) public Vehicle(String make, String model) { this(make, model, 2023); // Call the other constructor } // No-argument constructor public Vehicle() { this("Unknown", "Unknown", 2023); // Call the main constructor } } public class Car extends Vehicle { private int numberOfDoors; // Call the 3-parameter superclass constructor public Car(String make, String model, int year, int numberOfDoors) { super(make, model, year); this.numberOfDoors = numberOfDoors; } // Call the 2-parameter superclass constructor public Car(String make, String model, int numberOfDoors) { super(make, model); // Uses the current year by default this.numberOfDoors = numberOfDoors; } // Call the no-argument superclass constructor public Car(int numberOfDoors) { super(); // Uses default values for make, model, and year this.numberOfDoors = numberOfDoors; } }
When a subclass object is created, the constructors execute in a specific sequence:
Object
)This sequence ensures that the superclass is fully initialized before the subclass code runs, regardless of whether the superclass constructor is called implicitly or explicitly.
All classes in Java ultimately inherit from the Object
class. This means every constructor chain eventually calls the Object
constructor. The Object
constructor is a no-argument constructor that initializes the fundamental state of any Java object.
// Complete constructor chain for Student object new Student(...); // Start with Student constructor super(...); // Student calls Person constructor super(); // Person calls Object constructor // Object constructor executes // Person constructor completes // Student constructor completes // Student object fully initialized
If your superclass doesn't have a no-argument constructor and you don't explicitly call another constructor, you'll get a compilation error:
// Superclass with no default constructor public class Person { private String name; // Only constructor requires a name public Person(String name) { this.name = name; } } // This will cause a compilation error public class Student extends Person { private String studentId; // Error: Person doesn't have a no-arg constructor public Student(String studentId) { // Implicit super() call fails this.studentId = studentId; } } // Fix: Explicitly call the available constructor public class Student extends Person { private String studentId; public Student(String name, String studentId) { super(name); // Explicitly call the available constructor this.studentId = studentId; } }
The call to the superclass constructor must be the first statement in the subclass constructor:
// This will cause a compilation error public Student(String name, int age, String studentId) { this.studentId = studentId; // Error: This must come after super() super(name, age); // Error: super() must be first statement }
Make sure all instance variables are properly initialized:
// Problematic: Doesn't initialize age public Student(String name, String studentId) { super(name); // Only initializes name, not age this.studentId = studentId; } // Better: Ensures all variables are initialized public Student(String name, String studentId) { super(name, 0); // Initialize age with a default value this.studentId = studentId; }
Let's look at a complete example with multi-level inheritance:
// Top-level superclass public class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; System.out.println("Person constructor executed"); } // Getters and other methods... } // Mid-level class public class Student extends Person { private String studentId; public Student(String name, int age, String studentId) { super(name, age); this.studentId = studentId; System.out.println("Student constructor executed"); } // Additional methods... } // Subclass of Student public class GraduateStudent extends Student { private String researchArea; public GraduateStudent(String name, int age, String studentId, String researchArea) { super(name, age, studentId); this.researchArea = researchArea; System.out.println("GraduateStudent constructor executed"); } // Additional methods... } // Using the classes public class Main { public static void main(String[] args) { GraduateStudent grad = new GraduateStudent("John", 25, "G12345", "Computer Science"); // Output: // Person constructor executed // Student constructor executed // GraduateStudent constructor executed } }
The output shows the execution order of constructors, from the top of the hierarchy down to the most specific subclass.
Always call the appropriate superclass constructor: Choose the constructor that best initializes the inherited state.
Pass meaningful values to the superclass: Don't just pass default values unless appropriate.
Document constructor requirements: Make it clear which superclass constructor is called and why.
Keep constructors focused: Each constructor should only initialize the state of its class.
Provide multiple constructors if needed: Different initialization scenarios may require different constructors.
Consider using constructor chaining: Use this()
to call other constructors in the same class to reduce duplication.
public class Student extends Person { private String studentId; private String major; // Primary constructor public Student(String name, int age, String studentId, String major) { super(name, age); this.studentId = studentId; this.major = major; } // Secondary constructor with default major public Student(String name, int age, String studentId) { this(name, age, studentId, "Undeclared"); // Call the primary constructor } }
Constructors play a vital role in inheritance by ensuring that objects are properly initialized at all levels of the class hierarchy. Remember that constructors are not inherited, but subclass constructors must call a superclass constructor (either explicitly with super()
or implicitly). The super()
call must be the first statement in the subclass constructor, and it provides the values needed to initialize the inherited instance variables. By following these rules and best practices, you can create class hierarchies where objects are correctly initialized with all the necessary data, maintaining the integrity of your object-oriented designs.