Back to Blog
Java

Java Parent Class Child Class Inheritance Explained

java parent class child class: Understand Java parent class child class inheritance: syntax, method overriding, constructors, and runtime behavior with practical, main...

Java inheritanceextends keywordmethod overridingsuper keywordpolymorphism
Diagram showing a parent class and child class in Java with an arrow indicating inheritance, illustrating the extends relationship.

java parent class child class requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

In Java, the relationship between a parent class and a child class is defined using the extends keyword. When you write class Child extends Parent, the child class inherits fields and methods from the parent, and can override or extend them. This is the core of single inheritance in Java, and it directly affects how you design object hierarchies, reuse code, and manage polymorphic behavior.

Core Syntax and Minimal Example

The simplest form of a parent-child relationship requires a parent class, a child class that extends it, and an instantiation that shows the inheritance in action.

class Animal { void eat() { System.out.println("Animal eats"); } } class Dog extends Animal { void bark() { System.out.println("Dog barks"); } } public class Main { public static void main(String[] args) { Dog d = new Dog(); d.eat(); // inherited from Animal d.bark(); // defined in Dog } }

Here, Dog is the child class and Animal is the parent. The child inherits eat(), so you can call it on a Dog instance. This is the expected behavior: the child has all accessible members of the parent, plus its own additions. The child can also change the behavior of inherited methods through overriding, which we'll cover next.

Method Overriding Rules

Overriding is the process of redefining a parent method in the child class with the same signature. The child method must have the same name, parameter list, and return type (or a covariant return type). The access modifier cannot be more restrictive than the parent's.

class Animal { void makeSound() { System.out.println("Generic animal sound"); } } class Cat extends Animal { @Override void makeSound() { System.out.println("Meow"); } }

Without the @Override annotation, the code compiles, but if you misspell the method name, you'll unintentionally create a new method rather than override the parent's. Adding @Override forces the compiler to check that a parent method with the same signature exists. This is a compile-time safety net, so always use it when you intend to override.

Overriding matters because it enables runtime polymorphism. When you hold a reference of the parent type but instantiate a child, the overridden method in the child executes:

Animal a = new Cat(); a.makeSound(); // prints "Meow"

This is the basis for code that works with the parent type but exhibits child-specific behavior. The JVM decides which method to invoke at runtime based on the actual object type, not the reference type.

Constructors and the super Keyword

A child class does not inherit constructors from the parent. The child's constructor must call a parent constructor explicitly, or the compiler inserts a call to the no-argument parent constructor. If the parent has no no-argument constructor, the child must call a specific constructor using super(...).

class Vehicle { String name; Vehicle(String name) { this.name = name; } } class Car extends Vehicle { Car(String name) { super(name); // required because parent has no default constructor } }

If you omit the super(name) call, the compiler attempts to call Vehicle() which does not exist, resulting in a compilation error. The super keyword can also be used to call an overridden parent method from within the child:

class ElectricCar extends Car { ElectricCar(String name) { super(name); } void display() { super.display(); // if parent had such a method } }

Using super is essential when a child override needs to extend the parent's behavior rather than replace it entirely. For example, a child's method can call the parent's version and then add extra logic. This keeps the shared logic in the parent and reduces duplication.

Access Modifiers and Visibility

The private members of a parent class are not inherited and cannot be accessed directly from the child. They exist in the parent's object, but the child only sees them through public or protected methods. The protected modifier allows direct access from child classes, even if they are in a different package. public members are accessible everywhere.

class Parent { private int secret = 42; protected int protectedValue = 10; protected void helper() { System.out.println(secret); } } class Child extends Parent { void show() { // System.out.println(secret); // error: secret is private System.out.println(protectedValue); // OK helper(); // OK } }

The reasoning is that private data is meant to be internal to the parent. Exposing it to children would break encapsulation, because the parent cannot control how the child modifies its internal state. Using protected gives you a controlled way to share implementation details with subclasses.

When designing a class to be extended, think about which fields and methods should be protected versus private. Overusing protected can lead to tight coupling, where a child class depends on internal details of the parent, making future changes to the parent risky. On the other hand, making everything private forces children to go through public APIs, which is more rigid but safer.

Inheritance and Runtime Type Behavior

Java uses dynamic dispatch for method calls. When you call an overridden method on a parent reference, the JVM looks up the actual class of the object and invokes the override. This is different from static methods, which are resolved at compile time based on the reference type.

The instanceof operator helps you check the actual type at runtime before casting:

Animal a = new Dog(); if (a instanceof Dog) { Dog d = (Dog) a; d.bark(); }

Without such a check, casting a parent reference to an unrelated child type throws ClassCastException. The rule is that you can cast a parent reference to a child type only if the actual object is that child type or one of its descendants. This is a runtime check, so use it consciously.

Inheritance also affects equality and identity. The equals() method, if not overridden, uses reference equality. If you want value equality across a class hierarchy, you must override equals() and hashCode() consistently, and decide how to handle cases where a parent object is compared with a child object. Typically, you define equality in the parent, and children call super.equals() to validate the parent fields.

Avoiding Fragile Base Class Problems

The main risk with deep inheritance hierarchies is the fragile base class problem. Changes to a parent class can have unintended effects on all subclasses, even those you do not directly control. For example, adding a new method to a parent that is not final might unintentionally shadow a similar method in a child, altering behavior in unexpected ways.

To mitigate this:

  • Prefer composition over inheritance when the relationship is not a true "is-a" relationship. For example, a Car is a Vehicle, but a Car does not "have" an Engine via inheritance; instead, it holds an Engine reference.
  • Mark methods that should not be overridden as final. If a method's behavior is critical for the class's invariants, making it final prevents subclasses from breaking it.
  • Keep the parent class small and focused. A parent with too many responsibilities is hard to extend without breaking things.
  • Use abstract classes or interfaces to define contracts, rather than forcing implementation details down the hierarchy.

A concrete symptom of the fragile base class problem appears when a parent class adds a new method that collides with an existing method in a child that was not intended to override. The child may have a method with the same signature but different semantics, and after the parent update, calls to that method on a parent reference may dispatch to the child's implementation, which might not fit the parent's expectations. This is why you should design parents with care, or use final on methods you do not want changed.

When to Favor Composition Over Inheritance

Inheritance is useful when you have a clear hierarchical relationship and the child genuinely is a more specific version of the parent. But it comes with coupling: the child is tightly bound to the parent's implementation. Composition, where a class holds a reference to another class instead of extending it, offers looser coupling and more flexibility.

class Engine { void start() { System.out.println("Engine starts"); } } class Car { private Engine engine; Car(Engine engine) { this.engine = engine; } void start() { engine.start(); } }

Here, Car does not inherit from Engine; it delegates to an Engine instance. This is composition. It allows you to swap the engine implementation without changing the Car class, as long as the new engine has the same interface. Inheritance, in contrast, would require you to change the parent or create a new child class.

Use inheritance when:

  • The child class is a true subtype of the parent, and you want to reuse code from the parent.
  • You need polymorphic behavior where a parent reference can point to multiple child types.
  • The parent's methods are designed to be overridden, and the relationship is stable.

Use composition when:

  • The relationship is "has-a" rather than "is-a".
  • You want to change behavior at runtime by swapping components.
  • The parent class is large, complex, or likely to change.
  • You need to avoid the fragility of an inheritance chain.

A practical rule is: if you are not sure, start with composition. It is easier to convert to inheritance later than the reverse. Inheritance adds compile-time coupling that can become entrenched across many classes.

Inheritance and Object Creation Costs

Inheritance has a subtle runtime cost in terms of memory for the child object. Each child object contains all fields from its ancestor classes, plus its own. If you have a deep hierarchy, a single object may carry a lot of inherited state that is never used. For example, a Dog object includes all fields from Animal even if Animal has fields that are irrelevant to Dog.

This matters in memory-constrained environments or when you create many objects. The overhead is usually minor for typical applications, but it becomes noticeable when you have many levels of hierarchy and large parent classes. Virtual method calls also have a slightly higher cost than non-virtual calls because of dynamic dispatch, but modern JVMs optimize this with inlining, so it is rarely a bottleneck.

Do not design your class hierarchy around micro-optimizations. Instead, be aware that every inherited field occupies space in the child object. If a parent class has many fields that are only relevant to a few subclasses, consider moving those fields down into the subclasses that need them. This reduces memory for other subclasses.

Practical Example: Extending a Parent for New Behavior

Let's put these ideas together in a realistic scenario. Suppose you have a PaymentProcessor parent class that handles common steps like validation and logging, and you want to create a CreditCardPaymentProcessor child that adds credit card specific logic.

abstract class PaymentProcessor { protected void log(String message) { System.out.println("Payment log: " + message); } abstract void process(double amount); } class CreditCardPaymentProcessor extends PaymentProcessor { @Override void process(double amount) { validate(amount); log("Processing credit card payment of " + amount); chargeCard(amount); } private void validate(double amount) { if (amount <= 0) { throw new IllegalArgumentException("Amount must be positive"); } } private void chargeCard(double amount) { // Imagine a call to a payment gateway API. System.out.println("Charged " + amount + " to credit card"); } }

The parent defines a log method that the child can reuse. The process method is abstract, forcing each child to implement it. The child adds its own validate and chargeCard methods. This separation keeps shared logging in the parent and payment-specific logic in the child, making each class easier to test and maintain.

Notice that the log method is protected, so the child can call it, but external code cannot. This is an example of the controlled sharing we discussed earlier.

Inheriting From Abstract vs. Concrete Classes

An abstract class cannot be instantiated directly; it exists to provide a common base for subclasses. It can contain concrete methods that are inherited as is, and abstract methods that the child must implement. This is useful when you have shared implementation details and a common contract.

abstract class Shape { abstract double area(); void describe() { System.out.println("Area: " + area()); } } class Circle extends Shape { private double radius; Circle(double radius) { this.radius = radius; } @Override double area() { return Math.PI * radius * radius; } }

A concrete class, on the other hand, can be instantiated as is. You can extend a concrete class, but the parent's methods are invoked unless overridden. This is fine, but you must be careful that the parent's methods are designed to be overridden. If they are not, overriding them can break the parent's internal expectations.

A general principle: prefer abstract classes over concrete classes when you expect subclasses to provide specific implementations. Abstract classes title the developer to focus on the contract rather than being forced to inherit unwanted concrete behavior.

Designing for Maintainability

When you design a class to be a parent, you are making a long-term commitment. Other developers may subclass it, and you cannot foresee every future need. To make it maintainable:

  • Document which methods are intended to be overridden and which are not.
  • Keep the parent class free of implementation details that are likely to change.
  • Use final for methods that should not be overridden, to restrict the extension points.
  • Prefer protected access over public for methods that are meant for subclasses only.
  • Test the parent class with several representative child classes to catch surprising interactions.

An example of a design flaw is a parent class that has a public method that calls an overridable method internally. If a child overrides that internal method, the behavior of the public method changes. This can be intentional, but if it is not, it leads to confusing bugs. A safer pattern is to make the internal method final or private, so the child cannot interfere unless the parent explicitly intends it.

Where Inheritance Usually Breaks

A common failure is using inheritance to reuse a method that is not logically a "is-a" relationship. For example, creating a Stack class that extends ArrayList is often flawed because a stack should not expose list methods like add(int, Object). The better design is to compose a Stack with a private ArrayList and expose only stack-specific methods.

Another failure is overridden methods that behave unexpectedly due to changes in the parent's contract. For example, if the parent's equals() method checks getClass(), then a child that adds fields will never be equal to a parent object, which may or may not be desirable. You must carefully decide the equality contract for a hierarchy.

These issues are not about syntax but about design. The Java language permits many things that are not wise. The decision to use inheritance should be made with a clear understanding of the tradeoffs in coupling, maintainability, and runtime behavior.

java parent class child class: Practical Usage and Code Exam | RYUSLOG DEV