Panoramica
Come sviluppatori Java, potremmo aver incontrato il tipo Void in qualche occasione e esserci chiesti quale fosse il suo scopo.
In questo rapido tutorial, impareremo a conoscere questa particolare classe e vedremo quando e come usarla e come evitare di usarla quando possibile.
Cos’è il tipo Void
Dal JDK 1.1, Java ci fornisce il tipo Void. Il suo scopo è semplicemente quello di rappresentare il tipo di ritorno void come una classe e contenere un valore pubblico Class<Void>. Non è istanziabile perché il suo unico costruttore è privato.
Pertanto, l’unico valore che possiamo assegnare a una variabile Void è null. Può sembrare un po’ inutile, ma ora vedremo quando e come usare questo tipo.
Usi
Ci sono alcune situazioni in cui usare il tipo Void può essere interessante.
3.1. Riflessione
In primo luogo, potremmo usarlo quando facciamo la riflessione. Infatti, il tipo di ritorno di qualsiasi metodo void corrisponderà alla variabile Void.TYPE che contiene il valore Class<Void> menzionato prima.
Immaginiamo una semplice classe Calculator:
public class Calculator { private int result = 0; public int add(int number) { return result += number; } public int sub(int number) { return result -= number; } public void clear() { result = 0; } public void print() { System.out.println(result); }}
Alcuni metodi restituiscono un intero, altri non restituiscono nulla. Ora, diciamo che dobbiamo recuperare, per riflessione, tutti i metodi che non restituiscono alcun risultato. Ci riusciremo usando la variabile Void.TYPE:
@Testvoid givenCalculator_whenGettingVoidMethodsByReflection_thenOnlyClearAndPrint() { Method calculatorMethods = Calculator.class.getDeclaredMethods(); List<Method> calculatorVoidMethods = Arrays.stream(calculatorMethods) .filter(method -> method.getReturnType().equals(Void.TYPE)) .collect(Collectors.toList()); assertThat(calculatorVoidMethods) .allMatch(method -> Arrays.asList("clear", "print").contains(method.getName()));}
Come possiamo vedere, solo i metodi clear() e print() sono stati recuperati.
3.2. Generici
Un altro uso del tipo Void è con le classi generiche. Supponiamo di chiamare un metodo che richiede un parametro Callable:
public class Defer { public static <V> V defer(Callable<V> callable) throws Exception { return callable.call(); }}
Ma il Callable che vogliamo passare non deve restituire nulla. Pertanto, possiamo passare un Callable<Void>:
@Testvoid givenVoidCallable_whenDiffer_thenReturnNull() throws Exception { Callable<Void> callable = new Callable<Void>() { @Override public Void call() { System.out.println("Hello!"); return null; } }; assertThat(Defer.defer(callable)).isNull();}
Potremmo usare un tipo casuale (ad esempio Callable<Integer>) e restituire null o nessun tipo (Callable), ma usando Void dichiariamo chiaramente le nostre intenzioni.
Possiamo anche applicare questo metodo ai lambda. In effetti, il nostro Callable avrebbe potuto essere scritto come un lambda. Immaginiamo un metodo che richiede una Function, ma vogliamo usare una Function che non restituisce nulla. Allora dobbiamo solo farle restituire Void:
public static <T, R> R defer(Function<T, R> function, T arg) { return function.apply(arg);}
@Testvoid givenVoidFunction_whenDiffer_thenReturnNull() { Function<String, Void> function = s -> { System.out.println("Hello " + s + "!"); return null; }; assertThat(Defer.defer(function, "World")).isNull();}
Come evitare di usarla?
Ora, abbiamo visto alcuni usi del tipo Void. Tuttavia, anche se il primo uso è del tutto corretto, potremmo voler evitare di usare Void nei generici, se possibile. Infatti, incontrare un tipo di ritorno che rappresenta l’assenza di un risultato e che può contenere solo null può essere ingombrante.
Vedremo ora come evitare queste situazioni. Per prima cosa, consideriamo il nostro metodo con il parametro Callable. Per evitare di usare un Callable<Void>, potremmo offrire un altro metodo che prenda invece un parametro Runnable:
public static void defer(Runnable runnable) { runnable.run();}
Possiamo quindi passargli un Runnable che non restituisce alcun valore e liberarci così dell’inutile return null:
Runnable runnable = new Runnable() { @Override public void run() { System.out.println("Hello!"); }};Defer.defer(runnable);
Ma poi, cosa succede se la classe Defer non è nostra da modificare? In tal caso, possiamo attenerci all’opzione Callable<Void> o creare un’altra classe che prende un Runnable e rinvia la chiamata alla classe Defer:
public class MyOwnDefer { public static void defer(Runnable runnable) throws Exception { Defer.defer(new Callable<Void>() { @Override public Void call() { runnable.run(); return null; } }); }}
Facendo questo, incapsuliamo la parte ingombrante una volta per tutte nel nostro metodo, permettendo ai futuri sviluppatori di usare un’API più semplice.
Ovviamente, lo stesso può essere fatto per Function. Nel nostro esempio, la Function non restituisce nulla, quindi possiamo fornire un altro metodo che prende un Consumer:
public static <T> void defer(Consumer<T> consumer, T arg) { consumer.accept(arg);}
E se la nostra funzione non prende alcun parametro? Possiamo usare un Runnable o creare la nostra interfaccia funzionale (se questo sembra più chiaro):
public interface Action { void execute();}
Allora, sovraccarichiamo di nuovo il metodo defer():
public static void defer(Action action) { action.execute();}
Action action = () -> System.out.println("Hello!");Defer.defer(action);
Conclusione
In questo breve articolo, abbiamo coperto la classe Java Void. Abbiamo visto qual è il suo scopo e come usarla. Abbiamo anche imparato alcune alternative al suo utilizzo.
Come al solito, il codice completo di questo articolo può essere trovato sul nostro GitHub.
Inizia con Spring 5 e Spring Boot 2, attraverso il corso Learn Spring:
>> CHECK OUT THE COURSE