JVM / Java / OOP / Inheritance
"Prefer composition/interfaces over inheritance" is a good advice most of the time.
But inheritance is still the right tool when there's a true "is-a" relationship + shared state + shared base behavior.
1. Payment Processing System
We can have different payment methods:
- Credit card
- PayPal
- Bank transfer
They all:
- Have an amout
- Have a common processing flow
- But differ in how the payment is executed
package com.minte9.oop.inheritance;
public class PaymentApp {
public static void main(String[] args) {
Payment p1 = new CreditCardPayment(100);
Payment p2 = new PayPalPayment(200);
p1.processPayment();
p2.processPayment();
}
}
abstract class Payment {
protected double amount;
public Payment(double amount) {
this.amount = amount;
}
public void processPayment() {
validate();
execute();
sendReceipt();
}
public void validate() {
System.out.println("Validating payment of $" + amount);
}
protected abstract void execute();
public void sendReceipt() {
System.out.println("Sending receipt...\n");
}
}
class CreditCardPayment extends Payment {
public CreditCardPayment(double amount) {
super(amount);
}
@Override
protected void execute() {
System.out.println("Processing credit card payment...");
}
}
class PayPalPayment extends Payment {
public PayPalPayment(double amount) {
super(amount);
}
@Override
protected void execute() {
System.out.println("Processing PayPal payment...");
}
}
2. Why inheritance is the right choise here?
a) Strong "is-a" relationship:
- CreditCardPayment IS-a payment
- PayPalPayment IS-a payment
b) Shared logic that should NOT be duplicated.
You don't want every class reimplementing this.
processPayment()
c) Sublcasses only customize:
execute()
3. Why not use interfaces?
You could do:
interface Payment() {
void process();
}
But then:
- You lose shared state (amount)
- You duplicate workflow logic
- You can't enforce the sequence (validate -> execute -> receipt)
4. Rule of thumb
Use inheritance when:
- There is a true hierachy (is-a)
- You need shared state
- You want to enforce a common algorithm/flow
- Subclasses only tweak specific steps
Use interfaces when:
- You want capabilities/roles (Serializable, Runnable)
- Behavior varies wildly
- No shared implementation is needed
5. Payment system (done wrong)
A developer might try to be "modern" and do everything with interfaces.
package com.minte9.oop.inheritance.wrong_implementation;
public class PaymentWrongApp {
public static void main(String[] args) {
Payment p1 = new CreditCardPayment(100);
Payment p2 = new PayPalPayment(200);
p1.processPayment();
p2.processPayment();
}
}
interface Payment {
void processPayment();
}
class CreditCardPayment implements Payment {
private double amount;
public CreditCardPayment(double amount) {
this.amount = amount;
}
@Override
public void processPayment() {
System.out.println("Validating $" + amount);
System.out.println("Processing credit card...");
System.out.println("Sending receipt...\n");
}
}
class PayPalPayment implements Payment {
private double amount;
public PayPalPayment(double amount) {
this.amount = amount;
}
@Override
public void processPayment() {
System.out.println("Validating $" + amount);
System.out.println("Processing PayPal...");
System.out.println("Sending receipt...\n");
}
}
What's wrong here?
a) Massive duplication, every class repeats:
System.out.println("Validating $" + amount);
System.out.println("Sending receipt...\n");
If you change validation logic, you must update every class.
b) No shared state
Every class defines amount, but that's cleary common.
c) No control over workflow
Nothing enforces:
validate -> execute -> sendRecipt
A developer could accidentally do:
sendReceipt();
execute();
6. Abstract Keyword
An abstract class CANNOT be instantiated directly.
What exactly is a Payment?
- Is it a credit card payment? A PayPal payment? A bank transfer?
- We don't know, so Payment should be abstract.
An abstract class is meant to be extended by subclasses.
An abstract method defines behavior that subclasses must implement.
package com.minte9.oop.inheritance.abstract_note;
public class AbstractKeyword {
public static void main(String[] args) {
CreditCardPayment payment = new CreditCardPayment();
payment.setAmount(250);
payment.processPayment();
}
}
abstract class Payment {
protected double amount;
public abstract void processPayment();
public void setAmount(double amount) {
this.amount = amount;
}
}
class CreditCardPayment extends Payment {
@Override
public void processPayment() {
System.out.println("Processing credit card payment of $" + amount);
}
}
7. Override Annotation
The @Override annotation acts as a compile-time safeguard.
It tells the compiler:
This method must override a method from the parent class.
Without @Override, a typo creates a new method instead of overriding,
and the error may go unnoticed until runtime behavior is wrong.
package com.minte9.oop.inheritance.override_note;
public class OverrideAnnotation {
public static void main(String[] args) {
CreditCardPayment payment = new CreditCardPayment();
payment.processPayment();
}
}
abstract class Payment {
public abstract void processPayment();
}
class CreditCardPayment extends Payment {
public void processsPayment() {
System.out.println("Processing credit card payment...");
}
@Override
public void processPayment() {
System.out.println("Wrong method called!");
}
}