Inheritance — The Foundation
Inheritance is the mechanism by which one class acquires all the properties and behaviours of another class. It is Java's primary tool for code reuse and establishing a genuine IS-A relationship between classes.
extends.🏛️ Superclass (Parent / Base)
The existing class whose properties are being inherited. Also called parent class or base class. Defines the common attributes and behaviours shared by all subclasses.
🌿 Subclass (Child / Derived)
The new class that inherits from the superclass. Also called child class or derived class. It extends the superclass and can add its own attributes and methods, or override existing ones.
Inheritance should ONLY be used when there is a genuine IS-A relationship. A Dog IS-A Animal ✅. A Car IS-A Vehicle ✅. Do NOT use inheritance simply to reuse a few methods — that is a design mistake. Use composition (HAS-A) for code reuse without a true specialization relationship.
eat(), sleep(), breathe()
bark(), fetch()
retrieveItem()
purr(), climb()
fly(), sing()
Types of Inheritance in Java
| Type | Description | Supported in Java? |
|---|---|---|
| Single | One class inherits from exactly one superclass | ✅ Yes |
| Multilevel | Chain: A → B → C (B inherits A, C inherits B) | ✅ Yes |
| Hierarchical | Multiple classes inherit from one superclass | ✅ Yes |
| Multiple | One class inherits from two or more classes | ❌ Not via classes ✅ Via interfaces |
| Hybrid | Combination of the above types | ⚠️ Partial via interfaces |
// SUPERCLASS class Animal { String name; int age; Animal(String name, int age) { this.name = name; this.age = age; } void eat() { System.out.println(name + " is eating."); } void breathe(){ System.out.println(name + " is breathing."); } } // SUBCLASS — inherits from Animal using 'extends' class Dog extends Animal { String breed; // new attribute specific to Dog Dog(String name, int age, String breed) { super(name, age); // call superclass constructor this.breed = breed; } void bark() { // new method specific to Dog System.out.println(name + " says: Woof!"); } } class Main { public static void main(String[] args) { Dog d = new Dog("Bruno", 3, "Labrador"); d.eat(); // inherited from Animal → "Bruno is eating." d.breathe(); // inherited → "Bruno is breathing." d.bark(); // own method → "Bruno says: Woof!" System.out.println("Breed: " + d.breed); System.out.println("Age: " + d.age); // inherited field } }
- IS-A test: Before using
extends, ask — is a Dog an Animal? Yes ✅. Is an Engine a Car? No ❌ (it HAS a car → use composition). - Java does NOT support multiple inheritance through classes — to avoid the "Diamond Problem".
- A subclass inherits all members EXCEPT: private members (accessible via getters), constructors (not inherited), static members (shared but not inherited per se).
- The keyword is
extendsfor classes,implementsfor interfaces.
The super Keyword
super is a reference variable that refers to the immediate parent class object. It is used in three key ways — calling the parent constructor, accessing parent methods, and accessing parent variables.
① super() — Constructor Call
Calls the superclass constructor. Must be the FIRST statement in the subclass constructor. Used to initialise the inherited portion of the object.
② super.method() — Method Call
Calls a method of the superclass that has been overridden in the subclass. Allows you to extend superclass behaviour rather than replace it entirely.
③ super.variable — Field Access
Accesses a variable of the superclass when the subclass defines a variable with the same name (variable hiding/shadowing).
class Shape { String color; Shape(String color) { this.color = color; System.out.println("Shape constructor: color = " + color); } void draw() { System.out.println("Drawing a " + color + " shape"); } } class Circle extends Shape { double radius; String color; // shadows Shape.color → variable hiding Circle(String color, double radius) { super(color); // ① calls Shape(color) — MUST be first! this.radius = radius; this.color = "Bright " + color; // subclass color } void draw() { // overrides Shape.draw() super.draw(); // ② calls superclass draw() first System.out.println("Drawing circle of radius " + radius); } void showColors() { System.out.println("Subclass color : " + this.color); System.out.println("Superclass color: " + super.color); // ③ parent field } } class Main { public static void main(String[] args) { Circle c = new Circle("Red", 5.0); c.draw(); c.showColors(); } }
super() calls the superclass constructor; this() calls another constructor in the same class. Both must be the FIRST statement in a constructor — so they CANNOT both appear in the same constructor.
| Usage | Syntax | Purpose |
|---|---|---|
| Constructor call | super(args) | Invoke parent class constructor — must be first line |
| Method call | super.methodName() | Call overridden parent method from subclass |
| Field access | super.fieldName | Access parent field when subclass field has same name |
Member Access in Derived Classes & protected Visibility
Not all members of a superclass are directly accessible in a subclass. Java's access modifiers control exactly which members are visible and where.
| Modifier | Same Class | Same Package | Subclass (diff. pkg) | Any Other Class |
|---|---|---|---|---|
private |
✅ | ❌ | ❌ | ❌ |
default (no modifier) |
✅ | ✅ | ❌ | ❌ |
protected |
✅ | ✅ | ✅ KEY FOR INHERITANCE | ❌ |
public |
✅ | ✅ | ✅ | ✅ |
protected members are accessible within the same package AND in any subclass (even from a different package). It is designed specifically for inheritance — giving subclasses access to superclass internals without making them fully public. This is the recommended access level for fields intended to be used by subclasses.class Vehicle { private String chassisNo; // NOT accessible in subclass directly protected String brand; // accessible in subclass ✅ protected int speed; // accessible in subclass ✅ public String type; // accessible everywhere ✅ protected void startEngine() { System.out.println(brand + " engine started."); } private String getChassis() { // can be called by Vehicle only return chassisNo; } } class Car extends Vehicle { int doors; Car(String brand, int speed, int doors) { this.brand = brand; // ✅ protected — accessible here this.speed = speed; // ✅ protected this.doors = doors; // this.chassisNo = "..."; ← ❌ COMPILE ERROR — private } void details() { startEngine(); // ✅ protected method — accessible System.out.println("Brand: " + brand + " Doors: " + doors); } }
- private members are NEVER directly inherited — but they ARE present in the subclass object in memory; they are just not accessible directly. Use getters/setters.
protectedis the bridge — it's the right choice for fields you want subclasses to use without exposing to the world.- Constructors are NEVER inherited regardless of access modifier.
Method Overriding & Variable Redefinition
A subclass can redefine (override) methods and variables inherited from its superclass to provide a specific implementation for its own type.
class Employee { String name; double basicSalary; Employee(String name, double basic) { this.name = name; this.basicSalary = basic; } double calculateBonus() { return basicSalary * 0.10; // 10% bonus for all employees } void display() { System.out.println("Name: " + name + " Bonus: " + calculateBonus()); } } // Manager IS-A Employee (specialisation) class Manager extends Employee { int teamSize; Manager(String name, double basic, int teamSize) { super(name, basic); this.teamSize = teamSize; } @Override double calculateBonus() { // OVERRIDES Employee.calculateBonus() return basicSalary * 0.20 // 20% base bonus + teamSize * 1000; // extra per team member } } class Main { public static void main(String[] args) { Employee e = new Employee("Aarav", 30000); Manager m = new Manager("Priya", 50000, 8); e.display(); // bonus = 3000 (Employee version) m.display(); // bonus = 18000 (Manager version — overridden) } }
Overriding vs Overloading
| Feature | Method Overriding | Method Overloading |
|---|---|---|
| Class | Two different classes (parent & child) | Same class |
| Signature | Same name, same parameters, same return type | Same name, different parameters |
| Resolved at | Runtime (dynamic binding) | Compile time (static binding) |
| Keyword | @Override (annotation, recommended) | No special keyword |
| Purpose | Change inherited behaviour for a subtype | Multiple versions of same task |
| Polymorphism | Runtime polymorphism | Compile-time polymorphism |
The overriding method CANNOT have more restrictive access (e.g., if parent is public, child cannot be private). You CANNOT override final methods, static methods (that's method hiding, not overriding), or constructors. The @Override annotation is optional but strongly recommended — it catches errors at compile time.
If a subclass declares a variable with the same name as a superclass variable, the superclass variable is hidden (not overridden). The subclass variable shadows the parent's. Use super.varName to access the parent's version. Unlike method overriding, variable resolution is based on the reference type (compile time), not the object type.
Abstract Classes
Sometimes a superclass is so general that it doesn't make sense to create objects directly from it. Abstract classes enforce a contract — subclasses MUST provide the implementation.
abstract keyword. It cannot be instantiated (you cannot create objects of it directly). It may contain abstract methods (methods without a body — only signature) that MUST be overridden in non-abstract subclasses.abstract and has no body — only a method signature ending with a semicolon. Any class containing at least one abstract method MUST itself be declared abstract.// ABSTRACT CLASS — cannot instantiate directly abstract class Shape { String color; Shape(String color) { this.color = color; } // ABSTRACT METHODS — no body, must be overridden abstract double area(); abstract double perimeter(); // CONCRETE METHOD — has a body, inherited as-is void display() { System.out.println("Color: " + color + " Area: " + area() + " Perimeter: " + perimeter()); } } // Concrete subclass — MUST implement all abstract methods class Circle extends Shape { double radius; Circle(String color, double radius) { super(color); this.radius = radius; } @Override double area() { return Math.PI * radius * radius; } @Override double perimeter() { return 2 * Math.PI * radius; } } // Another concrete subclass class Rectangle extends Shape { double length, width; Rectangle(String color, double l, double w) { super(color); this.length = l; this.width = w; } @Override double area() { return length * width; } @Override double perimeter() { return 2 * (length + width); } } class Main { public static void main(String[] args) { // Shape s = new Shape("Red"); ← ❌ COMPILE ERROR: cannot instantiate abstract Shape c = new Circle("Blue", 5.0); // ✅ polymorphic reference Shape r = new Rectangle("Red", 4.0, 6.0); // ✅ c.display(); r.display(); } }
| Feature | Abstract Class | Concrete Class |
|---|---|---|
| Instantiation | ❌ Cannot create objects | ✅ Can create objects |
| Abstract methods | Can have (0 or more) | Cannot have |
| Concrete methods | Can have | Can have |
| Constructor | Can have (called via super()) | Can have |
| Subclass obligation | Must implement all abstract methods | No obligation |
- Abstract class CAN have constructors — called indirectly via
super()from subclass. - If a subclass does NOT implement all abstract methods, it must also be declared
abstract. - An abstract class can have zero abstract methods — that's valid (prevents instantiation without forcing override).
- abstract + final is illegal — final prevents subclassing, abstract requires it.
- abstract + static for a method is also illegal.
The Object Class
Every class in Java implicitly extends java.lang.Object. This is the root of the entire Java class hierarchy — every object you ever create is ultimately an instance of Object.
Object is the ultimate superclass of every Java class. If a class does not explicitly extend any class, it automatically extends Object. This means all classes inherit a common set of methods from Object.| Method in Object | Signature | Purpose (ISC Relevant) |
|---|---|---|
toString() | public String toString() | Returns a String representation. Override to give meaningful output. |
equals() | public boolean equals(Object obj) | Checks logical equality. Default: reference equality (same as ==). Override for value equality. |
hashCode() | public int hashCode() | Returns hash code — linked to equals(). Override together. |
getClass() | public final Class getClass() | Returns the runtime class of the object. Cannot override. |
clone() | protected Object clone() | Creates a copy of the object. |
class Point { // implicitly extends Object int x, y; Point(int x, int y) { this.x = x; this.y = y; } @Override public String toString() { // overrides Object.toString() return "(" + x + ", " + y + ")"; } @Override public boolean equals(Object obj) { // overrides Object.equals() if (this == obj) return true; if (!(obj instanceof Point)) return false; Point other = (Point) obj; return this.x == other.x && this.y == other.y; } } class Main { public static void main(String[] args) { Point p1 = new Point(3, 4); Point p2 = new Point(3, 4); System.out.println(p1); // calls toString() → (3, 4) System.out.println(p1.equals(p2)); // → true (value equality) System.out.println(p1 == p2); // → false (reference equality) System.out.println(p1.getClass().getName()); // → "Point" } }
Polymorphism & Dynamic Binding
Polymorphism — "many forms" — is the ability of an object to take on many forms. It is the third pillar of OOP (after Encapsulation and Inheritance). Java supports two types.
🕐 Compile-time Polymorphism
Also called static binding or method overloading. The method call is resolved by the compiler at compile time based on method signatures. Less flexible — binding is fixed before runtime.
Resolved at: COMPILE TIME
⚡ Runtime Polymorphism
Also called dynamic binding or method overriding. The method call is resolved at runtime based on the actual type of the object (not the reference type). This is the heart of OOP power.
Resolved at: RUNTIME
abstract class Shape { abstract void draw(); void describe() { System.out.println("I am a shape."); } } class Circle extends Shape { @Override void draw() { System.out.println("Drawing CIRCLE"); } } class Rectangle extends Shape { @Override void draw() { System.out.println("Drawing RECTANGLE"); } } class Triangle extends Shape { @Override void draw() { System.out.println("Drawing TRIANGLE"); } } class Main { public static void main(String[] args) { // POLYMORPHIC array — Shape ref, different object types Shape[] shapes = { new Circle(), new Rectangle(), new Triangle(), new Circle() }; // Same call → different results based on ACTUAL object type // This is DYNAMIC BINDING in action! for (Shape s : shapes) { s.draw(); // resolved at runtime — which draw() to call? } // OUTPUT: // Drawing CIRCLE // Drawing RECTANGLE // Drawing TRIANGLE // Drawing CIRCLE // Upcasting (implicit) — always safe Shape ref = new Circle(); // Circle IS-A Shape ✅ ref.draw(); // calls Circle.draw() at runtime // Downcasting (explicit) — needs instanceof check if (ref instanceof Circle) { Circle c = (Circle) ref; // safe downcast } } }
The reference type determines what methods you can call (compile-time check). The object type (actual runtime type) determines which version of that method runs. This is the essence of runtime polymorphism.
| Concept | Upcasting | Downcasting |
|---|---|---|
| Direction | Subclass → Superclass reference | Superclass ref → Subclass reference |
| Safety | Always safe (implicit) | May throw ClassCastException |
| Cast keyword | Not needed | Explicit: (SubclassName) |
| Best practice | Used widely in polymorphism | Always use instanceof first |
| Example | Shape s = new Circle(); | Circle c = (Circle) s; |
- Key rule: Method resolution in dynamic binding: reference type for compile-time check; object type for runtime execution.
- Variable access (fields) uses the reference type — NOT the object type. This is the difference between method overriding and variable hiding.
instanceoftests whether an object is an instance of a given class or its subclass.- Polymorphic arrays (
Animal[] arr = {new Dog(), new Cat()}) are a classic ISC exam pattern — study them well. - Dynamic binding only applies to instance methods — NOT static methods, NOT variables.
Interfaces in Java
Interfaces solve a critical design problem: sometimes a class designer wants to enforce a contract (what methods must exist) without dictating how they work. Interfaces are Java's way of achieving this — and also of enabling multiple inheritance.
interface. A class implements an interface using the keyword implements.When creating a reusable Sorter class, the class designer knows how to sort, but doesn't know what criterion to use (which field? ascending or descending?). By using an interface (e.g., Comparable), the designer delegates the comparison logic to the end user of the class — who knows the business rules. This is the defining motivation for interfaces.
+ getArea() : double
// INTERFACE — defines a contract interface Drawable { // All methods are implicitly public abstract void draw(); double getArea(); // All fields are implicitly public static final (constants) double PI = 3.14159; // same as: public static final double PI = 3.14159 } // Another interface interface Resizable { void resize(double factor); } // CLASS implementing ONE interface class Circle implements Drawable { double radius; Circle(double r) { this.radius = r; } @Override public void draw() { System.out.println("Drawing circle with radius " + radius); } @Override public double getArea() { return PI * radius * radius; } } // CLASS implementing MULTIPLE interfaces (solves diamond problem!) class Square implements Drawable, Resizable { double side; Square(double s) { this.side = s; } @Override public void draw() { System.out.println("Drawing square, side " + side); } @Override public double getArea() { return side * side; } @Override public void resize(double factor) { side *= factor; System.out.println("Square resized to side: " + side); } } class Main { public static void main(String[] args) { Drawable[] shapes = { new Circle(5), new Square(4) }; for (Drawable d : shapes) { d.draw(); System.out.println("Area: " + d.getArea()); } Square sq = new Square(3); sq.resize(2.0); // from Resizable sq.draw(); // from Drawable } }
Practical Interface — Comparable (User-defined sorting)
// Interface for comparing — end user defines the logic interface Comparable { int compareTo(Object other); // Returns: negative if this < other, 0 if equal, positive if this > other } // Student class — USER decides: compare by marks class Student implements Comparable { String name; int marks; Student(String n, int m) { name = n; marks = m; } @Override public int compareTo(Object other) { Student s = (Student) other; return this.marks - s.marks; // ascending by marks } } // Reusable Sorter — works for ANY Comparable class! class Sorter { static void bubbleSort(Comparable[] arr) { int n = arr.length; for (int i = 0; i < n-1; i++) { for (int j = 0; j < n-i-1; j++) { if (arr[j].compareTo(arr[j+1]) > 0) { Comparable tmp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = tmp; } } } } } class Main { public static void main(String[] args) { Student[] students = { new Student("Priya", 87), new Student("Aarav", 92), new Student("Rahul", 75) }; Sorter.bubbleSort(students); for (Student s : students) System.out.println(s.name + ": " + s.marks); // Output: Rahul: 75, Priya: 87, Aarav: 92 } }
Interface vs Abstract Class vs Concrete Class
| Feature | Interface | Abstract Class | Concrete Class |
|---|---|---|---|
| Keyword | interface | abstract class | class |
| Instantiation | ❌ | ❌ | ✅ |
| Methods | Abstract only (default: public abstract) | Both abstract + concrete | Concrete only |
| Fields | Constants only (public static final) | Both constants + instance vars | Instance variables |
| Constructor | ❌ None | ✅ Yes | ✅ Yes |
| Multiple inheritance | ✅ A class can implement many | ❌ Only one | ❌ Only one |
| Access modifiers (methods) | Always public | Any modifier | Any modifier |
| Used for | Contract / Capability | Partial implementation | Full implementation |
The word "interface" has two meanings: (1) In everyday English, it means the set of method prototypes of a class — what a class exposes publicly. (2) In Java, interface is a specific language construct that defines a pure abstract type with no implementation. The ISC exam explicitly asks you to distinguish these two — always clarify which you mean.
// Interfaces can extend other interfaces using 'extends' interface Printable { void print(); } interface Saveable extends Printable { // interface extends interface void save(String filename); } class Document implements Saveable { // must implement both print() AND save() String content; Document(String c) { content = c; } @Override public void print() { System.out.println(content); } @Override public void save(String filename) { System.out.println("Saving to: " + filename); } }
- All methods in an interface are implicitly public and abstract — you don't need to write these modifiers (but you can).
- All fields in an interface are implicitly public, static, and final — i.e., constants.
- A class can implement multiple interfaces:
class X implements A, B, C { ... } - An interface can extend multiple interfaces:
interface C extends A, B { } - If a class doesn't implement all interface methods, it must be declared
abstract. - Interfaces cannot have constructors — they are not classes.
- The key motivation: interfaces allow unrelated classes to share a common behaviour contract without forcing them into the same inheritance hierarchy.
⚡ Quick Reference — Revision Card
🎯 Practice Quiz
10 carefully curated questions — covering all ISC exam patterns for this topic.
super() call inside a subclass constructor do?Animal a = new Dog(); When a.sound() is called (method overridden in Dog), which version runs?Object class is AUTOMATICALLY called when an object is used in a System.out.println()?Shape s = new Circle();. Which operation requires an explicit cast?