Lesson 3 of 71
Classes, Interfaces, and Inheritance
This lesson covers how Java code is organized into classes and interfaces and how one type extends another. After it, a reader can predict the order in which an object is initialized, state the rules for overriding, resolve a default-method conflict, and choose between inheritance and composition. It builds on Types, Variables, and Expressions and Strings and Text.
| Topic | Key point |
|---|---|
Constructors, this(...) |
One constructor delegates to another |
| Initialization order | Superclass first, then field initializers and instance blocks, then the constructor body |
| Access modifiers | private, package-private, protected, public |
| Overriding | Same signature, covariant return, no weaker access |
abstract, final |
Must be extended; cannot be extended or overridden |
| Interfaces | Abstract, default, static, and private methods |
| Nested types | Static nested, inner, local, anonymous |
Example 1: fields, constructors, and static members
A class declares fields (state) and methods (behavior). A constructor initializes a new object; this(...) calls another constructor of the same class and must be the first statement. A static member belongs to the class, and an instance member belongs to each object.
public class Account {
private static int created = 0; // one per class
private static final String PREFIX = "AC-";
private final String id; // one per object
private final String owner;
private long balanceCents;
public Account(String owner) {
this(owner, 0); // delegate to the full constructor
}
public Account(String owner, long openingCents) {
if (openingCents < 0) throw new IllegalArgumentException("negative opening balance");
created++;
this.id = PREFIX + created;
this.owner = owner;
this.balanceCents = openingCents;
}
public void deposit(long cents) {
balanceCents += cents;
}
static int createdCount() {
return created; // static methods see only static state
}
@Override
public String toString() {
return id + " " + owner + " " + balanceCents;
}
public static void main(String[] args) {
Account a = new Account("ada");
Account b = new Account("bob", 500);
a.deposit(250);
System.out.println(a);
System.out.println(b);
System.out.println("created = " + Account.createdCount());
}
}AC-1 ada 250
AC-2 bob 500
created = 2this.id = ... distinguishes the field from a parameter with the same name. A final field must be assigned exactly once in every constructor path, which the compiler checks. The one-argument constructor delegates, so validation and counting live in one place. A static method has no this, so it cannot read owner or balanceCents. toString overrides the version inherited from Object; @Override is explained in Example 3.
Example 2: initialization order
When new runs, the JVM first loads and initializes the class (static field initializers and static blocks, in source order, superclass first). Then, for each object, the superclass constructor runs, then the subclass field initializers and instance initializer blocks in source order, then the rest of the subclass constructor.
public class InitOrder {
static StringBuilder log = new StringBuilder();
static String note(String s) {
log.append(s).append(' ');
return s;
}
static class Base {
static { note("Base.static"); }
String baseField = note("Base.field");
{ note("Base.block"); }
Base() {
note("Base()");
describe(); // virtual call from a constructor
}
void describe() { note("Base.describe"); }
}
static class Derived extends Base {
static { note("Derived.static"); }
String name = note("Derived.field");
{ note("Derived.block"); }
Derived() {
super(); // implicit if omitted
note("Derived()");
}
@Override
void describe() { note("Derived.describe(name=" + name + ")"); }
}
public static void main(String[] args) {
new Derived();
System.out.println(log.toString().trim().replace(" ", "\n"));
log.setLength(0);
new Derived();
System.out.println("second: " + log.toString().trim().replace(" ", ", "));
}
}Base.static
Derived.static
Base.field
Base.block
Base()
Derived.describe(name=null)
Derived.field
Derived.block
Derived()
second: Base.field, Base.block, Base(), Derived.describe(name=null), Derived.field, Derived.block, Derived()Static initialization happens once per class, so the second new Derived() logs no static lines. The superclass is fully constructed before any subclass field is set. Base() calls describe(), which dispatches to the Derived override, and at that moment name is still null. Calling an overridable method from a constructor exposes a half-built object; constructors call only private, static, or final methods.
Example 3: inheritance, overriding, and super
A subclass extends one superclass and inherits its non-private members. Overriding replaces an inherited instance method with one that has the same name and parameter types. @Override asks the compiler to check that a method really overrides something.
public class Shapes {
static class Shape {
protected final String label;
Shape(String label) { this.label = label; }
double area() { return 0; }
Shape copy() { return new Shape(label); }
static String kind() { return "shape"; }
String describe() { return label + " area=" + String.format("%.2f", area()); }
}
static class Circle extends Shape {
private final double r;
Circle(double r) {
super("circle"); // superclass constructor
this.r = r;
}
@Override
double area() { return Math.PI * r * r; }
@Override
public Circle copy() { return new Circle(r); } // covariant return, wider access
static String kind() { return "circle"; } // hides, does not override
@Override
String describe() { return "[" + super.describe() + "]"; }
}
public static void main(String[] args) {
Shape s = new Circle(1);
System.out.println(s.describe());
System.out.println(s.copy().getClass().getSimpleName());
Circle c = new Circle(2).copy(); // no cast needed
System.out.println(c.describe());
System.out.println(Shape.kind() + " " + Circle.kind());
System.out.println(s instanceof Circle);
}
}[circle area=3.14]
Circle
[circle area=12.57]
shape circle
trueinstanceof tests whether an object is an instance of a type. The call s.describe() uses the runtime class (Circle), not the declared type (Shape); this is dynamic dispatch. super.describe() calls the superclass version, which itself calls area() and again reaches the override. The rules for an override:
| Rule | Allowed |
|---|---|
| Return type | Same, or a subtype (covariant return) |
| Access | Same or wider (protected to public), never narrower |
| Checked exceptions | Same, fewer, or narrower |
static methods |
Hidden, not overridden; the declared type picks the method |
private and final methods |
Cannot be overridden |
Access modifiers control visibility: private (the class only), package-private with no keyword (the same package), protected (the package plus subclasses), and public (everywhere). A package is a namespace declared with package com.example.orders; at the top of a file, and it matches the directory path. The examples here use one file, so every class is in the unnamed package. Modules add another layer, covered in Packages, Modules, and Reflection.
Statements before super(...) (Java 25)
Before Java 25, this(...) or super(...) had to be the first statement of a constructor, so argument checks ran only after the superclass constructor. Flexible constructor bodies (JEP 513, final in Java 25) allow statements before that call, as long as they do not read the object under construction (no this fields or instance methods). They may assign the class's own fields, which then hold values before the superclass constructor runs.
public class Main {
public static void main(String[] args) {
System.out.println(new PositiveAccount(250).describe());
try {
new PositiveAccount(-5);
} catch (IllegalArgumentException e) {
System.out.println("rejected: " + e.getMessage());
}
}
}
class Account {
Account(long balance) {
System.out.println("Account constructor, balance=" + balance + ", kind=" + kind());
}
String kind() { return "plain"; }
}
class PositiveAccount extends Account {
private final String label;
PositiveAccount(long balance) {
if (balance < 0) { // runs before super(...) since Java 25
throw new IllegalArgumentException("negative balance " + balance);
}
label = "positive"; // own field, assigned before super(...)
super(balance);
}
@Override
String kind() { return label; } // called from Account's constructor
String describe() { return "account " + label; }
}Account constructor, balance=250, kind=positive
account positive
rejected: negative balance -5The check rejects -5 without running the Account constructor. Because label is assigned before super(balance), the overridden kind() that the superclass constructor calls already sees "positive". Before Java 25 the same call would see null, a known trap of calling overridable methods from constructors.
Example 4: abstract classes and final
An abstract class cannot be instantiated and can declare abstract methods without a body that subclasses must implement. A final method cannot be overridden and a final class cannot be extended.
public class Abstracts {
static abstract class Report {
private final StringBuilder out = new StringBuilder();
public final String render() { // template: fixed steps
out.append(header()).append('\n');
for (String row : rows()) out.append(" ").append(row).append('\n');
out.append(footer());
return out.toString();
}
protected abstract String header();
protected abstract String[] rows();
protected String footer() { return "-- end --"; } // optional hook
}
static final class TradeReport extends Report {
@Override protected String header() { return "TRADES"; }
@Override protected String[] rows() { return new String[] {"BUY 100 ABC", "SELL 50 XYZ"}; }
}
static final class EmptyReport extends Report {
@Override protected String header() { return "EMPTY"; }
@Override protected String[] rows() { return new String[0]; }
@Override protected String footer() { return "(no rows)"; }
}
public static void main(String[] args) {
System.out.println(new TradeReport().render());
System.out.println(new EmptyReport().render());
}
}TRADES
BUY 100 ABC
SELL 50 XYZ
-- end --
EMPTY
(no rows)This is the template method pattern: the abstract class fixes the order of steps in a final method, and subclasses fill in the steps. Making render final stops a subclass from skipping a step. String, Integer, and the other wrappers are final classes, which guarantees that a String reference never points to a subclass with different behavior. An abstract class can hold state and constructors; an interface cannot hold instance state.
Example 5: interfaces with default, static, and private methods
An interface declares a type that classes implement. Since Java 8 it can have default methods (inherited implementations) and static methods; since Java 9 it can have private methods shared by its default methods. A class can implement many interfaces.
public class Interfaces {
interface Priced {
long priceCents();
default String formatted() { return "$" + dollars() + "." + cents(); }
private long dollars() { return priceCents() / 100; }
private String cents() { return String.format("%02d", priceCents() % 100); }
static Priced free() {
return new Priced() {
@Override public long priceCents() { return 0; }
};
}
}
interface Taxed {
default String formatted() { return "taxed"; }
}
static class Item implements Priced, Taxed {
private final long cents;
Item(long cents) { this.cents = cents; }
@Override public long priceCents() { return cents; }
@Override
public String formatted() { // required: two defaults conflict
return Priced.super.formatted() + " (" + Taxed.super.formatted() + ")";
}
}
public static void main(String[] args) {
Priced p = new Item(1999);
System.out.println(p.formatted());
System.out.println(Priced.free().formatted());
System.out.println(p instanceof Taxed);
}
}$19.99 (taxed)
$0.00
trueItem inherits formatted() from two interfaces, so the compiler requires it to override the method; without the override, compilation fails with "types Priced and Taxed are incompatible" (class Item inherits unrelated defaults for formatted()). Priced.super.formatted() calls one specific default. The resolution rules are: a method in a class or superclass wins over any default; a default in a more specific interface (a subinterface) wins over one in its parent; otherwise the class must override. Interface fields are always public static final constants. Priced.free() uses an anonymous class, explained in Example 7.
Example 6: composition over inheritance
Inheritance couples a subclass to the implementation details of its superclass. Composition means a class holds an instance of another class and forwards calls to it, so it depends only on the public contract. The classic case is counting additions to a HashSet.
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class Composition {
static class CountingSet extends HashSet<String> {
int added = 0;
@Override public boolean add(String s) { added++; return super.add(s); }
@Override
public boolean addAll(java.util.Collection<? extends String> c) {
added += c.size();
return super.addAll(c); // HashSet.addAll calls add() for each element
}
}
static class ForwardingCountingSet {
private final Set<String> inner = new HashSet<>();
int added = 0;
boolean add(String s) { added++; return inner.add(s); }
boolean addAll(List<String> items) {
added += items.size();
return inner.addAll(items); // inner's own add() is not ours
}
int size() { return inner.size(); }
}
public static void main(String[] args) {
List<String> symbols = List.of("AAPL", "MSFT", "GOOG");
CountingSet broken = new CountingSet();
broken.addAll(symbols);
System.out.println("inheritance: added=" + broken.added + " size=" + broken.size());
ForwardingCountingSet ok = new ForwardingCountingSet();
ok.addAll(symbols);
System.out.println("composition: added=" + ok.added + " size=" + ok.size());
}
}inheritance: added=6 size=3
composition: added=3 size=3The addAll override needs the parameter type Collection<? extends String>, a wildcard type taught in Generic Types, Bounds, and Wildcards. HashSet.addAll is inherited from AbstractCollection, whose implementation calls add for each element. That call reaches the subclass override, so every element is counted twice. The subclass broke because of a self-use detail that the superclass never promised. The forwarding class holds a Set and calls it; the inner set's internal calls never reach the wrapper. HashSet<String> and List.of are collection types covered in Lists, Deques, and Iteration; here they only store strings.
| Choose | When |
|---|---|
| Inheritance | A true "is-a" relation, the superclass is designed and documented for extension, both are in the same code base |
| Composition | Reusing behavior of a class not designed for extension, wrapping to add behavior (logging, counting, caching), swapping the implementation |
Example 7: nested, inner, local, and anonymous classes
A static nested class is declared inside another class and has no link to an outer instance. An inner class (non-static) holds a hidden reference to the outer object that created it. A local class is declared inside a method, and an anonymous class is declared and instantiated in one expression.
public class Nesting {
private final String venue;
private int fills = 0;
Nesting(String venue) { this.venue = venue; }
static class Config { // static nested: no outer instance
final int maxQty;
Config(int maxQty) { this.maxQty = maxQty; }
}
class FillRecorder { // inner: has Nesting.this
void record(int qty) {
fills++;
System.out.println(venue + " fill " + qty + " (#" + fills + ")");
}
}
interface Check { boolean ok(int qty); }
Check limitCheck(Config config) {
int min = 1; // effectively final local
class Range implements Check { // local class
@Override public boolean ok(int qty) { return qty >= min && qty <= config.maxQty; }
}
return new Range();
}
public static void main(String[] args) {
Config config = new Config(500);
Nesting nyse = new Nesting("NYSE");
Nesting.FillRecorder recorder = nyse.new FillRecorder();
recorder.record(100);
recorder.record(50);
Check check = nyse.limitCheck(config);
System.out.println(check.ok(100) + " " + check.ok(0) + " " + check.ok(900));
Check evenLots = new Check() { // anonymous class
@Override public boolean ok(int qty) { return qty % 100 == 0; }
};
System.out.println(evenLots.ok(200) + " " + evenLots.ok(150));
System.out.println(recorder.getClass().getName() + " " + evenLots.getClass().getName());
}
}NYSE fill 100 (#1)
NYSE fill 50 (#2)
true false false
true false
Nesting$FillRecorder Nesting$1nyse.new FillRecorder() binds the inner object to nyse, so record reads venue and updates fills of that object. That hidden reference keeps the outer object reachable as long as the inner object lives, which can cause memory leaks when inner objects are stored in long-lived caches or listeners; a static nested class avoids it. Local and anonymous classes can read local variables only if they are final or effectively final (never reassigned), because the class captures a copy of the value. The compiler names these classes Nesting$FillRecorder, Nesting$1Range, and Nesting$1. Lambdas replace most anonymous classes for single-method interfaces; they are taught in Lambdas and Functional Interfaces.
Interview points
- Initialization order: superclass static, subclass static (once per class), then per object: superclass constructor, subclass field initializers and instance blocks in source order, subclass constructor body.
- Calling an overridable method from a constructor sees uninitialized subclass fields.
- Overrides may return a subtype and widen access; they may not narrow access or add broader checked exceptions.
- Static methods are hidden, not overridden; the compile-time type selects them.
- Two conflicting interface defaults force the class to override;
X.super.m()picks one. - Class methods win over interface defaults; more specific interfaces win over less specific ones.
- Inheritance breaks when a superclass changes its self-use; composition with forwarding avoids it.
- Inner classes hold a reference to the outer instance; prefer static nested classes unless that reference is needed.
- Abstract classes can hold state and constructors; interfaces cannot hold instance state.
Summary
- Before Java 25,
this(...)andsuper(...)had to be the first statement of a constructor. Since Java 25 (JEP 513), statements that do not readthismay come first, including assignments to the class's own fields. staticmembers belong to the class; instance members belong to each object.- Access levels:
private, package-private,protected,public. @Overridemakes the compiler check an override.finalmethods cannot be overridden;finalclasses cannot be extended.- Interfaces have abstract,
default,static(Java 8), andprivate(Java 9) methods. - Local and anonymous classes capture effectively final locals.
- Nested class files are named
Outer$Inner,Outer$1Local, andOuter$1.
