Przegląd
Jako programiści Javy, być może spotkaliśmy się kiedyś z typem Void i zastanawialiśmy się, jaki jest jego cel.
W tym krótkim tutorialu, dowiemy się o tej osobliwej klasie i zobaczymy kiedy i jak jej używać, jak również jak unikać jej używania kiedy to możliwe.
Co to jest typ Void
Od JDK 1.1, Java dostarcza nam typ Void. Jego celem jest po prostu reprezentacja typu zwrotnego void jako klasy i zawiera wartość publiczną Class<Void>. Nie jest on instancjonowalny, ponieważ jego jedyny konstruktor jest prywatny.
Dlatego jedyną wartością, jaką możemy przypisać zmiennej Void jest null. Może się to wydawać trochę bezużyteczne, ale teraz zobaczymy, kiedy i jak używać tego typu.
Usages
Istnieją pewne sytuacje, w których użycie typu Void może być interesujące.
3.1. Reflection
Po pierwsze, możemy go użyć podczas wykonywania refleksji. Rzeczywiście, typ zwracany każdej metody void będzie pasował do zmiennej Void.TYPE, która przechowuje wartość Class<Void> wspomnianą wcześniej.
Wyobraźmy sobie prostą klasę 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); }}
Niektóre metody zwracają liczbę całkowitą, niektóre nie zwracają nic. Załóżmy teraz, że musimy pobrać, poprzez refleksję, wszystkie metody, które nie zwracają żadnego wyniku. Osiągniemy to za pomocą zmiennej 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()));}
Jak widzimy, tylko metody clear() i print() zostały pobrane.
3.2. Generics
Innym zastosowaniem typu Void jest użycie w klasach generycznych. Załóżmy, że wywołujemy metodę, która wymaga parametru Callable:
public class Defer { public static <V> V defer(Callable<V> callable) throws Exception { return callable.call(); }}
Ale, Callable, które chcemy przekazać nie musi nic zwracać. Dlatego możemy przekazać 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();}
Moglibyśmy albo użyć losowego typu (np. Callable<Integer>) i zwrócić null albo w ogóle nie zwracać żadnego typu (Callable), ale użycie Void jasno określa nasze intencje.
Możemy również zastosować tę metodę do lambdas. W gruncie rzeczy, nasze Callable mogło być napisane jako lambda. Wyobraźmy sobie metodę wymagającą Funkcji, ale chcemy użyć Funkcji, która nic nie zwraca. Wtedy po prostu musimy sprawić, że zwróci ona 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();}
How to Avoid Using It?
Widzieliśmy już kilka przypadków użycia typu Void. Jednakże, nawet jeśli pierwsze użycie jest całkowicie w porządku, możemy chcieć uniknąć użycia Void w generycznych, jeśli to możliwe. Rzeczywiście, napotkanie typu zwrotnego, który reprezentuje brak wyniku i może zawierać tylko null, może być kłopotliwe.
Zobaczymy teraz, jak uniknąć takich sytuacji. Po pierwsze, rozważmy naszą metodę z parametrem Callable. Aby uniknąć użycia parametru Callable<Void>, możemy zaproponować inną metodę przyjmującą zamiast niego parametr Runnable:
public static void defer(Runnable runnable) { runnable.run();}
Możemy więc przekazać jej Runnable, który nie zwraca żadnej wartości i w ten sposób pozbyć się bezużytecznego return null:
Runnable runnable = new Runnable() { @Override public void run() { System.out.println("Hello!"); }};Defer.defer(runnable);
Ale co wtedy, jeśli klasa Defer nie jest nasza do modyfikacji? Wtedy możemy albo trzymać się opcji Callable<Void> albo stworzyć inną klasę przyjmującą Runnable i odraczającą wywołanie do klasy 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; } }); }}
Robiąc to, enkapsulujemy kłopotliwą część raz na zawsze w naszej własnej metodzie, pozwalając przyszłym programistom używać prostszego API.
Oczywiście, to samo można osiągnąć dla Function. W naszym przykładzie, Function nie zwraca niczego, więc możemy dostarczyć inną metodę przyjmującą Consumer zamiast tego:
public static <T> void defer(Consumer<T> consumer, T arg) { consumer.accept(arg);}
A co jeśli nasza funkcja nie przyjmuje żadnego parametru? Możemy albo użyć Runnable albo stworzyć własny interfejs funkcjonalny (jeśli to wydaje się bardziej zrozumiałe):
public interface Action { void execute();}
Wtedy, przeciążamy metodę defer() ponownie:
public static void defer(Action action) { action.execute();}
Action action = () -> System.out.println("Hello!");Defer.defer(action);
Podsumowanie
W tym krótkim artykule, omówiliśmy klasę Java Void. Zobaczyliśmy jakie jest jej przeznaczenie i jak z niej korzystać. Poznaliśmy również kilka alternatyw dla jej użycia.
Jak zwykle, pełny kod tego artykułu można znaleźć na naszym GitHubie.
Zacznij przygodę ze Spring 5 i Spring Boot 2, dzięki kursowi Learn Spring:
>> CHECK OUT THE COURSE
.