En tus aplicaciones Java, típicamente trabajarás con varios tipos de objetos. Y es posible que desees realizar operaciones como ordenar, buscar e iterar en estos objetos.

Antes de la introducción del marco de colecciones en JDK 1.2, habrías utilizado Arrays y Vectors para almacenar y gestionar un grupo de objetos. Pero tenían sus propias desventajas.

El marco de colecciones de Java tiene como objetivo superar estos problemas al proporcionar implementaciones de alto rendimiento de estructuras de datos comunes. Estas te permiten centrarte en escribir la lógica de la aplicación en lugar de centrarte en operaciones de bajo nivel.

Luego, la introducción de Genéricos en JDK 1.5 mejoró significativamente el marco de colecciones de Java. Los Genéricos te permiten hacer cumplir la seguridad de tipos para los objetos almacenados en una colección, lo que mejora la robustez de tus aplicaciones. Puedes leer más sobre Genéricos en Java aquí.

En este artículo, te guiaré sobre cómo utilizar el marco de colecciones de Java. Discutiremos los diferentes tipos de colecciones, como Listas, Conjuntos, Colas y Mapas. También proporcionaré una breve explicación de sus características clave como:

  • Mecanismos internos

  • Manejo de duplicados

  • Soporte para valores nulos

  • Ordenamiento

  • Sincronización

  • Rendimiento

  • Métodos clave

  • Implementaciones comunes

También repasaremos algunos ejemplos de código para una mejor comprensión, y mencionaré la clase de utilidad Collections y su uso.

Tabla de contenidos:

  1. Entendiendo el Marco de Colecciones de Java

  2. Interfaces de Colección de Java

  3. Clase de utilidad de colecciones

  4. Conclusión

Comprendiendo el Marco de Colecciones de Java

Según la documentación de Java, “Una colección es un objeto que representa un grupo de objetos. Un marco de colecciones es una arquitectura unificada para representar y manipular colecciones.”

En términos simples, el Marco de Colecciones de Java te ayuda a gestionar un grupo de objetos y realizar operaciones sobre ellos de manera eficiente y organizada. Facilita el desarrollo de aplicaciones al ofrecer varios métodos para manejar grupos de objetos. Puedes añadir, eliminar, buscar y ordenar objetos de manera efectiva utilizando el Marco de Colecciones de Java.

Interfaces de Colección

En Java, una interfaz especifica un contrato que debe ser cumplido por cualquier clase que la implemente. Esto significa que la clase que implementa debe proporcionar implementaciones concretas para todos los métodos declarados en la interfaz.

En el Marco de Colecciones de Java, varias interfaces de colección como Set, List y Queue extienden la interfaz Collection, y deben cumplir con el contrato definido por la interfaz Collection.

Descifrando la Jerarquía del Marco de Colecciones de Java

Consulta este diagrama interesante de este artículo que ilustra la Jerarquía de Colecciones de Java:

Comenzaremos desde arriba y descenderemos para que puedas entender lo que muestra este diagrama:

  1. En la raíz del Marco de Colecciones de Java se encuentra la interfaz Iterable, que te permite iterar sobre los elementos de una colección.

  2. La interfaz Collection extiende la interfaz Iterable. Esto significa que hereda las propiedades y el comportamiento de la interfaz Iterable y agrega su propio comportamiento para añadir, eliminar y recuperar elementos.

  3. Interfaces específicas como List, Set y Queue extienden aún más la interfaz Collection. Cada una de estas interfaces tiene otras clases que implementan sus métodos. Por ejemplo, ArrayList es una implementación popular de la interfaz List, HashSet implementa la interfaz Set, y así sucesivamente.

  4. La interfaz Map es parte del Marco de Colecciones de Java, pero no extiende la interfaz Collection, a diferencia de las mencionadas anteriormente.

  5. Todas las interfaces y clases en este marco son parte del paquete java.util.

Nota: Una fuente común de confusión en el Marco de Colecciones de Java gira en torno a la diferencia entre Collection y Collections. Collection es una interfaz en el marco, mientras que Collections es una clase de utilidad. La clase Collections proporciona métodos estáticos que realizan operaciones en los elementos de una colección.

Interfaces de Colecciones de Java

En este punto, ya estás familiarizado con los diferentes tipos de colecciones que forman la base del marco de colecciones. Ahora vamos a examinar más de cerca las interfaces List, Set, Queue y Map.

En esta sección, discutiremos cada una de estas interfaces mientras exploramos sus mecanismos internos. Examinaremos cómo manejan los elementos duplicados y si admiten la inserción de valores nulos. También comprenderemos el orden de los elementos durante la inserción y su soporte para la sincronización, que trata con el concepto de seguridad de hilos. Luego repasaremos algunos métodos clave de estas interfaces y concluiremos revisando implementaciones comunes y su rendimiento para varias operaciones.

Antes de comenzar, hablemos brevemente sobre Sincronización y Rendimiento.

  • La sincronización controla el acceso a objetos compartidos por múltiples hilos, asegurando su integridad y evitando conflictos. Esto es crucial para mantener la seguridad de los hilos.

  • Cuando se elige un tipo de colección, un factor importante es su rendimiento durante operaciones comunes como la inserción, eliminación y recuperación. El rendimiento suele expresarse utilizando la notación Big-O. Puedes aprender más al respecto aquí.

Lists

Una List es una colección ordenada o secuencial de elementos. Sigue un índice basado en cero, lo que permite insertar, eliminar o acceder a los elementos utilizando su posición de índice.

  1. Mecanismo interno: Una List está internamente respaldada por un array o una lista enlazada, dependiendo del tipo de implementación. Por ejemplo, un ArrayList utiliza un array, mientras que un LinkedList utiliza una lista enlazada internamente. Puedes leer más sobre LinkedList aquí. Una List cambia dinámicamente de tamaño al agregar o eliminar elementos. La recuperación basada en índices lo convierte en un tipo de colección muy eficiente.

  2. Duplicados: Se permiten elementos duplicados en una List, lo que significa que puede haber más de un elemento en una List con el mismo valor. Cualquier valor puede recuperarse basado en el índice en el que está almacenado.

  3. Nulo: También se permiten valores nulos en una List. Dado que se permiten duplicados, también se pueden tener múltiples elementos nulos.

  4. Orden: Una List mantiene el orden de inserción, lo que significa que los elementos se almacenan en el mismo orden en el que se añaden. Esto es útil cuando deseas recuperar elementos en el orden exacto en el que fueron insertados.

  5. Sincronización: Una Lista no está sincronizada de forma predeterminada, lo que significa que no tiene una forma incorporada de manejar el acceso por múltiples hilos al mismo tiempo.

  6. Métodos clave: Aquí hay algunos métodos clave de una interfaz List: add(E elemento), get(int índice), set(int índice, E elemento), remove(int índice) y size(). Veamos cómo utilizar estos métodos con un programa de ejemplo.

     importar java.util.ArrayList;
     importar java.util.List;
    
     público clase EjemploLista {
         público estático vacío principal(String[] args) {
             // Crear una lista
             List<String> lista = nuevo ArrayList<>();
    
             // add(E elemento)
             lista.add("Manzana");
             lista.add("Plátano");
             lista.add("Cereza");
    
             // get(int índice)
             String segundoElemento = lista.get(1); // "Plátano"
    
             // set(int índice, E elemento)
             lista.set(1, "Arándano");
    
             // remove(int índice)
             lista.remove(0); // Elimina "Manzana"
    
             // size()
             int tamaño = lista.size(); // 2
    
             // Imprimir la lista
             System.out.println(lista); // Salida: [Arándano, Cereza]
    
             // Imprimir el tamaño de la lista
             System.out.println(tamaño); // Salida: 2
         }
     }
    
  7. Implementaciones comunes: ArrayList, LinkedList, Vector, Stack

  8. Rendimiento: Normalmente, las operaciones de inserción y eliminación son rápidas tanto en ArrayList como en LinkedList. Pero la recuperación de elementos puede ser lenta porque hay que atravesar los nodos.

Operación ArrayList LinkedList
Inserción Rápida al final – O(1) amortizado, lenta al principio o en medio – O(n) Rápida al principio o en medio – O(1), lenta al final – O(n)
Eliminación Rápida al final – O(1) amortizado, lenta al principio o en medio – O(n) Rápida – O(1) si la posición es conocida
Recuperación Rápida – O(1) para acceso aleatorio Lenta – O(n) para acceso aleatorio, ya que implica atravesar

Conjuntos

Un Set es un tipo de colección que no permite elementos duplicados y representa el concepto de un conjunto matemático.

  1. Mecanismo interno: Un Set está respaldado internamente por un HashMap. Dependiendo del tipo de implementación, es compatible con un HashMap, LinkedHashMap o un TreeMap. He escrito un artículo detallado sobre cómo funciona internamente un HashMap aquí. Asegúrate de revisarlo.

  2. Duplicados: Dado que un Set representa el concepto de un conjunto matemático, no se permiten elementos duplicados. Esto garantiza que todos los elementos sean únicos, manteniendo la integridad de la colección.

  3. Nulo: Se permite un máximo de un valor nulo en un Set porque no se permiten duplicados. Pero esto no se aplica a la implementación de TreeSet, donde los valores nulos no están permitidos.

  4. Orden: El orden de los elementos en un Set depende del tipo de implementación.

    • HashSet: El orden no está garantizado, y los elementos pueden ser colocados en cualquier posición.

    • LinkedHashSet: Esta implementación mantiene el orden de inserción, por lo que puedes recuperar los elementos en el mismo orden en que fueron insertados.

    • TreeSet: Los elementos se insertan basados en su orden natural. Alternativamente, puedes controlar el orden de inserción especificando un comparador personalizado.

  5. Sincronización: Un Set no está sincronizado, lo que significa que podrías encontrarte con problemas de concurrencia, como condiciones de carrera, que pueden afectar la integridad de los datos si dos o más hilos intentan acceder a un objeto Set simultáneamente

  6. Métodos clave: Aquí hay algunos métodos clave de una interfaz Set: add(E elemento), remove(Object o), contains(Object o), y size(). Veamos cómo usar estos métodos con un programa de ejemplo.

     import java.util.HashSet;
     import java.util.Set;
    
     public class SetExample {
         public static void main(String[] args) {
             // Crear un conjunto
             Set<String> set = new HashSet<>();
    
             // Agregar elementos al conjunto
             set.add("Manzana");
             set.add("Plátano");
             set.add("Cereza");
    
             // Eliminar un elemento del conjunto
             set.remove("Plátano");
    
             // Comprobar si el conjunto contiene un elemento
             boolean contieneManzana = set.contains("Manzana");
             System.out.println("Contiene Manzana: " + contieneManzana);
    
             // Obtener el tamaño del conjunto
             int tamaño = set.size();
             System.out.println("Tamaño del conjunto: " + tamaño);
         }
     }
    
  7. Implementaciones comunes: HashSet, LinkedHashSet, TreeSet

  8. Rendimiento: Las implementaciones de Set ofrecen un rendimiento rápido para operaciones básicas, excepto para un TreeSet, donde el rendimiento puede ser relativamente más lento porque la estructura de datos interna implica ordenar los elementos durante estas operaciones.

Operación HashSet LinkedHashSet TreeSet
Inserción Rápido – O(1) Rápido – O(1) Más lento – O(log n)
Eliminación Rápido – O(1) Rápido – O(1) Más lento – O(log n)
Recuperación Rápido – O(1) Rápido – O(1) Más lento – O(log n)

Colas

Una Cola es una colección lineal de elementos utilizada para contener múltiples elementos antes de procesarlos, generalmente siguiendo el orden FIFO (primero en entrar, primero en salir). Esto significa que los elementos se agregan en un extremo y se eliminan en el otro, por lo que el primer elemento agregado a la cola es el primero en ser eliminado.

  1. Mecanismo interno: El funcionamiento interno de una Cola puede variar según su implementación específica.

    • LinkedList – utiliza una lista doblemente enlazada para almacenar elementos, lo que significa que se puede recorrer tanto hacia adelante como hacia atrás, lo que permite operaciones flexibles.

    • PriorityQueue – está respaldada internamente por un montículo binario, que es muy eficiente para las operaciones de recuperación.

    • ArrayDeque – se implementa utilizando un array que se expande o contrae a medida que se agregan o eliminan elementos. Aquí, los elementos se pueden agregar o eliminar desde ambos extremos de la cola.

  2. Duplicados: En una Queue, se permiten elementos duplicados, lo que permite insertar múltiples instancias del mismo valor

  3. Nulo: No puedes insertar un valor nulo en una Queue porque, por diseño, algunos métodos de una Queue devuelven nulo para indicar que está vacía. Para evitar confusiones, no se permiten valores nulos.

  4. Ordenamiento: Los elementos se insertan según su orden natural. Alternativamente, puedes controlar el orden de inserción especificando un comparador personalizado.

  5. Sincronización: Una Queue no está sincronizada de forma predeterminada. Sin embargo, puedes utilizar una implementación de ConcurrentLinkedQueue o BlockingQueue para lograr la seguridad en hilos.

  6. Métodos clave: Aquí hay algunos métodos clave de una interfaz Queue: add(E element), offer(E element), poll() y peek(). Veamos cómo usar estos métodos con un programa de ejemplo.

     import java.util.LinkedList;
     import java.util.Queue;
    
     public class QueueExample {
         public static void main(String[] args) {
             // Crear una cola usando LinkedList
             Queue<String> queue = new LinkedList<>();
    
             // Usar el método add para insertar elementos, lanza una excepción si la inserción falla
             queue.add("Elemento1");
             queue.add("Elemento2");
             queue.add("Elemento3");
    
             // Usar el método offer para insertar elementos, devuelve false si la inserción falla
             queue.offer("Elemento4");
    
             // Mostrar la cola
             System.out.println("Cola: " + queue);
    
             // Ver el primer elemento (sin eliminarlo)
             String primerElemento = queue.peek();
             System.out.println("Mirar: " + primerElemento); // muestra "Element1"
    
             // Extraer el primer elemento (lo obtiene y lo elimina)
             String elementoExtraido = queue.poll();
             System.out.println("Extraer: " + elementoExtraido); // muestra "Element1"
    
             // Mostrar la cola después de extraer
             System.out.println("Cola después de extraer: " + queue);
         }
     }
    
  7. Implementaciones comunes: LinkedList, PriorityQueue, ArrayDeque

  8. Rendimiento: Implementaciones como LinkedList y ArrayDeque suelen ser rápidas para agregar y quitar elementos. La PriorityQueue es un poco más lenta porque inserta elementos basándose en el orden de prioridad establecido.

Operación LinkedList PriorityQueue ArrayDeque
Inserción Rápida al principio o en medio – O(1), lenta al final – O(n) Más lenta – O(log n) Rápida – O(1), Lenta – O(n), si implica cambiar el tamaño del array interno
Eliminación Rápida – O(1) si se conoce la posición Más lenta – O(log n) Rápida – O(1), Lenta – O(n), si implica cambiar el tamaño del array interno
Recuperación Lenta – O(n) para acceso aleatorio, ya que implica recorrer Rápida – O(1) Rápida – O(1)

Mapas

Un Map representa una colección de pares clave-valor, donde cada clave se asigna a un único valor. Aunque Map forma parte del framework de colecciones de Java, no extiende la interfaz java.util.Collection.

  1. Mecanismo interno: Un Map funciona internamente utilizando un HashTable basado en el concepto de hashing. He escrito un artículo detallado sobre este tema, así que léelo para comprender mejor.

  2. Duplicados: Un Map almacena datos como pares clave-valor. Aquí, cada clave es única, por lo que no se permiten claves duplicadas. Sin embargo, se permiten valores duplicados.

  3. Nulo: Dado que no se permiten claves duplicadas, un Map puede tener solo una clave nula. Como se permiten valores duplicados, puede tener múltiples valores nulos. En la implementación de TreeMap, las claves no pueden ser nulas porque ordena los elementos según las claves. Sin embargo, se permiten valores nulos.

  4. Orden: El orden de inserción de un Map varía según la implementación:

    • HashMap – el orden de inserción no está garantizado ya que se determina según el concepto de hash.

    • LinkedHashMap – se conserva el orden de inserción y se pueden recuperar los elementos en el mismo orden en que se agregaron a la colección.

    • TreeMap – Los elementos se insertan según su orden natural. Alternativamente, puede controlar el orden de inserción especificando un comparador personalizado.

  5. Sincronización: Un Map no está sincronizado por defecto. Pero puedes usar Collections.synchronizedMap() o las implementaciones de ConcurrentHashMap para lograr seguridad en hilos.

  6. Métodos clave: Aquí hay algunos métodos clave de una interfaz Map: put(K key, V value), get(Object key), remove(Object key), containsKey(Object key) y keySet(). Veamos cómo usar estos métodos con un programa de ejemplo.

     import java.util.HashMap;
     import java.util.Map;
     import java.util.Set;
    
     public class EjemploMetodosMapa {
         public static void main(String[] args) {
             // Crear un nuevo HashMap
             Map<String, Integer> mapa = new HashMap<>();
    
             // put(K key, V value) - Inserta pares clave-valor en el mapa
             mapa.put("Manzana", 1);
             mapa.put("Plátano", 2);
             mapa.put("Naranja", 3);
    
             // get(Object key) - Devuelve el valor asociado con la clave
             Integer valor = mapa.get("Plátano");
             System.out.println("Valor de 'Plátano': " + valor);
    
             // remove(Object key) - Elimina el par clave-valor para la clave especificada
             mapa.remove("Naranja");
    
             // containsKey(Object key) - Verifica si el mapa contiene la clave especificada
             boolean tieneManzana = mapa.containsKey("Manzana");
             System.out.println("Contiene 'Manzana': " + tieneManzana);
    
             // keySet() - Devuelve una vista de conjunto de las claves contenidas en el mapa
             Set<String> claves = mapa.keySet();
             System.out.println("Claves en el mapa: " + claves);
         }
     }
    
  7. Implementaciones comunes: HashMap, LinkedHashMap, TreeMap, Hashtable, ConcurrentHashMap

  8. Rendimiento: La implementación de HashMap es ampliamente utilizada principalmente debido a sus características de rendimiento eficientes representadas en la tabla a continuación.

Operación HashMap LinkedHashMap TreeMap
Inserción Rápida – O(1) Rápida – O(1) Más lenta – O(log n)
Eliminación Rápida – O(1) Rápida – O(1) Más lenta – O(log n)
Recuperación Rápida – O(1) Rápida – O(1) Más lenta – O(log n)

Clase de utilidad de Colecciones

Como se destacó al principio de este artículo, la clase de utilidad Collections tiene varios métodos estáticos útiles que le permiten realizar operaciones comúnmente utilizadas en los elementos de una colección. Estos métodos le ayudan a reducir el código repetitivo en su aplicación y le permiten centrarse en la lógica del negocio.

Aquí hay algunas características clave y métodos, junto con lo que hacen, enumerados brevemente:

  1. Ordenamiento: Collections.sort(List<T>) – este método se utiliza para ordenar los elementos de una lista en orden ascendente.

  2. Búsqueda: Collections.binarySearch(List<T>, clave) – este método se utiliza para buscar un elemento específico en una lista ordenada y devolver su índice.

  3. Orden inverso: Collections.reverse(List<T>) – este método se utiliza para invertir el orden de los elementos en una lista.

  4. Operaciones Min/Max: Collections.min(Collection<T>) y Collections.max(Collection<T>) – estos métodos se utilizan para encontrar los elementos mínimo y máximo en una colección, respectivamente.

  5. Sincronización: Collections.synchronizedList(List<T>) – este método se utiliza para hacer una lista segura para subprocesos sincronizándola.

  6. Colecciones no modificables: Collections.unmodifiableList(List<T>) – este método se utiliza para crear una vista de solo lectura de una lista, evitando modificaciones.

Aquí tienes un programa de ejemplo en Java que muestra varias funcionalidades de la clase de utilidad Collections:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CollectionsExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(5);
        numbers.add(3);
        numbers.add(8);
        numbers.add(1);

        // Ordenamiento
        Collections.sort(numbers);
        System.out.println("Sorted List: " + numbers);

        // Búsqueda
        int index = Collections.binarySearch(numbers, 3);
        System.out.println("Index of 3: " + index);

        // Orden inverso
        Collections.reverse(numbers);
        System.out.println("Reversed List: " + numbers);

        // Operaciones de mínimo/máximo
        int min = Collections.min(numbers);
        int max = Collections.max(numbers);
        System.out.println("Min: " + min + ", Max: " + max);

        // Sincronización
        List<Integer> synchronizedList = Collections.synchronizedList(numbers);
        System.out.println("Synchronized List: " + synchronizedList);

        // Colecciones no modificables
        List<Integer> unmodifiableList = Collections.unmodifiableList(numbers);
        System.out.println("Unmodifiable List: " + unmodifiableList);
    }
}

Este programa demuestra el ordenamiento, la búsqueda, la inversión, la búsqueda de los valores mínimo y máximo, la sincronización y la creación de una lista no modificable utilizando la clase de utilidad Collections.

Conclusión

En este artículo, has aprendido sobre el Marco de Colecciones de Java y cómo ayuda a gestionar grupos de objetos en aplicaciones Java. Exploramos varios tipos de colecciones como Listas, Conjuntos, Colas y Mapas y obtuvimos información sobre algunas de las características clave y cómo cada uno de estos tipos las soporta.

Aprendiste sobre rendimiento, sincronización y métodos clave, obteniendo información valiosa para elegir las estructuras de datos adecuadas para tus necesidades.

Al comprender estos conceptos, puedes aprovechar al máximo el Marco de Colecciones de Java, lo que te permitirá escribir código más eficiente y crear aplicaciones sólidas.

Si encontraste este artículo interesante, no dudes en consultar mis otros artículos en freeCodeCamp y conectarte conmigo en LinkedIn.