Best practice da «Effective Java (3rd Edition)»
Estratto da «Effective Java (3rd Edition) » di Joshua Bloch
1. Restituisci collection o array vuoti, non null
Se il metodo restituisce un Set, per un risultato vuoto non scrivere return null;, ma return Set.of();.
2. Valuta i metodi factory statici al posto dei costruttori
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
Non sono sempre migliori, ma in alcuni casi d’uso offrono numerosi vantaggi:
- Un vantaggio dei metodi factory statici è che, a differenza dei costruttori, hanno un nome.
- Non sono obbligati a creare un nuovo oggetto ogni volta che vengono invocati.
- Possono restituire un oggetto di qualsiasi sottotipo del loro tipo di ritorno.
- La classe dell’oggetto restituito può variare da una chiamata all’altra in funzione dei parametri di input.
- La classe dell’oggetto restituito non deve necessariamente esistere quando viene scritta la classe che contiene il metodo.
Svantaggi:
- Il limite principale dell’offrire solo metodi factory statici è che le classi senza costruttori public o protected non possono essere estese.
- Un secondo difetto dei metodi factory statici è che per i programmatori sono difficili da trovare.
3. Valuta un builder quando i parametri del costruttore sono tanti
Factory statiche e costruttori hanno un limite in comune: non scalano bene quando i parametri opzionali sono molti. Questo punto ha colpito molto duramente alcuni progetti a cui ho lavorato.
4. Imponi la proprietà di singleton con un costruttore privato o un tipo enum
Tutto ciò che non ha bisogno di più istanze dovrebbe essere istanziato una sola volta, che sia con @ApplicationScoped o con un altro pattern singleton.
5. Impedisci l’istanziazione con un costruttore privato
Ogni tanto vorrai scrivere una classe che è solo un raggruppamento di metodi statici e campi statici. Classi del genere si sono fatte una cattiva reputazione, perché alcuni ne abusano per evitare di ragionare in termini di oggetti, ma hanno usi legittimi.
private MyStaticClass() {
// private constructor to prevent instantiation
}
6. Evita di creare oggetti non necessari
String s = new String("bikini"); // DON'T DO THIS!
Oppure:
// Hideously slow! Can you spot the object creation?
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
Soluzione: questo programma dà la risposta giusta, ma è molto più lento del dovuto, a causa di un errore di battitura di un solo carattere. La variabile sum è dichiarata come Long invece che come long, il che significa che il programma costruisce circa 2^31 istanze di Long non necessarie (all’incirca una per ogni volta che il long i viene sommato al Long sum).
7. Evita finalizer e cleaner
I finalizer sono imprevedibili, spesso pericolosi e in genere superflui. Il loro uso può causare comportamenti erratici, prestazioni scadenti e problemi di portabilità.
8. Preferisci try-with-resources a try-finally
// try-with-resources - the best way to close resources!
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
9. Valuta di implementare Comparable
Implementando Comparable permetti alla tua classe di interagire con tutti i numerosi algoritmi generici e le implementazioni di collection che dipendono da questa interfaccia.
10. Riduci al minimo l’accessibilità di classi e membri
Il fattore più importante in assoluto che distingue un componente ben progettato da uno progettato male è il grado in cui il componente nasconde i propri dati interni e gli altri dettagli implementativi agli altri componenti. Un componente ben progettato nasconde tutti i suoi dettagli implementativi, separando nettamente la sua API dalla sua implementazione. I componenti comunicano quindi solo tramite le loro API e ignorano il funzionamento interno degli altri. Questo concetto, noto come information hiding o incapsulamento, è un principio fondamentale della progettazione del software.
11. Nelle classi pubbliche usa metodi di accesso, non campi pubblici
A volte potresti essere tentato di scrivere classi degeneri che non servono ad altro che a raggruppare campi di istanza:
// Degenerate classes like this should not be public!
class Point {
public double x;
public double y;
}
Poiché ai campi dati di queste classi si accede direttamente, esse non offrono i vantaggi dell’incapsulamento. Non puoi cambiare la rappresentazione senza cambiare l’API, non puoi imporre invarianti e non puoi eseguire azioni accessorie quando si accede a un campo. I programmatori orientati agli oggetti più intransigenti ritengono che classi del genere siano un anatema e vadano sempre sostituite da classi con campi privati e metodi di accesso pubblici (getter) e, per le classi mutabili, da mutatori (setter):
// Encapsulation of data by accessor methods and mutators
class Point {
private double x;
private double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() { return x; }
public double getY() { return y; }
public void setX(double x) { this.x = x; }
public void setY(double y) { this.y = y; }
}
Certo, quando si tratta di classi pubbliche gli intransigenti hanno ragione: se una classe è accessibile al di fuori del suo package, fornisci metodi di accesso per preservare la flessibilità di cambiarne la rappresentazione interna. Se una classe pubblica espone i suoi campi dati, ogni speranza di cambiarne la rappresentazione è perduta, perché il codice client può essere distribuito in lungo e in largo.
Se invece una classe è package-private o è una classe annidata privata, non c’è nulla di intrinsecamente sbagliato nell’esporne i campi dati, ammesso che descrivano adeguatamente l’astrazione fornita dalla classe.
12. Riduci al minimo la mutabilità
Una classe immutabile è semplicemente una classe le cui istanze non possono essere modificate. Tutte le informazioni contenute in ogni istanza restano fisse per tutta la vita dell’oggetto, quindi non si può mai osservare alcun cambiamento. Le librerie della piattaforma Java contengono molte classi immutabili, tra cui String, le classi dei primitivi boxed, BigInteger e BigDecimal. I buoni motivi sono tanti: le classi immutabili sono più facili da progettare, implementare e usare rispetto a quelle mutabili. Sono meno soggette a errori e più sicure. Per rendere immutabile una classe, segui queste cinque regole:
- Non fornire metodi che modificano lo stato dell’oggetto (i cosiddetti mutatori).
-
Assicurati che la classe non possa essere estesa. Così eviti che sottoclassi scritte con negligenza o malevole compromettano il comportamento immutabile della classe, comportandosi come se lo stato
dell’oggetto fosse cambiato. In genere si impedisce l’estensione rendendo la classe
final, per esempio. - Rendi final tutti i campi. Esprime chiaramente le tue intenzioni in un modo che il sistema fa rispettare. Inoltre è necessario per garantire un comportamento corretto quando un riferimento a un’istanza appena creata passa da un thread a un altro senza sincronizzazione, come specificato nel memory model.
- Rendi privati tutti i campi. Così impedisci ai client di accedere agli oggetti mutabili referenziati dai campi e di modificarli direttamente. Sebbene tecnicamente sia consentito che le classi immutabili abbiano campi public final contenenti valori primitivi o riferimenti a oggetti immutabili, non è consigliabile, perché impedisce di cambiare la rappresentazione interna in una release successiva.
-
Garantisci l’accesso esclusivo a eventuali componenti mutabili. Se la tua classe ha campi che fanno riferimento a oggetti mutabili, assicurati che i client della classe non possano ottenere riferimenti a
questi oggetti. Non inizializzare mai un campo del genere con un riferimento fornito dal client e non restituire il campo da un metodo di accesso. Crea copie difensive nei costruttori, nei metodi di accesso e nei metodi
readObject.
13. Preferisci la composizione all’ereditarietà
L’ereditarietà è un modo potente per riutilizzare il codice, ma non sempre è lo strumento migliore. Usata in modo inappropriato, porta a software fragile. È sicuro usare l’ereditarietà all’interno di un package, dove le implementazioni della sottoclasse e della superclasse sono sotto il controllo degli stessi programmatori. È sicuro anche usarla quando si estendono classi progettate e documentate apposta per essere estese. Ereditare da normali classi concrete oltre i confini del package, invece, è pericoloso.
A differenza dell’invocazione di metodi, l’ereditarietà viola l’incapsulamento. Potresti pensare che estendere una classe sia sicuro se ti limiti ad aggiungere nuovi metodi e non sovrascrivi quelli esistenti. Anche se questo tipo di estensione è molto più sicuro, non è privo di rischi. Se in una release successiva la superclasse acquisisce un nuovo metodo e tu hai avuto la sfortuna di dare alla sottoclasse un metodo con la stessa firma e un tipo di ritorno diverso, la tua sottoclasse non compilerà più. Se invece hai dato alla sottoclasse un metodo con la stessa firma e lo stesso tipo di ritorno del nuovo metodo della superclasse, ora lo stai sovrascrivendo, e quindi sei esposto ai problemi descritti in precedenza. Inoltre è dubbio che il tuo metodo rispetti il contratto del nuovo metodo della superclasse, perché quel contratto non era ancora stato scritto quando hai scritto il metodo della sottoclasse.
Per fortuna esiste un modo per evitare tutti i problemi descritti sopra. Invece di estendere una classe esistente, dai alla tua nuova classe un campo privato che fa riferimento a un’istanza della classe esistente. Questo design si chiama composizione, perché la classe esistente diventa un componente di quella nuova. Ogni metodo di istanza della nuova classe invoca il metodo corrispondente sull’istanza contenuta della classe esistente e ne restituisce i risultati. Questo si chiama inoltro (forwarding), e i metodi della nuova classe si chiamano metodi di inoltro. La classe risultante sarà solidissima, senza alcuna dipendenza dai dettagli implementativi della classe esistente. Persino l’aggiunta di nuovi metodi alla classe esistente non avrà alcun impatto sulla nuova classe.
14. Preferisci le interfacce alle classi astratte
Java ha due meccanismi per definire un tipo che ammette più implementazioni: le interfacce e le classi astratte. Dall’introduzione dei metodi default nelle interfacce in Java 8, entrambi i meccanismi permettono di fornire implementazioni per alcuni metodi di istanza.
Una differenza fondamentale è che, per implementare il tipo definito da una classe astratta, una classe deve essere una sottoclasse della classe astratta. Poiché Java consente solo l’ereditarietà singola, questa restrizione sulle classi astratte ne limita fortemente l’uso come definizioni di tipo. Qualsiasi classe che definisca tutti i metodi richiesti e rispetti il contratto generale può implementare un’interfaccia, indipendentemente da dove si trovi nella gerarchia delle classi. Le classi esistenti possono essere facilmente adattate per implementare una nuova interfaccia.
- Le interfacce sono ideali per definire mixin.
- Le interfacce permettono di costruire framework di tipi non gerarchici.
- Le interfacce consentono estensioni di funzionalità sicure e potenti tramite l’idioma della classe wrapper. Se usi classi astratte per definire i tipi, a chi vuole aggiungere funzionalità non lasci altra alternativa che l’ereditarietà.
15. Generics: non usare i raw type
Non scopri l’errore fino al runtime, molto tempo dopo che si è verificato, e in un punto del codice che può essere lontano da quello che contiene l’errore. Quando vedi la ClassCastException, devi cercare in tutta la codebase l’invocazione del metodo che ha messo la moneta nella collezione di francobolli. Il compilatore non può aiutarti.
16. Generics: elimina gli unchecked warning
Quando programmi con i generics, vedrai molti warning del compilatore:
- unchecked cast warning
- unchecked method invocation warning
- unchecked parameterized vararg type warning
- unchecked conversion warning.
Più esperienza acquisisci con i generics, meno warning riceverai, ma non aspettarti che il codice appena scritto compili senza problemi. Molti unchecked warning sono facili da eliminare. Supponiamo, per esempio, che tu scriva per sbaglio questa dichiarazione:
Set<Lark> exaltation = new HashSet();
Cambiala così:
Set<Lark> exaltation = new HashSet<>();
Alcuni warning saranno molto più difficili da eliminare. Quando ricevi warning che richiedono qualche riflessione, non mollare! Elimina ogni unchecked warning che puoi.
Se elimini tutti i warning, hai la certezza che il tuo codice sia typesafe, ed è un’ottima cosa. Significa che non riceverai una ClassCastException a runtime e aumenta la tua fiducia
che il programma si comporterà come previsto. Se non riesci a eliminare un warning, ma puoi dimostrare che il codice che lo ha provocato è typesafe, allora (e solo allora) sopprimi il
warning con un’annotazione @SuppressWarnings("unchecked"). Se sopprimi i warning senza prima aver dimostrato che il codice è typesafe, ti dai un falso senso
di sicurezza.
Il codice potrà anche compilare senza emettere warning, ma a runtime potrà comunque lanciare una ClassCastException.
Ogni volta che usi un’annotazione @SuppressWarnings("unchecked"), aggiungi un commento che spieghi perché farlo è sicuro.
17. Generics: quando hanno senso
Ahi: una classe che ha un disperato bisogno di generics:
Una cosa del genere dovrebbe essere intercettata al più tardi nella merge request!
18. Generics: come non usarli
Scusa, non servono parole. Per favore, MAI fare il merge di questo:
public abstract class JpaLearningCardBaseContentService<LC extends LearningCardBaseContent<L, LC, AC>, J extends JpaLearningCardBaseContent<L, LC, AC>, L extends
LearningCardBase<LC, A>, A extends AnswerOptionBase<L, AC>, AC extends AnswerOptionBaseContent<LC, AC, A>, AS extends AnswerOptionBaseService<A, AC, L, LC>, ACS extends
AnswerOptionBaseContentService<AC, A, L, LC>, LS extends LearningCardBaseService<L>>
extends JpaLightPersistenceService<LC, J, Long> implements LearningCardBaseContentService<LC, L, A, AC> {
// ...
}
Per capire una semplice dichiarazione di classe non dovrebbero mai servire 5 minuti.
19. Preferisci le liste agli array
Gli array differiscono dai tipi generici in due modi importanti. Primo, gli array sono covarianti. Questa parola dal suono spaventoso significa semplicemente che, se Sub è un sottotipo di Super, allora
il tipo array Sub[] è un sottotipo del tipo array Super[]. I generics, invece, sono invarianti: per due tipi distinti qualsiasi Type1 e Type2,
List<Type1> non è né un sottotipo né un supertipo di List<Type2>.
20. Preferisci le lambda alle classi anonime
// Anonymous class instance as a function object - obsolete!
Collections.sort(words, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});
Fai invece così:
// Lambda expression as function object (replaces anonymous class)
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
21. Riduci al minimo lo scope delle variabili locali
La tecnica più efficace per ridurre al minimo lo scope di una variabile locale è dichiararla nel punto in cui viene usata per la prima volta...
22. Preferisci i cicli for-each ai cicli for tradizionali
// The preferred idiom for iterating over collections and arrays
for (Element e : elements) {
... // Do something with e
}
23. Conosci e usa le librerie
Supponiamo che tu voglia generare numeri interi casuali compresi tra zero e un certo limite superiore. Davanti a questo compito comune, molti programmatori scriverebbero un piccolo metodo simile a questo:
// Common but deeply flawed!
static Random rnd = new Random();
static int random(int n) {
return Math.abs(rnd.nextInt()) % n;
}
Questo metodo può sembrare valido, ma ha tre difetti. Il primo è che, se n è una piccola potenza di due, la sequenza di numeri casuali si ripete dopo un periodo piuttosto breve. Il secondo difetto è che, se n non è una potenza di due, alcuni numeri verranno restituiti in media più spesso di altri. Se n è grande, questo effetto può essere piuttosto marcato.
Il terzo difetto del metodo random è che, in rari casi, può fallire in modo catastrofico, restituendo un numero al di fuori dell’intervallo specificato. Questo accade perché il
metodo tenta di mappare il valore restituito da rnd.nextInt() su un int non negativo chiamando Math.abs. Se nextInt() restituisce
Integer.MIN_VALUE, anche Math.abs restituirà Integer.MIN_VALUE e l’operatore di resto (%) restituirà un numero negativo, supponendo che
n non sia una potenza di due. Questo farà quasi certamente fallire il tuo programma, e il malfunzionamento potrebbe essere difficile da riprodurre.
24. Evita float e double se servono risultati esatti
Per gli importi monetari usa BigDecimal. float e double producono errori di arrotondamento.
Supponiamo per esempio di avere in tasca 1,03 $ e di spendere 42 ¢. Quanti soldi ti restano? Ecco un frammento di programma ingenuo che cerca di rispondere a questa domanda:
System.out.println(1.03 - 0.42);
Purtroppo stampa 0.6100000000000001. Non è un caso isolato. Supponiamo di avere in tasca un dollaro e di comprare nove rondelle da dieci centesimi l’una. Quanto
resto ricevi?
System.out.println(1.00 - 9 * 0.10);
Secondo questo frammento di programma, ricevi $0.09999999999999998.
Potresti pensare che il problema si risolva semplicemente arrotondando i risultati prima di stamparli, ma purtroppo non sempre funziona. Supponiamo per esempio di avere in tasca un dollaro e di vedere uno scaffale con una fila di deliziose caramelle da 10 ¢, 20 ¢, 30 ¢ e così via, fino a un dollaro. Compri una caramella di ogni tipo, partendo da quella da 10 ¢, finché non puoi più permetterti la caramella successiva sullo scaffale.
25. Preferisci i tipi primitivi ai primitivi boxed
// Broken comparator - can you spot the flaw?
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
26. Fai riferimento agli oggetti tramite le loro interfacce
Esempio corretto:
Set<Son> sonSet = new LinkedHashSet<>();
Esempio sbagliato:
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();
27. Preferisci le interfacce alla reflection
Altrimenti:
- Perdi tutti i vantaggi del controllo dei tipi in fase di compilazione, compreso il controllo delle eccezioni. Se un programma tenta di invocare tramite reflection un metodo inesistente o inaccessibile, fallirà a runtime, a meno che tu non abbia preso precauzioni particolari.
- Il codice necessario per l’accesso tramite reflection è goffo e prolisso. È noioso da scrivere e difficile da leggere.
- Le prestazioni ne risentono. L’invocazione di metodi tramite reflection è molto più lenta di quella normale.
28. Rispetta le convenzioni di denominazione generalmente accettate
La piattaforma Java ha un insieme consolidato di convenzioni di denominazione, molte delle quali sono contenute in The Java Language Specification [JLS, 6.1].
29. Usa le eccezioni solo per condizioni eccezionali
// Horrible abuse of exceptions. Don't ever do this!
try {
int i = 0;
while (true) {
range[i++].climb();
}
} catch (ArrayIndexOutOfBoundsException e) {
// catch it
}
Che cosa fa questo codice? Leggendolo non è affatto ovvio, e questo basta già come motivo per non usarlo. Si scopre che è un idioma pessimo e mal concepito per scorrere gli elementi di un
array. Il ciclo infinito termina lanciando, catturando e ignorando una ArrayIndexOutOfBoundsException quando
tenta di accedere al primo elemento oltre i limiti dell’array. Dovrebbe essere equivalente all’idioma standard per scorrere un array, che qualsiasi programmatore Java riconosce
all’istante:
for (Mountain m : range) {
m.climb();
}
Le eccezioni, come dice il nome, vanno usate solo per condizioni eccezionali; non vanno mai usate per il normale flusso di controllo.
30. Usa eccezioni checked per condizioni recuperabili ed eccezioni runtime per errori di programmazione
Usa le eccezioni checked per condizioni da cui ci si può ragionevolmente aspettare che il chiamante si riprenda. Mettendo l’utente di fronte a un’eccezione checked, chi progetta l’API gli impone di gestire la condizione. L’utente può ignorare questo obbligo catturando l’eccezione e ignorandola, ma di solito è una pessima idea.
Usa le eccezioni runtime per segnalare errori di programmazione. La grande maggioranza delle eccezioni runtime indica violazioni di precondizioni. Una violazione di precondizione è semplicemente il mancato rispetto, da parte
del client di un’API, del contratto stabilito dalla specifica dell’API. Per esempio, il contratto per l’accesso agli array specifica che l’indice dell’array deve essere compreso tra zero e la lunghezza
dell’array meno uno, inclusi. ArrayIndexOutOfBoundsException indica che questa precondizione è stata violata.
31. Evita l’uso non necessario di eccezioni checked
In sintesi, se usate con parsimonia, le eccezioni checked possono aumentare l’affidabilità dei programmi; se se ne abusa, rendono le API faticose da usare.
Pair<Boolean, LearningCardGroup> answerLearningCard(@NotNull LearningCardGroup learningCardGroup,
LearningCardAssignment learningCardAssignment,
@NotNull Collection<AnswerOption> answers)
throws InvalidTrainingStateException, PersistenceException, LearningCardGroupAlreadyCompletedException,
ConsecutiveCorrectCountExceededException,
PreviousLearningCardGroupUncompletedException, LearningCardGroupCooldownException,
LearningCardGroupHasUncompletedLearningCardsException, InvalidParticipantException, TrainingNotSignedException; // but why ???
// this is a mistake seen a lot with junior devs
// exceptions are exceptionally slow and should be avoided for mere validation
// the performance gets even worse if a stacktrace is printed, since logging and printing is one of the slowest possible actions
32. Preferisci le eccezioni standard
Una caratteristica che distingue i programmatori esperti da quelli meno esperti è che gli esperti puntano a un alto grado di riutilizzo del codice, e di solito lo raggiungono.
Le eccezioni non fanno eccezione alla regola secondo cui il riutilizzo del codice è una buona cosa. Le librerie Java forniscono un insieme di eccezioni che copre la maggior parte delle esigenze della maggior parte delle API. Riutilizzare le eccezioni standard ha diversi vantaggi. Il principale è che rende la tua API più facile da imparare e da usare, perché segue convenzioni consolidate che i programmatori conoscono già. Subito dopo viene il fatto che i programmi che usano la tua API sono più leggibili, perché non sono ingombri di eccezioni sconosciute. Infine (e meno importante), meno classi di eccezione significano un minore consumo di memoria e meno tempo speso a caricare classi.
Il tipo di eccezione riutilizzato più spesso è IllegalArgumentException. In genere è l’eccezione da lanciare quando il chiamante passa un argomento con un valore inappropriato. Per
esempio, sarebbe l’eccezione da lanciare se il chiamante passasse un numero negativo in un parametro che rappresenta quante volte un’azione va ripetuta. Un’altra eccezione riutilizzata spesso
è IllegalStateException. In genere è l’eccezione da lanciare se l’invocazione non è lecita a causa dello stato dell’oggetto ricevente. Per esempio, sarebbe
l’eccezione da lanciare se il chiamante tentasse di usare un oggetto prima che sia stato inizializzato correttamente.
Si può sostenere che ogni invocazione di metodo errata si riduca a un argomento o a uno stato non leciti, ma per determinati tipi di argomenti e stati non leciti si usano di norma altre eccezioni. Se un chiamante
passa null in un parametro per cui i valori null sono vietati, la convenzione vuole che venga lanciata una NullPointerException anziché una IllegalArgumentException.
Allo stesso modo, se un chiamante passa un valore fuori intervallo in un parametro che rappresenta un indice in una sequenza, va lanciata una IndexOutOfBoundsException anziché una IllegalArgumentException.
Non riutilizzare direttamente Exception, RuntimeException, Throwable o Error. Tratta queste classi come se fossero astratte. RuntimeException si può comunque usare se vuoi
convertire un’eccezione checked in un’eccezione runtime e sai davvero cosa stai facendo.
33. Non ignorare le eccezioni
// Empty catch block ignores exception - Highly suspect!
try {
...
} catch (SomeException e) {
}
Un blocco catch vuoto vanifica lo scopo delle eccezioni, che è costringerti a gestire le condizioni eccezionali. Ignorare un’eccezione è come ignorare un allarme antincendio, e per di più spegnerlo, così che nessun altro abbia la possibilità di controllare se c’è davvero un incendio.