Abstract classes
Inheritance, interfaces and polymorphism11 min readLesson 4 of 6
4.1 A class that cannot be instantiated
An abstract class declares behavior that subclasses complete. Mark the class abstract; the compiler then rejects new for it. It can still have fields, constructors, and ordinary methods.
An abstract method ends with a semicolon and has no body. Every concrete subclass must implement it, or the subclass must also be abstract. A class with at least one abstract method must be declared abstract.
The abstract type describes a role: a shape has an area, a task has work to do. The base class can hold shared state and leave the variable parts to subclasses.
public class Main {
public static void main(String[] args) {
Shape shape = new Circle(2);
System.out.println(shape.area());
shape.describe();
}
}
abstract class Shape {
abstract double area();
void describe() { System.out.println("Area: " + area()); }
}
class Circle extends Shape {
double radius;
Circle(double radius) { this.radius = radius; }
@Override
double area() { return 3.14 * radius * radius; }
}Output
12.56 Area: 12.56
4.1.1 Shared code in the base
An abstract class can implement methods that use its abstract ones. describe() can call area() even though area() has no body yet. The call is resolved on the actual subclass when the method runs.
This is the template method pattern: a concrete method fixes the order of steps, and subclasses fill in the steps. The base class can also mark the template final so a subclass cannot replace the sequence.
Keep the shared code in the base only when it applies to every subclass. If one subclass needs a different version, overriding the concrete method is allowed unless it is final.
public class Main {
public static void main(String[] args) {
new Report().run();
}
}
abstract class Task {
final void run() {
start();
work();
System.out.println("Done");
}
void start() { System.out.println("Start"); }
abstract void work();
}
class Report extends Task {
@Override
void work() { System.out.println("Writing report"); }
}Output
Start Writing report Done
4.1.2 Constructors and fields
An abstract class can have constructors, and subclasses call them with super(...). The constructor runs when a subclass object is created, even though the abstract class itself is never instantiated.
Fields declared in the abstract class are available to subclasses through protected access or getters. A private field with a protected getter keeps the value under the base class's control.
Do not put every field in the base class. Move a field down when only one subclass uses it. The base class should describe what all subclasses share.
public class Main {
public static void main(String[] args) {
Employee employee = new Manager("Ravi", 4);
employee.printRole();
}
}
abstract class Employee {
String name;
Employee(String name) { this.name = name; }
abstract String role();
void printRole() { System.out.println(name + ": " + role()); }
}
class Manager extends Employee {
int reports;
Manager(String name, int reports) { super(name); this.reports = reports; }
@Override
String role() { return "manager of " + reports; }
}Output
Ravi: manager of 4
4.1.3 Abstract type as a parameter
A method can accept the abstract type and work with any subclass: printArea(Shape shape). The method calls area(), and each object supplies its own version. Adding a subclass does not change the method.
This is polymorphism in a practical form. The parameter type states what the method needs, and the object provides the behavior. The method stays small and testable.
The same idea works for arrays and lists of the abstract type. A loop over Shape[] can process circles and squares with one call.
public class Main {
public static void main(String[] args) {
printArea(new Circle(1));
printArea(new Square(3));
}
static void printArea(Shape shape) {
System.out.printf("%.2f%n", shape.area());
}
}
abstract class Shape { abstract double area(); }
class Circle extends Shape {
double radius;
Circle(double radius) { this.radius = radius; }
double area() { return 3.14 * radius * radius; }
}
class Square extends Shape {
double side;
Square(double side) { this.side = side; }
double area() { return side * side; }
}Output
3.14 9.00
4.1.4 Choosing an abstract class
Use an abstract class when subclasses share fields, constructors, or concrete methods, and when a partial implementation makes sense. Java allows one superclass, so the choice also uses up the single inheritance slot.
An interface is a better fit when the contract is a capability that unrelated classes can implement, or when a class needs several contracts. Interfaces can have default methods, which cover some shared behavior without a superclass.
If the base class has no state and no shared implementation, an interface is usually the simpler description. The next lesson covers interfaces in detail.
public class Main {
public static void main(String[] args) {
System.out.println(new Circle(2));
}
}
abstract class Shape {
abstract double area();
@Override
public String toString() { return "area=" + area(); }
}
class Circle extends Shape {
double radius;
Circle(double radius) { this.radius = radius; }
double area() { return 3.14 * radius * radius; }
}Output
area=12.56
4.1.5 Practice
- Create an abstract
Shapewith abstractarea()and a concretedescribe(). - Add
CircleandSquare, then print each area and description. - Add a constructor to
Shapethat stores a name, and pass it from both subclasses. - Write a method
printArea(Shape shape)and call it with both objects. - Try
new Shape()and read the compiler error. Then try a subclass that omitsarea()and read that error too.
For step 5, the two errors mention different problems. The first objects to creating an abstract type, and the second asks the subclass to implement the missing method.
