10 Inheritance and Polymorphism

A practical guide to Java inheritance, overriding, polymorphism, type relationships, abstract classes, and choosing composition appropriately.

The relationship

lets a new class reuse common state and behavior from an existing class while adding specialized capabilities. The existing class is the , and the new class is the .

In Java, the extends keyword declares this relationship. If Dog extends Animal, a Dog object can use accessible members defined by Animal and can also define behavior such as bark() that belongs specifically to dogs.

The relationship should express a genuine is-a connection: a dog is an animal, a bicycle is a vehicle, and a circle is a shape. A should also be usable wherever the is expected without violating the 's behavioral expectations.

Constructors are not inherited. A constructor can initialize state by calling super(...). The super keyword can also invoke an overridden method when the wants to preserve the original behavior and add to it.

Takeaway: Use to represent a meaningful specialization, not merely to reuse a few unrelated methods.

Changing inherited behavior

A can change inherited instance behavior through . The overriding method uses the same method name and a compatible parameter list as the inherited method.

For example, Animal might define makeSound(), while Dog and Cat each provide their own implementation. Writing @Override above the method is recommended because the compiler can detect a mismatch instead of treating the method as an unrelated overload.

Overriding is different from overloading. Overriding replaces inherited behavior in a . Overloading creates multiple methods with the same name but different parameter lists, usually within one class. Static methods are hidden rather than overridden with runtime dispatch.

A can call super.makeSound() when it needs the original behavior before adding specialized behavior. Be cautious about declaring fields with the same name as fields: this creates separate fields and can make the resulting code difficult to understand.

Takeaway: Use @Override for intended instance-method replacement, and distinguish a changed implementation from a method with a different parameter list.

and runtime behavior

allows one general reference type to work with objects of multiple specialized types. For example, both Animal first = new Dog() and Animal second = new Cat() are valid because each object is compatible with the Animal type.

When code calls an overridden instance method through one of these references, selects the implementation belonging to the actual runtime object. Thus, first.makeSound() can produce the dog behavior while second.makeSound() produces the cat behavior, even though both references are declared as Animal.

This makes shared processing concise. A method that accepts an Animal can call makeSound() without testing whether the object is a dog, cat, or another . The same principle works with arrays and other collections: a loop can invoke the common operation on every element while each object supplies its specialized implementation.

Takeaway: Program against a useful contract or interface, and let overridden methods provide type-specific behavior.

General and specialized references

A object can be assigned to a reference through . For example, an Animal reference may refer to a Dog object. This conversion is safe because every Dog is an Animal.

The reference determines which members are directly visible at compile time. If bark() exists only in Dog, an Animal reference cannot call it directly, even when it currently refers to a dog. The reference can call members declared by Animal, including methods that use dynamic dispatch.

is the reverse operation: converting a reference to a subtype reference so that subtype-specific members can be used. Check the object's actual type before casting, for example with instanceof. A cast is invalid when the object is not an instance of the target subtype and can cause ClassCastException at runtime.

Frequent casts and type tests often indicate that the contract is missing an appropriate operation. Prefer polymorphic methods when the behavior can be expressed cleanly through the general type.

Takeaway: Upcast freely when the subtype satisfies the general contract; downcast only when the runtime type has been established and the subtype-specific operation is genuinely needed.

Abstract designs and

An defines a common structure without representing a complete object that can be created directly. It may contain shared fields, implemented methods, and abstract methods that concrete subclasses must implement.

For example, an abstract Shape class can require an area() method. Circle then implements area() using the circle-specific formula, while another shape supplies its own formula. Code can still hold a Shape reference and call area() polymorphically.

An is useful when related types share state or implementation as well as a required set of operations. It provides more shared structure than a purely abstract contract while ensuring that each concrete subtype supplies the operations that depend on its specific representation.

is not always the best design. expresses a "has-a" relationship: a Car has an Engine, but a car is not an engine. The car can delegate start() to its engine without inheriting the engine's entire interface.

Prefer when the relationship is not genuinely "is-a," when reuse is the only motivation, or when a would impose behavior that the proposed might need to disable or contradict. often reduces coupling and makes helper objects easier to replace.

Takeaway: Choose for a substitutable specialization with meaningful shared behavior; choose for contained parts, delegation, or more flexible reuse.