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
33: Abstract Classes
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 brandfield 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:
WashingMachineandRefrigerator. - 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.
There are no comments for now.