10 Ejercicios resueltos de Genéricos y Colecciones en Java
Aprende a dominar los genéricos y las colecciones en Java con esta guía práctica. Descubre cómo crear código más seguro, flexible y reutilizable a través de 10 ejercicios resueltos paso a paso, ideales para afianzar tus conocimientos y mejorar tu lógica de programación.
Aprendizaje teórico
¿Qué son los Genéricos y las Colecciones en Java?
En Java, las Colecciones son estructuras de datos avanzadas que nos permiten almacenar, organizar y manipular grupos de objetos de forma eficiente. Piensa en ellas como cajas especializadas: algunas mantienen el orden exacto (como List), otras evitan automáticamente los duplicados (Set), y otras guardan los datos en pares de clave-valor (Map).
Por otro lado, los Genéricos (representados comúnmente con símbolos como <T>) son una característica que nos permite especificar exactamente qué tipo de dato guardará esa «caja».
¿Para qué sirven?
Antes de que existieran los genéricos, las colecciones guardaban objetos de tipo Object, lo que provocaba errores sorpresivos al ejecutar el código si mezclábamos datos. Los genéricos aportan seguridad de tipos: el compilador te avisa de inmediato si intentas guardar un número en una lista que creaste para textos.
Ejemplo breve: en lugar de crear una clase para enteros y otra para textos, creas una clase genérica Caja<T>. Al instanciarla, decides su contenido: Caja<String> o Caja<Integer>. Esto hace tu código mucho más reutilizable.
Ejercicio 1: Clase Genérica Simple
Crea una clase genérica llamada Caja que pueda almacenar un objeto de cualquier tipo. La clase debe tener un constructor para inicializar el objeto, un método para guardarlo (modificarlo) y otro para obtenerlo. Pruébala guardando un texto y luego un número.
Pista
Utiliza la convención <T> al definir el nombre de la clase para indicar que trabajará con un tipo de dato genérico.
class Caja<T> {
private T contenido;
public Caja(T contenido) {
this.contenido = contenido;
}
public T getContenido() {
return contenido;
}
public void setContenido(T contenido) {
this.contenido = contenido;
}
}
public class Main {
public static void main(String[] args) {
// Creamos una caja para textos
Caja<String> cajaTexto = new Caja<>("Hola Mundo");
System.out.println("Caja de texto: " + cajaTexto.getContenido());
// Creamos una caja para números enteros
Caja<Integer> cajaNumero = new Caja<>(42);
System.out.println("Caja de número: " + cajaNumero.getContenido());
}
}
terminal Resultado esperado:
Caja de texto: Hola Mundo Caja de número: 42
Ejercicio 2: Método Genérico
Escribe una clase que contenga un método genérico capaz de recibir un arreglo de cualquier tipo (ya sean números, textos, etc.) y lo imprima elemento por elemento en la consola, separados por una coma.
Pista
El parámetro de tipo <T> debe colocarse justo antes del tipo de retorno del método (por ejemplo, antes de void).
public class Main {
// Método genérico
public static <T> void imprimirArreglo(T[] arreglo) {
for (int i = 0; i < arreglo.length; i++) {
System.out.print(arreglo[i]);
if (i < arreglo.length - 1) {
System.out.print(", ");
}
}
System.out.println();
}
public static void main(String[] args) {
Integer[] numeros = {1, 2, 3, 4, 5};
String[] palabras = {"Java", "es", "genial"};
System.out.print("Arreglo de números: ");
imprimirArreglo(numeros);
System.out.print("Arreglo de palabras: ");
imprimirArreglo(palabras);
}
}
terminal Resultado esperado:
Arreglo de números: 1, 2, 3, 4, 5 Arreglo de palabras: Java, es, genial
Ejercicio 3: Lista de tareas con ArrayList
Crea un programa que utilice la colección ArrayList para gestionar una lista de tareas pendientes. Debes añadir tres tareas en formato de texto, eliminar la segunda tarea de la lista y mostrar por consola las tareas restantes.
Pista
Utiliza el método add() para agregar elementos y remove(indice) para eliminar, recordando que los índices en Java empiezan en 0.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
// Declaramos la lista genérica para Strings
List<String> tareas = new ArrayList<>();
// Añadimos elementos
tareas.add("Aprender Genéricos");
tareas.add("Hacer las compras");
tareas.add("Practicar con colecciones");
System.out.println("Lista original: " + tareas);
// Eliminamos la segunda tarea (índice 1)
tareas.remove(1);
System.out.println("Lista actualizada: " + tareas);
}
}
terminal Resultado esperado:
Lista original: [Aprender Genéricos, Hacer las compras, Practicar con colecciones] Lista actualizada: [Aprender Genéricos, Practicar con colecciones]
Ejercicio 4: Eliminar duplicados con HashSet
Imagina que tienes una lista de números enteros donde varios valores se repiten. Utiliza la colección HashSet para crear una nueva estructura que contenga únicamente los números únicos y muéstrala en consola.
Pista
La colección Set en Java no permite elementos duplicados. Puedes pasar la lista original directamente al constructor del HashSet para limpiarla al instante.
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class Main {
public static void main(String[] args) {
List<Integer> numerosConDuplicados = new ArrayList<>();
numerosConDuplicados.add(5);
numerosConDuplicados.add(10);
numerosConDuplicados.add(5);
numerosConDuplicados.add(20);
numerosConDuplicados.add(10);
System.out.println("Con duplicados: " + numerosConDuplicados);
// Al pasar la lista al HashSet, los duplicados se eliminan automáticamente
Set<Integer> numerosUnicos = new HashSet<>(numerosConDuplicados);
System.out.println("Sin duplicados: " + numerosUnicos);
}
}
terminal Resultado esperado:
Con duplicados: [5, 10, 5, 20, 10] Sin duplicados: [20, 5, 10]
Ejercicio 5: Registro de calificaciones con HashMap
Utiliza un HashMap para crear un registro de calificaciones. Guarda el nombre de tres estudiantes como clave (String) y su calificación numérica como valor (Double). Luego, recupera e imprime la calificación de un estudiante en particular.
Pista
Usa el método put(clave, valor) para guardar la información y get(clave) para recuperarla fácilmente mediante el nombre.
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
// Map con clave String (nombre) y valor Double (calificación)
Map<String, Double> calificaciones = new HashMap<>();
// Guardamos los datos
calificaciones.put("Ana", 8.5);
calificaciones.put("Carlos", 9.2);
calificaciones.put("Lucia", 7.8);
// Buscamos a un estudiante
String estudianteABuscar = "Carlos";
Double nota = calificaciones.get(estudianteABuscar);
if (nota != null) {
System.out.println("La calificación de " + estudianteABuscar + " es: " + nota);
} else {
System.out.println("Estudiante no encontrado.");
}
}
}
terminal Resultado esperado:
La calificación de Carlos es: 9.2
Ejercicio 6: Clase genérica con múltiples tipos
Crea una clase genérica llamada Pareja que acepte dos tipos de datos diferentes (uno para un identificador y otro para el valor). Instancia la clase para guardar el nombre de una persona y su edad, y luego muéstralos.
Pista
Declara la clase utilizando dos letras para los genéricos, por ejemplo class Pareja<K, V>, y define los atributos utilizando esos mismos tipos.
class Pareja<K, V> {
private K clave;
private V valor;
public Pareja(K clave, V valor) {
this.clave = clave;
this.valor = valor;
}
public void mostrarDatos() {
System.out.println("Clave: " + clave + " | Valor: " + valor);
}
}
public class Main {
public static void main(String[] args) {
// Usamos String para la clave y Integer para el valor
Pareja<String, Integer> persona = new Pareja<>("Alejandro", 30);
persona.mostrarDatos();
// Podemos reusar la clase para otros tipos de datos
Pareja<Integer, String> codigoPostal = new Pareja<>(1001, "Zona Central");
codigoPostal.mostrarDatos();
}
}
terminal Resultado esperado:
Clave: Alejandro | Valor: 30 Clave: 1001 | Valor: Zona Central
Ejercicio 7: Restricciones de tipo (Bounded Types)
Crea una clase genérica llamada Calculadora que tenga una restricción: solo debe aceptar números (clases que deriven de Number). Implementa un método que reciba un valor genérico y devuelva su doble exacto convertido a tipo double.
Pista
Utiliza la palabra clave extends en la declaración del genérico (<T extends Number>). Esto te permitirá usar el método doubleValue() que comparten todos los números en Java.
// Restringimos T para que solo acepte subclases de Number
class Calculadora<T extends Number> {
private T numero;
public Calculadora(T numero) {
this.numero = numero;
}
public double obtenerDoble() {
// Al saber que es un Number, podemos usar doubleValue()
return numero.doubleValue() * 2;
}
}
public class Main {
public static void main(String[] args) {
Calculadora<Integer> calcEntero = new Calculadora<>(15);
System.out.println("Doble de 15 (Entero): " + calcEntero.obtenerDoble());
Calculadora<Double> calcDecimal = new Calculadora<>(10.5);
System.out.println("Doble de 10.5 (Decimal): " + calcDecimal.obtenerDoble());
// Calculadora<String> error = new Calculadora<>("Hola"); // Esto daría error de compilación
}
}
terminal Resultado esperado:
Doble de 15 (Entero): 30.0 Doble de 10.5 (Decimal): 21.0
Ejercicio 8: Ordenar objetos con Comparable
Crea una clase Jugador que tenga un nombre y un puntaje. Haz que esta clase implemente la interfaz genérica Comparable<Jugador> para que los jugadores puedan ordenarse de mayor a menor puntaje usando Collections.sort().
Pista
Sobrescribe el método compareTo(Jugador otro). Para ordenar de mayor a menor (descendente), compara el puntaje del otro jugador con el tuyo.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
class Jugador implements Comparable<Jugador> {
private String nombre;
private int puntaje;
public Jugador(String nombre, int puntaje) {
this.nombre = nombre;
this.puntaje = puntaje;
}
@Override
public int compareTo(Jugador otro) {
// Orden descendente (mayor a menor)
return Integer.compare(otro.puntaje, this.puntaje);
}
@Override
public String toString() {
return nombre + " (" + puntaje + " pts)";
}
}
public class Main {
public static void main(String[] args) {
List<Jugador> jugadores = new ArrayList<>();
jugadores.add(new Jugador("Mario", 1500));
jugadores.add(new Jugador("Luigi", 2100));
jugadores.add(new Jugador("Peach", 1800));
System.out.println("Sin ordenar: " + jugadores);
// Collections.sort() utiliza el método compareTo internamente
Collections.sort(jugadores);
System.out.println("Ordenados: " + jugadores);
}
}
terminal Resultado esperado:
Sin ordenar: [Mario (1500 pts), Luigi (2100 pts), Peach (1800 pts)] Ordenados: [Luigi (2100 pts), Peach (1800 pts), Mario (1500 pts)]
Ejercicio 9: Comodines (Wildcards) con colecciones
Escribe un método estático que reciba una lista, pero con una condición: la lista debe poder contener cualquier tipo numérico (Double, Integer, Float). El método debe sumar todos los elementos y devolver el total.
Pista
En los parámetros del método, utiliza el comodín o wildcard ? combinado con extends Number, así: List<? extends Number>.
import java.util.Arrays;
import java.util.List;
public class Main {
// Método que acepta una lista de cualquier tipo que herede de Number
public static double sumarLista(List<? extends Number> lista) {
double suma = 0.0;
for (Number num : lista) {
suma += num.doubleValue();
}
return suma;
}
public static void main(String[] args) {
List<Integer> enteros = Arrays.asList(1, 2, 3, 4);
List<Double> decimales = Arrays.asList(1.5, 2.5, 3.0);
System.out.println("Suma de enteros: " + sumarLista(enteros));
System.out.println("Suma de decimales: " + sumarLista(decimales));
}
}
terminal Resultado esperado:
Suma de enteros: 10.0 Suma de decimales: 7.0
Ejercicio 10: Integración de Genéricos y Colecciones
Crea un mini sistema de inventario. Define una clase genérica Producto<T> (donde T representa el tipo de código del producto, que puede ser alfanumérico o numérico). Luego, utiliza un HashMap para guardar el inventario, asociando el código del producto como clave y el objeto Producto como valor. Por último, imprime el inventario.
Pista
El Map utilizará la misma declaración de tipo que el identificador del producto. Si T es un String, el Map se definirá como Map<String, Producto<String>>.
import java.util.HashMap;
import java.util.Map;
// Clase producto genérica para soportar distintos formatos de códigos
class Producto<T> {
private T codigo;
private String nombre;
private double precio;
public Producto(T codigo, String nombre, double precio) {
this.codigo = codigo;
this.nombre = nombre;
this.precio = precio;
}
public T getCodigo() {
return codigo;
}
@Override
public String toString() {
return nombre + " - $" + precio;
}
}
public class Main {
public static void main(String[] args) {
// Inventario donde el código es de tipo String
Map<String, Producto<String>> inventario = new HashMap<>();
Producto<String> prod1 = new Producto<>("A-101", "Teclado Mecánico", 45.99);
Producto<String> prod2 = new Producto<>("B-202", "Ratón Inalámbrico", 25.50);
inventario.put(prod1.getCodigo(), prod1);
inventario.put(prod2.getCodigo(), prod2);
// Imprimir todo el inventario iterando sobre el Map
System.out.println("--- Inventario de la Tienda ---");
for (Map.Entry<String, Producto<String>> entrada : inventario.entrySet()) {
System.out.println("Código: " + entrada.getKey() + " | Detalles: " + entrada.getValue());
}
}
}
terminal Resultado esperado:
--- Inventario de la Tienda --- Código: A-101 | Detalles: Teclado Mecánico - $45.99 Código: B-202 | Detalles: Ratón Inalámbrico - $25.50
Preguntas frecuentes
¿Cuál es la diferencia entre ArrayList y LinkedList?
Ambas implementan la interfaz List. ArrayList usa un arreglo dinámico, siendo más rápido para buscar elementos. LinkedList enlaza los datos como una cadena, por lo que es más eficiente al insertar o eliminar elementos en medio de la lista.
¿Por qué usar genéricos en lugar de Object?
Los genéricos proporcionan seguridad de tipos en tiempo de compilación. Si usas Object, puedes guardar cualquier cosa, pero al recuperar los datos debes hacer conversiones manuales (casteos), lo que suele causar errores inesperados al ejecutar el programa.
¿Qué diferencia hay entre List, Set y Map?
List guarda elementos en orden y permite duplicados. Set no mantiene un orden específico y prohíbe terminantemente los elementos repetidos. Map funciona diferente, ya que almacena la información relacionándola siempre mediante pares de clave y valor.
Conclusión
Dominar las colecciones y los genéricos marca la diferencia entre un código rígido y uno verdaderamente profesional. Como hemos visto en estos ejercicios, estas herramientas te permiten crear estructuras de datos adaptables, seguras y altamente reutilizables.
Te animo a seguir experimentando: cambia los tipos de datos, combina diferentes colecciones o crea tus propias clases genéricas. La práctica constante es la mejor forma de afianzar estos conceptos en tu mente. ¡Sigue programando y elevando la calidad de tus proyectos en Java!
Sigue aprendiendo
Guía profunda sobre el funcionamiento de los genéricos, explicada por los creadores del lenguaje.
Aprende a fondo la jerarquía completa de colecciones y sus interfaces principales.
Herramientas recomendadas para practicar
Java es un lenguaje de programación orientado a objetos, tipado y multiplataforma, respaldado por la JVM. Su principio de «escribe una vez, ejecuta en cualquier lugar» lo hace útil para sistemas empresariales, microservicios en la nube, escritorio y Android.
Sigue practicando
Otros ejercicios prácticos recomendados
10 Ejercicios resueltos de Archivos y Directorios en Java
Aprende a gestionar archivos y directorios en Java con estos 10 ejercicios resueltos. Descubre cómo crear, leer, escribir y organizar carpetas paso a paso. Práctica ideal para afianzar tus conocimientos intermedios y apl...
10 Ejercicios resueltos de Manejo de Excepciones en Java
Descubre cómo crear programas a prueba de fallos con estos 10 ejercicios resueltos de manejo de excepciones en Java. Aprenderás a usar try-catch, prever errores comunes y construir aplicaciones estables y profesionales....
10 Ejercicios resueltos de ArrayList y HashMap en Java
Aprende a gestionar colecciones de datos dinámicas con estos 10 ejercicios resueltos de ArrayList y HashMap en Java. Mejora tu lógica de programación, organiza la información eficientemente y domina el nivel intermedio c...