Overview
This unit explains inheritance and interfaces in object-oriented programming, with emphasis on Java-style rules, design decisions and runtime behaviour expected at Class 12 level. You will study how classes inherit fields and methods, how subclasses specialise or reuse behaviour, and how interfaces provide a way to express multiple types and contracts. The unit covers types of inheritance used in practice, syntax and rules for extending classes, use of super and this, constructor chaining and initialisation order, method overriding and dynamic dispatch (polymorphism), abstract classes and interfaces and how to choose between them, default methods and conflict resolution, access control effects on inheritance, final/static/private interactions, and composition as an alternative. Practical examples, design guidance and common pitfalls are included so you can write correct programs, predict outputs, design healthy class hierarchies and answer board-style questions. Understanding these topics matters because inheritance and interfaces are fundamental for building modular, extensible and maintainable software: they reduce duplication, enable polymorphic APIs and support code reuse when designed carefully.
Learning Objectives
- Explain the concept and purpose of inheritance in object-oriented programming.
- Distinguish between single, multilevel and hierarchical inheritance and describe how interfaces enable multiple inheritance of type.
- Demonstrate class syntax for inheritance, use of super and this, and constructor chaining with correct order of initialisation.
- Implement and differentiate abstract classes and interfaces, and state appropriate use-cases for each.
- Explain and illustrate method overriding, dynamic method dispatch and runtime polymorphism with examples.
- Apply access modifiers correctly and understand their effects on inherited members and overriding.
- Identify when to prefer composition over inheritance and recognise common design pitfalls and debugging techniques.
- Use default methods in interfaces and resolve default-method conflicts according to language rules.
Topics in this chapter
17 topics · tap a topic title to jump straight to it.
Introduction to Inheritance
What is inheritance?
Inheritance is a mechanism in object-oriented programming where one class (the subclass) acquires members—fields and methods—of another class (the superclass). This creates a hierarchy of types where shared features live in a common base class and specialised features appear in derived classes. In practice, inheritance expresses an "is-a" relationship: if B extends A then every B is also an A.
Why inheritance matters
Inheritance supports code reuse: common data and behaviour are placed once in a superclass, avoiding duplication. It supports polymorphism: code can use a superclass type to work uniformly with many subclass instances. It helps model real-world relationships clearly, for example Person -> Student or Vehicle -> Car. Proper use of inheritance improves maintainability and readability by centralising shared logic.
What is inherited and what is not
Subclasses inherit accessible fields and instance methods of the superclass, subject to access control. Private members are not directly accessible in subclasses though their effects can be exposed through public or protected accessors. Constructors are not inherited; instead subclasses call superclass constructors to properly initialise inherited state. Static members belong to the class and are not inherited in the polymorphic sense; they are accessed through the class name.
When to use inheritance
Use inheritance when there is a clear subtype relationship and substitutability is required: a subclass should behave as an instance of the superclass without surprising side effects. If the relationship is more about using behaviour rather than being a subtype, composition may be a better choice. Avoid deep inheritance chains which make code brittle and harder to change.
Risks and trade-offs
Improper inheritance leads to fragile base class problems: changes in the superclass can break subclasses. Also, exposing too many internals to subclasses through protected fields increases coupling. Balance reuse with encapsulation: prefer private fields and protected/public methods for controlled extension. Think about future requirements before fixing a deep hierarchy.
Practical summary
Think of inheritance as a tool to express type hierarchies and to share code. Combine it with encapsulation and polymorphism to build modular programs. Always check the meaning of the relationship: prefer inheritance for true "is-a" relations and composition for "has-a" relations.
- Superclass Animal with method eat(); subclass Dog extends Animal and adds method bark().
- Superclass Shape with field colour and method draw(); subclass Circle overrides draw() to draw a circle.
- Vehicle superclass; Car and Truck subclasses inherit speed and start() and add their specific features.
- Subclass declaration: class Subclass extends Superclass { ... }
- IS-A relationship: If B extends A then B is-a A.
OOP concepts relevant to inheritance
Recap of classes and objects
Classes describe the blueprint of objects: what data they contain (fields) and what they can do (methods). Objects are instances created from classes. Inheritance allows a class to build on another class's blueprint: the subclass reuses fields and methods of the superclass and may add its own. This reuse reduces repetition and groups common behaviour logically.
Encapsulation and access control
Encapsulation bundles data and methods and hides internal details from other parts of the program. Access modifiers—public, protected, default (package-private) and private—control who can use a member. In inheritance, protected members are accessible to subclasses (and to other classes in the same package), while private members remain hidden and require public/protected accessors if subclasses should use them. Proper encapsulation prevents subclasses from depending on fragile implementation details.
Abstraction and abstract types
Abstraction focuses on essential behaviour and hides specifics. Abstract classes and interfaces express abstract types: they describe what must be done without specifying exactly how. An abstract class can define some shared behaviour and state; interfaces define contracts that many unrelated classes can implement. Abstraction helps design systems where the user of a type need not know the concrete implementation.
Polymorphism and substitutability
Polymorphism allows code to treat different objects through the same interface. When a subclass extends a superclass, an instance of the subclass can be used wherever the superclass type is expected. This substitutability enables flexible code: a method that accepts a superclass type can operate on any subclass instance, and overridden methods provide specialised runtime behaviour.
Design interplay
These concepts work together in inheritance: encapsulation controls how safely members are inherited and accessed; abstraction defines stable contracts for subclasses; polymorphism makes code extensible by relying on abstract or base types rather than concrete classes. For Class 12, make sure you can show examples of each concept and explain how they influence inheritance choices in program design.
Practical considerations
Always think which members should be private or protected, prefer exposing behaviour via methods (getters/setters) rather than exposing fields, and design abstract classes or interfaces to clearly express the intended extension points. This reduces bugs and improves long-term maintainability.
- Encapsulation example: class Person has private name and protected age; subclasses access age directly but use a getter for name.
- Polymorphism example: Animal a = new Dog(); a.sound() calls Dog's sound method at runtime.
- Abstraction example: abstract class Account defines abstract method calculateInterest(), subclasses implement it for specific account types.
- Access hierarchy in inheritance: public > protected > default (package) > private
- Polymorphic assignment: Superclass ref = new Subclass();
Types of Inheritance and language constraints
Classification of inheritance
Inheritance is classified by structure: single inheritance, multilevel inheritance, hierarchical inheritance and multiple inheritance. Each form affects design and behaviour in different ways and different languages support them differently.
Single inheritance
One class extends exactly one superclass. This is simple and avoids many conflicts. Example: class Car extends Vehicle. Single inheritance is easy to reason about because there is a single path of inheritance for any chain.
Multilevel inheritance
Here a class derives from a class which itself derives from another class: A <- B <- C. Members flow down the chain, so C inherits A’s members through B as well. Constructor chaining and initialisation order become more visible here: superclass constructors are invoked first, then subclass constructors.
Hierarchical inheritance
One base class is extended by multiple subclasses: Vehicle is extended by Car, Bike, Truck. Common behaviour is factored in the single base class so all subclasses share it. This is common when many concrete types share a conceptually common entity.
Multiple inheritance
Multiple inheritance means inheriting from more than one class directly. This can create ambiguity: if two parents define the same member differently, which version should the child inherit? Many languages allow multiple inheritance but Java does not allow a class to extend multiple classes because of such ambiguities. Java instead allows a class to implement multiple interfaces, which provides multiple type relationships without multiple implementation inheritance.
Interfaces and multiple inheritance of type
Interfaces let a class present many roles simultaneously: class C implements I1, I2. Since interfaces traditionally declare only method signatures and constants, there was no conflict of implementation. Modern interfaces may provide default implementations; Java defines rules to resolve conflicts: class implementations override interface defaults, and the implementing class must explicitly resolve conflicts between multiple defaults.
Design advice
Prefer single inheritance or shallow multilevel hierarchies for clarity. Use interfaces to express role-like capabilities that many classes should share. Avoid deep inheritance chains that make maintenance difficult. If multiple concrete behaviours are needed, favour composition and interfaces to combine roles safely.
- Single: class Dog extends Animal {}
- Multilevel: class A; class B extends A; class C extends B
- Hierarchical: class Vehicle; class Car extends Vehicle; class Bike extends Vehicle
- Multilevel chain: class C extends B; class B extends A => C inherits A and B
- Implementing interfaces: class X implements I1, I2 { ... }
Syntax of inheritance: declarations, overriding and constructors
Declaring inheritance
To create a subclass in Java use the extends keyword: class Subclass extends Superclass { /* members */ }. A class may extend only one class directly in Java. The subclass inherits accessible fields and methods from the superclass and can introduce new members or override inherited methods to change behaviour.
Method overriding and @Override
To override an inherited instance method, provide a method in the subclass with the same signature and a compatible return type. Annotate it with @Override to let the compiler check you are truly overriding a superclass method; this prevents accidental overloading and mistakes. The overriding method must not reduce the visibility of the method and must respect exception rules for checked exceptions.
Constructors and their rules
Constructors are not inherited. A subclass constructor must call a superclass constructor, either explicitly with super(arguments) as the first statement or implicitly the compiler inserts super() if no explicit call is present and a no-argument superclass constructor exists. If the superclass does not provide a no-arg constructor, the subclass must explicitly call a suitable superclass constructor or compilation fails.
Calling superclass members
Use super.methodName(...) to invoke the superclass version of an overridden method when you want to reuse base behaviour and then add extra steps. Use super.fieldName to access a field of the superclass when a subclass field hides it. Avoid field hiding where possible; prefer different names or protected accessors to maintain clarity.
Overriding and exceptions
When overriding a method that declares checked exceptions, the overriding method cannot declare broader checked exceptions than the superclass method; it may declare fewer or narrower ones. This ensures callers relying on the superclass contract are not surprised by new checked exceptions from subclass implementations.
Best practices
Keep constructors simple and delegate complex initialisation to helper methods. Do not call overridable methods from constructors because subclasses may not be fully initialised when such methods execute. Use @Override for clarity, declare accessors rather than exposing fields, and document extension points clearly so subclass authors know how to extend safely.
- class A { void show(){} } class B extends A { @Override void show() { /* subclass behaviour */ } }
- Constructor chaining: class A { A(int x){} } class B extends A { B(){ super(10); } }
- super usage: class B extends A { void f(){ super.f(); } }
- Subclass declaration: class Sub extends Super { ... }
- Constructor chaining: subclass() -> super(args) -> superclass() -> subclass body
super and this usage and constraints
this keyword
The keyword this refers to the current object inside instance methods and constructors. Use this to distinguish instance variables from parameters (for example this.x = x) and to pass the current object as an argument to other methods. Use this(...) to call another constructor in the same class; this(...) must be the first statement in that constructor.
super keyword
super refers to the superclass portion of the current object. Use super.methodName(...) to invoke the superclass method when it has been overridden, and super(fieldName) to access an inherited field that is hidden by a subclass field. Use super(...) as the first statement in a subclass constructor to invoke a parent constructor and initialise inherited state properly.
Rules and limitations
You cannot use both this(...) and super(...) in the same constructor because each must be the first statement. super cannot be used in static contexts, since static methods do not belong to instances. Private superclass members are not accessible via super; subclasses must use exposed public/protected methods to interact with private state.
Practical patterns
1) Constructor chaining: subclass constructors call super(...) to ensure the base class is initialised. 2) Extending behaviour: overridden methods often call super.methodName() to reuse base logic before or after adding subclass-specific steps. 3) Field disambiguation: when both classes declare a field name, use super.field to reference the superclass field. Prefer avoiding such hiding to reduce confusion.
Good practice
Limit the use of protected fields; prefer private fields with protected getters and setters to maintain control. Use this(...) for constructor reuse within a class, and use super(...) to initialise inherited state from a parent class. Document any expected behaviours when subclasses must call super implementations to maintain correct object state.
- Constructor chaining: class A { A(int x){} } class B extends A { B(){ super(10); } }
- Using this: class Point { int x; Point(int x){ this.x = x; } }
- Using super in method: @Override void print(){ super.print(); System.out.println("more"); }
- Constructor order: when creating Subclass(), super(...) runs first, then subclass body.
- this(...) calls another constructor in same class; super(...) calls superclass constructor.
Method overriding: rules, annotations and exceptions
Definition
Method overriding occurs when a subclass supplies a new implementation for a method declared in a superclass and with the same signature. Overriding allows subclass-specific behaviour when methods are invoked on superclass references that point to subclass objects.
Key rules
1) Method signature must match: same name and parameter types. 2) Return type must be the same or covariant (a subtype) of the superclass return type. 3) Overriding method’s access level must be at least as visible as the superclass method (cannot reduce visibility). 4) Overriding method must not throw checked exceptions broader than those declared by the superclass method; it may throw fewer or narrower ones. 5) Static methods are not overridden; they are hidden. 6) final methods cannot be overridden.
Use of @Override
Annotating methods with @Override helps the compiler validate your intent. If the signature does not actually match a superclass method, the compiler reports an error, preventing accidental overloading instead of overriding.
Covariant return types
Java allows the overriding method to return a subtype of the original return type. For example, if superclass method returns Animal, subclass override may return Dog. This makes code more specific while maintaining compatibility.
Exceptions and overriding
If the superclass method declares checked exceptions, the overriding method in the subclass cannot declare new checked exceptions that are broader. It can declare none, a subset or narrower checked exceptions. Unchecked exceptions (runtime exceptions) are not checked by the compiler and can be thrown freely but should be used carefully.
Design and pitfalls
Avoid overloading by mistake—change of parameter list creates a new method instead of overriding. Remember private methods are not visible to subclasses and therefore cannot be overridden; a method with same signature in subclass is a new method. Be careful when overriding equals, hashCode and toString; keep contracts intact to avoid logic errors in collections and comparisons.
- Superclass: void show() throws IOException; Subclass: @Override public void show() throws FileNotFoundException { }
- Covariant return: class A { A get(){} } class B extends A { @Override B get(){} }
- Static hiding: static void f() in superclass and static void f() in subclass hides the superclass version.
- Override constraints: signature same + return same/covariant + access not more restrictive + checked exceptions compatible
- Polymorphic call: Super s = new Sub(); s.method() -> Sub.method() executed at runtime
Abstract classes: purpose, features and examples
What is an abstract class?
An abstract class is a class that may contain abstract methods—methods declared without an implementation—and cannot be instantiated on its own. It serves as a template for concrete subclasses to provide specific implementations. Abstract classes may also contain concrete methods and fields to share common code and state among subclasses.
Why use an abstract class?
If several related classes share both behaviour and state, an abstract class allows you to put common code and fields in one place. Subclasses inherit the shared implementation and must implement the abstract methods, ensuring a consistent contract. Abstract classes are useful when classes share a common identity and implementation details that should be reused.
Members allowed
Abstract classes can have constructors, instance fields, concrete methods, abstract methods, and static members. Constructors are used to initialise inherited state and are invoked by subclass constructors using super(...). Fields in abstract classes hold shared state for use by subclasses. Abstract methods enforce that subclasses supply required behaviour.
Rules for subclasses
A concrete subclass of an abstract class must implement all inherited abstract methods, otherwise it must itself be declared abstract. Subclasses can call protected helper methods or access protected fields from the abstract base if the design exposes them. Keep abstract classes focused and avoid forcing irrelevant methods on subclasses.
Design comparison with interfaces
Abstract classes are chosen when shared code or state is needed. Interfaces are chosen to express roles or capabilities that many unrelated classes might implement. A class can extend only one abstract class but can implement many interfaces. In practice, a common pattern is to define an interface for behaviour and an abstract class that provides a partial implementation to make it easier for implementors.
Practical example
abstract class Vehicle { protected int speed; Vehicle(int s){ this.speed = s; } abstract void move(); void setSpeed(int s){ this.speed = s; } } Concrete subclasses Car and Bicycle implement move() and reuse setSpeed and the speed field. This reduces duplication and enforces a consistent API.
- abstract class Animal { abstract void sound(); void breathe(){ System.out.println("breathing"); } }
- class Dog extends Animal { @Override void sound(){ System.out.println("bark"); } }
- abstract class Vehicle { protected int speed; Vehicle(int s){ this.speed = s; } abstract void move(); }
- Abstract class declaration: abstract class ClassName { abstract returnType methodName(params); }
- Subclass rule: If subclass does not implement all abstract methods, subclass must be abstract.
Interfaces: contracts, default methods and static members
What is an interface?
An interface defines a contract: a set of method signatures that an implementing class agrees to provide. Interfaces allow unrelated classes to share the same API so code can treat different classes uniformly. Traditionally interfaces had only abstract methods, but modern interfaces can include default and static methods with implementations.
Declaring and implementing
Use interface keyword: interface I { void m(); } Implement with implements: class C implements I { public void m(){ ... } } Methods in interfaces are implicitly public, so implementing classes must declare them public. Fields in interfaces are implicitly public, static and final (constants).
Default and static methods
Default methods (declared with default) allow interfaces to provide a fallback implementation without forcing all implementors to change. Static methods in interfaces are utility methods called on the interface itself. Default methods help in API evolution but can produce conflicts if multiple interfaces provide the same default.
Multiple interfaces
A class may implement multiple interfaces, giving it multiple types. This is Java’s approach to multiple inheritance of type without inheriting multiple implementations. When multiple defaults conflict, rules decide which method is chosen: class implementations win over interface defaults, the more specific interface default wins, otherwise the class must override to resolve the conflict.
Design uses
Interfaces express roles (for example Comparable, Runnable). Prefer interfaces when you need loose coupling and multiple unrelated classes should provide the same behaviour. Use abstract classes for shared state and implementation. Often frameworks define interfaces and provide an abstract base class to simplify common tasks.
Practical notes
Use interfaces to program to an API rather than a concrete class. Keep interfaces cohesive and stable to avoid breaking implementors. When using default methods, document the intended behaviour and consider how conflicts will be resolved by implementors.
- interface Flyable { void fly(); } class Bird implements Flyable { public void fly(){ System.out.println("flies"); } }
- Multiple: class Amphibian implements Swimmable, Walkable { public void swim() {} public void walk() {} }
- Default methods: interface I { default void show(){ System.out.println("default"); } }
- Interface declaration: interface I { /* method signatures */ }
- Implementing: class C implements I1, I2 { ... }
Multiple inheritance issues and diamond problem
Multiple inheritance and ambiguity
Multiple inheritance occurs when a class inherits behaviour from more than one parent. This may create ambiguous situations when the same member is inherited through more than one path. The diamond problem is a classic example: a top base class A is extended by two classes B and C, both override method m(), and class D inherits from both B and C. Which m() should D inherit? This ambiguity is problematic for predictable behaviour.
How Java avoids class-based multiple inheritance
Java prevents a class from extending more than one class. This design decision removes the diamond ambiguity for implementations. Instead Java allows a class to implement multiple interfaces. Since traditional interfaces had no method bodies, there was no implementation conflict. Modern interfaces may include default methods which necessitate explicit conflict resolution rules.
Default method conflict resolution
When a class implements two interfaces that provide the same default method signature, the compiler requires the class to override the method to resolve the conflict. The rules are: (1) A concrete method in the class or its superclass overrides any interface default (class wins). (2) If there is no class method, the most specific interface default is chosen (an interface that extends another provides a more specific default). (3) If two unrelated interfaces provide conflicting defaults, the implementing class must override and supply an implementation; it may delegate to a chosen default using InterfaceName.super.method().
Practical examples
Suppose I1 and I2 both define default void m(). class C implements I1, I2 { public void m(){ I1.super.m(); } } resolves the conflict by explicitly calling one default implementation. If C extends a class that provides m(), that class method takes precedence and no conflict arises.
Design advice
Avoid depending on multiple interface defaults for critical behaviour. Prefer to provide a clear class implementation when behaviour must be unambiguous. Use interfaces to express contracts and keep default methods as conveniences, not as the sole source of complex logic that could create conflicting expectations.
- Diamond illustration: A->B and A->C and B & C -> D leads to ambiguity if B and C override A.m().
- Java interface conflict: interface I1 { default void m(){} } interface I2 { default void m(){} } class C implements I1, I2 { public void m(){ I1.super.m(); } }
- Rule: Class method overrides interface default methods.
- Conflict rule: If two interfaces provide same default, implementing class must override.
Polymorphism and dynamic method dispatch in practice
Polymorphism recap
Polymorphism lets one interface represent many different underlying forms. In an inheritance context, this means a variable declared with a superclass type can hold references to objects of any subclass. The same call written once can produce different results depending on the actual object at runtime.
How dynamic method dispatch works
Dynamic method dispatch is the runtime mechanism that selects which overridden method implementation to execute. At compile time the compiler checks that the method exists for the declared reference type. At runtime the JVM inspects the actual object's class and calls the implementation from that class or its nearest superclass that provides it. This allows writing general code that relies on specific behaviours from concrete classes when executed.
Examples and typical uses
Consider Animal a = new Dog(); a.sound(); The compiler knows a has a sound() method; the JVM calls Dog.sound() at runtime if Dog overrides it. A collection of the base type, for example List
What is not polymorphic
Static methods, private methods and constructors are not polymorphic. Static methods are bound to the declared type at compile time and therefore are hidden, not overridden. Private methods are not visible to subclasses so they cannot be overridden. Relying on polymorphism for static methods is a common source of mistakes.
Design benefits and guidelines
Polymorphism encourages programming to an interface or abstract class rather than concrete classes. Methods that accept abstract types or interfaces are flexible and can work with future implementations without change. Avoid frequent instanceof checks; instead design abstract methods so subclasses provide their own behaviour. Use polymorphism to implement patterns like Strategy and Template Method.
Debugging and testing
If a method call seems to execute the wrong implementation, log the runtime type using getClass() or use a debugger to inspect the actual object. Write unit tests for each subclass and tests for the polymorphic behaviour to ensure the correct method executes under different concrete instances. These practices make polymorphic code reliable and easier to maintain.
- Animal[] zoo = { new Dog(), new Cat() }; for(Animal a : zoo) a.sound(); each object’s sound runs at runtime.
- Shape s = new Circle(); double area = s.area(); calls Circle.area() at runtime.
- Compile-time type determines available methods; runtime type determines which implementation executes.
- Polymorphic call pattern: Super ref = new Sub(); ref.method() -> Sub.method() if overridden.
Constructors, initialization order and inheritance pitfalls
Order of initialisation
When an object of a subclass is created, initialization proceeds from the top to the bottom of the class hierarchy. First, default values are assigned to fields of the top-most superclass, then static initialiser blocks (at class load time) and instance initialiser blocks and field initialisers run in order, followed by the superclass constructor body. After the superclass constructor completes, control returns down the chain to run subclass initialisers and finally the subclass constructor body. This ensures inherited state is set up before subclass initialisation.
Constructor chaining rules
A constructor may call either this(...) to invoke another constructor in the same class or super(...) to invoke a superclass constructor; each, if used, must be the first statement. If no explicit super(...) call is provided, the compiler inserts a call to the no-argument superclass constructor. If the superclass lacks a no-arg constructor and no explicit super(...) is used, compilation fails. Therefore, when designing base classes intended for extension, provide appropriate constructors or document required parameters.
Instance initialisers and field initialisers
Field initialisers and instance initializer blocks execute after the superclass constructor completes and before the subclass constructor body executes, in the textual order they appear. Static initializer blocks run once when the class is first loaded, in superclass-to-subclass order. Understanding this order helps avoid surprises when initialising fields that depend on superclass state.
Common pitfalls
Calling overridable methods from constructors is hazardous: the subclass override may execute before subclass fields are initialised, causing NullPointerException or incorrect state. Assuming field initialisation order without checking textual order leads to bugs. Not providing a suitable superclass constructor forces subclasses to write explicit super(...) calls or to fail compilation.
Practical advice
Keep constructors short and delegate complex setup to well-documented helper methods that are safe to call. Prefer final or private helper methods in constructors to avoid inadvertent overriding. Provide explicit constructors in base classes intended for extension. Test object construction paths with simple examples and logs to ensure initialization happens as expected.
Example reminder
class A { A(int x) { /* initialise */ } } class B extends A { B(){ super(5); /* subclass init */ } } Creating new B() will invoke A(5) first, followed by B() body and instance initialisers.
- If superclass has only parameterised constructor A(int x), then subclass must call super(value) in its constructor.
- Field initialiser example: class C { int x = compute(); C(){ ... } } compute() runs after superclass constructor.
- Constructor order: Object() -> Superclass() -> ... -> Subclass()
- First statement rule: constructor must start with this(...) or super(...), if present.
Access control: visibility rules and inheritance
Access modifiers explained
Java provides four visibility levels: public (visible everywhere), protected (visible within same package and to subclasses), default/package-private (visible only within the same package), and private (visible only within the declaring class). These modifiers control which members are accessible to subclasses and other classes.
Effect on inheritance
Protected members are designed for subclass use: they allow subclasses to access necessary internals while restricting access from unrelated classes. Private members are not accessible directly in subclasses and therefore cannot be overridden. Subclasses must use public or protected accessors to interact with private state of the superclass.
Overriding and visibility
An overriding method must not reduce visibility relative to the superclass method. For example, a public superclass method cannot be overridden as protected in a subclass. The compiler enforces this rule to maintain substitutability: code that uses the superclass should still be able to access the method on a subclass instance.
Package considerations
If a subclass resides in a different package from the superclass, it cannot access package-private members. Protected access allows subclass methods to access the protected member when using inheritance, but not via a reference to a superclass object from outside the package. Be careful with protected static members since they are accessed via the class rather than instance.
Design recommendations
Prefer private fields with protected or public getter/setter methods to maintain control over mutation. Avoid making fields protected unless necessary because protected mutable fields couple subclasses tightly to the superclass internals. Use final for values that must not change and document protected APIs clearly so subclass authors know how to use them safely.
Security and robustness
Careful use of access modifiers protects internal state and allows safer refactoring. Changing a member from private to protected is a design decision with maintenance implications; avoid exposing internals without a good reason.
- private int count; protected int size; public void show(){}; subclass can access size and show() but not count directly.
- Overriding visibility: public void f() in superclass -> subclass must use public void f(), cannot change to protected.
- Visibility rule for overriding: overridingMethod.visibility >= superclassMethod.visibility
- Access levels: public > protected > package-private > private
final, static and private: effects on inheritance
final modifier
Applying final to a class prevents it from being extended. This is useful when a class should have unchangeable behaviour for safety or correctness. Applying final to a method prevents any subclass from overriding that method, ensuring the implementation remains identical for all subclasses. Applying final to a variable makes it a constant reference: for primitive types the value cannot change, and for object references the reference cannot be reassigned though the object’s internal state may still be mutable.
static members and inheritance
Static fields and methods belong to the class rather than to instances. Static methods are resolved according to the compile-time type and therefore are not polymorphic. If a subclass declares a static method with the same signature, it hides the superclass static method rather than overriding it. This hiding can lead to confusion when code expects polymorphic behaviour; prefer calling static methods through class names (ClassName.method()) to avoid ambiguity.
private members and subclassing
Private members are accessible only within the class that declares them. Subclasses do not have direct access to private fields or methods and cannot override private methods since they are not visible to the subclass. If a subclass declares a method with the same signature as a private superclass method, it defines a new independent method rather than overriding the original.
Interactions and common pitfalls
Calling overridable methods from constructors can cause bugs because subclass overrides may execute when subclass fields are not yet initialised. Declaring such methods final prevents this class of error by forbidding overriding. Relying on static fields shared across subclasses can produce unexpected shared state; be cautious when using mutable static fields. Also, hiding fields in subclasses (declaring a field with the same name as in the superclass) leads to different values being observed when accessed through references of different types.
Design recommendations
Use final for classes or methods when behaviour must remain fixed. Use static for utilities and constants and avoid static state for per-object data. Keep critical helpers private and expose limited protected or public APIs for extension. Prefer composition to sharing mutable static state, and document intended extension points clearly so subclass authors do not misuse internal details.
Practical example
final class Constants cannot be extended; static utility methods in Utils are called as Utils.doWork(); private helper() in a base class cannot be overridden by derived classes and therefore cannot accidentally change base class behaviour.
- final class Constants { ... } // cannot extend
- static void util() { } // called as ClassName.util()
- private void helper() { } // subclass cannot override helper()
- Cannot override: final method -> compile-time error
- Static binding: ClassRef.staticMethod() chosen at compile time
When to use inheritance and when to use composition
Two reuse strategies
Inheritance and composition are the main ways to reuse code. Inheritance models an "is-a" relationship: a subclass is a specialised version of a superclass and can be used wherever the superclass is expected. Composition models a "has-a" relationship: a class contains other objects and delegates work to them.
When to use inheritance
Choose inheritance when there is a natural subtype relationship and substitutability holds: a subclass can stand in for the superclass without breaking expectations. Use inheritance when you want polymorphism and to share a significant portion of implementation and state across related classes.
When to use composition
Use composition when the relationship is not a true subtype or when you need to change behaviour at runtime. Composition leads to lower coupling because the containing class controls how its components are used and can hide implementation details. It avoids the fragile base class problem where changes in the superclass unexpectedly affect subclasses.
Advantages and disadvantages
Inheritance gives simple polymorphic substitution but creates tight coupling between base and derived classes and can lead to deep hierarchies that are hard to modify. Composition is more flexible and supports better encapsulation, but may require writing additional delegating methods and interfaces to expose component behaviour.
Refactoring from inheritance to composition
If inheritance forces unnatural overrides or exposes too many internals, refactor by extracting a component class and make the original subclass hold it as a field. Delegate appropriate calls to the component. This reduces coupling and often makes the code easier to test and extend.
Practical rule of thumb
Ask whether the relation truly reads as "is-a". If yes, consider inheritance. If not, or if you need multiple behaviours mixed into one object, favour composition and interfaces. Modern design often prefers composition with small interfaces for better maintainability and flexibility.
- Inheritance example: Square extends Shape because a square is-a shape and can be used where Shape is expected.
- Composition example: Car has Engine as a field and delegates start() to engine.start() rather than extending Engine.
- Refactor: class Printer extends OldPrinter -> use class Printer { OldPrinter old; public void print(){ old.print(); } }
- is-a -> inheritance; has-a -> composition
- Prefer composition for reuse when subtype relationship is not natural
Interfaces vs abstract classes: choosing and design patterns
Core differences
Abstract classes and interfaces both let you express common behaviour, but they differ: abstract classes can have instance fields, constructors and concrete methods as well as abstract methods; interfaces (especially historically) declare method signatures and constants and cannot hold instance state. Modern interfaces can provide default and static methods but still cannot hold per-instance state. A class can extend only one abstract class but implement many interfaces.
When to choose an abstract class
Choose an abstract class when related classes share significant code or state that can be factored into the base. Use it to provide a common implementation and protected helpers for subclasses. Abstract classes are suitable when all subclasses belong to the same conceptual family and will share internal representation.
When to choose an interface
Use interfaces to define capabilities or roles (for example, Comparable, Runnable) that many unrelated classes may implement. Interfaces support multiple inheritance of type and encourage loose coupling: code depends on behaviour rather than concrete classes. Prefer interfaces for stable API contracts and for composing behaviour across hierarchies.
Combining both
A common pattern is to define an interface for the API and an abstract base class that implements the interface partially. Implementors may either extend the abstract class to reuse code or implement the interface directly if they have unrelated superclasses. This approach gives flexibility and code reuse while preserving the option to implement the interface from scratch.
Evolution and compatibility
Adding methods to an interface can break existing implementors unless default methods are supplied. Abstract classes allow adding protected helper methods without breaking subclasses. When evolving APIs, prefer stable interfaces and provide default methods or adapter/abstract classes to preserve backward compatibility.
Practical recommendation
Program to interfaces where possible; use abstract classes when you need to share code. Keep interfaces small and focused, and document default behaviour clearly if using default methods. When designing libraries, provide both an interface and an abstract adapter to simplify common tasks for users.
- Interface example: interface Comparable<T> { int compareTo(T o); } classes implement it to provide ordering.
- Abstract example: abstract class Stream { abstract int read(); protected byte[] buffer; }
- Combined pattern: interface List<E> and abstract class AbstractList<E> providing partial implementation.
- Rule: Use interface for multiple roles, abstract class for shared code/state.
- A class can extend one abstract class but implement multiple interfaces.
Practical example: design a small library system using inheritance and interfaces
Problem statement
Design a small library model where items in the library include books and DVDs. Some items are borrowable while others (reference copies) are not. We want shared properties for all items and specific details for each type, and a way to mark borrowable behaviour.
Design decisions
Create an abstract class Item to hold shared fields such as id and title and common methods like getTitle(). Make Item abstract if we do not want plain Item instances. Create subclasses Book and DVD that extend Item and add fields like author or duration. Define an interface Borrowable with methods borrow(String memberId) and returnItem() to represent the borrowable behaviour.
Why this design
An abstract Item groups shared data and implementation; it can provide common logic like toString() or equality based on id. Borrowable is an interface so only those item types that can be borrowed implement it; this allows unrelated classes to be borrowable in future without forcing them into Item's hierarchy. Using an interface separates role (borrowable) from identity (Item type).
Implementation notes
Item constructor sets id and title and may be called by subclasses using super(id,title). Book implements Borrowable and provides concrete methods to record borrower and due date. In client code, maintain a List
Polymorphic use
Because Book and DVD extend Item, you can store both in a List
Extensions and edge cases
Handle reference items by not implementing Borrowable. Consider concurrency and unique ID generation. For more flexible behaviour, composition could be used: an Item could have a LendablePolicy object that encapsulates lending rules, allowing dynamic policy changes without altering the class hierarchy.
- abstract class Item { protected String id, title; Item(String i,String t){ id=i; title=t; } String getTitle(){ return title; } }
- interface Borrowable { void borrow(String memberId); void returnItem(); }
- class Book extends Item implements Borrowable { private String author; public void borrow(String m){ /* record borrower */ } public void returnItem(){ } }
- Design rule: shared data -> abstract base class; behaviour role -> interface
- Polymorphism: List<Item> items = Arrays.asList(new Book(...), new DVD(...));
Common pitfalls, debugging and best practices
Common pitfalls
1) Overloading instead of overriding: changing parameter list creates a new method rather than overriding. 2) Wrong access modifiers: making overriding method more restrictive leads to compiler errors. 3) Calling overridable methods from constructors: subclass may be partially initialised leading to bugs. 4) Expecting static methods to be polymorphic: static methods are resolved by reference type at compile time. 5) Field hiding: a subclass field with same name hides superclass field and can cause unexpected values when accessed through superclass references.
How to detect problems
Use @Override annotation to help the compiler catch mistaken overloads. Pay attention to compile-time errors about missing constructors or incompatible access levels. Insert logs or breakpoints in constructors and methods to trace the order of calls and determine which class's method executed. Write unit tests for each subclass and for polymorphic behaviour to detect regressions.
Debugging strategies
1) Print statements in constructors and methods to observe initialization order and method dispatch. 2) Use getClass() or instanceof to confirm runtime type of an object. 3) Inspect fields via getters rather than directly to avoid confusion from hiding. 4) If default method conflicts arise, explicitly override the conflicting method in the implementing class and delegate as needed using InterfaceName.super.method().
Best practices
Prefer private fields and expose controlled access with protected or public accessors. Use final on methods that should not be changed. Keep inheritance hierarchies shallow and focused—deep hierarchies are hard to maintain. Favor composition when reuse is needed without a natural subtype relationship. Program to interfaces: write methods that depend on interface types rather than concrete classes so implementations can change without affecting callers.
Refactoring advice
When inheritance creates brittle code, refactor by extracting responsibilities into separate classes and use composition. Provide adapter or abstract classes to ease migration. Ensure comprehensive tests before major refactors to keep behaviour intact.
Summary
Understanding how inheritance interacts with visibility, constructors and method dispatch helps avoid many bugs. Use compiler tools, annotations and tests to detect and prevent mistakes, and follow design guidelines to build maintainable class hierarchies.
- Mistake: class A { private void f(){} } class B extends A { void f(){} } // B.f() is not overriding A.f()
- Field hiding: superclass has int x=5; subclass int x=10; accessing via superclass reference observes superclass x unless accessed via accessor.
- @Override helps detect errors: if method signature wrong, compiler warns
- Rule: static methods are hidden, not overridden
Key Concepts
- Inheritance
- A mechanism where a new class acquires properties and behaviours of an existing class.
- Subclass/Derived class
- A class that extends another class and inherits its members.
- Superclass/Base class
- A class whose members are inherited by subclasses.
- Interface
- A contract that declares method signatures an implementing class must fulfil.
- Abstract class
- A class that may contain abstract methods and cannot be instantiated directly.
- Method overriding
- Providing a new implementation for an inherited method with the same signature.
- Polymorphism
- Ability of different object types to be accessed through the same interface, producing different behaviours at runtime.
- super
- A keyword used to refer to superclass members and constructors from a subclass.
- this
- A keyword referring to the current object instance or to call another constructor in the same class.
- final
- A modifier that prevents a class from being extended, a method from being overridden, or a variable from being reassigned.
- static
- A modifier indicating a member belongs to the class rather than an instance; static methods are not polymorphic.
- constructor chaining
- The process where a subclass constructor invokes a superclass constructor to initialise inherited state.
- Access modifiers
- Keywords (public, protected, default, private) that determine visibility of class members.
- Default method
- A method in an interface that provides a default implementation for implementors.
- Composition
- A design technique where a class contains instances of other classes to reuse behaviour ('has-a').
- Diamond problem
- Ambiguity arising from multiple inheritance where the same base method could be inherited through multiple paths.
- Dynamic method dispatch
- Runtime mechanism that selects which overridden method implementation to invoke based on the actual object type.
Practice Questions
-
Explain inheritance with an example. / उत्तराधिकार (इन्हेरिटेन्स) को एक उदाहरण के साथ समझाइए।
Show answer
Inheritance allows a class to acquire properties and methods from another class; for example, class Vehicle {int speed;} class Car extends Vehicle {void horn(){}} — Car inherits the speed field from Vehicle so Car objects have speed. / उत्तराधिकार एक वर्ग को दूसरे वर्ग की गुणों और विधियों को प्राप्त करने की अनुमति देता है; उदाहरण के लिए, class Vehicle {int speed;} class Car extends Vehicle {void horn(){}} — Car, Vehicle से speed फ़ील्ड विरासत में पाता है, इसलिए Car वस्तुओं के पास speed होता है।
-
What is method overriding and state two rules for it. / विधि ओवरराइडिंग क्या है और इसके दो नियम बताइए।
Show answer
Overriding is defining a method in a subclass with the same signature as in the superclass so the subclass version runs at runtime; rules: (1) The overriding method must have the same signature and not a more restrictive access level, (2) It cannot throw new checked exceptions not declared in the superclass method. / ओवरराइडिंग वह है जब उपवर्ग में सुपरक्लास जैसी ही सिग्नेचर वाली विधि परिभाषित की जाती है ताकि रनटाइम पर उपवर्ग का संस्करण चले; नियम: (1) ओवरराइड करने वाली विधि की सिग्नेचर वही हो और एक्सेस स्तर अधिक प्रतिबंधात्मक नहीं होना चाहिए, (2) यह सुपरक्लास द्वारा घोषित नहीं की गई नई चेक्ड अपवाद नहीं फेंक सकती।
-
How does Java resolve ambiguity when a class implements two interfaces that have the same default method? / जब एक वर्ग दो इंटरफेस लागू करता है जिनमें एक ही डिफ़ॉल्ट विधि होती है, तो Java अस्पष्टता को कैसे हल करता है?
Show answer
If two interfaces provide the same default method, the implementing class must override the method to resolve the conflict. It can call a specific interface default with InterfaceName.super.method() if desired. / यदि दो इंटरफेस समान डिफ़ॉल्ट विधि प्रदान करते हैं, तो उसे लागू करने वाले वर्ग को विवाद सुलझाने के लिए विधि को ओवरराइड करना चाहिए। यदि चाहें तो यह InterfaceName.super.method() का उपयोग करके किसी विशिष्ट इंटरफेस डिफ़ॉल्ट को कॉल कर सकता है।
-
Describe constructor chaining in inheritance. / उत्तराधिकार में कन्स्ट्रक्टर चेनिंग का वर्णन कीजिए।
Show answer
Constructor chaining means when creating a subclass object, superclass constructors are invoked first (via super(...)) up the hierarchy before the subclass constructor body runs; if no explicit super(...) is given, the no-arg superclass constructor is called implicitly. / कन्स्ट्रक्टर चेनिंग का अर्थ है कि उपवर्ग वस्तु बनाते समय सुपरक्लास कन्स्ट्रक्टर पहले (super(... ) के माध्यम से) विरासत श्रृंखला में ऊपर बुलाए जाते हैं और उसके बाद उपवर्ग का कन्स्ट्रक्टर बॉडी चलता है; यदि कोई स्पष्ट super(...) नहीं दिया गया है, तो न-आर्ग सुपरक्लास कन्स्ट्रक्टर स्वतः बुलाया जाता है।
-
Give two reasons to prefer composition over inheritance. / विरासत की तुलना में संरचना (कम्पोज़िशन) को प्राथमिकता देने के दो कारण बताइए।
Show answer
Composition reduces coupling and makes classes easier to change because behaviour is delegated to contained objects; it avoids brittle base class dependencies and supports runtime replacement of components. / कम्पोज़िशन कपलिंग घटाती है और कक्षाओं को बदलना आसान बनाती है क्योंकि व्यवहार निहित वस्तुओं को सौंपा जाता है; यह भंगुर बेस क्लास निर्भरताओं से बचती है और घटकों के रनटाइम प्रतिस्थापन का समर्थन करती है।
-
What does the keyword final mean when applied to a class, method and variable? / final कीवर्ड का अर्थ क्या है जब इसे कक्षा, विधि और चर पर लागू किया जाता है?
Show answer
final class cannot be extended; final method cannot be overridden by subclasses; final variable cannot be reassigned after initialization. / final कक्षा का विस्तार नहीं किया जा सकता; final विधि को उपवर्ग ओवरराइड नहीं कर सकता; final चर आरम्भ के बाद पुनः असाइन नहीं किया जा सकता।
-
Write a short Java snippet showing an abstract class and a concrete subclass implementing an abstract method. / एक छोटा Java स्निपेट लिखिए जिसमें एक abstract क्लास और उसका एक concrete उपवर्ग हो जो abstract विधि को लागू कर रहा हो।
Show answer
Example: abstract class Animal { abstract void sound(); } class Dog extends Animal { @Override void sound() { System.out.println("bark"); } } This shows Animal is abstract and Dog implements sound(). / उदाहरण: abstract class Animal { abstract void sound(); } class Dog extends Animal { @Override void sound() { System.out.println("bark"); } } यह दर्शाता है कि Animal abstract है और Dog ने sound() को लागू किया है।
-
Explain why static methods are not polymorphic. / बताइए कि स्थैतिक (static) विधियाँ बहुरूपी क्यों नहीं होतीं।
Show answer
Static methods belong to the class, not instances, and are bound at compile time to the reference type; invoking a static method uses the type known at compile time, so overridden-looking static methods are actually hidden rather than dispatched dynamically. / स्थैतिक विधियाँ कक्षा से संबंधित होती हैं न कि उदाहरणों से, और कंपाइल समय पर संदर्भ प्रकार के साथ बाइंड होती हैं; स्थैतिक विधि को कॉल करना उस प्रकार का उपयोग करता है जो कंपाइल समय पर ज्ञात होता है, इसलिए ओवरराइड जैसा दिखने वाला व्यवहार छुपा होता है न कि गतिशील रूप से डिस्पैच होता है।
-
A superclass method is public void show(). Can a subclass override it with protected void show()? Explain. / एक सुपरक्लास विधि public void show() है। क्या उसका उपवर्ग इसे protected void show() के साथ ओवरराइड कर सकता है? समझाइए।
Show answer
No. An overriding method cannot be more restrictive. Changing public to protected reduces visibility and would break substitutability; compiler will report an error. / नहीं। ओवरराइड करने वाली विधि अधिक प्रतिबंधात्मक नहीं हो सकती। public को protected में बदलना दृश्यता को कम करता है और प्रतिस्थापन सिद्धांत (substitutability) को तोड़ता है; कंपाइलर त्रुटि देगा।
-
Given interface I { void m(); } and class C implements I { public void m(){} }, can C be used where I is expected? / दिया गया है interface I { void m(); } और class C implements I { public void m(){} }, क्या C को उस जगह उपयोग किया जा सकता है जहाँ I अपेक्षित है?
Show answer
Yes. Any instance of C is also an I because C implements I; you can write I ref = new C(); and call ref.m(). / हाँ। C का कोई भी उदाहरण I भी होता है क्योंकि C ने I को लागू किया है; आप लिख सकते हैं I ref = new C(); और ref.m() कॉल कर सकते हैं।
Related Laws & Principles
Explore allFoundational laws & principles connected to this chapter — tap to open in the Laws Explorer.