Cuando nos enfrentamos a la necesidad de gestionar trabajos que deben procesarse en orden, como en un servidor de impresión, la estructura de datos que mejor se adapta es una cola. En Java, podemos implementar esta estructura de manera eficiente utilizando listas enlazadas y aprovechando las ventajas de las interfaces para mantener la flexibilidad y facilitar el mantenimiento del código.
Imaginemos que tenemos una impresora que solo puede procesar un trabajo a la vez, y que cada trabajo puede tardar varios minutos en completarse. Para evitar que los trabajos se impriman fuera de orden, necesitamos una cola que gestione estos trabajos de forma secuencial, respetando el orden en que llegan. Así, si un empleado envía un trabajo y otro lo hace justo después, el segundo se colocará detrás del primero en la cola, garantizando que se impriman en el orden correcto.
Para empezar, es recomendable definir una interfaz que represente nuestra cola de procesos. Esto nos permite trabajar con una abstracción que puede tener múltiples implementaciones sin necesidad de cambiar el resto del código. Por ejemplo, podríamos definir métodos para encolar un trabajo, obtener el trabajo que está en la cabeza de la cola y eliminar ese trabajo una vez procesado.
La implementación más sencilla para esta cola es mediante una lista enlazada, donde cada nodo contiene un trabajo y una referencia al siguiente nodo. Para ello, creamos una clase privada interna que represente el nodo, con un campo para el trabajo y otro para el siguiente nodo. El constructor del nodo recibe el trabajo y, por defecto, el siguiente apunta a null.
En nuestra clase de cola, mantenemos dos referencias importantes: una a la cabeza de la cola y otra al último nodo. La cabeza nos permite acceder al primer elemento, mientras que el último facilita la inserción rápida de nuevos elementos al final de la cola, evitando recorrer toda la lista.
Cuando encolamos un nuevo trabajo, primero comprobamos si la cola está vacía (es decir, si la cabeza es null). Si está vacía, creamos un nuevo nodo y hacemos que tanto la cabeza como el último apunten a este nodo, ya que es el único elemento. Si la cola no está vacía, añadimos el nuevo nodo después del último y actualizamos la referencia del último para que apunte a este nuevo nodo. De esta forma, mantenemos la consistencia de la estructura.
Para obtener el trabajo que está en la cabeza, simplemente verificamos si la cabeza es null. Si lo es, devolvemos null para indicar que la cola está vacía. En caso contrario, devolvemos el trabajo contenido en el nodo de la cabeza. Es importante no devolver el nodo en sí, ya que es una estructura interna y queremos proteger la integridad de la cola.
Eliminar un trabajo de la cola consiste en avanzar la referencia de la cabeza al siguiente nodo. Antes de hacer esto, guardamos una referencia al nodo que vamos a eliminar para limpiar sus referencias y facilitar la recolección de basura. Si al eliminar el nodo la cola queda vacía (es decir, la cabeza apunta a null), también actualizamos la referencia del último a null para mantener la coherencia.
Aunque esta implementación básica cubre las operaciones esenciales de una cola, en escenarios reales podríamos necesitar funcionalidades adicionales. Por ejemplo, permitir cancelar un trabajo que está en una posición intermedia de la cola, o comprobar si la cola está vacía para mostrar mensajes adecuados al usuario. Estas extensiones pueden adaptarse según las necesidades específicas del sistema.
A continuación, mostramos un ejemplo de cómo podría quedar la implementación de esta cola en Java:
public interface ColaProceso<T> {
void encolar(T trabajo);
T obtener();
void eliminar();
}
public class ColaProcesoListaEnlazada<T> implements ColaProceso<T> {
private class NodoProceso {
T trabajo;
NodoProceso siguiente;
NodoProceso(T trabajo) {
this.trabajo = trabajo;
this.siguiente = null;
}
}
private NodoProceso cabeza;
private NodoProceso ultimo;
public ColaProcesoListaEnlazada() {
cabeza = null;
ultimo = null;
}
@Override
public void encolar(T trabajo) {
NodoProceso nuevoNodo = new NodoProceso(trabajo);
if (cabeza == null) {
cabeza = nuevoNodo;
ultimo = nuevoNodo;
} else {
ultimo.siguiente = nuevoNodo;
ultimo = nuevoNodo;
}
}
@Override
public T obtener() {
if (cabeza == null) {
return null;
}
return cabeza.trabajo;
}
@Override
public void eliminar() {
if (cabeza != null) {
NodoProceso nodoAEliminar = cabeza;
cabeza = cabeza.siguiente;
nodoAEliminar.siguiente = null; // Ayuda al recolector de basura
if (cabeza == null) {
ultimo = null;
}
}
}
}
Con esta estructura, podemos gestionar trabajos de impresión o cualquier otro tipo de tareas que requieran un procesamiento secuencial, manteniendo el orden y la eficiencia en la gestión de los nodos. Además, al trabajar con interfaces, podemos cambiar la implementación de la cola sin afectar al resto del sistema, lo que facilita la evolución y mantenimiento del código.