January 28, 2024

lealceldeiro

What are abstract classes in Java?

In a previous post we learned what's a Java class. To be more precise, the examples mentioned there are concrete classes. Usually, when there's no mention whether a class is concrete or abstract, it's inferred we're talking about a concrete class. Likewise, if we want to talk about an abstract one, we usually make the distinction by saying ... an abstract class....

So, then, when is a class abstract?

The abstract keyword

A class is abstract when the abstract keyword ( ClassModifier ) is used in the class declaration. It is a class that is incomplete, or to be considered incomplete ( §8.1.1.1 ). Hence, the syntax for the full declaration of an abstract class is the same we saw already:


{ClassModifier} class TypeIdentifier [TypeParameters] [ClassExtends] [ClassImplements] [ClassPermits] ClassBody

Let's see an example


abstract class Animal {
}

Abstract methods

Ok, the code looks simple. By just adding abstract to our class declaration we made it abstract. But what's so especial about it?

Well, when a class is abstract we can declare abstract methods in its body. An abstract method is a method without a body (no implementation). Then, subclasses that extend the abstract class must provide concrete implementations for these abstract methods. For example:


abstract class Animal {
    abstract void move();
}

Use cases for abstract classes

Before seeing some elaborated examples, let's understand this: why would we want to declare an abstract class to begin with?

Let's recap: we use classes to model our domain, our real world entities and concepts as well as their interactions with each other. And, as you might guess, some of those concept and entities are represented in our minds as abstract ones.

For example: if we're modeling a software for a Zoo, we need to model animals, the staff, food, cages, etc. Some of these concepts need to be modeled at a higher level of abstraction to represent them properly. Imagine we want to have a class Tiger to handle information about tigers, Snake to handle information about the snakes in the Zoo, Crocodile, Parrot, ... you get the idea.

All of these animals have many things in common; for example, they all move, they eat, they breathe, and so on. But they also have many differences: some of them walk, some crawl, some fly, etc.

It's exactly because of their differences that we need to create separate classes for each of them —that's clear—. But this leaves us with a problem, what do we do with the things ( properties and behaviors) they share? Do we just duplicate those fields and methods in each class? That doesn't sound good. Let's see an example of how it would look like:


class Tiger {
    String name;
    int energy;

    Tiger(String name) {
        this.name = name;
        energy = 100;
    }

    void move() {
        energy -= 3;
        System.out.println(name + " walked");
    }

    void eat() {
        energy++;
        System.out.println(name + " ate");
    }
}

class Snake {
    String name;
    int energy;

    Snake(String name) {
        this.name = name;
        energy = 100;
    }

    void move() {
        energy -= 2;
        System.out.println(name + " crawled");
    }

    void eat() {
        energy++;
        System.out.println(name + " ate");
    }
}

class Parrot {
    String name;
    int energy;

    Parrot(String name) {
        this.name = name;
        energy = 100;
    }

    void move() {
        energy--;
        System.out.println(name + " flew");
    }

    void eat() {
        energy++;
        System.out.println(name + " ate");
    }
}

There's a LOT of duplication in that code! Actually, everything, except for the move method is the same. This design is difficult to maintain at a bigger scale. Imagine a production code base with hundreds of classes and several team members working simultaneously on it. That's not how we should design it for the long term.

This is one of the scenarios where abstract classes shine. They allow us to represent abstract concepts and "delegate" the specificities to the concrete classes that implement them.

Example

Let's refactor the previous code to use an abstract class to keep the same fields and methods in it and delegate the specifics of each animal to the subclasses.


abstract class Animal {
    String name;
    int energy;

    Animal(String name) {
        this.name = name;
        energy = 100;
    }

    abstract void move();

    void eat() {
        energy++;
        System.out.println(name + " ate");
    }
}

class Tiger extends Animal {
    Tiger(String name) {
        super(name);
    }

    @Override
    void move() {
        energy -= 3;
        System.out.println(name + " walked");
    }
}

class Snake extends Animal {
    Snake(String name) {
        super(name);
    }

    @Override
    void move() {
        energy -= 2;
        System.out.println(name + " crawled");
    }
}

class Parrot extends Animal {
    Parrot(String name) {
        super(name);
    }

    @Override
    void move() {
        energy--;
        System.out.println(name + " flew");
    }
}

Now, this is better. Now we can see clearly what fields and methods belong to an Animal. Everything else belongs to the details of specific classes (Tiger, Parrot, etc.)

Notice in the previous code snippet we've used the @Override annotation to make sure we're overriding properly the move method.

Instantiation

Now we can use the abstract class and its subclasses in our code, but, because abstract classes cannot be instantiated directly by using the new keyword (see next section), we need to create objects of the concrete subclasses and use them polymorphically.


public static void main(String[] args) {
    Animal teo = new Tiger("Teo");
    Animal sisi = new Snake("Sisi");
    Animal pepi = new Parrot("Pepi");

    teo.move();
    teo.eat();

    sisi.move();
    sisi.eat();

    pepi.move();
    pepi.eat();
}

This will print in console:


Teo walked
Teo ate
Sisi crawled
Sisi ate
Pepi flew
Pepi ate

No matter all the objects (teo, sisi, pepi) are stored in a variable (reference) of type Animal, their specific types are different at runtime (such as Tiger) and so their behaviors are different (different texts printed in console).

Considerations

With abstract classes (and interfaces) we can simplify complex concepts in the software by modeling these classes based on the real-world concepts we're representing. We could talk infinitely about many of the advantages of this design. Some of the most remarkable ones are:

However, they're not a silver bullet, we must use them responsibly and have in mind what's their purpose and the problem they solve:

Final thoughts

Abstract classes represent a powerful tool at our disposal when following the OOP paradigm. Properly implemented, they enhance our code and make it more robust, secure, maintainable and profitable (why not?) in the long term. This article is by no means a complete guide to this topic, and there's much more you can review on your own; but hopefully this is a good start for you to dive deeper into the documentation and information linked here.