Abstract Class: An Idea to Implement Abstraction in Java

We use abstract classes to implement abstraction in Java. It is one of the key ideas to enable a higher level of abstraction in our code, hide implementation details, enforce polymorphism and promote code reusability.

What is an abstract class?

In Java, abstract classes are similar to regular classes, but they are declared using the abstract keyword and cannot be instantiated. So what is the purpose of an abstract class if it can not be instantiated? Here is an idea: We mostly use an abstract class as a base class for other classes. It provides a set of common attributes and behaviours that the subclasses can inherit and specialize based on their specific requirements.

public abstract class AbstractClass {

    // Fields (if any)

    // Constructors (if any)

    // Abstract Methods
    public abstract void abstractMethod1();
    public abstract void abstractMethod2();

    // Non-Abstract Methods
    public void concreteMethod() {
        // Implementation
    }

    // Other methods (if any)
}

Some key points to remember!

  • An abstract class can have abstract methods (methods without implementations) and non-abstract methods (methods with implementations).
  • Abstract methods are declared without an implementation using the abstract keyword. What is the purpose of such methods? The idea is: It provides a contract that the concrete subclasses must fulfil by implementing them. In other words, subclasses must provide implementations for all the abstract methods inherited from the abstract class.
  • Non-abstract methods will have a complete implementation. Subclasses inherit these methods and can directly use or override them.
  • We can create instances of concrete subclasses that extend the abstract class.
  • Abstract classes can implement interfaces and provide default implementations for interface methods. This is a good example of a flexible design that includes both abstract class features and interface contracts.
  • Just like regular classes, abstract classes can have constructors and different access modifiers (e.g., public, private, protected). So, we can use access modifiers to control the visibility and accessibility of the members within the class hierarchy.
  • It can have static and final methods.

Now, one critical question still remains: Why can't an abstract class be instantiated? The idea is: It is often incomplete on its own, mainly because of the presence of abstract methods. Therefore, due to this incomplete or undefined behaviour, attempting to create an instance of the abstract class would not make sense.

Let's understand via an example Java code

abstract class Animal {
    private String name;

    public Animal(String name) {
        this.name = name;
    }
  
    // Abstract method: No implementation provided
    public abstract void makeSound();

    public void sleep() {
        System.out.println(getName() + " is sleeping.");
    }

    public void eat() {
        System.out.println(getName() + " is eating.");
    }
    // Getter method to retrieve the name of the animal
    public String getName() {
        return name;
    }
}

class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

    // Implementing the abstract method from the superclass
    @Override
    public void makeSound() {
        System.out.println("Woof woof!");
    }

    @Override
    public void sleep() {
        System.out.println("The dog named " + getName() + " is sleeping.");
    }
}

class Cat extends Animal {
    public Cat(String name) {
        super(name);
    }

    // Implementing the abstract method from the superclass
    @Override
    public void makeSound() {
        System.out.println("Meow meow!");
    }

    @Override
    public void sleep() {
        System.out.println("The cat named " + getName() + " is sleeping.");
    }
}

public class Main {
    public static void main(String[] args) {
        // Instantiating Cat using Animal reference
        Animal cat = new Cat("Whiskers"); 
        // Calling the overridden makeSound() method of Cat
        cat.makeSound(); 
        // Calling the overridden sleep() method of Cat
        cat.sleep();
        // Calling the inherited eat() method from Animal
        cat.eat();
        
        // Instantiating Dog using Animal reference
        Animal dog = new Dog("Buddy");
        // Calling the overridden makeSound() method of Dog
        dog.makeSound();
        // Calling the overridden sleep() method of Dog
        dog.sleep();
        // Calling the inherited eat() method from Animal
        dog.eat();
    }
}

Let's understand the different concepts used in the above code.

  • Animal class is declared as an abstract class i.e. it cannot be instantiated. We are using it as a base class for subclasses Dog and Cat.
  • Animal class contains an abstract method makeSound(), which does not have an implementation.
  • Dog and Cat subclasses override makeSound() with their own implementations.
  • Animal class defines common behaviours (sleep and eat) that are shared by all animals.
  • The sleep() method in Dog and Cat classes overrides the sleep() method inherited from the Animal class to provide their specific sleep behaviour.
  • Objects of Cat and Dog classes are instantiated using the Animal reference. This will help us to treat these objects as Animal objects, even though they are instances of specific subclasses.
  • We have called the makeSound(), sleep(), and eat() methods on the Animal references. So the specific implementations from the subclasses will be executed based on the actual object types.
  • Animal, Dog and Cat classes have constructors that accept a name parameter.
  • We used super(name) in the constructor of these subclasses to invoke the constructor of the superclass (Animal). This will set the name of the animal in the Animal class.

Abstract Methods

As mentioned above, an abstract method is a method declared in an abstract class that does not have any implementation. It only provides the method signature (name, parameters, and return type) and a contract for the subclasses to implement. Note: Methods declared inside an interface are by default abstract.

  • It is declared using the abstract keyword and does not include a method body or curly braces.
  • It helps the abstract class to define the expected behaviour or functionality that subclasses should provide while leaving the specific implementation details to the subclasses. In other words, It is the responsibility of the subclass to provide an implementation for the abstract method by overriding it.
  • We can use a reference of the abstract class to call the abstract method, and the specific implementation in the subclass will be executed at runtime.
abstract class Shape {
    public abstract double calculateArea();
    public abstract double calculatePerimeter();
}

class Square extends Shape {
    private double side;

    public Square(double side) {
        this.side = side;
    }

    public double calculateArea() {
        return side * side;
    }
    
    public double calculatePerimeter() {
        return 4 * side;
    }
}

public class Main {
    public static void main(String[] args) {
        Shape square = new Square(5);
        double area = square.calculateArea();
        double perimeter = square.calculatePerimeter();
        System.out.println("Square area: " + area);
        System.out.println("Square perimeter: " + perimeter);
    }
}

A class containing one or more abstract methods must also be declared abstract. On the other hand, an abstract class may or may not contain abstract methods.

abstract class Vehicle {
    public void start() {
        System.out.println("Vehicle started.");
    }

    public void stop() {
        System.out.println("Vehicle stopped.");
    }
}

class Car extends Vehicle {
    // Additional Car-specific methods and fields
}

We use abstract methods to enforce certain behaviours in subclasses. So, failure to implement an abstract method in a subclass will result in a compilation error.

For example, when you compile the following code, you will encounter a compilation error because the Square class is attempting to inherit from the Shape abstract class but has not implemented the abstract method calculatePerimeter().

abstract class Shape {
    public abstract double calculateArea();
    public abstract double calculatePerimeter();
}

class Square extends Shape {
    private double side;

    public Square(double side) {
        this.side = side;
    }

    public double calculateArea() {
        return side * side;
    }
}

public class Main {
    public static void main(String[] args) {
        Shape square = new Square(5);
        // Compilation error: Square is not abstract and does not override abstract method calculatePerimeter() in Shape
        double area = square.calculateArea();
        double perimeter = square.calculatePerimeter();
        System.out.println("Square area: " + area);
        System.out.println("Square perimeter: " + perimeter);
    }
}

To make the above code work, we either need to override all the abstract methods of the abstract class or, make the subclass abstract.

  • We can extend an abstract class by another abstract class. This implies: We do not need to implement all the abstract methods in the child's abstract class.
  • In other words, if a child class does not provide an implementation for all of the abstract methods in its parent class, it should be declared abstract so that the next level of child classes can implement the remaining abstract methods.

For example, In the following code, we made the Square class abstract because the calculatePerimeter() method is not implemented in the Square class.

abstract class Shape {
    public abstract double calculateArea();
    public abstract double calculatePerimeter();
}

abstract class Square extends Shape {
    private double side;

    public Square(double side) {
        this.side = side;
    }

    public double calculateArea() {
        return side * side;
    }

    // calculatePerimeter() method not implementated
}

Advantages of using abstract classes

  • Using abstract classes, subclasses can focus on providing their own implementation while abstracting away the underlying implementation details. It defines a contract or set of obligations that the subclasses must fulfil. So, this will help in achieving abstraction.
  • Abstract classes allow clients to interact with objects of different subclasses through the common interface, without needing to know the specific implementation details of each subclass.
  • It promotes code reusability. We can implement common methods and behaviours in the abstract class and reuse them in the subclasses.

Disadvantages of using abstract classes

  • Java does not support multiple inheritance, so a class can only inherit from a single abstract class. So if a class already inherits from an abstract class, it cannot inherit from any other class.
  •  Code reusability of abstract classes comes at the cost of tight coupling. Any changes made to the abstract class may require modifying all its subclasses.
  • When we have a large number of complex hierarchies, maintaining the relationships between abstract classes and their subclasses can be difficult.

Note: If there is a need to extend a class with additional behaviour from multiple sources, the use of interfaces or composition might be more appropriate.

Examples of Abstract Classes in JDK

  • AbstractMap : Provides a skeletal implementation of Map interface.
  • AbstractSet : Provides a skeletal implementation of the Set interface. 
  • AbstractCollection : Provides a skeletal implementation of Collection interface.

Thanks to Ankit Nishad for his contribution in creating the first version of this content. If you have any queries or feedback, please write us at contact@enjoyalgorithms.com. Enjoy learning, enjoy OOPS!

More from EnjoyAlgorithms

Self-paced Courses and Blogs