8 Inheritance and Object-Oriented Design

A progressive guide to designing Java object hierarchies with encapsulation, access control, inheritance, method overriding, polymorphism, and composition.

The object-oriented design problem

Object-oriented design organizes a program around objects. An object combines state, represented by fields or properties, with behavior, represented by methods. A class describes the structure and behavior shared by objects of that kind.

The central design problem is deciding what an object should expose and what it should keep under its own control. The ideas in this guide fit together:

  • protects an object’s state and implementation.

  • determines who can use each class or member.

  • expresses a general-to-specific relationship.

  • gives a subtype specialized behavior.

  • lets clients use a general type while receiving subtype-specific behavior.

  • provides reuse without creating an relationship.

A useful overall goal is to make each class responsible for its own rules while allowing other code to depend on a small, stable interface.

Takeaway: Good object-oriented design separates what clients need to know from how an object accomplishes its work.

Protecting state with

keeps data and the operations that govern that data together. A class can make its fields private and expose operations that validate requests before changing the object. For example, a bank-account class can expose deposit and withdraw operations while preventing callers from assigning an arbitrary balance directly.

This protects an : a condition that must remain true for every valid object state. If the balance must never be negative, the class can reject an invalid opening balance and reject withdrawals that exceed the current balance. The class, rather than every caller, becomes responsible for preserving that rule.

does not require a getter and setter for every field. An unrestricted setter may expose the same risks as a public field. Prefer operations that represent valid domain actions, such as changing an address through a method that validates the new address or adding funds through a method that accepts only positive amounts.

also separates a public behavior from an internal representation. A temperature object might store Celsius internally and provide a method that returns Fahrenheit. Clients need the conversion behavior, not knowledge of the stored unit.

Takeaway: Keep state private when possible and expose meaningful, validated operations instead of unrestricted field access.

Choosing access levels

determines where a class, field, constructor, or method may be used. Java commonly provides four member-access levels:

  • private allows use only within the declaring class.

  • Package-private access, represented by no modifier, allows use within the same package.

  • protected allows use within the declaring class, the same package, and subclasses subject to Java’s protected-access rules.

  • public allows use from any class that can access the declaring type.

Choose the most restrictive level that supports the intended design. Use private for implementation details and state that outside code should not modify directly. Use package-private access for functionality intended only for closely related classes in one package. Use protected cautiously for deliberate extension points. Use public for stable operations that form the class’s external contract.

A public member creates a commitment to client code. Changing or removing it can break callers, so public methods should describe behavior that the class can support consistently. Private fields also make representation changes easier: callers can continue using the same public method even if the class changes how it stores its data.

Takeaway: Visibility is a design decision, not merely a convenience. Narrow interfaces reduce coupling and preserve implementation freedom.

Modeling relationships with

creates a general-to-specific relationship. A superclass represents a broader concept, and a subclass represents a specialized form of that concept. In Java, a class declares its direct superclass with extends, and each class has one direct superclass apart from the root class Object.

For example, SalariedEmployee can extend Employee because every salaried employee is an employee. The subclass can call the superclass constructor with super(...) and can use inherited accessible methods such as a public name accessor. It does not automatically gain access to the superclass’s private fields; private state remains controlled by the superclass.

is appropriate only when the subtype relationship is meaningful. A Car has an Engine, but a car is not an engine. That relationship is better modeled with : the car contains or delegates to an engine object. Using only to reuse implementation can create rigid hierarchies and dependencies between parent and child classes.

A superclass should contain behavior that genuinely applies to all of its subtypes. It should not accumulate child-specific rules merely because those rules can be shared through a parent. A class or method declared final cannot be extended or overridden, which can protect a design when further specialization would be unsafe.

Takeaway: Use for a real “is-a” relationship and for a “has-a” relationship or for reuse without .

Specializing behavior safely

lets a subclass preserve a common operation while implementing it according to its own rules. The overriding method must match the inherited method’s name and parameter types, cannot reduce visibility, and must use the same return type or a permitted subtype. A private method is not overridden because subclasses do not inherit it, a final method cannot be overridden, and a static method is hidden rather than overridden.

Use the @Override annotation whenever a method is intended to override an inherited method. The compiler can then detect a misspelled name or an incorrect parameter list. Without the annotation, such a mistake may silently create a new method and leave the inherited behavior unchanged.

An overriding method may call super when it should extend the parent implementation rather than replace it completely. This is useful when the superclass provides a meaningful default operation. If the superclass implementation has no valid meaning for every subtype, an abstract method or a different abstraction may communicate the design more clearly.

Do not confuse overriding with . uses the same method name with different parameter lists and is selected using the arguments known at compile time. Overriding supplies a subtype-specific implementation of an inherited instance method and supports runtime .

Takeaway: Override deliberately, preserve the parent contract, and use @Override so the compiler can verify your intent.

Using through a common type

allows code to work with a general type while the runtime object supplies the appropriate specialized behavior. Suppose Employee declares calculatePay, while SalariedEmployee and HourlyEmployee each override it. A method that accepts an array of Employee objects can call employee.calculatePay() for every element without checking which concrete class each element has.

The declared type of a reference controls which operations are available to the caller, while the runtime object determines which overridden instance implementation executes. This lets a payroll method depend on the stable Employee abstraction rather than on a growing list of special cases.

The design becomes extensible: adding another employee subtype can involve creating a class with its own implementation instead of rewriting every method that processes employees. The caller remains focused on the operation it needs, such as calculating pay, rather than on the internal classification of each object.

Reliable requires . Ask whether an object of the proposed subtype can be used anywhere the superclass is expected without surprising the client. If a subtype cannot provide a meaningful result for an inherited operation or must violate documented rules, consider , a separate interface, or a redesigned abstraction.

Avoid unnecessary checks such as branching on every concrete employee class. When each subtype knows how to perform the shared operation, invoke that operation through the general reference.

Takeaway: Program to the general type, let each subtype provide its own behavior, and ensure every subtype honors the parent contract.

Applying the design principles

A practical design process connects the mechanisms rather than treating them as isolated language features:

  1. Identify the common concept and place only genuinely shared state and behavior in the superclass.

  2. Define the public contract as a small set of operations that clients actually need.

  3. Keep representation details private and protect important invariants inside the class.

  4. Check the subtype relationship by asking whether every proposed child is meaningfully a kind of the parent.

  5. Decide whether is more accurate when one object merely uses another or when code reuse is the main goal.

  6. Override intentionally, preserve documented expectations, and use @Override.

  7. Make methods accept a superclass or interface when they do not need concrete-class details.

  8. Test every subtype through the parent interface, not only through direct calls to the concrete class.

Common warning signs include public fields, broad use of protected, chosen only for code reuse, child classes that depend on many parent fields, and callers filled with concrete-type checks. These patterns increase coupling and make future changes harder.

and can support each other when the superclass exposes carefully designed behavioral extension points. They conflict when subclasses depend directly on the superclass’s internal representation. Protected methods that express behavior can be safer than protected fields, but protected should still be used only when subclass dependence is intentional.

Final checklist:

  • Is each important state rule enforced by the class that owns the state?

  • Is each member no more visible than necessary?

  • Does each subclass satisfy the parent’s documented expectations?

  • Are overriding and being distinguished correctly?

  • Could express the relationship more accurately?

  • Can client code use the general type without concrete-class branches?