Java Top

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Introduction

Abstraction is one of the Object-Oriented programming key features. It allows us to hide the implementation complexities just by providing functionalities via simpler interfaces. In Java, we achieve abstraction by using either an interface or an abstract class.

In this article, we'll discuss when to use an interface and when to use an abstract class while designing applications. Also, the key differences between them and which one to choose based on what we're trying to achieve.

2. Class vs. Interface

First, let's look at the differences between a normal concrete class vs. an interface.

A class is a user-defined type that acts as a blueprint for object creation. It can have properties and methods that represent the states and behaviors of an object, respectively.

An interface is also a user-defined type that is syntactically similar to a class. It can have a collection of field constants and method signatures that will be overridden by interface implementing classes.

In addition to these, Java 8 new features support static and default methods in interfaces to support backward compatibility. Methods in an interface are implicitly abstract if they are not static or default and all are public.

However, starting with Java 9, we can also add private methods in interfaces.

3. Interface vs. Abstract Class

An abstract class is nothing but a class that is declared using the abstract keyword. It also allows us to declare method signatures using the abstract keyword (abstract method) and forces its subclasses to implement all the declared methods. Suppose if a class has a method that is abstract, then the class itself must be abstract.

Abstract classes have no restrictions on field and method modifiers, while in an interface, all are public by default. We can have instance and static initialization blocks in an abstract class, whereas we can never have them in the interface. Abstract classes may also have constructors which will get executed during the child object's instantiation.

Java 8 introduced functional interfaces, an interface with a restriction of no more than one declared abstract method. Any interface with a single abstract method other than static and default methods is considered a functional interface. We can use this feature to restrict the number of abstract methods to be declared. While in abstract classes, we can never have this restriction on the number of abstract methods declaration.

Abstract classes are analogous to interfaces in some ways:

  • We can't instantiate either of them. i.e., we cannot use the statement new TypeName() directly to instantiate an object. If we used the aforementioned statement, we have to override all the methods using an anonymous class
  • They both might contain a set of methods declared and defined with or without their implementation. i.e., static & default methods(defined) in an interface, instance methods(defined) in abstract class, abstract methods(declared) in both of them

4. When to Use an Interface

Let's look at some scenarios when one should go with an interface:

  • If the problem needs to be solved using multiple inheritances and is composed of different class hierarchies
  • When unrelated classes implement our interface. For example, Comparable provides the compareTo() method that can be overridden to compare two objects
  • When application functionalities have to be defined as a contract, but not concerned about who implements the behavior. i.e., third-party vendors need to implement it fully

Consider using the interface when our problem makes the statement “A is capable of [doing this]”. For example, “Clonable is capable of cloning an object”, “Drawable is capable of drawing a shape”, etc.

Let us consider an example that makes use of an interface:

public interface Sender {
    void send(File fileToBeSent);
}
public class ImageSender implements Sender {
    @Override
    public void send(File fileToBeSent) {
        // image sending implementation code.
    }
}

Here, Sender is an interface with a method send(). Hence, “Sender is capable of sending a file” we implemented it as an interface. ImageSender implements the interface for sending an image to the target. We can further use the above interface to implement VideoSender, DocumentSender to accomplish various jobs.

Consider a unit test case the makes use of the above interface and its implemented class:

@Test
void givenImageUploaded_whenButtonClicked_thenSendImage() { 
 
    File imageFile = new File(IMAGE_FILE_PATH);
 
    Sender sender = new ImageSender();
    sender.send(imageFile);
}

5. When to Use an Abstract Class

Now, let's see some scenarios when one should use the abstract class:

  • When trying to use the inheritance concept in code (share code among many related classes), by providing common base class methods that the subclasses override
  • If we have specified requirements and only partial implementation details
  • While classes that extend abstract classes have several common fields or methods (that require non-public modifiers)
  • If one wants to have non-final or non-static methods to modify the states of an object

Consider using abstract classes and inheritance when our problem makes the evidence “A is a B”. For example, “Dog is an Animal”, “Lamborghini is a Car”, etc.

Let's look at an example that uses the abstract class:

public abstract class Vehicle {
    
    protected abstract void start();
    protected abstract void stop();
    protected abstract void drive();
    protected abstract void changeGear();
    protected abstract void reverse();
    
    // standard getters and setters
}
public class Car extends Vehicle {

    @Override
    protected void start() {
        // code implementation details on starting a car.
    }

    @Override
    protected void stop() {
        // code implementation details on stopping a car.
    }

    @Override
    protected void drive() {
        // code implementation details on start driving a car.
    }

    @Override
    protected void changeGear() {
        // code implementation details on changing the car gear.
    }

    @Override
    protected void reverse() {
        // code implementation details on reverse driving a car.
    }
}

In the above code, the Vehicle class has been defined as abstract along with other abstract methods. It provides generic operations of any real-world vehicle and also has several common functionalities. The Car class, which extends the Vehicle class, overrides all the methods by providing the car's implementation details (“Car is a Vehicle”).

Hence, we defined the Vehicle class as abstract in which the functionalities can be implemented by any individual real vehicle like cars and buses. For example, in the real world, starting a car and bus is never going to be the same (each of them needs different implementation details).

Now, let's consider a simple unit test that makes use of the above code:

@Test
void givenVehicle_whenNeedToDrive_thenStart() {

    Vehicle car = new Car("BMW");

    car.start();
    car.drive();
    car.changeGear();
    car.stop();
}

6. Conclusion

This article discussed the overview of interfaces and abstract classes and the key differences between them. Also, we examined when to use each of them in our work to accomplish writing flexible and clean code.

The complete source code for the examples given in this article is available over on GitHub.

Java bottom

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

>> CHECK OUT THE COURSE
Junit footer banner
2 Comments
Oldest
Newest
Inline Feedbacks
View all comments
Comments are closed on this article!