Lesson 23Phase 3: Abstraction and design
Abstract classes
Classes you cannot instantiate, methods with no body, and forcing subclasses to fill in the blanks.
By BasithUpdated 55 min
- abstract class
- abstract method
- cannot instantiate
- template
- partial implementation
You can now write a parent class, extend it, override its methods, and point a parent-typed variable at a child object so the right behavior runs. One awkward gap is left. Some parent classes describe a kind of thing that should never exist on its own. What sound does a plain, generic Animal make? There is no honest answer, and yet nothing has stopped you from writing new Animal(). This lesson closes that gap.
- Explain why some classes should exist only to be extended, never built directly.
- Mark a class
abstract, and read the compiler's message when code callsnewon it. - Write an
abstractmethod with no body, and say what it forces every subclass to do. - Tell a concrete subclass from one that is still abstract because it left a method unwritten.
- Use an abstract class as a template: shared fields, a constructor and finished methods in the parent, and abstract steps for each subclass to fill in.
A parent that should never be built directly#
Picture the animal family you have been building. Animal is the parent. Dog, Cat and Cow extend it, and each one overrides speak() to return its own sound. Polymorphism lets you keep them all in one Animal[] and call speak() on each.
That all works, but there is a quiet problem in the parent. Animal exists to be a shared shape for real animals. On its own it is not a thing you could point at in the world: a real animal is always a dog, a cat or some other particular kind. So this program makes no sense, even though Java has not objected to it yet:
public class Main { public static void main(String[] args) { Animal a = new Animal(); System.out.println(a.speak()); }} class Animal { String speak() { return "..."; }}It prints ..., a sound made up so that Animal would compile. That fake body is a warning sign: it exists only to answer a question nobody should be able to ask. The honest statement is that a generic animal has no sound, because a generic animal is not a real thing. You want the language to enforce that, so nobody can build one by accident.
Key ideaSome classes exist only to be a shared base for others. They describe a category, not a particular thing. You want to stop anyone building one directly, and to get rid of every fake placeholder body.
Marking a class abstract#
The keyword is abstract, written in front of class. To start with, it means one thing: no object of this class can be made with new. The class can still be extended, and its subclasses can still be built. Only a direct new is forbidden. This program tries it anyway:
public class Main { public static void main(String[] args) { Animal a = new Animal(); System.out.println("This line never runs."); }} abstract class Animal { String name;}Main.java:3: error: Animal is abstract; cannot be instantiated
Animal a = new Animal();
^
1 errorThe compiler refuses before the program ever runs. The message names the line, 3, and the reason: Animal is abstract; cannot be instantiated. To instantiate a class is to make an object of it, an instance. The language caught the nonsense for you.
The variable type Animal is still perfectly legal. A variable can be typed Animal and point at a Dog; what you cannot do is make a bare Animal object for it to point at. Here is the legal version, where Dog extends the abstract Animal:
public class Main { public static void main(String[] args) { Animal pet = new Dog(); pet.name = "Rex"; System.out.println(pet.name + " is an animal"); }} abstract class Animal { String name;} class Dog extends Animal {}It prints Rex is an animal. Step through it and look at the memory view: the variable pet is typed Animal, but the one object it points at is tagged Dog #1. An abstract class can be the type of a variable, but never the class of an object.
abstract changes nothing about how the objects of its subclasses behave. It is a promise written into the type system: this class is a base, not a destination. It tells every other programmer, including you next month, what the class is for, and it lets the compiler hold everyone to it.Animal is abstract and Dog extends it. Which line does not compile?abc module gives the same idea: a class that inherits from ABC and marks a method with @abstractmethod cannot be instantiated. Python finds out when the program runs and raises a TypeError; Java refuses to compile the program at all.An abstract method: a blank every subclass must fill#
Forbidding new Animal() is half the idea. The other half: you can remove the fake speak() body entirely and keep only the method's first line. That is an abstract method: a method with a return type, a name and parameters, ending in a semicolon where the body would be.
public class Main { public static void main(String[] args) { System.out.println("compiles fine; Animal cannot be built"); }} abstract class Animal { String name; abstract String speak();}Read abstract String speak(); as a sentence: “every animal can speak and hands back a Stringwhen it does, but I, the parent, will not say what the sound is. That is each subclass's job.” There are no braces, because there is no code. The method is a hole with an exact shape, and the shape is the contract every subclass must meet. The program only prints a line, which shows that the declaration compiles.
Key ideaAn abstract method is a promise with no code behind it. It says that every concrete subclass must provide this exact method, and the compiler will not let a subclass forget.
A class with an abstract method must be abstract
One rule follows on its own, and it trips people up: a class with even one abstract method must itself be declared abstract. It has to be. If Animal has an unanswered speak(), then Animal is incomplete, and building one would give you the impossible object again. Leave the keyword off the class and javac stops you:
public class Main { public static void main(String[] args) { System.out.println("This line never runs."); }} class Animal { abstract String speak();}Main.java:7: error: Animal is not abstract and does not override abstract method speak() in Animal
class Animal {
^
1 errorThe message reads oddly at first: Animal is not abstract and does not override abstract method speak() in Animal. javac expects a class that is not abstract to be complete, finds a method with no body, and says so. The fix is one word: abstract class Animal.
Concrete subclasses fill every blank#
A subclass that gives a real body to every abstract method it inherits is called a concrete class. Concrete means complete: no holes left, so you may build it with new. Here are three concrete animals, each overriding speak(), all driven through one parent-typed array.
public class Main { public static void main(String[] args) { Animal[] zoo = new Animal[3]; zoo[0] = new Dog("Rex"); zoo[1] = new Cat("Mimi"); zoo[2] = new Cow("Daisy"); int i = 0; while (i < zoo.length) { zoo[i].introduce(); i = i + 1; } }} abstract class Animal { String name; abstract String speak(); void introduce() { System.out.println(name + " says " + speak()); }} class Dog extends Animal { Dog(String n) { name = n; } String speak() { return "Woof"; }} class Cat extends Animal { Cat(String n) { name = n; } String speak() { return "Meow"; }} class Cow extends Animal { Cow(String n) { name = n; } String speak() { return "Moo"; }}It prints Rex says Woof, Mimi says Meow and Daisy says Moo. Step through and watch the loop. The array is typed Animal[], but the memory view shows what each slot really holds: Dog #1, Cat #1, Cow #1. When introduce() calls speak(), polymorphism picks the version that belongs to the actual object. The parent never knew the sound and never needed to. It relied on every concrete subclass supplying one, and the abstract method made that reliance safe.
speak() is abstract, the compiler guarantees that Dog, Cat and Cow each have it. So inside introduce(), calling speak() is always safe. You get to write code in the parent that depends on behavior the parent itself does not provide.What if a subclass forgets?
Suppose a parent declares two abstract methods, start() and move(), and a subclass writes only start(). That subclass still has a hole, so it is still incomplete, so it is still abstract, whether you meant it to be or not. Java makes you either write the missing method or mark the subclass abstract too. You cannot quietly leave a blank and then build the object.
public class Main { public static void main(String[] args) { Robot r = new Robot(); r.start(); }} abstract class Machine { abstract void start(); abstract void move();} class Robot extends Machine { void start() { System.out.println("booting"); } // move() is missing, so Robot still has a blank}Main.java:13: error: Robot is not abstract and does not override abstract method move() in Machine
class Robot extends Machine {
^
1 errorShape with an abstract area(), a Square that fills it in, and a main that prints the area of a 3 by 3 square through a Shape variable. The program prints 9.Tap a line, then tap where it should go. Or use the arrows.
Abstract classes are not empty shells#
An abstract class can hold ordinary members alongside its abstract ones. It can have fields, it can have constructors, and it can have finished methods with normal bodies. So one abstract base class can share code and force a contract at the same time. This is the partial implementation an abstract class offers: the parts every subclass shares are written once, and the parts that differ are left for each subclass.
You already saw this above: introduce() was a finished method inside the abstract Animal, and every subclass got it without rewriting it. Here is the same pattern again. An abstract Shape declares one abstract method, area(), and provides a finished describe() that all shapes share.
public class Main { public static void main(String[] args) { Shape[] shapes = new Shape[2]; shapes[0] = new Circle(2.0); shapes[1] = new Rectangle(3.0, 4.0); int i = 0; while (i < shapes.length) { shapes[i].describe(); i = i + 1; } }} abstract class Shape { String name; abstract double area(); void describe() { System.out.println(name + " has area " + area()); }} class Circle extends Shape { double radius; Circle(double r) { radius = r; name = "Circle"; } double area() { return 3.14 * radius * radius; }} class Rectangle extends Shape { double width; double height; Rectangle(double w, double h) { width = w; height = h; name = "Rectangle"; } double area() { return width * height; }}This prints Circle has area 12.56, then Rectangle has area 12.0. Look at how the work is split. Shape owns the name field and the whole describe() method, written once and shared by both shapes. What Shapewill not do is compute an area, because there is no single formula for “a shape”. So area() is abstract, and each subclass brings its own formula.
Constructors in an abstract class
It can feel contradictory at first. If you cannot call new on an abstract class, why would it have a constructor? Because the constructor is not for building a bare Account. It runs as part of building a subclass. When you write new Savings(...), Java builds the Savingsobject, and the parent's constructor sets up the part of the object the parent is responsible for. The subclass reaches it with the super(...) call from inheritance. Both constructors below print a line. Decide the order before you run it.
public class Main { public static void main(String[] args) { Account a = new Savings("Aisha"); a.describe(); }} abstract class Account { String owner; Account(String owner) { this.owner = owner; System.out.println("Account constructor"); } abstract String kind(); void describe() { System.out.println(owner + " has a " + kind() + " account"); }} class Savings extends Account { Savings(String owner) { super(owner); System.out.println("Savings constructor"); } String kind() { return "savings"; }}main builds one Savings account and asks it to describe itself. What are the three lines, in order?
It prints Account constructor, then Savings constructor, then Aisha has a savings account. new Savings("Aisha") starts the Savings constructor, whose first line, super(owner), runs the Account constructor. That one stores the owner and prints its line, and only then does the Savings constructor carry on and print its own. Step through it in the playground and you will see both constructors on the call stack at once. You never built a bare Account, yet its constructor ran, setting up the shared field for the subclass.
A template: the parent fixes the steps#
Look again at describe(): a finished method in the parent that calls an abstract method the children fill in. That shape has a name, the template method pattern. The parent writes the outline of a job, the steps and their order, and leaves some of the steps blank. Each subclass fills in the blanks and never touches the outline. A form works the same way: the layout is printed once, and each person writes in their own answers.
Here every document prints the same way: a header line with its title, then its body, then an end line. print() is the template. title() and printBody() are the blanks.
public class Main { public static void main(String[] args) { Document[] docs = { new Receipt(), new Invitation() }; for (Document d : docs) { d.print(); } }} abstract class Document { void print() { System.out.println("== " + title() + " =="); printBody(); System.out.println("== end =="); } abstract String title(); abstract void printBody();} class Receipt extends Document { String title() { return "Receipt"; } void printBody() { System.out.println("2 coffees: 7.0"); }} class Invitation extends Document { String title() { return "Invitation"; } void printBody() { System.out.println("Dear Mia, please come to the party."); }}Both documents come out in the same frame: == Receipt ==, the body, == end ==, then the same for the invitation. Step through print()and watch the call stack: the steps run in the parent's order, and each blank jumps into the subclass for its answer. A new kind of document only has to say what goes in the blanks. The frame, and any fix to it, lives in one place.
print() itself and change the order. To forbid that, a parent marks the template method final, which you meet in static and final.monthlyPay() in Employee an abstract method with no body, then give each subclass its own monthlyPay(): a salaried employee earns yearly / 12 a month, and an hourly one earns hours * rate.Abstract versus concrete, side by side#
Keep these two words straight and most of the confusion goes away. Abstract means incomplete on purpose and not buildable. Concrete means complete and buildable.
abstract.new Animal() does not compile.new.Key ideaAn abstract class gives you two things in one base: shared code that subclasses inherit, and a list of blanks that subclasses are forced to fill.
A short checklist for writing one
- 1Notice a parent that should never be built, or a method that has no sensible default body. Mark the class
abstract. - 2For each behavior every subclass must provide but the parent cannot, write
abstract ReturnType name(parameters);with a semicolon and no body. - 3Put the code the subclasses share (fields, constructors and finished methods) in the abstract class, so every subclass inherits it.
- 4In each concrete subclass, override every abstract method with a real body. Only then can you call
newon it.
Try it
Answer each one in your head or on paper first, then open the answer.
new Animal() forbidden once Animal is marked abstract, while Animal a = new Dog(); is still allowed?abstract void start(); but will not compile. What single change fixes it?Shape example. describe() calls area(), yet Shape never gives area() a body. Why is that call safe?Vehicle is abstract, with two abstract methods, start() and stop(). Class Car extends Vehicle writes only start(). Can you write new Car()? Why or why not?Documents.java, a new class Ticket extends Document. Which methods must it write, and which does it get for free?Practice
Work through these on your own. Each one builds on this page.
Shape example in the playground and add a third concrete subclass, Triangle, with fields base and height and an area() of 0.5 * base * height. Add it to the array, which now needs three slots, and check its line of output.Shape s = new Shape(); to main. Second, in a fresh file, write abstract void move(); inside a class that is not marked abstract. Run both with Java on your own computer, since the compiler is what catches them.Character for a small game. Every character has a name and a number of health points, and a finished method takeHit(int damage) that lowers the health and prints what is left. Each kind of character attacks differently, so int attackPower() is abstract. Write two concrete characters, such as a Knight and a Wizard, and a main in which each one attacks the other once.abstract lets the compiler catch that you would otherwise only discover when the program ran. Use the fake "..." sound from the start of this page as your example.Further reading#
Spotted a mistake, or a part that did not make sense? Report it by email