Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 9

Abstract Classes: Defining Abstract and Concrete Methods

Recommended resources

Hello! Welcome to the second module of our course. In the previous module, we established the core principles of OOP, culminating in a deep dive into polymorphism. You saw how upcasting allows you to treat specialized objects like EmailService and SMSService through a general NotificationService reference, enabling flexible and powerful code.

However, you might have noticed a conceptual gap in our NotificationService example. We could create an instance of NotificationService itself (new NotificationService()), but what does it mean to send a "generic" notification without a specific delivery method? It's a bit too abstract to be a concrete object.

This lesson introduces the perfect solution to this problem: abstract classes. We'll explore how to define a class that serves as a blueprint for its children—a class that cannot be instantiated on its own but can provide a mix of shared functionality and required behaviors. This is a fundamental concept for building robust and scalable backend systems.

What is an Abstract Class and Why Use It?

An abstract class is a restricted class that cannot be used to create objects. Its purpose is to serve as a base for subclasses, providing a common structure and potentially some shared implementation. Think of it as a template that other, more specific classes will fill in.

The primary motivations for using an abstract class are:

  1. To prevent instantiation of generic concepts: You can't create an object of a class that is too abstract to exist on its own (like a generic "Animal" or "Shape").
  2. To enforce a contract on subclasses: You can define methods that every subclass must implement, ensuring they all share a common interface.
  3. To share common code: You can still write regular, implemented methods that all subclasses inherit, avoiding code duplication.

This short video from "Coding with John" provides an excellent, clear introduction to the "what" and "why" of abstract classes.

Abstract Classes and Methods in Java Explained in 7 Minutes

This video explains the core concept of an abstract class and its primary use case in just a few minutes.

First, watch the segment on what an abstract class is. Note the abstract keyword and the fact that it cannot be instantiated. Next, watch the explanation for why they are useful. The Animal example is a classic and effective way to understand the motivation.

As the video illustrates, by declaring the Animal class abstract, you ensure that no one can create a generic Animal object. Instead, they are forced to create specific, concrete instances like Cat or Dog, which makes logical sense and leads to more robust code.


The Two Flavors of Methods: Abstract and Concrete

The real power of an abstract class comes from its ability to contain two types of methods:

  • Abstract Methods: These are method declarations without a body (no curly braces {}). They are marked with the abstract keyword and act as a contract. Any concrete (non-abstract) class that extends the abstract class must provide an implementation for these methods.
  • Concrete Methods: These are regular methods with a full implementation. They are inherited by all subclasses and provide shared functionality, promoting code reuse.

Let's continue with the "Coding with John" video to see this distinction in action.

Abstract Classes and Methods in Java Explained in 7 Minutes

This part of the video clearly demonstrates how abstract and concrete methods coexist within an abstract class.

Watch the segment on abstract methods. Pay close attention to how makeNoise() is declared without a body and how the Cat class is forced to implement it. Then, see how concrete methods like printName() are also included, providing functionality that all subclasses can use directly.

This hybrid nature is what makes abstract classes so useful. You can define a common structure and enforce required behaviors with abstract methods, while simultaneously providing shared, reusable code with concrete methods.

The following diagram illustrates this perfectly with a Shape class.

This diagram shows an abstract `Shape` class. It contains an abstract `area()` method, which has no implementation and must be defined by subclasses like `Circle` and `Rectangle`. It also contains a concrete `getColor()` method, which has an implementation and is simply inherited.

Syntax and a Code Walkthrough

Now let's ground these concepts in actual Java code. The syntax is straightforward: you use the abstract keyword for both the class and its abstract methods. A critical rule to remember is: if a class contains even one abstract method, the class itself must be declared abstract.

The following article from GeeksforGeeks provides a concise summary and a clear, complete code example.

Abstract Class in Java

This article presents the formal definition, syntax, and a working example of an abstract class in Java.

Please read the following sections to see how these concepts are translated into code: Begin with the introduction to understand the definition and its purpose. Study the full code example. Observe the Shape class with its constructor, abstract area() method, and concrete getColor() method. See how the Circle subclass extends Shape and implements area(). Review the syntax summary for a quick reference. Finally, read the crucial rule under "Observation 8": if a class has an abstract method, it must be abstract.

From this example, notice a few key things:

  • The Shape class has a constructor. Even though you can't instantiate Shape directly, its constructor is called via super() when you create an instance of a subclass (Circle). This is used to initialize common fields like color.
  • The main method creates a Circle object but stores it in a Shape reference: Shape s = new Circle(...). This is upcasting, which you learned about in the previous lesson. It works seamlessly with abstract classes.
  • You can then call both the concrete method (s.getColor()) and the overridden abstract method (s.area()) on this reference, demonstrating polymorphism.

A Practical Case Study: Building a Workout Logger

To see how abstract classes are applied in a larger, more practical context, let's consider the design of a workout logging application. This scenario is perfect for an abstract class because different types of exercises (barbell, dumbbell, timed) share common properties but need to be displayed differently.

This article from Smashing Magazine walks through this exact problem.

Mastering OOP: A Practical Guide To Inheritance, Interfaces, And Abstract Classes — Smashing Magazine

This article provides a real-world case study, showing how an abstract class can be used to model different types of exercises in an application.

First, read the section that sets up the problem, from "Workout Log Case Study". This explains the need for a flexible design. Next, examine the abstract Exercise class. Notice how it stores common data (fields) and contains both a concrete method (getFormattedOutput) and an abstract one (getSetData). Finally, look at how the concrete subclasses are implemented, from BarbellExercise to BodyweightExercise. See how each one provides its own specific implementation for getSetData.

The design in this article is a great example of the Template Method pattern. The concrete getFormattedOutput() method in the Exercise abstract class defines the overall skeleton of an algorithm (print name, then print set data). However, it delegates the specific details of printing the set data to its subclasses by calling the abstract getSetData() method. This allows you to enforce a consistent overall structure while giving subclasses the flexibility to fill in the variable parts.


Your Turn: Refactoring the Notification System

Let's apply what you've learned by refactoring the NotificationService from our previous exercises. It's a perfect candidate for an abstract class.

Task:

  1. Make NotificationService Abstract:

    • Modify the NotificationService class declaration to be public abstract class NotificationService.
    • Make the send(String message) method abstract: public abstract void send(String message);. Remove its implementation body. The logic here is that each notification type has its own unique way of being "sent," so there's no sensible default implementation.
  2. Add a Concrete Method:

    • Add a new, non-abstract (concrete) method to the NotificationService class called logStatus. This method will represent shared functionality.
    public void logStatus(String status) {
        // In a real app, this would use a proper logging framework
        System.out.println("[" + java.time.LocalTime.now() + "] STATUS: " + status);
    }
    
  3. Update Subclasses:

    • Modify EmailService and SMSService to extend the abstract NotificationService.
    • Your IDE will now show an error because they don't implement the abstract send method.
    • Use the @Override annotation and provide a concrete implementation for the send(String message) method in both EmailService and SMSService. The implementation should print a message specific to the service (e.g., "[EMAIL] Sent: ...", "[SMS] Delivered: ...").
  4. Test Your Implementation:

    • In your Main class, try to create an instance of NotificationService: NotificationService ns = new NotificationService();. Observe that this now results in a compile-time error. This is the abstract class restriction in action!
    • Create an array of NotificationService and populate it with instances of EmailService and SMSService (this uses upcasting).
    • Loop through the array. For each notification in the array:
      • Call notification.send("Your order confirmation.");
      • Call notification.logStatus("PROCESSED");
    • Run your Main class and observe the polymorphic behavior.

This exercise solidifies your understanding by having you transform an existing class hierarchy to use an abstract class, directly demonstrating the benefits of enforcing a contract while sharing code.


Conclusion

In this lesson, we took a significant step towards creating more structured and robust object-oriented designs. You've learned how to define class blueprints that blend contractual obligations with reusable code.

Key Takeaways:

  • An abstract class is declared with the abstract keyword and cannot be instantiated.
  • It serves as a template for subclasses.
  • It can contain abstract methods (declarations without a body) which must be implemented by any concrete subclass.
  • It can also contain concrete methods (with implementation) which are inherited by subclasses, promoting code reuse.
  • Abstract classes are ideal for modeling concepts that are too generic to exist on their own and for sharing common code and structure among related classes.

In our next lesson, we will explore interfaces. While abstract classes are about sharing a common identity and implementation (is-a relationship), interfaces are purely about defining a capability (can-do relationship). We'll look at situations where unrelated classes need to share a common behavior, a problem that abstract classes can't solve as elegantly.