Advanced Inheritance in Java: Modifiers, Object, Composition
-
Last Updated: September 17, 2025
-
By: javahandson
-
Series
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 control how members of a class, i.e., fields and methods, can be accessed in subclasses.
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.
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.
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:
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
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.
Some of the most commonly used methods that all Java classes inherit are:
| Method | Modifier | Description |
| toString() | public | Returns string representation of the object |
| equals() | public | Compares objects for equality |
| hashCode() | public | Returns the hash code for the object |
| getClass() | public | Returns a string representation of the object |
| clone() | protected | Creates and returns a copy of the object |
| finalize() | protected | Called before garbage collection |
| wait() | public final | Makes the current thread wait until another thread calls #notify() or #notifyAll() on the same object. |
| notify() | public final | Wakes up one thread waiting on the object’s monitor. |
| notifyAll() | public final | Wakes 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
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.
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
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
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.
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
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.
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.
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.
Inheritance in Java is a mechanism where one class acquires the properties and behaviors (fields and methods) of another class using the extends keyword.
Java supports single, multilevel, and hierarchical inheritance with classes. Multiple inheritance is not supported with classes but allowed via interfaces.
The super keyword is used to access the parent class’s methods, variables, or constructors from a subclass.
Private members are not accessible in subclasses directly, but they do exist in the object and can be accessed through public or protected methods.
Inheritance creates an IS-A relationship (subclass extends superclass), while composition creates a HAS-A relationship (one class contains another class).