Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
28: Inheritance and super
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
Employeethat handles thenameandemployeeIdfields via a constructor. - Make
FullTimeEmployeeandContractorextendEmployee. - In the
Employeebase class, implement a methodprintDetails()that prints the name and ID. - In the
FullTimeEmployeeclass, overrideprintDetails(). Usesuper.printDetails()to print the basic info, and then add a line that prints theirannualSalary. - In the
Contractorclass, overrideprintDetails(). Usesuper.printDetails()and then add a line that prints theirhourlyRate.
Ensure that both child classes properly call the Employee constructor using super() to avoid compilation errors.
There are no comments for now.