17. Oltre le Classi
Indice
- 17.1 Interfacce
- 17.2 Tipi sealed, non-sealed e final
- 17.3 Enum
- 17.4 Record (Java 16+)
- 17.5 Classi Annidate in Java
- 17.6 Nesting delle Interfacce in Java
Questo capitolo presenta diversi meccanismi avanzati di tipo (type) oltre quello, già visto, della Classe: interfacce, enum, classi sealed / non-sealed, record e classi annidate.
17.1 Interfacce
Un’interfaccia in Java è un tipo di riferimento che definisce un contratto di metodi che una classe accetta di implementare.
Una interface è implicitamente abstract e non può essere marcata come final: come per le classi top-level, un’interfaccia può dichiarare visibilità come public o default (package-private).
Una classe Java può implementare un numero qualsiasi di interfacce tramite la keyword implements.
Un interface può a sua volta estendere più interfacce usando la keyword extends.
Le interfacce abilitano astrazione, accoppiamento lasco e ereditarietà multipla di tipo.
17.1.1 Cosa Possono Contenere le Interfacce
- Metodi astratti (implicitamente
publiceabstract) - Metodi concreti
- Metodi default (includono codice e sono implicitamente
public) - Metodi static (dichiarati come
static, includono codice e sono implicitamentepublic) - Metodi private (Java 9+) per riuso interno
- Metodi default (includono codice e sono implicitamente
- Costanti → implicitamente
public static finale inizializzate alla dichiarazione
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
Poiché i metodi astratti delle interfacce sono implicitamente public, non puoi ridurre il livello di accesso su un metodo di implementazione.
17.1.2 Implementare un’Interfaccia
class BasicCalc implements Calculator {
public int add(int a, int b) { return a + b; }
}
Note
Ogni metodo astratto deve essere implementato a meno che la classe non sia astratta essa stessa.
17.1.3 Ereditarietà Multipla
Una classe può implementare più interfacce.
interface A { void a(); }
interface B { void b(); }
class C implements A, B {
public void a() {}
public void b() {}
}
17.1.4 Ereditarietà delle Interfacce e Conflitti
Se due interfacce forniscono metodi default con la stessa signature, la classe che implementa deve fare override del metodo.
interface X { default void run() { } }
interface Y { default void run() { } }
class Z implements X, Y {
public void run() { } // mandatory
}
Se vuoi comunque accedere a una particolare implementazione del metodo default ereditato, puoi usare la seguente sintassi:
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 Metodi Default
Un metodo default (dichiarato con la parola chiave default) è un metodo che definisce un implementazione e può essere sovrascritto da una classe che implementa l interfaccia.
- Un metodo default contiene codice ed è implicitamente
public; - Un metodo default non può essere
abstract,staticofinal; - Un'interfaccia può ridichiarare un metodo default e fornire una implementazione alternativa ;
- Una sottointerfaccia può ridichiarare un metodo statico di una superinterfaccia come metodo
default; - Come abbiamo visto appena sopra, se due interfacce forniscono metodi default con la stessa firma, la classe che implementa deve sovrascrivere il metodo ;
- Una classe che implementa può naturalmente fare affidamento sull implementazione fornita dal metodo
defaultsenza sovrascriverlo ; - Il metodo
defaultpuò essere invocato su un istanza della classe che implementa e NON come metodostaticdell interfaccia che lo contiene ; - Una classe (o un interfaccia) può invocare esplicitamente un metodo default di un interfaccia che è direttamente menzionata nella sua clausola
implements(oextends) utilizzando la sintassiInterfaceName.super.methodName(); ciò è tipicamente utilizzato per risolvere ambiguità tra più metodi default ereditati ; - Questa sintassi può essere utilizzata solo se l interfaccia è esplicitamente menzionata nella clausola
implements(oextends) ; non può essere utilizzata per invocare un metodo default proveniente da un interfaccia ereditata indirettamente ; - La sintassi
InterfaceName.super.methodName()si applica esclusivamente ai metodidefaulte non può essere utilizzata per metodi astratti, metodi statici, metodi privati di interfaccia o campi.
Example: Uso valido
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: Uso errato
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
- Una sottointerfaccia può ridichiarare un metodo statico di una superinterfaccia come metodo
default;
Esempio:
interface Parent {
static void p() { }
}
interface Child extends Parent {
default void p() { } // VALID, static method redeclared as default
}
Note
- Un interfaccia è autorizzata a ridichiarare un metodo
defaultereditato da una superinterfaccia e a trasformarlo in un metodoabstract.
Quando ciò accade, l implementazione default proveniente dalla superinterfaccia viene effettivamente rimossa nella sottointerfaccia.
Di conseguenza, qualsiasi classe che implementa la sottointerfaccia NON erediterà l implementazione default originale e dovrà fornire una propria implementazione.
Esempio:
interface Parent {
default void greet() {
System.out.println("Hello from Parent");
}
}
interface Child extends Parent {
void greet(); // ridichiarato come abstract
}
class Demo implements Child {
public void greet() { // obbligatorio
System.out.println("Hello from Demo");
}
}
Spiegazione:
Parentfornisce un implementazione di default digreet().Childridichiaragreet()senzadefault, rendendolo nuovamente astratto.Demonon può ereditare l implementazione di default diParent.- Pertanto,
Demodeve implementare esplicitamentegreet().
17.1.6 Metodi static
- Un’interfaccia può fornire
static methods(tramite la keywordstatic) che sono implicitamentepublic; - I metodi static devono includere un corpo del metodo e sono accessibili usando il nome dell’interfaccia;
- I metodi static non possono essere
abstractofinal;
17.1.7 Metodi private nelle interfacce
Tra tutti i metodi concreti che un’interfaccia può implementare, abbiamo anche:
- Metodi
private: visibili solo all’interno dell’interfaccia dichiarante e che possono essere invocati solo da un contestonon-static(metodidefaulto altrinon-static private methods). - Metodi
private static: visibili solo all’interno dell’interfaccia dichiarante e che possono essere invocati da qualsiasi metodo dell’interfaccia contenitore.
17.2 Tipi sealed, non-sealed e final
Le classi e le interfacce sealed (Java 17+) restringono quali altre classi (o interfacce) possono estenderle o implementarle.
Un sealed type è dichiarato mettendo il modificatore sealed subito prima della keyword class (o interface), e aggiungendo, dopo il nome del Tipo, la keyword permits seguita dalla lista dei tipi che possono estenderlo (o implementarlo).
public sealed class Shape permits Circle, Rectangle { }
final class Circle extends Shape { }
non-sealed class Rectangle extends Shape { }
17.2.1 Regole
- Un tipo sealed deve dichiarare tutti i sottotipi permessi.
- Un sottotipo permesso deve essere final, sealed o non-sealed; poiché le interfacce non possono essere final, possono essere marcate solo
sealedonon-sealedquando estendono un’interfaccia sealed. - Se una sealed class appartiene a un
namedmodule, allora tutte le classi elencate nella sua permits clause devono anch esse appartenere a quello stessomodule. - Se una sealed class appartiene a un
unnamed module, allora tutte le classi elencate nella sua permits clause devono essere dichiarate nellostesso package.
17.3 Enum
Le enum definiscono un insieme fisso di valori costanti.
Le enum possono dichiarare attributi, costruttori e metodi come le classi regolari ma non possono essere estese.
La lista dei valori dell’enum deve terminare con un punto e virgola (;) nel caso di Enum Complesse, ma questo non è obbligatorio per Enum Semplici.
17.3.1 Definizione di Enum Semplice
enum Day { MON, TUE, WED, THU, FRI, SAT, SUN } // punto e virgola omesso
17.3.2 Enum Complesse con Stato e Comportamento
enum Level {
LOW(1), MEDIUM(5), HIGH(10); // punto e virgola obbligatorio
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 Metodi delle Enum
values()– restituisce un array di tutti i valori costanti che possono essere usati, per esempio, in un ciclofor-eachvalueOf(String)– restituisce la costante per nomeordinal()– indice (int) della costante
17.3.4 Regole
- I costruttori delle enum sono implicitamente
private; - Le enum possono contenere metodi
staticeinstance; - Le enum possono implementare
interfaces; - Le enum non possono essere estese.
17.4 Record (Java 16+)
Un record è una classe speciale progettata per modellare dati immutabili: sono infatti implicitamente final.
Non puoi estendere un record, ma è permesso implementare un’interfaccia regolare o sealed.
Fornisce automaticamente:
- campi private final per ogni componente;
- costruttore con parametri nello stesso ordine della dichiarazione del record;
- getters (con nome degli attributi);
equals(),hashCode(),toString(): è inoltre permesso fare override di questi metodi;- I Record possono includere
nested classes,interfaces,records,enumseannotations.
public record Point(int x, int y) { }
var element = new Point(11, 22);
System.out.println(element.x);
System.out.println(element.y);
Se ti serve validazione o trasformazione aggiuntiva dei campi forniti, puoi definire un costruttore lungo o un costruttore compatto.
17.4.1 Riepilogo delle Regole di Base per i Record
Un record può essere dichiarato in tre posizioni:
- Come record top-level (direttamente in un package)
- Come record member (come membro, all’interno di una classe o interfaccia)
- Come record local (all’interno di un metodo)
Tutte le classi record member e local sono implicitamente static.
- Un record member può dichiarare
staticin modo ridondante. - Un record local non deve dichiarare
staticesplicitamente.
Ogni classe record è implicitamente final.
- Dichiarare
finalesplicitamente è consentito ma ridondante. - Un record non può essere dichiarato
abstract,sealedonon-sealed.
La superclasse diretta di ogni record è java.lang.Record.
- Un record non può dichiarare una clausola
extends. - Un record non può estendere nessun’altra classe.
La serializzazione dei record è diversa rispetto alle classi serializzabili ordinarie.
- Durante la deserializzazione viene invocato il costruttore canonico.
Il corpo di un record può contenere:
- Costruttori
- Metodi
- Campi statici
- Blocchi di inizializzazione statici
Il corpo di un record NON deve contenere:
- Dichiarazioni di campi di istanza
- Blocchi di inizializzazione di istanza
- Metodi
abstract - Metodi
native
17.4.2 Costruttore Lungo
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;
}
}
Puoi anche definire costruttori in overload, purché alla fine deleghino a quello canonico usando this(...):
public record Point(int x, int y) {
// Overloaded constructor (NOT canonical)
public Point(int value) {
this(value, value); // deve invocare, come prima istruzione, un altro costruttore overloaded e, in ultima istanza, il costruttore canonico.
}
}
Note
- Il compilatore non inserirà un costruttore se ne fornisci manualmente uno con la stessa lista di parametri nell’ordine definito;
- In questo caso, devi impostare esplicitamente ogni campo manualmente;
17.4.3 Costruttore Compatto
Puoi definire un costruttore compatto che imposta implicitamente tutti i campi, permettendoti di eseguire validazioni e trasformazioni su campi specifici.
Java eseguirà il costruttore completo, impostando tutti i campi, dopo che il costruttore compatto è terminato.
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
- Se provi a modificare un attributo di Record dentro un Costruttore Compatto, il tuo codice non compilerà
17.4.4 Pattern Matching per i Record
Quando usi pattern matching con instanceof o con switch, un record pattern deve specificare:
- Il tipo del record;
- Un pattern per ogni campo del record (corrispondendo al numero corretto di componenti, e tipi compatibili);
Esempio 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 e Matching dei Record con var e Generics
I nested record patterns permettono di destrutturare record che contengono altri record o tipi complessi, estraendo valori ricorsivamente direttamente nel pattern stesso.
Combinano la potenza della destrutturazione dei record con il pattern matching, dandoti un modo conciso ed espressivo per navigare strutture dati gerarchiche.
17.4.5.1 Nested Record Pattern di Base
Se un record contiene un altro record, puoi destrutturare entrambi in una volta:
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");
}
}
Nell’esempio sopra, il pattern Person include un pattern Address annidato.
Entrambi sono matchati strutturalmente.
17.4.5.2 Nested Record Patterns con var
Invece di specificare tipi esatti per ogni campo, puoi usare var dentro il pattern per lasciare al compilatore l’inferenza del tipo.
switch (obj) {
case Person(var name, Address(var city, var country)) -> System.out.println(name + " — " + city + ", " + country);
}
var nei pattern funziona come var nelle variabili locali: significa "inferisci il tipo".
Warning
- Ti serve ancora il tipo del record contenitore (Person, Address);
- solo i tipi dei campi possono essere sostituiti con
var.
17.4.5.3 Nested Record Patterns e Generics
I record patterns funzionano anche con record generici.
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 questo esempio:
- Il pattern richiede esattamente
Box<String>, nonBox<Integer>. - Dentro il pattern,
var vcattura il valore generico unboxed.
17.4.5.4 Errori Comuni con i Nested Record Patterns
Struttura record non corrispondente
// ❌ ERROR: pattern does not match record structure
case Person(var n, var city) -> ...
Person ha 2 campi, ma uno di questi è un record. Devi destrutturare correttamente.
Numero errato di componenti
// ❌ ERROR: Address has 2 components, not 1
case Person(var n, Address(var onlyCity)) -> ...
Mismatch generico
// ❌ ERROR: expecting Box<String> but found Box<Integer>
case Wrapper(Box<Integer>(var v)) -> ...
Posizionamento illegale di var
// ❌ var cannot replace the record type itself
case var(Person(var n, var a)) -> ...
Note
varnon può sostituire l’intero pattern, solo i singoli componenti.
17.5 Classi Annidate in Java
Java supporta diversi tipi di classi annidate — classi dichiarate dentro un’altra classe.
Sono uno strumento fondamentale per incapsulamento, organizzazione del codice, pattern di event-handling e rappresentazione di gerarchie logiche.
Una classe annidata appartiene sempre a una classe contenitore e ha regole speciali di accessibilità e istanziazione a seconda della sua categoria.
Java definisce quattro tipi di classi annidate:
- Static Nested Classes – dichiarate con
staticdentro un’altra classe. - Inner Classes (non-static nested classes).
- Local Classes – dichiarate dentro un blocco (metodo, costruttore o initializer).
- Anonymous Classes – classi senza nome create inline, di solito per fare override di un metodo o implementare un’interfaccia.
Warning
staticsi applica solo alle classi membro nested- Le classi
Top-level→ non possono essere static - Le classi
Local(dichiarate nei metodi) → non possono essere static - Le classi
Anonymous→ non possono essere static - Una classe
static nestednon può accedere ai membri di istanza senza un riferimento esplicito a un oggetto esterno.
17.5.1 Static Nested Classes
Una static nested class si comporta come una classe top-level con namespace dentro la sua classe contenitore.
Non può accedere ai membri d’istanza della classe esterna ma può accedere ai membri statici.
Non mantiene un riferimento a un’istanza della classe contenitore.
Una classe annidata static può contenere variabili membro non statiche.
17.5.1.1 Sintassi e Regole di Accesso
- Dichiarata usando
static classdentro un’altra classe. - Può accedere solo ai membri static della classe esterna.
- Non ha un riferimento implicito all’istanza contenitore.
- Può essere istanziata senza un’istanza esterna.
- Può contenere variabili membro non statiche
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 Errori Comuni
- Le static nested classes non possono accedere alle variabili d’istanza:
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)
Una inner class è associata a un’istanza della classe esterna e può accedere a tutti i membri della classe esterna, inclusi quelli private.
17.5.2.1 Sintassi e Regole di Accesso
- Dichiarata senza
static. - Ha un riferimento implicito all’istanza contenitore.
- Può accedere sia ai membri statici sia ai membri d’istanza della classe esterna.
- Poiché non è statica, deve essere creata tramite un’istanza della classe contenitore.
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();
}
}
All’interno di una classe interna non-static, è possibile fare riferimento all’oggetto esterno (enclosing object) utilizzando OuterClass.this.
L’espressione InnerClass.this, equivalente a this, si riferisce invece all’oggetto Inner corrente.
- Esempio:
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 Errori Comuni
- Le inner classes non possono dichiarare membri statici eccetto static final constants.
class Outer {
class Inner {
// static int x = 10; // ❌ Compile error
static final int OK = 10; // ✔ Allowed (constant)
}
}
Warning
- Istanziare una inner class SENZA un’istanza esterna è illegale.
17.5.3 Classi Locali
Una classe locale è una classe annidata definita dentro un blocco — più comunemente un metodo.
Non ha modificatori di accesso ed è visibile solo dentro il blocco in cui è dichiarata.
17.5.3.1 Caratteristiche
- Dichiarata dentro un metodo, costruttore o initializer.
- Può accedere ai membri della classe esterna.
- Può accedere a variabili locali se sono effectively final.
- Non può dichiarare membri statici (eccetto 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();
}
}
Una classe locale, proprio come una classe interna membro, possiede un riferimento implicito all’istanza esterna tramite OuterClass.this.
Dispone inoltre di LocalClass.this, equivalente a this, che è valido all’interno del corpo della classe locale.
- Esempio:
class Outer {
int x = 10;
void method() {
class Local {
void print() {
System.out.println(Outer.this.x); // ✔ valido
System.out.println(Local.this); // ✔ valido
}
}
}
}
17.5.3.2 Errori Comuni
basedeve essere effectively final; cambiarla rompe la compilazione.
void compute() {
int base = 5;
base++; // ❌ Now base is NOT effectively final
class Local {}
}
17.5.4 Classi Anonime
Una classe anonima è una classe one-off creata inline, di solito per implementare un’interfaccia o fare override di un metodo senza nominare una nuova classe.
17.5.4.1 Sintassi e Utilizzo
- Creata usando
new+ tipo + body. - Non può avere costruttori (nessun nome).
- Spesso usata per event handling, callbacks, comparators.
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Anonymous running");
}
};
17.5.4.2 Classe Anonima che Estende una Classe
Button b = new Button("Click");
b.onClick(new ClickHandler() {
@Override
public void handle() {
System.out.println("Handled!");
}
});
17.5.5 Confronto dei Tipi di Classi Annidate
Una tabella rapida che riassume tutti i tipi di classi annidate.
| Tipo | Ha un’Istanza Esterna? | Può Accedere ai Membri d’Istanza Esterna? | Può Avere Membri Statici? | Uso Tipico |
|---|---|---|---|---|
| Static Nested | No | No | Sì | Namespacing, helpers |
| Inner Class | Sì | Sì | No (eccetto costanti) | Comportamento legato all’oggetto |
| Local Class | Sì | Sì | No | Classi temporanee con scope |
| Anonymous Class | Sì | Sì | No | Personalizzazione inline |
17.6 Nesting delle Interfacce in Java
In Java, un’interfaccia può essere dichiarata in diverse posizioni e seguire regole specifiche riguardo al nesting e ai membri consentiti.
17.6.1 Dove può essere dichiarata un’interfaccia
Un’interfaccia può essere:
- Top-level (direttamente in un package)
- Nested member interface (dichiarata all’interno di una classe o di un’altra interfaccia)
- Local interface ❌ (non consentita)
- Anonymous interface ❌ (non esiste come dichiarazione, solo implementazioni anonime)
In Java non è permesso dichiarare un’interfaccia locale (cioè dentro un metodo o blocco).
Le interfacce possono essere solo top-level o member.
17.6.2 Interfacce annidate (Nested Interfaces)
Una Nested Interface può essere dichiarata dentro:
17.6.2.1 Interfaccia annidata in una Classe
- È implicitamente
static - Non può essere dichiarata
non-static -
Può essere dichiarata
public,protected,privateopackage-private -
Esempio:
class Outer {
interface InnerInterface {
void test();
}
}
La parola chiave static è implicita:
class Outer {
static interface InnerInterface { // consentito ma ridondante
void test();
}
}
17.6.2.2 Interfaccia annidata in una un’altra Interfaccia
- È implicitamente
publicestatic - Non può essere
privateoprotected
interface A {
interface B {
void test();
}
}
17.6.3 Regole di Accesso
Una nested interface:
- Non ha riferimento implicito a un’istanza della classe esterna
- Non può accedere direttamente ai membri di istanza della classe esterna
- Può accedere solo ai membri
staticdella classe esterna
17.6.4 Nested Types nelle Interfacce
Un’interfaccia può contenere:
- Classi annidate (implicitamente
public static) - Record annidati (implicitamente
public static) - Enum annidati (implicitamente
public static) - Altre interfacce annidate (implicitamente
public static)
17.6.5 Riassunto Essenziale
- Le interfacce nested sono sempre
static - Non esistono interfacce locali
- I campi sono sempre
public static final - I metodi sono implicitamente
public abstract(salvo default/static/private) - Possono contenere altri tipi nested