Java Inheritance and Polymorphism
A practical guide to Java inheritance, constructor chaining, method overriding, polymorphism, access control, and object behavior for tracing and writing AP Computer Science A programs.
Build Class Hierarchies
Inheritance organizes related classes into a hierarchy. A contains general state and behavior, while a represents a more specific kind of object and is declared with extends.
For example, Car extends Vehicle means that a Car is a Vehicle. A car can be used wherever a vehicle is expected, and it can reuse accessible methods such as getMake() and move() while adding its own fields or methods.
Use inheritance for an is-a relationship:
A
Caris aVehicle.A
Dogis anAnimal.A
SavingsAccountis anAccount.
Do not use inheritance merely because one class uses another. A Car has an engine, so that relationship is usually modeled with instead.
Java permits a class to have one direct , except that Object is at the top of the class hierarchy. A class may implement multiple interfaces. Constructors are not inherited, and private fields are not directly accessible from a .
Takeaway: Inheritance expresses an is-a relationship and lets specialized classes reuse and extend general behavior.
Initialize Inherited Objects
A constructor must initialize both the portion and the portion of the object. The call super(...) invokes a constructor in the direct and must be the first statement in the constructor.
For example, a checking account can initialize its inherited owner through super(owner) and then initialize its own fee field. The initialization occurs before initialization; this sequence is .
If a constructor does not explicitly call super(...), Java attempts to insert a call to super(). That implicit call causes a compile-time error when the does not have an accessible no-argument constructor. Explicitly selecting the appropriate constructor avoids this problem.
The calls this(...) and super(...) have different targets:
this(...)calls another constructor in the same class.super(...)calls a constructor in the direct .Each must be the first statement, so one constructor cannot explicitly call both.
A can call an inherited method without rewriting it, add new behavior, or call a version of an overridden method with super.methodName(...).
Takeaway: Constructors are not inherited. Use super(...) to initialize inherited state and remember that it must come first.
Control Access Across Classes
Access control determines how a works with inherited state. A private field belongs to its declaring class and cannot be accessed directly by a . The can instead provide a public or protected accessor or operation.
The main access levels are:
private: accessible only inside the declaring class.Package-private, with no modifier: accessible within the same package.
protected: accessible within the same package and by subclasses under Java's protected-access rules.public: accessible wherever the class itself is accessible.
Keeping fields private supports . Controlled methods can validate changes and help prevent an object from entering an invalid state. Although protected can be useful for inheritance, directly exposing mutable fields gives subclasses less protection from accidental misuse.
A may use an inherited public method such as getMake() even though the 's make field is private. This preserves the 's control over how its state is read or changed.
Takeaway: Prefer private state with carefully designed methods; inheritance does not grant direct access to private fields.
Specialize Behavior Safely
An instance method is overridden when a declares a method with the same name and parameter list as an inherited instance method. The @Override annotation asks the compiler to verify that the declaration really overrides a method.
An overriding method generally keeps the same parameter list and return type, or uses a compatible covariant return type. It may provide more access but may not reduce the accessibility of the inherited method. For example, a public method cannot be overridden as protected or private.
A can replace behavior completely or extend it by calling the implementation first:
super.speak()invokes the version declared in the .Additional statements can then provide specialized behavior.
is different from overriding. Methods such as print(String) and print(int) are overloaded because their parameter lists differ. Overloading is resolved at compile time, whereas enables runtime behavior based on the actual object.
Static methods are hidden rather than overridden. Fields are also hidden rather than dynamically dispatched when a declares a field with the same name as a field.
Takeaway: Same name plus the same parameter list in a indicates overriding; different parameter lists indicate overloading.
Trace Polymorphic Calls
lets one reference work with objects from several subclasses. In Animal pet = new Dog(), the reference type is Animal, while the runtime object type is Dog.
The reference type controls which methods the compiler allows the program to call. If Animal declares speak(), the call pet.speak() is allowed. If only Dog declares fetch(), pet.fetch() is not allowed through the Animal reference without a cast.
When an overridden instance method is called, selects the implementation according to the runtime object:
An
Animalreference containing aDogobject usesDog'sspeak()method.An
Animalreference containing aCatobject usesCat'sspeak()method.
Assigning a object to a reference is and is safe. A cast in the opposite direction is and is safe only when the object really has the target type. Use instanceof before a cast when a type check is necessary. Prefer polymorphic method calls over frequent casts when the class design supports them.
For tracing, distinguish three cases:
Overridden instance methods use the runtime object type.
Hidden fields use the reference type.
Hidden static methods are selected through the class or reference type used in the call.
Takeaway: The reference type limits available operations, but an overridden instance method runs according to the runtime object.
Use Object-Level Behavior
Every Java class ultimately inherits from the . Even a class that does not explicitly use extends has Object as its implicit .
Important methods from Object include:
toString()returns a text representation. Overriding it can make printing an object meaningful; passing an object toSystem.out.printlnautomatically invokes itstoString()method.equals(Object)can define logical equality. The default behavior compares object identity, while an override can compare field values.hashCode()returns an integer hash code. If a class overridesequals, it should also overridehashCodeso equal objects produce equal hash codes.getClass()reports the object's runtime class and cannot be overridden.
For example, two separately constructed Point objects can contain the same coordinates without being the same object. A reference comparison with == checks whether the references identify the same object, while a properly implemented equals method can report that their coordinate values are logically equal.
Takeaway: Understand both inherited Object methods and the difference between object identity and logical equality.
Apply a Reliable Tracing Strategy
A reliable tracing process combines the hierarchy, constructor order, access rules, and dispatch rules:
Identify the declared reference type and the runtime object type for each variable.
Trace constructor calls from the toward the . Check whether an explicit
super(...)is present or whether Java must attemptsuper().Determine whether a requested member is accessible through the reference type.
For an instance method, check whether the runtime class overrides it.
For a field or static method, use the reference or class context rather than runtime .
Distinguish an identity comparison with
==from a logical comparison withequals.Check casts: a downcast must match the object's actual runtime type.
A typical employee hierarchy demonstrates the pattern. An HourlyEmployee and a SalariedEmployee can both be stored in variables of type Employee. Calling getPay() through those references selects the hourly or salaried implementation at runtime. The hourly implementation can compute hourly rate multiplied by hours, while the salaried implementation can compute annual salary divided by twelve. A toString() method can also call super.toString() and append specialized information.
Common errors include assuming constructors are inherited, forgetting the implicit super() call, treating private fields as directly inherited access, confusing overloading with overriding, selecting a implementation solely from the reference type, and using a downcast without checking the runtime object.
Takeaway: For every call, separate compile-time visibility from runtime method selection; this distinction resolves most inheritance-tracing questions.