Skip to Content
Course content

28: Inheritance and super

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this happen a dozen times with junior devs: you're building a few different classes that feel very similar, and your first instinct is to just copy and paste the common fields and methods from one to the other. It feels fast in the moment, but it's a trap. Let's look at why this fails and how we actually handle it in Java using inheritance and the super keyword.

The temptation to copy-paste

Imagine we're building a payment processing system. You start with a CreditCardPayment class that has an amount, a transactionId, and a processPayment() method. Then, you're asked to add PayPalPayment. You realize it also needs an amount, a transactionId, and a processPayment() method, but it also needs a paypalEmail field.

// The naive approach: duplication
class CreditCardPayment {
    double amount;
    String transactionId;

    void processPayment() {
        System.out.println("Processing credit card payment of " + amount);
    }
}

class PayPalPayment {
    double amount;
    String transactionId;
    String paypalEmail;

    void processPayment() {
        System.out.println("Processing PayPal payment of " + amount + " for " + paypalEmail);
    }
}

This works fine for two classes. But what happens when your boss asks you to add a timestamp to every single payment type in the system? Or a validate() method? Suddenly, you're hunting through five different files, updating the same logic over and over. If you miss one, you've just introduced a bug that will be a nightmare to track down in production.

Consolidating the common ground

The professional way to handle this is to identify the "is-a" relationship. A PayPalPayment is a Payment. By creating a base class, we define the shared state and behavior once. This is where extends comes in. We move the amount and transactionId up into a parent class called Payment.

But here is where people usually get tripped up: constructors. The child class doesn't automatically "inherit" the parent's constructor logic in a way that lets you skip the parent's initialization. If the Payment class requires a transactionId to exist, the child class must ensure that the parent gets that ID. This is where super() enters the picture.

class Payment {
    double amount;
    String transactionId;

    Payment(double amount, String transactionId) {
        this.amount = amount;
        this.transactionId = transactionId;
    }

    void processPayment() {
        System.out.println("Validating transaction " + transactionId + "...");
    }
}

class PayPalPayment extends Payment {
    String paypalEmail;

    PayPalPayment(double amount, String transactionId, String paypalEmail) {
        super(amount, transactionId); // This calls the Payment constructor
        this.paypalEmail = paypalEmail;
    }
}

That super(amount, transactionId) call is non-negotiable if the parent doesn't have a default, no-argument constructor. You're essentially telling Java: "Before you do anything specific to a PayPal payment, go run the setup logic that applies to all payments."

Talking back to the parent class

Sometimes, you don't want to completely replace a method from the parent class; you just want to add to it. This is called overriding. In our PayPalPayment class, we might want to keep the validation logic from the Payment class but add a specific print statement for PayPal.

If you just redefine processPayment(), you wipe out whatever the parent was doing. To prevent that, you use super.methodName(). I use this constantly when I want to wrap existing functionality with a bit of extra specialized logic.

class PayPalPayment extends Payment {
    // ... constructor as above ...

    @Override
    void processPayment() {
        super.processPayment(); // Run the base validation first
        System.out.println("Redirecting to PayPal API for " + paypalEmail);
    }
}

Now, when you call processPayment() on a PayPal object, it does the general validation (from the parent) AND the PayPal-specific redirection. It's clean, it's DRY (Don't Repeat Yourself), and it's easy to maintain.

When inheritance becomes a burden

A quick word of caution: don't overdo it. It's tempting to create deep hierarchies—like Payment $\rightarrow$ DigitalPayment $\rightarrow$ WalletPayment $\rightarrow$ ApplePayPayment. I've seen codebases where changing one line in the base class broke twenty different child classes in ways the original developer never anticipated. This is known as the "fragile base class" problem.

Use inheritance when there is a genuine "is-a" relationship and shared behavior. If you're only inheriting because you want to reuse one single method, you're probably better off using composition (making the other class a field inside your current class) instead.




📋 Practical Task

Refactoring the Corporate Payroll Hierarchy

You have been handed a messy piece of code for a payroll system. Currently, the FullTimeEmployee and Contractor classes are completely separate, despite both having a name, an employeeId, and a calculatePay() method.

Your Task:

  • Create a base class called Employee that handles the name and employeeId fields via a constructor.
  • Make FullTimeEmployee and Contractor extend Employee.
  • In the Employee base class, implement a method printDetails() that prints the name and ID.
  • In the FullTimeEmployee class, override printDetails(). Use super.printDetails() to print the basic info, and then add a line that prints their annualSalary.
  • In the Contractor class, override printDetails(). Use super.printDetails() and then add a line that prints their hourlyRate.

Ensure that both child classes properly call the Employee constructor using super() to avoid compilation errors.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.