Resumen
Como desarrolladores de Java, puede que nos hayamos encontrado con el tipo Void en alguna ocasión y nos hayamos preguntado cuál es su propósito.
En este rápido tutorial, aprenderemos sobre esta peculiar clase y veremos cuándo y cómo utilizarla, así como cómo evitar su uso cuando sea posible.
¿Qué es el tipo Void
Desde el JDK 1.1, Java nos proporciona el tipo Void. Su propósito es simplemente representar el tipo de retorno void como una clase y contener un valor público Class<Void>. No es instanciable ya que su único constructor es privado.
Por tanto, el único valor que podemos asignar a una variable Void es null. Puede parecer un poco inútil, pero ahora veremos cuándo y cómo utilizar este tipo.
Usos
Hay algunas situaciones en las que utilizar el tipo Void puede ser interesante.
3.1. Reflexión
En primer lugar, podríamos utilizarlo al hacer reflexión. En efecto, el tipo de retorno de cualquier método void coincidirá con la variable Void.TYPE que contiene el valor de Class<Void> mencionado anteriormente.
Imaginemos una simple clase Calculadora:
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); }}
Algunos métodos devuelven un entero, otros no devuelven nada. Ahora, digamos que tenemos que recuperar, por reflexión, todos los métodos que no devuelven ningún resultado. Lo conseguiremos utilizando la variable 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()));}
Como vemos, sólo se han recuperado los métodos clear() y print().
3.2. Genéricos
Otro uso del tipo Void es con las clases genéricas. Supongamos que llamamos a un método que requiere un parámetro Callable:
public class Defer { public static <V> V defer(Callable<V> callable) throws Exception { return callable.call(); }}
Pero, el Callable que queremos pasar no tiene que devolver nada. Por lo tanto, podemos pasar un 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();}
Podríamos haber utilizado un tipo aleatorio (por ejemplo, Callable<Integer>) y devolver null o ningún tipo (Callable), pero el uso de Void indica claramente nuestras intenciones.
También podemos aplicar este método a las lambdas. De hecho, nuestro Callable podría haberse escrito como una lambda. Imaginemos un método que requiera una Función, pero queremos usar una Función que no devuelva nada. Entonces sólo tenemos que hacer que devuelva 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();}
¿Cómo evitar su uso?
Ahora, hemos visto algunos usos del tipo Void. Sin embargo, incluso si el primer uso está totalmente bien, podríamos querer evitar el uso de Void en los genéricos si es posible. En efecto, encontrarse con un tipo de retorno que representa la ausencia de un resultado y que sólo puede contener null puede ser engorroso.
Ahora veremos cómo evitar estas situaciones. Primero, consideremos nuestro método con el parámetro Callable. Para evitar el uso de un Callable<Void>, podríamos ofrecer otro método que tome un parámetro Runnable en su lugar:
public static void defer(Runnable runnable) { runnable.run();}
Entonces, podemos pasarle un Runnable que no devuelva ningún valor y así librarnos del inútil return null:
Runnable runnable = new Runnable() { @Override public void run() { System.out.println("Hello!"); }};Defer.defer(runnable);
Pero entonces, ¿qué pasa si la clase Defer no es nuestra para modificar? Entonces podemos ceñirnos a la opción Callable<Void> o crear otra clase tomando un Runnable y difiriendo la llamada a la clase 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; } }); }}
Al hacer eso, encapsulamos la parte engorrosa de una vez por todas en nuestro propio método, permitiendo a los futuros desarrolladores utilizar una API más sencilla.
Por supuesto, lo mismo se puede conseguir para Function. En nuestro ejemplo, la Función no devuelve nada, por lo que podemos proporcionar otro método que tome un Consumidor en su lugar:
public static <T> void defer(Consumer<T> consumer, T arg) { consumer.accept(arg);}
Entonces, ¿qué pasa si nuestra función no toma ningún parámetro? Podemos utilizar un Runnable o crear nuestra propia interfaz funcional (si parece más claro):
public interface Action { void execute();}
Entonces, sobrecargamos el método defer() de nuevo:
public static void defer(Action action) { action.execute();}
Action action = () -> System.out.println("Hello!");Defer.defer(action);
Conclusión
En este breve artículo, hemos cubierto la clase Void de Java. Vimos cuál era su propósito y cómo utilizarla. También aprendimos algunas alternativas a su uso.
Como siempre, el código completo de este artículo se puede encontrar en nuestro GitHub.
Iniciate con Spring 5 y Spring Boot 2, a través del curso Aprende Spring:
>> CONSULTA EL CURSO
.