Перейти к основному содержимому

Пустой тип в Java

· 5 мин. чтения

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 .