Parola chiave final in Java
Rendi costanti le variabili Java, impedisci l'override dei metodi e vieta l'ereditarietà delle classi con la parola chiave final.
final significa "impostalo una volta, poi non cambiarlo più." Si applica a quattro cose e l'effetto dipende da quale:
- Su una variabile, il binding non può essere riassegnato.
- Su un metodo, il metodo non può essere sovrascritto da una sottoclasse.
- Su una classe, la classe non può essere estesa.
- Su un parametro, il parametro non può essere riassegnato all'interno del metodo.
Il filo conduttore è "nessuna ulteriore modifica dopo il binding iniziale." Ciò che viene esattamente vietato dipende da ciò che viene contrassegnato.
Variabili locali final
La forma più semplice: una variabile locale final non può essere riassegnata dopo la sua prima assegnazione.
void demo() {
final int max = 100;
max = 50; // ERROR: cannot assign a value to final variable max
}Non è necessario inizializzarla alla dichiarazione — è possibile differire la prima assegnazione, purché si assegni esattamente una volta prima di qualsiasi lettura:
final int x;
if (condition) x = 1;
else x = 2;
System.out.println(x); // ok — x was definitely assignedfinal su una variabile locale è principalmente uno strumento di documentazione — dice a un futuro lettore (e al compilatore) "questo non cambierà dopo questo punto." Le acquisizioni di lambda e inner-class lo richiedono: qualsiasi variabile locale usata all'interno di esse deve essere final o effettivamente final (mai riassegnata).
Parametri final
Un parametro final non può essere riassegnato all'interno del corpo del metodo:
void greet(final String name) {
name = name.toUpperCase(); // ERROR
}Alcuni team richiedono final su ogni parametro; altri lo trovano rumore visivo. Entrambi vanno bene — ciò che conta è la coerenza all'interno di un codebase. L'abitudine di mutare i parametri che scoraggia è una vera fonte di bug.
Campi final
Un campo final viene assegnato esattamente una volta — alla dichiarazione, in un inizializzatore di istanza o nel costruttore — e poi mai più:
public class Point {
private final int x, y;
public Point(int x, int y) {
this.x = x; // assigned once
this.y = y;
}
// no setX or setY — there's no way to change the values
}Questo è il nucleo di una classe immutabile. Ogni campo è final; nulla può mutare l'oggetto dopo la costruzione.
Il compilatore verifica che ogni campo final venga assegnato esattamente una volta su ogni percorso del costruttore. Manca un percorso, si ottiene un errore di compilazione. Si assegna due volte, si ottiene un errore di compilazione.
static final — costanti
La forma standard per una costante è public static final più un nome UPPER_SNAKE:
public static final double PI = 3.141592653589793;
public static final int MAX_RETRY = 3;Static, perché non c'è motivo di mantenere una copia per istanza; final, perché è una costante. Per le costanti primitive e String, il compilatore integra il valore in ogni sito di chiamata. Una conseguenza: se si cambia il valore di tale costante, le classi che vi facevano riferimento mantengono il vecchio valore finché non vengono ricompilate — il letterale era stato incorporato nel loro bytecode al momento della loro compilazione.
final e i riferimenti
Un malinteso comune: final congela il riferimento, non l'oggetto a cui punta. Con un array o una lista final, è comunque possibile mutare il contenuto:
final int[] nums = {1, 2, 3};
nums[0] = 99; // ok — mutating the array, not reassigning the reference
nums = new int[5]; // ERROR — reassigning the reference
final List<String> names = new ArrayList<>();
names.add("Rex"); // ok — mutates the list
names = List.of(); // ERRORSe si vuole una lista il cui contenuto sia anch'esso immutabile, la si avvolge con List.copyOf(...) o Collections.unmodifiableList(...).
Metodi final
final su un metodo significa nessuna sottoclasse può sovrascriverlo:
public class Shape {
public final String describe() { // can never be overridden
return getClass().getSimpleName() + " area=" + area();
}
public double area() { return 0; }
}
public class Circle extends Shape {
public String describe() { return "circle"; } // ERROR
}Si contrassegna un metodo come final quando sovrascriverlo romperebbe le invarianti su cui la classe fa affidamento. È una scelta di design decisa — la maggior parte dei metodi non è final nei codebase reali — ma per i template che non devono cambiare, è lo strumento giusto.
Classi final
final su una classe significa nessuna classe può estenderla:
public final class Money { ... }
public class CryptoMoney extends Money { } // ERRORLo si usa quando l'estensione vanificherebbe il design. La libreria standard lo fa di routine: String, Integer, Long, Double e il resto dei wrapper primitivi sono tutti final — estenderli permetterebbe di introdurre comportamenti mutabili in un tipo che il linguaggio tratta come immutabile.
final non è immutabilità
Un campo final da solo non rende un oggetto immutabile; impedisce solo al riferimento di cambiare. La vera immutabilità richiede:
- Ogni campo
final. - La classe stessa
final, o costruita con cura per impedire alle sottoclassi di aggiungere stato mutabile. - Nessun metodo che esponga un oggetto interno mutabile (copie difensive in uscita).
- Nessun metodo che permetta a un chiamante di mutare lo stato attraverso di esso.
I record (trattati in records) raggruppano automaticamente la maggior parte di questo.
Un esempio pratico
Cosa c'è dopo
I campi private e l'immutabilità con final sono i mattoni dell'idea successiva: nascondere lo stato dietro i metodi di una classe del tutto. Il capitolo sull'incapsulamento unisce questi fili.