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?
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 {
}
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();
}
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.
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 themove
method.
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).
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:
Animal animal = new Animal("Teo"); < -- compilation error!
The closest we can get the previous code is to use an anonymous class — but this is something completely
different from what we're discussing here. Let's see it, for the sake of completeness:
Animal animal = new Animal("Tiger") {
@Override
void move() {
energy -=5;
System.out.println(name + " moved in an 'anonymous' way");
}
};
abstract
methods in a non-abstract
class (
§8.1.1.1
).
super(name);
.
abstract
methods are inherited. This means if a class inherits some abstract method, that class
must either: implement the method (override it), or the class itself must be declared abstract
as well. This case would be useful if we want to represent another level of abstraction. For example, if we'd
want to model in our software some different types of animal, such as
mammals,
reptiles,
oviparous, etc., we could create
abstract classes for each of them. It would look like this:
abstract class Mammal extends Animal {
}
// ... other classes follow the same pattern
private
(
§8.8.10
).
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.