Overblik
Som Java-udviklere er vi måske stødt på Void-typen ved en lejlighed og har spekuleret på, hvad dens formål var.
I denne hurtige vejledning vil vi lære om denne ejendommelige klasse og se, hvornår og hvordan den skal bruges, samt hvordan man kan undgå at bruge den, når det er muligt.
Hvad er Void-typen
Siden JDK 1.1 har Java givet os Void-typen. Dens formål er simpelthen at repræsentere void-returtypen som en klasse og indeholde en Class<Void> offentlig værdi. Den kan ikke instantieres, da dens eneste konstruktør er privat.
Derfor er den eneste værdi, vi kan tildele en Void-variabel, null. Det kan virke lidt ubrugeligt, men vi skal nu se, hvornår og hvordan vi kan bruge denne type.
Anvendelser
Der er nogle situationer, hvor det kan være interessant at bruge Void-typen.
3.1. Refleksion
Først kan vi bruge den, når vi laver refleksion. Returtypen for enhver void-metode vil nemlig passe til variablen Void.TYPE, der indeholder den tidligere nævnte Class<Void>-værdi.
Lad os forestille os en simpel Calculator-klasse:
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); }}
Nogle metoder returnerer et heltal, andre returnerer ikke noget. Lad os nu sige, at vi ved hjælp af refleksion skal hente alle metoder, der ikke returnerer noget resultat, frem. Det opnår vi ved at bruge variablen 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, er det kun metoderne clear() og print() der er hentet.
3.2. Generics
En anden anvendelse af typen Void er med generiske klasser. Lad os antage, at vi kalder en metode, 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, vi ønsker at videregive, behøver ikke at returnere noget. Derfor kan vi videregive 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 kunne enten have brugt en tilfældig type (f.eks. Callable<Integer>) og returneret nul eller slet ingen type (Callable), men ved at bruge Void angiver vi klart vores hensigter.
Vi kan også anvende denne metode på lambdas. Faktisk kunne vores Callable have været skrevet som en lambda. Lad os forestille os en metode, der kræver en Function, men vi ønsker at bruge en Function, der ikke returnerer noget. Så skal vi bare få den til at returnere 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();}
Hvordan undgår vi at bruge den?
Nu har vi set nogle anvendelser af typen Void. Men selv om den første brug er helt i orden, vil vi måske gerne undgå at bruge Void i generiske typer, hvis det er muligt. Det kan nemlig være besværligt at støde på en returtype, der repræsenterer fraværet af et resultat og kun kan indeholde null.
Vi vil nu se, hvordan vi kan undgå disse situationer. Lad os først se på vores metode med parameteren Callable. For at undgå at bruge en Callable<Void> kan vi tilbyde en anden metode, der tager en Runnable-parameter i stedet:
public static void defer(Runnable runnable) { runnable.run();}
Så kan vi give den en Runnable, der ikke returnerer nogen værdi, og dermed slippe for den ubrugelige return null:
Runnable runnable = new Runnable() { @Override public void run() { System.out.println("Hello!"); }};Defer.defer(runnable);
Men hvad så, hvis Defer-klassen ikke er vores til at ændre? Så kan vi enten holde os til Callable<Void>-muligheden eller oprette en anden klasse, der tager en Runnable og udskyder opkaldet til 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; } }); }}
Derved indkapsler vi den besværlige del én gang for alle i vores egen metode, så fremtidige udviklere kan bruge et enklere API.
Det samme kan selvfølgelig opnås for Function. I vores eksempel returnerer Function ikke noget, og derfor kan vi levere en anden metode, der tager en Consumer i stedet:
public static <T> void defer(Consumer<T> consumer, T arg) { consumer.accept(arg);}
Så hvad nu, hvis vores funktion ikke tager nogen parameter? Vi kan enten bruge en Runnable eller oprette vores egen funktionelle grænseflade (hvis det virker mere klart):
public interface Action { void execute();}
Så overbelaster vi defer()-metoden igen:
public static void defer(Action action) { action.execute();}
Action action = () -> System.out.println("Hello!");Defer.defer(action);
Konklusion
I denne korte artikel har vi gennemgået Java Void-klassen. Vi så, hvad dens formål var, og hvordan man bruger den. Vi lærte også nogle alternativer til dens brug.
Som sædvanlig kan den fulde kode til denne artikel findes på vores GitHub.
Kom godt i gang med Spring 5 og Spring Boot 2, gennem kurset Lær Spring:
>> CHECK OUT THE COURSE