Java · lesson 20 of 66
Beginner
17- 1 Introduction to Java
- 2 Setting Up Java
- 3 Data Types in Java
- 4 Operators in Java
- 5 Variables and Operators in Java
- 6 Control Flow in Java
- 7 Arrays in Java
- 8 Functions (Methods) in Java
- 9 Strings in Java
- 10 Methods in Java
- 11 Java OOP — Complete Guide
- 12 Introduction to Object-Oriented Programming in Java
- 13 Encapsulation in Java
- 14 Inheritance in Java
- 15 Polymorphism in Java
- 17 Abstraction in Java
- 18 Error Types in Java
Intermediate
38- 16 Regular Expressions in Java
- 19 Interfaces in Java — A Deep Dive
- 20 Abstract Classes in Java
- 21 Method Overloading in Java
- 22 Composition vs Inheritance in Java
- 23 Generics in Java
- 27 Exception Handling in Java
- 28 Java Streams API
- 29 Collections Framework in Java
- 30 Optional in Java
- 31 File Handling in Java
- 32 Records in Java
- 33 Java 8 Features
- 34 Sealed Classes and Interfaces in Java
- 35 Multithreading in Java
- 37 JDBC — Database Access in Java
- 39 Maven — Java Build and Dependency Management
- 42 Gradle — Modern Java Build Tool
- 44 Unit Testing with JUnit 5 and Mockito
- 45 Logging in Java with SLF4J and Logback
- 49 Spring Boot — Introduction
- 50 Dependency Injection in Spring Boot
- 51 Java Project Ideas
- 52 Building REST APIs with Spring Boot
- 53 Validation in Spring Boot
- 54 Spring Data JPA — Database Access
- 55 Exception Handling in Spring Boot
- 56 Spring Security — Authentication and Authorization
- 57 JWT Authentication in Spring Boot
- 58 Dockerizing Spring Boot Applications
- 59 Java Enums: Beyond Simple Constants
- 60 Inner and Anonymous Classes in Java
- 61 Java Generics: Bounded Types, Wildcards, and Generic Methods
- 62 Java Date/Time API: LocalDate, LocalDateTime, and Duration
- 63 Java NIO.2: Paths, Files, and WatchService
- 64 Java Annotations: Custom Types and Meta-Information
- 65 Java Reflection: Dynamic Type Inspection and Method Invocation
- 66 Java Localization: ResourceBundle and Locale Support
Advanced
11Also part of: Low-Level Design
Abstract Classes in Java
Master abstract classes — when to use them over interfaces, template method pattern, and how they enable shared behaviour across a class hierarchy.
By SkillByExample · Updated
An abstract class is a class that cannot be instantiated and is designed to be extended. It
sits between a concrete class (everything implemented) and an interface (no state), providing
shared fields, shared concrete methods, and abstract methods that subclasses must fill in. Every
example below is a single file run with the JDK 21 source launcher (java File.java), so you can
paste it and run it with no build step.
You cannot new an abstract class
Marking a class abstract is a compile-time rule: the compiler refuses to create an instance,
even if the class has no abstract methods at all.
public class NoNew {
public static void main(String[] args) {
Shape s = new Shape();
System.out.println(s);
}
}
abstract class Shape {
abstract double area();
}
$ java NoNew.java
NoNew.java:3: error: Shape is abstract; cannot be instantiated
Shape s = new Shape();
^
1 error
error: compilation failed
The error appears before any code runs. The fix is to instantiate a concrete subclass and keep the variable typed as the abstract class:
public class Shapes {
public static void main(String[] args) {
Shape[] shapes = { new Circle(1.0), new Square(2.0) };
for (Shape s : shapes) {
System.out.printf("%s area = %.2f%n", s.getClass().getSimpleName(), s.area());
}
}
}
abstract class Shape {
abstract double area();
}
class Circle extends Shape {
private final double r;
Circle(double r) { this.r = r; }
double area() { return Math.PI * r * r; }
}
class Square extends Shape {
private final double side;
Square(double side) { this.side = side; }
double area() { return side * side; }
}
$ java Shapes.java
Circle area = 3.14
Square area = 4.00
Shape s can point at any subclass, and s.area() dispatches to the subclass’s version at
runtime. The abstract class guarantees every element has an area(); it never has to know how
each one computes it.
Template method pattern
An abstract class can hold a final method that fixes the order of steps and leaves the steps
themselves abstract. That is the template method pattern: one skeleton, many variations.
import java.util.List;
public class Reports {
public static void main(String[] args) {
List<String> rows = List.of("Item A: $10", "Item B: $25");
System.out.println(new HtmlReport("Q3 Sales", rows).generate());
System.out.println(new MarkdownReport("Q3 Sales", rows).generate());
}
}
abstract class Report {
protected final String title;
protected final List<String> rows;
Report(String title, List<String> rows) { this.title = title; this.rows = rows; }
protected abstract String header();
protected abstract String row(String r);
public final String generate() { // the fixed skeleton
StringBuilder sb = new StringBuilder(header()).append('\n');
for (String r : rows) sb.append(row(r)).append('\n');
return sb.append("-- end of ").append(title).toString();
}
}
class HtmlReport extends Report {
HtmlReport(String t, List<String> r) { super(t, r); }
protected String header() { return "<h1>" + title + "</h1>"; }
protected String row(String r) { return "<p>" + r + "</p>"; }
}
class MarkdownReport extends Report {
MarkdownReport(String t, List<String> r) { super(t, r); }
protected String header() { return "# " + title; }
protected String row(String r) { return "- " + r; }
}
$ java Reports.java
<h1>Q3 Sales</h1>
<p>Item A: $10</p>
<p>Item B: $25</p>
-- end of Q3 Sales
# Q3 Sales
- Item A: $10
- Item B: $25
-- end of Q3 Sales
Both reports run the exact same generate(), so both end with the same -- end of footer line;
only header() and row() differ. Because generate() is final, no subclass can reorder the
steps or drop the footer — try overriding it and the compiler rejects the subclass.
Constructors and shared state
An abstract class can have fields and a constructor. Subclasses call it with super(...), and
it runs before the subclass constructor body.
public class Animals {
public static void main(String[] args) {
Dog rex = new Dog("Rex");
rex.feed();
rex.fetch();
rex.fetch();
System.out.println(rex.status());
}
}
abstract class Animal {
private final String name;
private int energy = 100;
Animal(String name) {
this.name = name;
System.out.println("Animal constructor: " + name);
}
abstract String food();
void feed() { energy = Math.min(100, energy + 20); System.out.println(name + " eats " + food()); }
void spend(int n) { energy = Math.max(0, energy - n); }
String name() { return name; }
String status() { return name + " energy=" + energy; }
}
class Dog extends Animal {
Dog(String name) {
super(name);
System.out.println("Dog constructor");
}
String food() { return "kibble"; }
void fetch() { spend(30); System.out.println(name() + " fetches"); }
}
$ java Animals.java
Animal constructor: Rex
Dog constructor
Rex eats kibble
Rex fetches
Rex fetches
Rex energy=40
Animal constructor prints first: super(name) must be the first statement in Dog’s
constructor, so the shared state is ready before the subclass touches it. feed() was capped at
100 by Math.min, so the energy went 100 → 100 → 70 → 40, not 120 → 90 → 60.
Abstract method called from the constructor
The order above has a trap: if the abstract class’s constructor calls an abstract method, the subclass’s override runs before the subclass’s own fields are initialised.
public class CtorTrap {
public static void main(String[] args) {
new Greeter("Hello");
}
}
abstract class Base {
Base() {
System.out.println("Base sees: " + describe());
}
abstract String describe();
}
class Greeter extends Base {
private final String greeting;
Greeter(String greeting) {
super();
this.greeting = greeting;
System.out.println("Greeter sees: " + describe());
}
String describe() { return "greeting=" + greeting; }
}
$ java CtorTrap.java
Base sees: greeting=null
Greeter sees: greeting=Hello
The first line prints null even though greeting is final: Base() runs while Greeter’s
fields still hold their default values. Keep constructors of abstract classes free of calls to
overridable methods; do that work in a separate method called after construction.
Implementing an interface partially
An abstract class can implement an interface and leave some of its methods abstract. The abstract class supplies the plumbing every subclass shares; the first concrete subclass supplies the rest.
import java.util.List;
public class Pipeline {
public static void main(String[] args) {
DataProcessor p = new UpperCaseProcessor();
p.run("input.txt");
}
}
interface DataProcessor {
List<String> read(String source);
List<String> process(List<String> data);
void write(List<String> data);
default void run(String source) { write(process(read(source))); }
}
abstract class BaseProcessor implements DataProcessor {
public List<String> read(String source) {
System.out.println("Reading from: " + source);
return List.of("line1", "line2", "line3"); // simulated I/O
}
public void write(List<String> data) { data.forEach(System.out::println); }
// process() is still abstract here
}
class UpperCaseProcessor extends BaseProcessor {
public List<String> process(List<String> data) {
return data.stream().map(String::toUpperCase).toList();
}
}
$ java Pipeline.java
Reading from: input.txt
LINE1
LINE2
LINE3
BaseProcessor never declares process(), yet it compiles because it is abstract: the obligation
passes down to UpperCaseProcessor. run() comes from the interface’s default method, read()
and write() from the abstract class, and only process() from the concrete class.
When to choose an abstract class vs an interface
Choose an abstract class when:
- subclasses share fields or constructor logic
- you want a template method that fixes an algorithm’s order
- the relationship is clearly “is-a” (every
HtmlReportis aReport)
Choose an interface when:
- unrelated classes share a capability (
Comparable,Iterable) - a class needs several supertypes — a class can extend only one class but implement many interfaces
- you want callers to depend on a contract with no state
Practice
- Add a
TextReportsubclass toReports.javathat renders the header as the title underlined with=and each row indented by two spaces.
Solution
import java.util.List;
public class TextReports {
public static void main(String[] args) {
System.out.println(new TextReport("Q3 Sales", List.of("Item A: $10", "Item B: $25")).generate());
}
}
abstract class Report {
protected final String title;
protected final List<String> rows;
Report(String title, List<String> rows) { this.title = title; this.rows = rows; }
protected abstract String header();
protected abstract String row(String r);
public final String generate() {
StringBuilder sb = new StringBuilder(header()).append('\n');
for (String r : rows) sb.append(row(r)).append('\n');
return sb.append("-- end of ").append(title).toString();
}
}
class TextReport extends Report {
TextReport(String t, List<String> r) { super(t, r); }
protected String header() { return title + "\n" + "=".repeat(title.length()); }
protected String row(String r) { return " " + r; }
}
$ java TextReports.java
Q3 Sales
========
Item A: $10
Item B: $25
-- end of Q3 Sales
generate() was not touched: the third subclass slots into the same skeleton, footer included.
- In
Animals.java, add aBirdsubclass whosefly()costs 50 energy. Feed it once, fly twice, then fly a third time and print the status. What stops energy going negative?
Solution
public class Birds {
public static void main(String[] args) {
Bird b = new Bird("Tweety");
b.feed();
b.fly();
b.fly();
b.fly();
System.out.println(b.status());
}
}
abstract class Animal {
private final String name;
private int energy = 100;
Animal(String name) { this.name = name; }
abstract String food();
void feed() { energy = Math.min(100, energy + 20); System.out.println(name + " eats " + food()); }
void spend(int n) { energy = Math.max(0, energy - n); }
String name() { return name; }
String status() { return name + " energy=" + energy; }
}
class Bird extends Animal {
Bird(String name) { super(name); }
String food() { return "seed"; }
void fly() { spend(50); System.out.println(name() + " flies"); }
}
$ java Birds.java
Tweety eats seed
Tweety flies
Tweety flies
Tweety flies
Tweety energy=0
spend() clamps with Math.max(0, ...), and because energy is private to Animal, Bird
cannot bypass the clamp — it has to go through spend().
- Try to override the
finalgenerate()method in a subclass ofReport. What does the compiler say?
Solution
public class FinalOverride {
public static void main(String[] args) {
System.out.println(new ShortReport().generate());
}
}
abstract class Report {
protected abstract String header();
public final String generate() { return header() + "\n-- end"; }
}
class ShortReport extends Report {
protected String header() { return "Short"; }
public String generate() { return header(); } // try to drop the footer
}
$ java FinalOverride.java
FinalOverride.java:14: error: generate() in ShortReport cannot override generate() in Report
public String generate() { return header(); } // try to drop the footer
^
overridden method is final
1 error
error: compilation failed
final turns “every report ends with a footer” from a convention into a rule the compiler enforces.
- Fix
CtorTrap.javasoBasestill prints a description, but only after the subclass is fully constructed.
Solution
public class CtorFixed {
public static void main(String[] args) {
Base g = new Greeter("Hello");
g.announce();
}
}
abstract class Base {
abstract String describe();
void announce() { System.out.println("Base sees: " + describe()); }
}
class Greeter extends Base {
private final String greeting;
Greeter(String greeting) { this.greeting = greeting; }
String describe() { return "greeting=" + greeting; }
}
$ java CtorFixed.java
Base sees: greeting=Hello
Moving the call out of the constructor into announce() means it runs after Greeter’s
constructor has assigned greeting.