Advanced Inheritance in Java: Modifiers, Object, Composition

  • Last Updated: September 17, 2025
  • By: javahandson
  • Series
img

Advanced Inheritance in Java: Modifiers, Object, Composition

Explore advanced inheritance in Java, including access modifiers, the Object class hierarchy, preventing inheritance with final, and when to prefer composition over inheritance. Understand common pitfalls, best practices, and make better OOP design decisions with clear examples and FAQs.

Access modifiers and inheritance

Access modifiers control how members of a class, i.e., fields and methods, can be accessed in subclasses.

private modifier

1. Subclasses do not inherit private members.

package com.javahandson;

class Student {
    private String name = "Shweta";
    public String getName() {
        return name; // can be accessed inside the same class
    }
}
class CollegeStudent extends Student {
    void printName() {
         System.out.println(name); // Compile-time error: name has private access
    }
}
public class Main {
    public static void main(String[] args) {
        CollegeStudent collegeStudent = new CollegeStudent();
        System.out.println(collegeStudent.getName());
        collegeStudent.printName();
    }
}
Output: java: name has private access in com.javahandson.Student

2. Private members are accessible only within the same class.

3. Even though private members are not inherited but they exist in the object.

package com.javahandson;

class Student {
    private String name = "Shweta";
    public String getName() {
        return name; // can be accessed inside the same class
    }
}
class CollegeStudent extends Student {
    void printName() {
        System.out.println("Sub class : "+getName()); // Accessed via public method
    }
}
public class Main {
    public static void main(String[] args) {
        CollegeStudent collegeStudent = new CollegeStudent();
        System.out.println("Super class : "+collegeStudent.getName());
        collegeStudent.printName();
    }
}
Output:
Super class : Shweta
Sub class : Shweta

The field ‘name’ is private in Student, so it’s not accessible directly in CollegeStudent. But it still exists in the object, hence we can access it through a public method like #getName(). This shows that private fields are part of the object, just not visible to child classes.

protected modifier

1. Protected members are inherited and accessible in subclasses.

2. They can be accessed directly inside a subclass, even in a different package.

Ex. Accessing a protected member in a subclass of the same package

package com.javahandson;

class Student {
    protected String name = "Shweta";
    public String getName() {
        return name; // can be accessed inside the same class
    }
}
class CollegeStudent extends Student {
    void printName() {
        System.out.println("Sub class directly calls protected member of Superclass : "+name); // accessing protected field from superclass
        System.out.println("Sub class calls public member of Superclass : "+getName()); // Accessed via public method
    }
}
public class Main {
    public static void main(String[] args) {
        CollegeStudent collegeStudent = new CollegeStudent();
        System.out.println("Super class : "+collegeStudent.getName());
        collegeStudent.printName();
    }
}
Output:
Super class : Shweta
Sub class directly calls protected member of Superclass : Shweta
Sub class calls public member of Superclass : Shweta

Ex. Accessing a protected member in a subclass of a different package

package com.javahandson;

public class Student {
    protected String name = "Shweta";
    public String getName() {
        return name; // can be accessed inside the same class
    }
}


package com.diff.pkg;
import com.javahandson.Student;

public class CollegeStudent extends Student {
    public void printName() {
        System.out.println("Different Pkg - Sub class directly calls protected variable of Superclass : "+name); // accessing protected field from superclass
        System.out.println("Different Pkg -  Sub class calls public method of Superclass : "+getName()); // Accessed via public method
    }
}

package com.diff.pkg;

public class Main {
    public static void main(String[] args) {
        CollegeStudent collegeStudent = new CollegeStudent();
        System.out.println("Super class : "+collegeStudent.getName());
        collegeStudent.printName();
    }
}
Output:
Super class : Shweta
Different Pkg - Sub class directly calls protected variable of Superclass : Shweta
Different Pkg -  Sub class calls public method of Superclass : Shweta

‘name’ variable is marked as protected in the Student class (in another package). CollegeStudent extends Student and accesses ‘name’ even though it is in a different package. This is allowed because protected gives access to subclasses, even across packages.

public modifier

1. Public members are inherited and accessible everywhere.

2. There are no restrictions on the inheritance of public members across packages or subclasses.

Ex. Accessing public members in a subclass of a different package

package com.javahandson;

public class Student {
    public String name = "Shweta";
    public String getName() {
        return name; // can be accessed inside the same class
    }
}

package com.diff.pkg;
import com.javahandson.Student;

public class CollegeStudent extends Student {
    public void printName() {
        System.out.println("Different Pkg - Sub class calls public variable of Superclass : "+name); // accessing protected field from superclass
        System.out.println("Different Pkg -  Sub class calls public method of Superclass : "+getName()); // Accessed via public method
    }
}

package com.diff.pkg;

public class Main {
    public static void main(String[] args) {
        CollegeStudent collegeStudent = new CollegeStudent();
        System.out.println("Super class : "+collegeStudent.getName());
        collegeStudent.printName();
    }
}
Output:
Super class : Shweta
Different Pkg - Sub class calls public variable of Superclass : Shweta
Different Pkg -  Sub class calls public method of Superclass : Shweta

Since name and #getName method are public, they are accessible everywhere, including:

  • Outside the package
  • From a subclass
  • Even from non-subclass classes

Default (no modifier, also called package-private)

1. If no access modifier is specified, the member has default (also called package-private) access.

2. The default member is inherited and accessible only within the same package.

3. They are not accessible in subclasses located in a different package.

Ex. Accessing default members in a subclass of the same package

package com.javahandson;

public class Student {
    String name = "Shweta";
    String getName() {
        return name; // can be accessed inside the same class
    }
}

package com.javahandson;

public class CollegeStudent extends Student {
    public void printName() {
        System.out.println("Same Pkg - Sub class calls default variable of Superclass : "+name);
        System.out.println("Same Pkg - Sub class calls default method of Superclass : "+getName());
    }
}

package com.javahandson;

public class Main {
    public static void main(String[] args) {
        CollegeStudent collegeStudent = new CollegeStudent();
        System.out.println("Super class : "+collegeStudent.getName());
        collegeStudent.printName();
    }
}
Output:
Super class : Shweta
Same Pkg - Sub class calls default variable of Superclass : Shweta
Same Pkg -  Sub class calls default method of Superclass : Shweta

Ex. Accessing default members in a subclass of a different package ( Compile-time Error )

package com.javahandson;

public class Student {
    String name = "Shweta";
    String getName() {
        return name; 
    }
}


package com.diff.pkg;
import com.javahandson.Student;

public class CollegeStudent extends Student {
    public void printName() {
        System.out.println("Same Pkg - Sub class calls default variable of Superclass : "+name);  // Compile-time Error 
        System.out.println("Same Pkg -  Sub class calls default method of Superclass : "+getName()); // Compile-time Error 
    }
}

package com.diff.pkg;

public class Main {
    public static void main(String[] args) {
        CollegeStudent collegeStudent = new CollegeStudent();
        collegeStudent.printName();
    }
}
Output:
java: name is not public in com.javahandson.Student; cannot be accessed from outside package
java: cannot find symbol
  symbol:   method getName()
  location: class com.diff.pkg.CollegeStudent

Inheritance hierarchy and Object class

In Java, every class either directly or indirectly extends java.lang.Object, even if we don’t explicitly mention it. This forms the root of the inheritance hierarchy in Java.

class Student {
    // no extends keyword
}

Java treats it as:

class Student extends Object {
    // behind the scenes
}

So the Student class inherits all public and protected methods from the Object class.

Common methods inherited from the Object class

Some of the most commonly used methods that all Java classes inherit are:

MethodModifierDescription
toString()publicReturns string representation of the object
equals()publicCompares objects for equality
hashCode()publicReturns the hash code for the object
getClass()publicReturns a string representation of the object
clone()protectedCreates and returns a copy of the object
finalize()protectedCalled before garbage collection
wait()public finalMakes the current thread wait until another thread calls #notify() or #notifyAll() on the same object.
notify()public finalWakes up one thread waiting on the object’s monitor.
notifyAll()public finalWakes up all threads waiting on the object’s monitor.
package com.javahandson;

class Student {
    String name = "Shweta";
}

public class Main {
    public static void main(String[] args) {
        Student student = new Student();
        System.out.println("toString method inherited: "+student.toString());  // inherited from Object
        System.out.println("equals method inherited: "+student.equals(student));  // inherited from Object
        System.out.println("hashCode method inherited: "+student.hashCode());  // inherited from Object
        System.out.println("getClass method inherited: "+student.getClass());  // inherited from Object
    }
}
Output:
toString method inherited: com.javahandson.Student@6acbcfc0
equals method inherited: true
hashCode method inherited: 1791741888
getClass method inherited: class com.javahandson.Student

Preventing inheritance

We can prevent inheritance by using the final keyword. This can be done at both the class level and method level, depending on what we want to restrict.

final class ( Cannot be extended )

When we declare a class as final, no other class can inherit from it.

package com.javahandson;

final class Student {
    String name = "Shweta";
}

class CollegeStudent extends Student {
}
Output: Compile time error : java: cannot inherit from final com.javahandson.Student

final method ( Cannot be overridden )

If we want to allow inheritance of a class but prevent a specific method from the parent class from being updated in a child class, then declare that method as final.

Ex. Without final method

package com.javahandson;

class Student {
    String name = "Shweta";
    void study() {
        System.out.println(name + "studies");
    }
}

class CollegeStudent extends Student {
    void study() {
        System.out.println("Suraj studies");
    }
}

public class Main {
    public static void main(String[] args) {
        Student student = new CollegeStudent();
        student.study(); // method is overridden
    }
}
Output: Suraj studies

Ex. With the final method

package com.javahandson;

class Student {
    String name = "Shweta";
    final void study() {
        System.out.println(name + "studies");
    }
}

class CollegeStudent extends Student {
    void study() {
        System.out.println("Suraj studies");
    }
}

public class Main {
    public static void main(String[] args) {
        Student student = new CollegeStudent();
        student.study(); // method is overridden
    }
}
Output: Compile time error : java: study() in com.javahandson.CollegeStudent cannot override study() in com.javahandson.Student
  overridden method is final

final variable

When applied to variables, the final keyword does not prevent inheritance, but it prevents reassignment. It means the value of that variable cannot be changed once assigned.

Ex. final variables can be inherited

package com.javahandson;

class Student {
    final String name = "Shweta";
    void study() {
        System.out.println(name + " studies");
    }
}

class CollegeStudent extends Student {
    void study() {
        System.out.println(name + " studies");
    }
}

public class Main {
    public static void main(String[] args) {
        Student student = new CollegeStudent();
        student.study(); // name is inherited and prints name from super class
    }
}
Output: Shweta studies

Ex. final variables cannot be modified

package com.javahandson;

class Student {
    final String name = "Shweta";
    void study() {
        System.out.println(name + " studies");
    }
}

class CollegeStudent extends Student {
    void study() {
        name = "Suraj";
        System.out.println(name + " studies");
    }
}

public class Main {
    public static void main(String[] args) {
        Student student = new CollegeStudent();
        student.study(); // name is inherited and prints name from super class
    }
}
Output: Compile time error java: cannot assign a value to final variable name

So final variables are inherited like regular fields. But they cannot be changed or reassigned in the subclass. They behave like constants – useful for defining fixed values.

When to use Inheritance

1. When there is a clear IS-A relationship – We have to use inheritance only if the subclass is a specific type of the superclass (e.g., Dog IS-A Animal).

2. To promote code reuse – Common fields and methods can be written once in the parent class and reused by child classes.

3. To improve readability and organization – Inheritance helps group related classes together in a logical hierarchy.

4. To enable polymorphism – Subclasses can override parent methods, allowing flexible behavior at runtime.

5. When we want shared behavior with minor changes – If most of the behavior is common but a few methods differ, inheritance helps avoid duplication.

Example ( correct use ) :

Car IS-A Vehicle

Teacher IS-A Person

Dog IS-A Animal

Incorrect use Example:

Creating an inheritance like:

class Engine extends Driver { }  // Wrong – Engine is not a Driver

Disadvantages of Inheritance

1. Tight Coupling – Subclasses become tightly dependent on parent classes, making changes risky and error-prone.

2. Fragile Hierarchies – A small change in the parent class can unintentionally break the behavior of all its subclasses.

3. Reduced Flexibility – We’re locked into a rigid class structure, making it hard to adapt or extend without affecting others.

4. Overuse for Code Reuse – Developers often misuse inheritance just to reuse code, even when there’s no IS-A relationship.

5. Difficulty in Maintenance and Testing – Deep inheritance chains are harder to understand, debug, and unit test effectively.

Inheritance vs Composition

When designing object-oriented systems, we developers often face a choice between inheritance and composition. Both are techniques for reusing code, but they have different purposes and trade-offs.

Inheritance allows one class (child/subclass) to inherit fields and methods from another class (parent/superclass). This creates a hierarchical relationship between classes.

package com.javahandson;

class Student {
    String name;
    void study() {
        System.out.println("Student studies");
    }
}

class SchoolStudent extends Student {
    void getLunchBox() {
        System.out.println("School student gets the lunch box");
    }
}

Here, SchoolStudent IS-A Student. It inherits the study() method and adds its own behavior.

So we should use inheritance when there is a clear IS-A relationship and when behavior is mostly shared across child classes.

Composition means building complex types by combining objects of other types. Instead of inheriting behavior, we delegate it to other objects.

class Engine {
    void start() {
        System.out.println("Engine started.");
    }
}

class Car {
    private Engine engine = new Engine(); // HAS-A relationship
    void drive() {
        engine.start(); // Delegation
        System.out.println("Car is moving.");
    }
}

Here, Car HAS-A Engine. It uses Engine internally, but doesn’t inherit from it.

So we should use composition in below cases:

1. When there is no logical IS-A relationship.

2. When we want flexibility to change behavior at runtime.

3. When we want to avoid the tight coupling of inheritance.

4. To promote modular, reusable, and testable code.

Best Practice is to “Favor composition over inheritance”. It encourages developers to prefer flexible and loosely-coupled designs, unless a strong IS-A relationship truly exists.

Conclusion

Inheritance is a fundamental concept in object-oriented programming that allows one class to reuse the fields and methods of another. It helps create a natural class hierarchy, promotes code reuse, and simplifies application design by modeling real-world IS-A relationships. However, while inheritance improves maintainability and organization, it should be used thoughtfully — only when a clear IS-A relationship exists. Overusing inheritance can lead to tight coupling and reduced flexibility. In cases where flexibility and modularity are more important, composition is often the better choice.

In short, inheritance is powerful – but like all tools in software design, it works best when used with care and clarity.

FAQ’s

What is inheritance in Java?

Inheritance in Java is a mechanism where one class acquires the properties and behaviors (fields and methods) of another class using the extends keyword.

What are the types of inheritance in Java?

Java supports single, multilevel, and hierarchical inheritance with classes. Multiple inheritance is not supported with classes but allowed via interfaces.

What is the use of the super keyword in Java inheritance?

The super keyword is used to access the parent class’s methods, variables, or constructors from a subclass.

Can private members be inherited in Java?

Private members are not accessible in subclasses directly, but they do exist in the object and can be accessed through public or protected methods.

What is the difference between inheritance and composition?

Inheritance creates an IS-A relationship (subclass extends superclass), while composition creates a HAS-A relationship (one class contains another class).

Leave a Comment