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

I've seen this happen a lot when developers start building systems that handle different "types" of the same thing. You create a base class to share some code, but you forget that the base class itself doesn't actually represent a real-world object.

The Problem: Allowing "Generic" Objects

class Payment {
    double amount;

    Payment(double amount) {
        this.amount = amount;
    }

    void processPayment() {
        // I'm not sure how to process a "generic" payment, 
        // so I'll just leave this empty or print a dummy message.
        System.out.println("Processing a generic payment of " + amount);
    }
}

class CreditCardPayment extends Payment {
    CreditCardPayment(double amount) { super(amount); }
    
    @Override
    void processPayment() {
        System.out.println("Charging $" + amount + " to Credit Card.");
    }
}

public class Main {
    public static void main(String[] args) {
        Payment p1 = new CreditCardPayment(100.0);
        p1.processPayment(); // Works great.

        // Here is the bug:
        Payment p2 = new Payment(50.0); 
        p2.processPayment(); // This shouldn't even be possible!
    }
}

In the code above, the logic is broken. In a real payment system, a "Payment" isn't a thing—it's a concept. You can have a Credit Card payment, a PayPal payment, or a Bank Transfer, but you can't just have a "Payment." By leaving the Payment class as a standard class, I've left the door open for another developer (or my future self) to instantiate a Payment object that does absolutely nothing useful.

The Fix: Enforcing a Contract with Abstract Classes

To fix this, we use the abstract keyword. When you mark a class as abstract, you're telling the Java compiler: "This class is incomplete. Never let anyone create an instance of it directly." We can also mark the processPayment method as abstract, which removes the method body and forces every subclass to provide its own specific implementation.

abstract class Payment {
    double amount;

    Payment(double amount) {
        this.amount = amount;
    }

    // No body here! Subclasses MUST implement this.
    abstract void processPayment();

    // Abstract classes can still have regular methods.
    void printReceipt() {
        System.out.println("Receipt for amount: $" + amount);
    }
}

class CreditCardPayment extends Payment {
    CreditCardPayment(double amount) { super(amount); }
    
    @Override
    void processPayment() {
        System.out.println("Charging $" + amount + " to Credit Card.");
    }
}

public class Main {
    public static void main(String[] args) {
        Payment p1 = new CreditCardPayment(100.0);
        p1.processPayment(); 

        // This line now causes a COMPILE-TIME ERROR:
        // Payment p2 = new Payment(50.0); 
    }
}

Now, the code is robust. If I try to call new Payment(), the compiler stops me immediately. More importantly, if I create a new PayPalPayment class but forget to write the processPayment method, Java will refuse to compile the code. It's a way of creating a strict contract for anyone extending your class.

Concrete Methods in Abstract Classes

You might be wondering why we wouldn't just use an interface. The key difference is that abstract classes can hold state (like the amount field) and concrete methods (like printReceipt()).

I use abstract classes when the subclasses share a common identity and common logic. If printReceipt() works exactly the same way for every single payment type, I write it once in the abstract class. This keeps my code DRY (Don't Repeat Yourself), while still ensuring that the unique parts—like the actual processing logic—are handled by the specific subclasses.




📋 Practical Task

Building a Smart Home Appliance Controller

You are designing a system to control various smart home appliances. Not all appliances work the same way, but they all share some basic properties.

  • Create an abstract class called Appliance.
  • Give it a String brand field and a constructor to initialize it.
  • Add a concrete method called plugIn() that prints: "[Brand] is now connected to power."
  • Add an abstract method called turnOn() (no body).
  • Create two subclasses: WashingMachine and Refrigerator.
  • Each subclass must implement the turnOn() method with a unique message (e.g., "Washing machine is starting the spin cycle...").

Testing your code: In your main method, ensure that you can create a list of Appliance objects containing both a WashingMachine and a Refrigerator, and call plugIn() and turnOn() for each. Verify that trying to instantiate the Appliance class directly results in a compiler error.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.