
Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:
Once the early-adopter seats are all used, the price will go up and stay at $33/year.
Last updated: January 8, 2024
There are many cases when implementing a contract where we want to postpone some parts of the implementation to be completed later. We can easily accomplish this in Java through abstract classes.
In this tutorial, we’ll learn the basics of abstract classes in Java, and in what cases they can be helpful.
Before diving into when to use an abstract class, let’s look at their most relevant characteristics:
To better understand these concepts, we’ll create a simple example.
Let’s have our base abstract class define the abstract API of a board game:
public abstract class BoardGame {
//... field declarations, constructors
public abstract void play();
//... concrete methods
}
Then, we can create a subclass that implements the play method:
public class Checkers extends BoardGame {
public void play() {
//... implementation
}
}
Now, let’s analyze a few typical scenarios where we should prefer abstract classes over interfaces and concrete classes:
Let’s keep in mind that all these scenarios are good examples of full, inheritance-based adherence to the Open/Closed principle.
Moreover, since the use of abstract classes implicitly deals with base types and subtypes, we’re also taking advantage of Polymorphism.
Note that code reuse is a very compelling reason to use abstract classes, as long as the “is-a” relationship within the class hierarchy is preserved.
And Java 8 adds another wrinkle with default methods, which can sometimes take the place of needing to create an abstract class altogether.
To understand more clearly the functionality that abstract classes bring to the table, let’s look at another example.
So, if we wanted to have several types of file readers, we might create an abstract class that encapsulates what’s common to file reading:
public abstract class BaseFileReader {
protected Path filePath;
protected BaseFileReader(Path filePath) {
this.filePath = filePath;
}
public Path getFilePath() {
return filePath;
}
public List<String> readFile() throws IOException {
return Files.lines(filePath)
.map(this::mapFileLine).collect(Collectors.toList());
}
protected abstract String mapFileLine(String line);
}
Note that we’ve made filePath protected so that the subclasses can access it if needed. More importantly, we’ve left something undone: how to actually parse a line of text from the file’s contents.
Our plan is simple: while our concrete classes don’t each have a special way to store the file path or walk through the file, they will each have a special way to transform each line.
At first sight, BaseFileReader may seem unnecessary. However, it’s the foundation of a clean, easily extendable design. From it, we can easily implement different versions of a file reader that can focus on their unique business logic.
A natural implementation is probably one that converts a file’s contents to lowercase:
public class LowercaseFileReader extends BaseFileReader {
public LowercaseFileReader(Path filePath) {
super(filePath);
}
@Override
public String mapFileLine(String line) {
return line.toLowerCase();
}
}
Or another might be one that converts a file’s contents to uppercase:
public class UppercaseFileReader extends BaseFileReader {
public UppercaseFileReader(Path filePath) {
super(filePath);
}
@Override
public String mapFileLine(String line) {
return line.toUpperCase();
}
}
As we can see from this simple example, each subclass can focus on its unique behavior without needing to specify other aspects of file reading.
Finally, using a class that inherits from an abstract one is no different than any other concrete class:
@Test
public void givenLowercaseFileReaderInstance_whenCalledreadFile_thenCorrect() throws Exception {
URL location = getClass().getClassLoader().getResource("files/test.txt")
Path path = Paths.get(location.toURI());
BaseFileReader lowercaseFileReader = new LowercaseFileReader(path);
assertThat(lowercaseFileReader.readFile()).isInstanceOf(List.class);
}
For simplicity’s sake, the target file is located under the src/main/resources/files folder. Hence, we used an application class loader for getting the path of the example file. Feel free to check out our tutorial on class loaders in Java.
In this quick article, we learned the basics of abstract classes in Java, and when to use them for achieving abstraction and encapsulating common implementation in one single place.