Abstract classes in Java

A detailed guide to Java Abstract classes, abstract methods, and their properties with practical abstractions using Java abstract classes.

Overview

An Abstract Class in Java is one of the ways of achieving abstractions. They help hide the implementation details and generalize common behaviours of their implementations.

This tutorial will focus on understanding the basics of Java Abstract Classes and their features with the help of examples.

What is an Abstract Class?

An abstract class is a Java class that we can create using the ‘abstract‘ keyword. It represents a very generalized supertype that is difficult to express in a specific state or behaviour. That is why we cannot instantiate an abstract class.

The Number class in Java is abstract because a number is a very generic term that doesn’t reveal any specifics about the actual type of a number. To make a perfect sense of the underlying number, we first need to know its kind, like integer, float, etc. That is why Java doesn’t allow instantiation of an abstract class.

Rules for Java Abstract Classes

  • They must have the ‘abstract‘ keyword.
  • We cannot instantiate them, and we can only extend them.
  • Abstract classes can have abstract and non-abstract (including static and final) methods.
  • They can have constructors, including default constructors; we still can’t instantiate them. Only the subclasses use these constructors.
  • A subclass must implement all the abstract methods of its superclass. Alternatively, the subclass can declare itself ‘abstract‘.

What is an Abstract Method?

An abstract method is a method that has the ‘abstract‘ keyword, and it has only the signature and no implementation or body. Sometimes an abstract class can’t define a particular behaviour until we know its specific subtype.

Let’s look at the ‘floatValue()‘ method of the Number class in Java.

public abstract float floatValue();Code language: Java (java)

We can’t derive a float value from a generic number, and to do so, we need to know if the number is an integer or long, etc. The ‘floatValue()‘ method correctly declares the ‘abstract’ keyword and doesn’t have a body.

Moreover, abstract methods represent a contract or rules for its non-abstract subclasses. That is because a non-abstract subclass must provide implementations to all abstract methods of its superclass. When the Number class defines the ‘abstract floatValue()‘ method, it also enforces its subclasses (like Integer, Double, etc.) to provide that behaviour.

Rules for Java Abstract Methods

  • They must have the ‘abstract‘ keyword.
  • They define signatures without a body.
  • Only an abstract class can have abstract methods.
  • All non-abstract subclasses must implement the abstract methods.
  • We cannot make abstract methods ‘final‘.

The last bullet is apparent because a subclass cannot overwrite ‘final’ methods, defeating the purpose of ‘abstract‘ methods.

Java Abstract Class vs Concrete Class

A concrete class in Java is a blueprint of an object which means it defines the structure and behaviour of an object. Every class instance will follow the same format and provide the same behaviour.

On the other hand, an abstract class can encapsulate the generalized behaviour of its subclasses. On top of that, an abstract class defines the API contract between the subclasses and the invokers.

Abstract ClassConcrete Class
Represents a generalized type for a set of classesRepresents a type for its objects
Builds API contracts for an abstractionDefines structure and behaviour of its objects
Cannot InstantiateCan Instantiate
Contains abstract and non-abstract MethodsContains only abstract methods
Abstract methods don’t have a body/implementationA concrete class must implement all the abstract methods of its superclass
An abstract class or an abstract method cannot be ‘final’A concrete class or any concrete method can become ‘final’

Abstract Class Vs Concrete Class Example

Concrete Class

Consider we want to model a circle and a square with the ability to calculate their area. We have all the details to build these classes so that we can make their concrete classes.

Circle.java

public Double calculateArea() {
  return radius * radius * Math.PI;
}
Code language: Java (java)

Square.java

public Double calculateArea() {
  return side * side;
}Code language: Java (java)

Abstract Class

Both the concrete classes represent the same API, and they belong to the shape category. Thus to make the consumer decouple from the actual implementations, we will create an abstract class on top of these two classes.

Shape.java

public abstract class Shape {
  public abstract Double calculateArea();
}Code language: Java (java)

Our specific shape classes can extend from the abstract Shape class and follow their contract.

The following diagram shows the structure of our Shape abstraction and shows the exact role of an abstract class and concrete subclasses.

Abstract Class Vs Concrete Class | amitph

The abstraction users can follow the ‘Program to interfaces and not implementations‘ principle and calculate areas of specific shapes.

Shape shape = new Circle(14.5);
System.out.println("Circle area: " + shape.calculateArea());

shape = new Square(10.1);
System.out.println("Square area: " + shape.calculateArea());Code language: Java (java)

The snippet of the abstraction user shows how loosely coupled they are with the actual implementations of shapes.

Abstract Class with abstract and non-abstract methods

Let’s write an example of an Abstract Class having a mix of abstract and non-abstract (concrete) methods. We wrote a Shape class with an abstract method in the previous section. Now, we want each shape to have the ability to draw itself by invoking a DrawingService.

drawingService.draw(shape);Code language: CSS (css)

As all the subclasses share this behaviour, the Shape class can encapsulate it.

public abstract class Shape {
  //Fields and Setter Methods

  public abstract Double calculateArea();

  public void draw() {
    drawingService.draw(this);
  }
}Code language: Java (java)

Abstract Class with parameterized constructor

The Shape class in the previous example has an instance of DrawingService. Let’s add a parameterized constructor to set the dependencies correct.

public abstract class Shape {
  private final DrawingService drawingService;

  public Shape(DrawingService drawingService) {
    this.drawingService = drawingService;
  }

  public abstract Double calculateArea();

  public void draw() {
    drawingService.draw(this);
  }
}
Code language: Java (java)

Adding the parameterized constructor in the Shape class will cause compilation errors in both subclasses. That is because the subclass constructor implicitly invokes the default constructor of the super, and the super doesn’t have one.

We will call the super constructor explicitly from subclass constructors to fix that.

Circle.java

public class Circle extends Shape {
  private final Double radius;

  public Circle(Double radius, DrawingService drawingService) {
    super(drawingService);
    this.radius = radius;
  }

  @Override
  public Double calculateArea() {
    return radius * radius * Math.PI;
  }
}
Code language: Java (java)

Shape.java

public class Square extends Shape {
  private final Double side;

  public Square(Double side, DrawingService drawingService) {
    super(drawingService);
    this.side = side;
  }

  @Override
  public Double calculateArea() {
    return side * side;
  }
}
Code language: Java (java)

Example of a Logger abstraction with Abstract Class

We will build an example of a simple Logger abstraction. There are two types of loggers – FileLogger and ConsoleLogger.

Let’s create an abstract Logger class, which will act as an API interface and encapsulate shared behaviours.

Logger.java

public abstract class Logger {
  private String logPattern = "%s | %s | %s";
  private final String className;

  public Logger(String className) {
    this.className = className;
  }

  public void setLogPattern(String logPattern) {
    this.logPattern = logPattern;
  }

  public abstract void print(String msg);
}Code language: Java (java)

Printing of the logs is specific to the logger types, so the ‘print()’ method is abstract. However, the log pattern is common across all the loggers; hence the ‘setLogPattern()‘ is a concrete method.

Let’s create the file logger and console logger implementations.

FileLogger.java

public class FileLogger extends Logger{

  public FileLogger(String className) {
    super(className);
  }

  @Override
  public void print(String msg) {
    // write to file
  }
}Code language: Java (java)

ConsoleLogger.java

public class ConsoleLogger extends Logger {
  public ConsoleLogger(String className) {
    super(className);
  }

  @Override
  public void print(String msg) {
    // Write To Console
  }
}Code language: Java (java)

Note that both of the implementations provide constructors matching the superclass.

Summary

This tutorial covered how to achieve abstraction using Java Abstract Classes. We started by having a basic understanding of an abstract class, and the abstract method features the difference between an abstract class and a concrete class. We also created a couple of examples to implement abstraction using abstract classes.

Refer to our Github Repository for the complete source code of the examples used in this tutorial.