17. Beyond Classes
Table of Contents
- 17.1 Interfaces
- 17.2 Sealed, non-sealed, and final Types
- 17.3 Enums
- 17.3.1 Simple Enum Definition
- 17.3.2 Complex Enums with State and Behavior
- 17.3.3 Enum Methods
- 17.3.4 Rules
- 17.4 Records Java 16+
- 17.4.1 Summary of Basic Rules for Records
- 17.4.2 Long Constructor
- 17.4.3 Compact Constructor
- 17.4.4 Pattern Matching for Records
- 17.4.5 Nested Record Patterns and Matching Records with var and Generics
- 17.5 Nested Classes in Java
- 17.6 Nesting of Interfaces in Java
This chapter presents several advanced type mechanisms beyond the Java Class design: interfaces, enums, sealed / non-sealed classes, records, and nested classes.
17.1 Interfaces
An interface in Java is a reference type that defines a contract of methods that a class agrees to implement.
An interface is implicitly abstract and cannot be marked as final: as with top-level classes, an interface can declare visibility as public or default (package-private).
A Java class may implement any number of interfaces through the implements keyword.
An interface may in turn extend multiple interfaces using the extends keyword.
Interfaces enable abstraction, loose coupling, and multiple inheritance of type.
17.1.1 What Interfaces Can Contain
- Abstract methods (implicitly
publicandabstract) - Concrete methods
- Default methods (include code and are implicitly
public) - Static methods (declared as
static, include code and are implicitlypublic) - Private methods (Java 9+) for internal reuse
- Default methods (include code and are implicitly
- Constants → implicitly
public static finaland initialized at declaration
interface Calculator {
int add(int a, int b); // abstract
default int mult(int a, int b) { // default method
return a * b;
}
static double pi() { return 3.14; } // static method
}
Warning
Because interface abstract methods are implicitly public, you cannot reduce the access level on an implementing method.
17.1.2 Implementing an Interface
class BasicCalc implements Calculator {
public int add(int a, int b) { return a + b; }
}
Note
Every abstract method must be implemented unless the class is abstract.
17.1.3 Multiple Inheritance
A class may implement multiple interfaces.
interface A { void a(); }
interface B { void b(); }
class C implements A, B {
public void a() {}
public void b() {}
}
17.1.4 Interface Inheritance and Conflicts
If two interfaces provide default methods with the same signature, the implementing class must override the method.
interface X { default void run() { } }
interface Y { default void run() { } }
class Z implements X, Y {
public void run() { } // mandatory
}
If you still want to access a particular implementation of the inherited default method, you can use the following syntax:
interface X { default int run() { return 1; } }
interface Y { default int run() { return 2; } }
class Z implements X, Y {
public int useARun(){
return Y.super.run();
}
}
17.1.5 Default methods
A default method (declared with the default keyword) is a method that defines an implementation and can be overridden by a class implementing the interface.
- A default method includes code and is implicitly
public; - A default method cannot be
abstract,static, orfinal; - An interface can redeclare a default method and provide a different implementation ;
- A subinterface is allowed to redeclare a static method from a superinterface as a
defaultmethod. - As we saw just above, if two interfaces provide default methods with the same signature, the implementing class must override the method;
- An implementing class may of course rely on the provided implementation of the
defaultmethod without overriding it; - The
defaultmethod can be invoked on an instance of the implementing class and NOT as astaticmethod of the containing interface; - A class (or an interface) can explicitly invoke a default method of an interface that is directly listed in its
implementsclause (orextendsclause) by using the syntaxInterfaceName.super.methodName(); this is typically used to disambiguate multiple inherited default methods; - This syntax can only be used if the interface is explicitly mentioned in the
implements(orextends) clause; it cannot be used to invoke a default method from an indirectly inherited interface; - The
InterfaceName.super.methodName()syntax applies only todefaultmethods and cannot be used for abstract methods, static methods, private interface methods, or fields.
Example: Valid Usage
interface A {
default void hello() {
System.out.println("Hello from A");
}
}
class B implements A {
@Override
public void hello() {
A.super.hello(); // ✅ allowed
System.out.println("Hello from B");
}
}
Example: Invalid Usage
interface A {
default void hello() {
System.out.println("Hello from A");
}
}
interface B extends A {
}
class C implements B {
public void test() {
A.super.hello(); // ❌ compilation error
}
}
Note
- A subinterface is allowed to redeclare a static method from a superinterface as a
defaultmethod.
Example:
interface Parent {
static void p() { }
}
interface Child extends Parent {
default void p() { } // VALID, static method redeclared as default
}
Note
- An interface is allowed to redeclare a
defaultmethod inherited from a superinterface and turn it into anabstractmethod.
When this happens, the default implementation from the superinterface is effectively removed in the subinterface.
As a consequence, any class that implements the subinterface will NOT inherit the original default implementation and must provide its own implementation.
Example:
interface Parent {
default void greet() {
System.out.println("Hello from Parent");
}
}
interface Child extends Parent {
void greet(); // redeclared as abstract
}
class Demo implements Child {
public void greet() { // mandatory
System.out.println("Hello from Demo");
}
}
Explanation:
Parentprovides a default implementation ofgreet().Childredeclaresgreet()withoutdefault, making it abstract again.Democannot inherit the default implementation fromParent.- Therefore,
Demomust implementgreet()explicitly.
17.1.6 Static methods
- An interface can provide
static methods(through the keywordstatic) which are implicitlypublic; - Static methods must include a method body and are accessed using the interface name;
- Static methods cannot be
abstractorfinal;
17.1.7 Private interface methods
Among all the concrete methods that an interface can implement, we also have:
privatemethods: visible only inside the declaring interface and which can only be invoked from anon-staticcontext (defaultmethods or othernon-static private methods).private staticmethods: visible only inside the declaring interface and which can be invoked by any method of the enclosing interface.
17.2 Sealed, non-sealed, and final Types
Sealed classes and interfaces (Java 17+) restrict which other classes (or interfaces) can extend or implement them.
A sealed type is declared by placing the sealed modifier right before the class (or interface) keyword, and adding, after the Type name, the permits keyword followed by the list of types that can extend (or implement) it.
public sealed class Shape permits Circle, Rectangle { }
final class Circle extends Shape { }
non-sealed class Rectangle extends Shape { }
17.2.1 Rules
- A sealed Type must declare all permitted subtypes.
- A permitted subtype must be final, sealed, or non-sealed; because interfaces cannot be final, they can only be marked
sealedornon-sealedwhen extending a sealed interface. - If a sealed class belongs to a
namedmodule, then all the classes listed in its permits clause must also belong to thatsame module. - If a sealed class belongs to an
unnamed module, then all the classes listed in its permits clause must be declared in thesame package.
17.3 Enums
Enums define a fixed set of constant values.
Enums can declare fields, constructors, and methods as regular classes do but they can't be extended.
The list of enum values must end with a semicolon (;) in case of Complex Enums, but this is not mandatory for Simple Enums.
17.3.1 Simple Enum Definition
enum Day { MON, TUE, WED, THU, FRI, SAT, SUN } // semicolon not present
17.3.2 Complex Enums with State and Behavior
enum Level {
LOW(1), MEDIUM(5), HIGH(10); // mandatory semicolon
private int code;
Level(int code) { this.code = code; }
public int getCode() { return code; }
}
public static void main(String[] args) {
Level.MEDIUM.getCode(); // invoking a method
}
17.3.3 Enum Methods
values()– returns an array of all the constant values that can be used, for example, in afor-eachloopvalueOf(String)– returns constant by nameordinal()– index (int) of the constant
17.3.4 Rules
- Enum constructors are implicitly
private; - Enums can contain
staticandinstancemethods; - Enums can implement
interfaces; - Enums cannot be extended.
17.4 Records (Java 16+)
A record is a special class designed to model immutable data: they are, in fact, implicitly final.
You can't extend a record, but it is allowed to implement a regular or sealed interface.
It automatically provides:
- private final fields for each component;
- constructor with parameters in the same order as in the record declaration;
- getters (named like fields);
equals(),hashCode(),toString(): you are also permitted to override those methods;- Records can include
nested classes,interfaces,records,enumsandannotations.
public record Point(int x, int y) { }
var element = new Point(11, 22);
System.out.println(element.x);
System.out.println(element.y);
If you need additional validation or transformation of the provided fields, you can define a long constructor or a compact constructor.
17.4.1 Summary of Basic Rules for Records
A record may be declared in three locations:
- As a top-level record (directly in a package)
- As a member record (inside a class or interface)
- As a local record (inside a method)
All member and local record classes are implicitly static.
- A member record may redundantly declare
static. - A local record must not declare
staticexplicitly.
Every record class is implicitly final.
- Declaring
finalexplicitly is permitted but redundant. - A record cannot be declared
abstract,sealed, ornon-sealed.
The direct superclass of every record is java.lang.Record.
- A record cannot declare an
extendsclause. - A record cannot extend any other class.
Serialization of records differs from ordinary serializable classes.
- During deserialization, the canonical constructor is invoked.
The body of a record may contain:
- Constructors
- Methods
- Static fields
- Static initializer blocks
The body of a record must NOT contain:
- Instance field declarations
- Instance initializer blocks
abstractmethodsnativemethods
17.4.2 Long Constructor
public record Person(String name, int age) {
public Person (String name, int age){
if (age < 0) throw new IllegalArgumentException();
this.name = name;
this.age = age;
}
}
You can still define overloaded constructors, as long as they ultimately delegate to the canonical one using this(...):
public record Point(int x, int y) {
// Overloaded constructor (NOT canonical)
public Point(int value) {
this(value, value); // must call, in the first line, another overloaded constructor and, ultimately, the canonical one.
}
}
Note
- The compiler will not insert a constructor if you manually provide one with the same list of parameters in the defined order;
- In this case, you must explicitly set every field manually;
17.4.3 Compact Constructor
You can define a compact constructor which implicitly sets all fields, while letting you perform validations and transformations on selected fields.
Java will execute the full constructor, setting all fields, after the compact constructor has completed.
public record Person(String name, int age) {
public Person {
if (age < 0) throw new IllegalArgumentException();
name = name.toUpperCase(); // This transformation is still (at this level of initialization) on the input parameter.
// this.name = name; // ❌ Does not compile.
}
}
Warning
- If you try to modify a Record field inside a Compact Constructor, your code will not compile
17.4.4 Pattern Matching for Records
When you use pattern matching with instanceof or with switch, a record pattern must specify:
- The record type;
- A pattern for each field of the record (matching the correct number of components, and compatible types);
Example record:
Object obj = new Point(3, 5);
if (obj instanceof Point(int a, int b)) {
System.out.println(a + b); // 8
}
17.4.5 Nested Record Patterns and Matching Records with var and Generics
Nested record patterns allow you to destructure records that contain other records or complex types, extracting values recursively directly within the pattern itself.
They combine the power of record deconstruction with pattern matching, giving you a concise and expressive way to navigate hierarchical data structures.
17.4.5.1 Basic Nested Record Pattern
If a record contains another record, you can destructure both at once:
record Address(String city, String country) {}
record Person(String name, Address address) {}
void printInfo(Object obj) {
switch (obj) {
case Person(String n, Address(String c, String co)) -> System.out.println(n + " lives in " + c + ", " + co);
default -> System.out.println("Unknown");
}
}
In the example above, the Person pattern includes a nested Address pattern.
Both are matched structurally.
17.4.5.2 Nested Record Patterns with var
Instead of specifying exact types for each field, you can use var inside the pattern to let the compiler infer the type.
switch (obj) {
case Person(var name, Address(var city, var country)) -> System.out.println(name + " — " + city + ", " + country);
}
var in patterns works like var in local variables: it means "infer the type".
Warning
- You still need the enclosing record type (Person, Address);
- only the field types can be replaced with
var.
17.4.5.3 Nested Record Patterns and Generics
Record patterns also work with generic records.
record Box<T>(T value) {}
record Wrapper(Box<String> box) {}
static void test(Object o) {
switch (o) {
case Wrapper(Box<String>(var v)) -> System.out.println("Boxed string: " + v);
default -> System.out.println("Something else");
}
}
In this example:
- The pattern requires exactly
Box<String>, notBox<Integer>. - Inside the pattern,
var vcaptures the unboxed generic value.
17.4.5.4 Common Errors with Nested Record Patterns
Mismatched record structure
// ❌ ERROR: pattern does not match record structure
case Person(var n, var city) -> ...
Person has 2 fields, but one of them is a record. You must destructure correctly.
Wrong number of components
// ❌ ERROR: Address has 2 components, not 1
case Person(var n, Address(var onlyCity)) -> ...
Generic mismatch
// ❌ ERROR: expecting Box<String> but found Box<Integer>
case Wrapper(Box<Integer>(var v)) -> ...
Illegal placement of var
// ❌ var cannot replace the record type itself
case var(Person(var n, var a)) -> ...
Note
varcannot stand in for the whole pattern, only for individual components.
17.5 Nested Classes in Java
Java supports several kinds of nested classes — classes declared inside another class.
They are a fundamental tool for encapsulation, code organization, event-handling patterns, and representing logical hierarchies.
A nested class always belongs to an enclosing class and has special accessibility and instantiation rules depending on its category.
Java defines four kinds of nested classes:
- Static Nested Classes – declared with
staticinside another class. - Inner Classes (non-static nested classes).
- Local Classes – declared inside a block (method, constructor, or initializer).
- Anonymous Classes – unnamed classes created inline, usually to override a method or implement an interface.
Warning
staticapplies only to nested member classesTop-levelclasses → cannot be staticLocalclasses (inside methods) → cannot be staticAnonymousclasses → cannot be static- A
static nestedclass cannot access instance members without an explicit outer object reference.
17.5.1 Static Nested Classes
A static nested class behaves like a top-level class that is namespaced inside its enclosing class.
It cannot access instance members of the outer class but can access static members.
It does not hold a reference to an instance of the enclosing class.
A static nested class can contain non-static member variables.
17.5.1.1 Syntax and Access Rules
- Declared using
static classinside another class. - Can access only static members of the outer class.
- Does not have an implicit reference to the enclosing instance.
- Can be instantiated without an outer instance.
- Can contain non-static member variables.
class Outer {
static int version = 1;
static class Nested {
void print() {
System.out.println("Version: " + version); // OK: accessing static member
}
}
}
class Test {
public static void main(String[] args) {
Outer.Nested n = new Outer.Nested(); // No Outer instance required
n.print();
}
}
17.5.1.2 Common Pitfalls
- Static nested classes cannot access instance variables:
class Outer {
int x = 10;
static class Nested {
void test() {
// System.out.println(x); // ❌ Compile error
}
}
}
17.5.2 Inner Classes (Non-Static Nested Classes)
An inner class is associated with an instance of the outer class and can access all members of the outer class, including private ones.
17.5.2.1 Syntax and Access Rules
- Declared without
static. - Has an implicit reference to the enclosing instance.
- Can access both static and instance members of the outer class.
- Since it is not static, it must be created through an instance of the enclosing class.
class Outer {
private int value = 100;
class Inner {
void print() {
System.out.println("Value = " + value); // OK: accessing private
}
}
void make() {
Inner i = new Inner(); // OK inside the outer class
i.print();
}
}
class Test {
public static void main(String[] args) {
Outer o = new Outer();
Outer.Inner i = o.new Inner(); // MUST be created from an instance
i.print();
}
}
Inside the non-static inner class, you can refer to the enclosing object using OuterClass.this and
InnerClass.this, which is equivalent to this, refers to the current Inner object:
- Example:
class Outer {
int x = 10;
class Inner {
int x = 20;
void print() {
System.out.println(x); // 20 (Inner.this.x)
System.out.println(this.x); // 20
System.out.println(Outer.this.x); // 10
}
}
}
17.5.2.2 Common Pitfalls
- Inner classes cannot declare static members except static final constants.
class Outer {
class Inner {
// static int x = 10; // ❌ Compile error
static final int OK = 10; // ✔ Allowed (constant)
}
}
Warning
- Instantiating an inner class WITHOUT an outer instance is illegal.
17.5.3 Local Classes
A local class is a nested class defined inside a block — most commonly a method.
It has no access modifier and is visible only within the block where it is declared.
17.5.3.1 Characteristics
- Declared inside a method, constructor, or initializer.
- Can access members of the outer class.
- Can access local variables if they are effectively final.
- Cannot declare static members (except static final constants).
class Outer {
void compute() {
int base = 5; // must be effectively final
class Local {
void show() {
System.out.println(base); // OK
}
}
new Local().show();
}
}
A local class, just like in a member inner class, has an implicit reference to the enclosing instance using OuterClass.this and also LocalClass.this, equivalent to this, is valid inside the local class body.
- Example:
class Outer {
int x = 10;
void method() {
class Local {
void print() {
System.out.println(Outer.this.x); // ✔ valid
System.out.println(Local.this); // ✔ valid
}
}
}
}
17.5.3.2 Common Pitfalls
basemust be effectively final; changing it breaks compilation.
void compute() {
int base = 5;
base++; // ❌ Now base is NOT effectively final
class Local {}
}
17.5.4 Anonymous Classes
An anonymous class is a one-off class created inline, usually to implement an interface or override a method without naming a new class.
17.5.4.1 Syntax and Usage
- Created using
new+ type + body. - Cannot have constructors (no name).
- Often used for event handling, callbacks, comparators.
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Anonymous running");
}
};
17.5.4.2 Anonymous Class Extending a Class
Button b = new Button("Click");
b.onClick(new ClickHandler() {
@Override
public void handle() {
System.out.println("Handled!");
}
});
17.5.5 Comparison of Nested Class Types
A quick table summarizing all kinds of nested classes.
| Type | Has Outer Instance? | Can Access Outer Instance Members? | Can Have Static Members? | Typical Use |
|---|---|---|---|---|
| Static Nested | No | No | Yes | Namespacing, helpers |
| Inner Class | Yes | Yes | No (except constants) | Object-bound behavior |
| Local Class | Yes | Yes | No | Temporary scoped classes |
| Anonymous Class | Yes | Yes | No | Inline customization |
17.6 Nesting of Interfaces in Java
In Java, an interface can be declared in different locations and follows specific rules regarding nesting and permitted members.
17.6.1 Where an Interface Can Be Declared
An interface can be:
- Top-level (directly inside a package)
- Nested member interface (declared inside a class or another interface)
- Local interface ❌ (not allowed)
- Anonymous interface ❌ (does not exist as a declaration, only anonymous implementations exist)
In Java, it is not permitted to declare a local interface (that is, inside a method or block).
Interfaces can only be top-level or member.
17.6.2 Nested Interfaces
A Nested Interface can be declared inside:
17.6.2.1 Interface Nested Inside a Class
- It is implicitly
static - It cannot be declared
non-static -
It may be declared
public,protected,private, orpackage-private -
Example:
class Outer {
interface InnerInterface {
void test();
}
}
The static keyword is implicit:
class Outer {
static interface InnerInterface { // allowed but redundant
void test();
}
}
17.6.2.2 Interface Nested Inside Another Interface
- It is implicitly
publicandstatic - It cannot be
privateorprotected
interface A {
interface B {
void test();
}
}
17.6.3 Access Rules
A nested interface:
- Does not have an implicit reference to an instance of the enclosing class
- Cannot directly access instance members of the enclosing class
- Can access only
staticmembers of the enclosing class
17.6.4 Nested Types Inside Interfaces
An interface may contain:
- Nested classes (implicitly
public static) - Nested records (implicitly
public static) - Nested enums (implicitly
public static) - Other nested interfaces (implicitly
public static)
17.6.5 Essential Summary
- Nested interfaces are always
static - Local interfaces do not exist
- Fields are always
public static final - Methods are implicitly
public abstract(except default/static/private) - They may contain other nested types