Croce degli sviluppatori java fin dall'inizio dei tempi, una eccezione non controllata, che può essere evitata solo se lo sviluppatore
prevede opportuni controlli sugli oggetti prima di usarli.
Con il tempo, il cosidetto Null object pattern è stato definito una cattiva scelta di design, un qualcosa che va evitato a tutti i costi,
perchè potrebbe portare alla scrittura di codice con bug non sempre facilmente individuabili.
Non si può essere in disaccordo con questa affermazione: dopo aver scritto una buona quantità di righe di codice, ci si rende conto che ritornare null
da un metodo costringe a scrivere ulteriori controlli a valle dell'invocazione del metodo stesso, per evitare l'eccezione NullPointerException.
Inoltre, un programma scritto in questo modo tende a fallire lentamente: il valore null potrebbe essere passato a diversi metodi, senza essere mai effettivamente
utilizzato (invocato un suo metodo), se non per poi lanciare una NPE in un contesto completamente diverso da quello d'origine (un altro metodo di un'altra classe,
addirittura un'altro thread o un'altra esecuzione del programma, se serializzato in qualche modo).
Consideriamo il seguente metodo:
public static Client getClientByID(int ID) {
String sql = "SELECT * FROM Clients WHERE ID=?";
try (Connection conn = connectDB(); PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, ID);
ResultSet rs = pstmt.executeQuery();
if(rs.next()) {
return new Client(rs);
}
} catch (SQLException e) {
Logger.error(e);
}
return null;
}
Questo è un metodo preso da un mio progetto, giusto per farvi capire quanto reale sia il problema.
Effettua una ricerca su un database, se trova il cliente cercato ritorna un oggetto Client
con i valori presi dalla tabella, null altrimenti.
Null in questo caso significa che nessun cliente con quell'ID è stato trovato.
Sembra una risposta ragionevole, ma se non volessimo usare null?
Le alternative esistono:
try...catch...finally
.public static Client getClientByID(int ID) throws ClientNotFoundException, SQLException {
String sql = "SELECT * FROM Clients WHERE ID=?";
try {
Connection conn = connectDB();
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, ID);
ResultSet rs = pstmt.executeQuery();
if(rs.next()) {
return new Client(rs);
}
} catch (SQLException sqle) {
Logger.error(e);
throw sqle;
}
throw new ClientNotFoundException();
}
Abbiamo aggiunto una nuova eccezione, ClientNotFoundException
, che viene lanciata al prosto del return null
, e rilanciato l'eccezione SQLException
,
dato che altrimenti chi invoca il metodo non potrebbe sapere se effettivamente non c'è un cliente con quell'ID o se semplicemente il database è andato offline.
Qualcuno preferisce invece ritornare istanze di un oggetto che simboleggi un valore vuoto.
Per quanto strano possa sembrare, ha una certa logica: sempre dall'esempio, la classe Client
potrebbe avere al suo interno una istanza statica chiamata,
ad esempio, NOBODY, con tutti i campi impostati a valori di default o nulli (riuscite a vedere il problema?).
Ovviamente c'è da considerare una modifica al metodo equals()
della classe Client
, ma niente di particolare.
Vediamo quindi il codice modificato:
public static Client getClientByID(int ID) throws SQLException {
String sql = "SELECT * FROM Clients WHERE ID=?";
Connection conn = connectDB();
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, ID);
ResultSet rs = pstmt.executeQuery();
if(rs.next()) {
return new Client(rs);
}
return Client.NOBODY;
}
In quest'ultimo snippet ho mantenuto il rilancio dell'eccezione, ma rimosso il try...catch
, perchè l'errore può essere gestito esternamente
(e non c'è più confusione sulla tipologia dello stesso).
Alcuni casi sono ancora più semplici: i metodi che devono ritornare una lista di valori possono ritornare una lista vuota, ad indicare che non è stato possibile
recuperare alcun oggetto da inservici.
Lo stesso approccio può essere utilizzato per tutti i vari tipi di Collection.
E per i tipi primitivi o le stringhe?
In quel caso si è particolarmente limitati: per le stringhe si può usare una stringa vuota, ma sfortunatamente nella maggior parte dei casi una stringa vuota può
comunque esprimere un valore.
Per i tipi primitivi, il problema si può porre solo se si utilizzano le classi wrapper, ad esempio Integer
per il tipo int, ma anche
in quel caso l'unico modo per risolvere il problema è quello di creare un'ulteriore classe wrapper, che contenga un valore nullo.
La classe Double
ha a disposizione una costante NaN (Not A Number) che è un fantastico candidato.
In conclusione quindi, return null
è davvero così nefasto come tutti dicono? Leggiamo questa
risposta ad una domanda posta su StackOverflow:
Returning null is usually the best idea if you intend to indicate that no data is available. An empty object implies data has been returned, whereas returning null clearly indicates that nothing has been returned. Additionally, returning a null will result in a null exception if you attempt to access members in the object, which can be useful for highlighting buggy code - attempting to access a member of nothing makes no sense. Accessing members of an empty object will not fail meaning bugs can go undiscovered.
Certamente la domanda era relativa a C#, ma essendo un linguaggio essenzialmente simile a java, non ci facciamo problemi e concentriamoci sul contenuto.
L'utente pone una questione vitale: se ritorniamo null, lo facciamo per indicare che nessun dato è disponibile, mentre se torniamo un oggetto vuoto o simile,
stiamo comunque ritornando un'informazione.
Vuota, sicuramente, ma comunque il fatto stesso che sia un dato vuoto è esso stesso un'informazione che può essere utilizzata!
L'ultima parte invece, almeno per me, non ha assolutamente senso: questo non è il modo corretto per fare debug.
Il problema comunque, almeno per me, è in realtà un altro.
Consideriamo un metodo d'esempio:
public Foo bar(int param);
Ipotizziamo che questo metodo possa ritornare null per un qualche valore di param
.
Quando andiamo ad usare questo metodo, saremo quindi costretti a verificare se l'oggetto sia diverso da null prima di accedere a qualche sua variabile o metodo:
Foo F = bar(0);
if(F!=null){
System.out.println(F.getValue());
}
Perchè ovviamente invocare direttamente F.getValue()
potrebbe lanciare NullPointerException.
Se utilizzassimo un'eccezione, chiamiamola ad es. FooNotFoundExeception
:
try {
Foo F = bar(0);
System.out.println(F.getValue());
} catch (FooNotFoundExeception fex) {
// handle me!
}
Proviamo quindi con un oggetto vuoto, staticamente dichiarato all'interno della classe Foo
:
Foo F = bar(0);
if(!Foo.EMPTY.equals(F)){
System.out.println(F.getValue());
}
Notato qualcosa?
Tutti e tre gli esempio implicano comunque un qualche tipo di controllo del valore ritornato dal metodo.
QUALSIASI sia l'approccio che voi usiate, dovrete SEMPRE verificare che il metodo abbia ritornato un qualcosa di utilizzabile, altrimenti il vostro codice
ritornerà un risultato inaspettato.
Che è poi quello che avevamo già letto nell commento citato precedentemente:
...attempting to access a member of nothing makes no sense. Accessing members of an empty object will not fail meaning bugs can go undiscovered.
Ma quindi qual è la verità? Semplicemente che l'odio verso null non ha assolutamente alcun fondamento.
Certo, questo non significa che dobbiate ritornare null a go-go, ma che per certi metodi, soprattuto quelli che devono effettuare un qualche tipo di ricerca,
ritornare null rimane sempre il modo più efficace di indicare l'assenza di risultati.
ATTENZIONE però! Assicuratevi sempre di indicare chiaramente se il metodo potrebbe ritornare null nella documentazione e/o javadoc, per evitare spiacevoli
sorprese a terzi o, più probabile, a voi stessi.
In altri casi, invece, soprattuto quando una mancanza di informazioni è risultato di uno o più errori, lanciare un'eccezione è estremamente consigliato.
Questo vi permetterà di gestire al meglio l'eventuale errore e di invocare, laddove necessario, altri metodi per completare il ciclo d'esecuzione del vostro codice.