Översikt
Som Javautvecklare kan vi ha stött på Void-typen vid något tillfälle och undrat vad dess syfte var.
I den här snabba handledningen ska vi lära oss om den här märkliga klassen och se när och hur vi kan använda den samt hur vi undviker att använda den när det är möjligt.
Vad är Void-typen
Sedan JDK 1.1 har Java försett oss med Void-typen. Dess syfte är helt enkelt att representera returtypen void som en klass och innehålla ett Class<Void> offentligt värde. Den är inte instantierbar eftersom dess enda konstruktör är privat.
Det enda värde vi kan tilldela en Void-variabel är därför null. Det kan verka lite onödigt, men vi ska nu se när och hur vi kan använda den här typen.
Användningar
Det finns några situationer då det kan vara intressant att använda Void-typen.
3.1. Reflektion
För det första kan vi använda den när vi gör reflektion. Returtypen för varje void-metod kommer nämligen att matcha variabeln Void.TYPE som innehåller värdet Class<Void> som nämndes tidigare.
Låt oss föreställa oss en enkel Calculator-klass:
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); }}
Vissa metoder returnerar ett heltal, andra returnerar ingenting. Låt oss nu säga att vi måste hämta, genom reflektion, alla metoder som inte returnerar något resultat. Vi uppnår detta genom att använda variabeln 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()));}
Som vi kan se har endast metoderna clear() och print() hämtats.
3.2. Generiska
En annan användning av typen Void är med generiska klasser. Låt oss anta att vi anropar en metod som kräver en Callable-parameter:
public class Defer { public static <V> V defer(Callable<V> callable) throws Exception { return callable.call(); }}
Men den Callable som vi vill skicka över behöver inte returnera något. Därför kan vi lämna över en 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();}
Vi kunde antingen ha använt en slumpmässig typ (t.ex. Callable<Integer>) och returnerat noll eller ingen typ alls (Callable), men genom att använda Void anges våra avsikter klart och tydligt.
Vi kan också tillämpa den här metoden på lambdas. Faktum är att vår Callable kunde ha skrivits som en lambda. Låt oss föreställa oss en metod som kräver en funktion, men vi vill använda en funktion som inte returnerar något. Då behöver vi bara få den att returnera 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();}
Hur man undviker att använda den?
Nu har vi sett några användningsområden för typen Void. Men även om den första användningen är helt okej vill vi kanske undvika att använda Void i generiska typer om det är möjligt. Det kan nämligen vara besvärligt att stöta på en returtyp som representerar avsaknaden av ett resultat och som bara kan innehålla null.
Vi ska nu se hur vi kan undvika dessa situationer. Låt oss först betrakta vår metod med parametern Callable. För att undvika att använda en Callable<Void> kan vi erbjuda en annan metod som tar en Runnable-parameter istället:
public static void defer(Runnable runnable) { runnable.run();}
Så kan vi ge den en Runnable som inte returnerar något värde och på så sätt bli av med den onödiga returen null:
Runnable runnable = new Runnable() { @Override public void run() { System.out.println("Hello!"); }};Defer.defer(runnable);
Men vad händer då om Defer-klassen inte är vår att modifiera? Då kan vi antingen hålla oss till alternativet Callable<Void> eller skapa en annan klass som tar en Runnable och skjuter upp anropet till Defer-klassen:
public class MyOwnDefer { public static void defer(Runnable runnable) throws Exception { Defer.defer(new Callable<Void>() { @Override public Void call() { runnable.run(); return null; } }); }}
Då kapslar vi in den besvärliga delen en gång för alla i vår egen metod, vilket gör det möjligt för framtida utvecklare att använda ett enklare API.
Det samma kan förstås åstadkommas för Function. I vårt exempel returnerar Function ingenting, så vi kan tillhandahålla en annan metod som tar en Consumer istället:
public static <T> void defer(Consumer<T> consumer, T arg) { consumer.accept(arg);}
Hur blir det då om vår funktion inte tar någon parameter? Vi kan antingen använda en Runnable eller skapa ett eget funktionellt gränssnitt (om det verkar tydligare):
public interface Action { void execute();}
Då överbelastar vi defer()-metoden igen:
public static void defer(Action action) { action.execute();}
Action action = () -> System.out.println("Hello!");Defer.defer(action);
Slutsats
I den här korta artikeln har vi behandlat Java Void-klassen. Vi såg vad som var dess syfte och hur man använder den. Vi lärde oss också några alternativ till dess användning.
Som vanligt finns den fullständiga koden för den här artikeln på vår GitHub.
Kom igång med Spring 5 och Spring Boot 2 genom kursen Lär dig Spring:
>> CHECK OUT THE COURSE