W3docs

Java Callable y Future

Retorna valores de tareas con Callable y consúmelos de forma asíncrona con Future: esperar, timeout, cancelar y propagar excepciones.

Runnable permite que un hilo realice trabajo, pero no permite que ese trabajo devuelva un valor ni lance una excepción comprobada. El par que sí lo hace es Callable<V> (el productor) y Future<V> (el consumidor). Envías un Callable<V> a un ExecutorService y obtienes un Future<V>, que es tu manejador para: esperar el resultado, leer el valor, capturar la excepción de la tarea o cancelarla.

Esta es la API de más bajo nivel consciente del resultado en el conjunto de herramientas concurrentes de Java. El próximo capítulo, CompletableFuture, añade cadenas, combinadores y pipelines; pero el contrato — "un resultado asíncrono en el que puedes esperar" — es lo que Future definió primero, y sigue siendo la herramienta adecuada para el simple "ve a hacer esto y dime cuando hayas terminado."

Callable<V> — Runnable con tipo de retorno

La interfaz:

@FunctionalInterface
public interface Callable<V> {
  V call() throws Exception;
}

Las dos diferencias respecto a Runnable:

  1. Devuelve V (el parámetro de tipo).
  2. Puede lanzar cualquier Exception — incluyendo excepciones comprobadas.

Al igual que Runnable, es una interfaz funcional — las lambdas y las referencias a métodos funcionan:

Callable<Integer> compute = () -> {
  Thread.sleep(100);
  return 42;
};

Callable<String> read = () -> Files.readString(Path.of("config.txt"));   // can throw IOException

Callable<List<Order>> query = () -> repo.findAll();                       // can throw SQLException

Callable es la forma correcta para cualquier trabajo de "ve a hacer esto y tráeme un valor". Runnable es la forma correcta solo cuando realmente no te importa el resultado.

Future<V> — el manejador de un resultado asíncrono

Cuando haces submit de un Callable<V>, el ejecutor devuelve un Future<V>:

public interface Future<V> {
  boolean cancel(boolean mayInterruptIfRunning);
  boolean isCancelled();
  boolean isDone();
  V get() throws InterruptedException, ExecutionException;
  V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}

Cinco métodos. Tres que usarás frecuentemente.

get()

Bloquea el hilo que llama hasta que la tarea se completa, y luego devuelve el resultado:

ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> f = pool.submit(() -> { Thread.sleep(100); return 42; });
Integer value = f.get();                              // blocks until done; returns 42

get() lanza tres cosas que debes manejar:

  • InterruptedException — el llamador fue interrumpido mientras esperaba. Tratamiento estándar: restablecer el flag de interrupción y propagarlo.
  • ExecutionException — la propia tarea lanzó algo. La excepción original está envuelta; accede a ella mediante .getCause().
  • CancellationException — alguien llamó a cancel() en el future.

Una forma común:

try {
  Integer v = f.get();
} catch (ExecutionException e) {
  Throwable cause = e.getCause();                     // the real exception the task threw
  // ... handle cause ...
} catch (InterruptedException e) {
  Thread.currentThread().interrupt();
  // ... bail out cooperatively ...
}

get(timeout, unit)

Igual que get() pero con un límite de tiempo. Lanza TimeoutException si la tarea no termina a tiempo:

try {
  Integer v = f.get(500, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
  f.cancel(true);                                     // give up; ask the task to stop
  throw new ServiceUnavailableException("timed out");
}

Esta es la forma correcta para "estoy llamando a un backend que debería responder en N ms; si no, falla rápido." Siempre combina el catch con un cancel(true) — de lo contrario, la tarea continúa ejecutándose en segundo plano, usando un hilo del que ya no te importa el resultado.

cancel(boolean)

Pide a la tarea que se detenga:

boolean cancelled = f.cancel(true);                   // true = interrupt the running thread

El argumento le indica al ejecutor si debe interrumpir el hilo trabajador. Con true, el trabajador recibe una InterruptedException de cualquier llamada bloqueante (sleep, wait, I/O); con false, la cancelación no tiene efecto si la tarea ya ha comenzado — solo las tareas no iniciadas se eliminan de la cola.

cancel es cooperativa. Una tarea que no comprueba Thread.currentThread().isInterrupted() y no tiene llamadas bloqueantes seguirá ejecutándose hasta que termine. La cancelación no es un interruptor de apagado — es una solicitud que la tarea debe respetar.

Excepciones: la regla de envoltura

Todo lo que lanza el Callable se envuelve en ExecutionException cuando llamas a get. La causa es el throwable original:

Future<Integer> f = pool.submit(() -> { throw new IOException("nope"); });
try {
  f.get();
} catch (ExecutionException e) {
  e.getCause();                                       // IOException("nope")
  e.getCause() instanceof IOException;                // true
}

Nota la asimetría: el Callable podría lanzar una excepción comprobada (el throws Exception en su firma), pero Future.get solo declara ExecutionException. El envoltorio es lo que permite que una firma transporte cualquier fallo posible.

La sobrecarga de Runnable.submitpool.submit(Runnable) — devuelve un Future<?> cuyo get() devuelve null en caso de éxito y sigue envolviendo cualquier RuntimeException no capturada del Runnable. Es la forma estándar de descubrir que un runnable "disparar y olvidar" realmente falló.

Los límites de Future

Future es un canal unidireccional: envías, esperas, obtienes el valor. No se puede componer:

  • No puedes decir "cuando esto termine, ejecuta aquello con el resultado."
  • No puedes decir "cuando cualquiera de estos N termine, haz X."
  • No puedes decir "combina los resultados de estos dos futures sin bloquear."

Para todo eso necesitas CompletableFuture (próximo capítulo). Future es la herramienta correcta cuando:

  • Solo quieres un valor de vuelta de una sola tarea.
  • Estás consumiendo una API que devuelve Futures y no necesitas componerlos.
  • El contrato más simple es suficiente.

Para código moderno que realiza mucha composición asíncrona, principalmente omitirás Future y llegarás directamente a CompletableFuture — pero Future es el tipo que el servicio ejecutor sigue devolviendo de submit, por lo que verás ambos.

FutureTask — la implementación detrás de submit

La clase que impulsa submit. Puedes usarla directamente:

FutureTask<Integer> task = new FutureTask<>(() -> compute());
new Thread(task).start();                              // FutureTask is a Runnable
Integer v = task.get();

La mayoría del código no construye FutureTask directamente; el framework de ejecutores lo hace por ti. Pero es útil cuando necesitas un Future y un Runnable en un solo objeto — por ejemplo, para programarlo en algo distinto de un ExecutorService.

Un ejemplo práctico: enviar, agotar el tiempo, propagar

El programa a continuación envía una tarea lenta, una tarea rápida y una tarea que falla; demuestra get, get(timeout), el desempaquetado de excepciones y la cancelación.

java— editable, runs on the server

Lo que se extrae de la ejecución:

  • La sección 1 es la forma más simple: enviar un Callable, llamar a get, recibir el valor. get bloqueó el hilo principal durante los 50 ms que tardó la tarea. Eso es todo lo que hace Future en su forma básica — un manejador tipado y bloqueante a un resultado que llega más tarde.
  • La sección 2 mostró la forma del timeout. La tarea lenta habría tardado 500 ms; get(100, MS) desistió después de 100 y lanzó TimeoutException. El cancel(true) posterior interrumpió el hilo en ejecución para que pudiera salir pronto. Sin la cancelación, la tarea habría seguido ejecutándose durante los 400 ms restantes — usando un hilo del que ya no te importaba el resultado.
  • La sección 3 mostró el envoltorio de excepciones. El Callable lanzó IOException; get() lo volvió a lanzar dentro de ExecutionException. e.getCause() devolvió el original. Este es el canal universal de fallos de la API — cualquier lanzamiento comprobado o no comprobado del cuerpo llega aquí.
  • La sección 4 mostró la cancelación de una tarea no iniciada. Con ambos hilos del pool ocupados en hog1 y hog2, la tarea queued estaba en la cola de trabajo; cancel(false) la eliminó sin ejecutarla nunca. Llamar a get() en el future cancelado lanzó CancellationException — un modo de fallo diferente a "la tarea lanzó algo" (que habría sido ExecutionException).
  • La sección 5 mostró invokeAny. La tarea más rápida (50 ms) ganó; las otras dos fueron canceladas por el ejecutor. invokeAny es la herramienta adecuada para consultas redundantes — llama a múltiples fuentes, usa el primer éxito, abandona el resto. Es el bloque de construcción detrás de los patrones de solicitudes con cobertura en sistemas reales.

Qué sigue

El próximo capítulo, Java CompletableFuture, introduce la API asíncrona componible — thenApply, thenCompose, allOf, anyOf, y los docenas de combinadores que convierten Future de un manejador de un solo resultado en un pipeline reactivo completo.

Práctica

Práctica
Llamas a `future.get()` y la tarea lanzó `SQLException` desde su método `call()`. ¿Qué excepción lanza `get()`?
Llamas a `future.get()` y la tarea lanzó `SQLException` desde su método `call()`. ¿Qué excepción lanza `get()`?
Was this page helpful?