1. Обзор
Как разработчики Java, мы могли сталкиваться с типом Void
в некоторых случаях и задаваться вопросом, какова его цель.
В этом кратком руководстве мы узнаем об этом своеобразном классе и посмотрим, когда и как его использовать, а также как по возможности избегать его использования.
2. Что такое тип пустоты
Начиная с JDK 1.1, Java предоставляет нам тип Void
. Его цель — просто представить возвращаемый тип void
как класс и содержать общедоступное значение Class<Void> .
Его нельзя создать, поскольку его единственный конструктор является закрытым.
Следовательно, единственное значение, которое мы можем присвоить переменной Void , — это
null
. Это может показаться немного бесполезным, но теперь мы посмотрим, когда и как использовать этот тип.
3. Использование
Есть некоторые ситуации, когда использование типа Void
может быть интересным.
3.1. Отражение
Во-первых, мы могли бы использовать его при отражении. Действительно, возвращаемый тип любого метода void
будет соответствовать переменной Void.TYPE
, которая содержит упомянутое ранее значение Class<Void>
.
Давайте представим простой класс 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);
}
}
Некоторые методы возвращают целое число, некоторые ничего не возвращают. Теперь предположим, что нам нужно путем отражения получить все методы, которые не возвращают никакого результата . Мы достигнем этого, используя переменную Void.TYPE
:
@Test
void 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()));
}
Как мы видим, были получены только методы clear()
и print() .
3.2. Дженерики
Другое использование типа Void
связано с универсальными классами. Предположим, мы вызываем метод, которому требуется параметр Callable
:
public class Defer {
public static <V> V defer(Callable<V> callable) throws Exception {
return callable.call();
}
}
Но Callable
, который мы хотим передать, не должен ничего возвращать. Поэтому мы можем передать Callable<Void>
:
@Test
void 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();
}
Как показано выше, чтобы вернуться из метода с типом возвращаемого значения Void
, нам просто нужно вернуть null
. Более того, мы могли бы либо использовать случайный тип (например, Callable<Integer>
) и возвращать null
, либо вообще не возвращать тип ( Callable)
, но использование Void
ясно указывает на наши намерения .
Мы также можем применить этот метод к лямбда-выражениям. На самом деле наш Callable
можно было бы записать как лямбду. Давайте представим метод, требующий Function
, но мы хотим использовать функцию
, которая ничего не возвращает. Тогда нам просто нужно заставить его возвращать Void
:
public static <T, R> R defer(Function<T, R> function, T arg) {
return function.apply(arg);
}
@Test
void givenVoidFunction_whenDiffer_thenReturnNull() {
Function<String, Void> function = s -> {
System.out.println("Hello " + s + "!");
return null;
};
assertThat(Defer.defer(function, "World")).isNull();
}
4. Как избежать его использования?
Теперь мы видели несколько вариантов использования типа Void
. Однако, даже если первое использование вполне нормально, мы можем по возможности избегать использования Void
в дженериках . Действительно, встреча с типом возвращаемого значения, который представляет отсутствие результата и может содержать только null
, может быть громоздкой.
Теперь посмотрим, как избежать таких ситуаций. Сначала рассмотрим наш метод с параметром Callable
. Чтобы избежать использования Callable<Void>
, мы могли бы вместо этого предложить другой метод, принимающий параметр Runnable
:
public static void defer(Runnable runnable) {
runnable.run();
}
Итак, мы можем передать ему Runnable
, который не возвращает никакого значения, и, таким образом, избавиться от бесполезного return null
:
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Hello!");
}
};
Defer.defer(runnable);
Но что, если класс Defer
не нам изменять? Затем мы можем либо придерживаться опции Callable<Void>
, либо создать другой класс, взяв Runnable
и отложив вызов класса 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;
}
});
}
}
Делая это, мы раз и навсегда инкапсулируем громоздкую часть в наш собственный метод, позволяя будущим разработчикам использовать более простой API.
Конечно, того же можно добиться и для Function
. В нашем примере Функция
ничего не возвращает, поэтому вместо этого мы можем предоставить другой метод, принимающий Потребителя :
public static <T> void defer(Consumer<T> consumer, T arg) {
consumer.accept(arg);
}
Тогда что, если наша функция не принимает никаких параметров? Мы можем либо использовать Runnable
, либо создать собственный функциональный интерфейс (если так понятнее):
public interface Action {
void execute();
}
Затем мы снова перегружаем метод defer()
:
public static void defer(Action action) {
action.execute();
}
Action action = () -> System.out.println("Hello!");
Defer.defer(action);
5. Вывод
В этой короткой статье мы рассмотрели класс Java Void .
Мы увидели, для чего он нужен и как его использовать. Мы также узнали некоторые альтернативы его использованию.
Как обычно, полный код этой статьи можно найти на GitHub .