Ejercicios mixtos de Java 3
En las unidades anteriores aprendiste varias herramientas para trabajar con datos. Ahora toca elegirlas y combinarlas. No busques usar todas en cada problema: la mejor solución suele ser la que expresa con claridad la necesidad real.
1. Inscripciones en orden
Enunciado. Guarda estas inscripciones en el orden de llegada: Ada, Linus, Ada, Grace. Deben conservarse las repeticiones. Muestra la primera y sustituye la última por Margaret.
Criterio. Necesitamos orden, posiciones y duplicados; por eso elegimos una List.
import java.util.ArrayList;
import java.util.List;
public class Inscripciones {
public static void main(String[] args) {
List<String> nombres = new ArrayList<>();
nombres.add("Ada"); nombres.add("Linus");
nombres.add("Ada"); nombres.add("Grace");
System.out.println("Primera: " + nombres.get(0));
nombres.set(nombres.size() - 1, "Margaret");
System.out.println(nombres);
}
}
Un Set eliminaría la segunda inscripción de Ada, aunque en este dominio representa otra llegada válida.
2. Etiquetas sin duplicados
Enunciado. A partir de java, api, java, datos, obtén las etiquetas únicas y comprueba si existe api. El orden no importa.
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class Etiquetas {
public static void main(String[] args) {
List<String> entrada = List.of("java", "api", "java", "datos");
Set<String> unicas = new HashSet<>(entrada);
System.out.println("Cantidad: " + unicas.size());
System.out.println("Contiene api: " + unicas.contains("api"));
}
}
Aquí la unicidad es una regla del problema. HashSet la garantiza sin búsquedas manuales, pero no promete orden de iteración.
3. Frecuencia de palabras
Enunciado. Cuenta cuántas veces aparece cada palabra en java datos java api datos java.
import java.util.HashMap;
import java.util.Map;
public class Frecuencias {
public static void main(String[] args) {
String[] palabras = "java datos java api datos java".split(" ");
Map<String, Integer> frecuencias = new HashMap<>();
for (String palabra : palabras) {
frecuencias.put(palabra, frecuencias.getOrDefault(palabra, 0) + 1);
}
frecuencias.forEach((palabra, cantidad) ->
System.out.println(palabra + ": " + cantidad));
}
}
Cada palabra es una clave única y su contador es el valor asociado. Una lista de pares obligaría a buscar la palabra antes de cada actualización.
4. Productos únicos por código
Enunciado. Un catálogo considera iguales dos productos con el mismo código. Añade dos instancias con código A-10 a un HashSet y consigue que solo quede una.
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
class Producto {
private final String codigo;
Producto(String codigo) { this.codigo = codigo; }
@Override
public boolean equals(Object otro) {
if (this == otro) return true;
if (!(otro instanceof Producto)) return false;
Producto producto = (Producto) otro;
return Objects.equals(codigo, producto.codigo);
}
@Override
public int hashCode() { return Objects.hash(codigo); }
}
public class Catalogo {
public static void main(String[] args) {
Set<Producto> productos = new HashSet<>();
productos.add(new Producto("A-10"));
productos.add(new Producto("A-10"));
System.out.println(productos.size());
}
}
La salida es 1. Como la identidad lógica depende del código, equals y hashCode usan el mismo dato.
5. Cantidad válida o excepción
Enunciado. Convierte un texto en una cantidad. Una cantidad negativa debe lanzar una excepción propia; un texto que no sea entero debe comunicarse por separado.
class CantidadInvalidaException extends Exception {
CantidadInvalidaException(String mensaje) { super(mensaje); }
}
public class ValidarCantidad {
static int leerCantidad(String texto) throws CantidadInvalidaException {
int cantidad = Integer.parseInt(texto);
if (cantidad < 0) {
throw new CantidadInvalidaException("La cantidad no puede ser negativa");
}
return cantidad;
}
public static void main(String[] args) {
try {
System.out.println(leerCantidad("12"));
} catch (NumberFormatException e) {
System.out.println("La cantidad debe ser un entero");
} catch (CantidadInvalidaException e) {
System.out.println(e.getMessage());
}
}
}
Prueba también "doce" y "-1". Distinguir los casos permite dar mensajes útiles.
6. Próximas caducidades
Enunciado. Dada la fecha 2026-10-08, indica si 2026-10-15 vence durante los siguientes siete días, incluidos ambos límites.
import java.time.LocalDate;
public class Caducidad {
public static void main(String[] args) {
LocalDate hoy = LocalDate.of(2026, 10, 8);
LocalDate limite = hoy.plusDays(7);
LocalDate vencimiento = LocalDate.parse("2026-10-15");
boolean proxima = !vencimiento.isBefore(hoy) && !vencimiento.isAfter(limite);
System.out.println(proxima);
}
}
LocalDate evita comparar textos y hace explícito que aquí no importan la hora ni la zona.
7. Guardar y recuperar un informe
Enunciado. Crea datos/resumen.txt, guarda dos líneas en UTF-8 y léelas. El programa debe crear el directorio si no existe y comunicar un fallo de entrada/salida.
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class Informe {
public static void main(String[] args) {
Path archivo = Path.of("datos", "resumen.txt");
try {
Files.createDirectories(archivo.getParent());
Files.write(archivo, List.of("Cursos: 3", "Activos: 2"), StandardCharsets.UTF_8);
Files.readAllLines(archivo, StandardCharsets.UTF_8)
.forEach(System.out::println);
} catch (IOException e) {
System.out.println("No se pudo actualizar el informe: " + e.getMessage());
}
}
}
La ruta es relativa al directorio de ejecución. No se oculta la excepción: el usuario recibe contexto.
8. Filtrar con una lambda
Enunciado. Elimina de una lista mutable las puntuaciones inferiores a 5 y muestra las restantes, sin usar streams.
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
public class Puntuaciones {
public static void main(String[] args) {
List<Integer> notas = new ArrayList<>(List.of(7, 3, 9, 4, 5));
Predicate<Integer> suspenso = nota -> nota < 5;
notas.removeIf(suspenso);
notas.forEach(nota -> System.out.println(nota));
}
}
La lambda describe la condición. removeIf modifica la lista, por eso partimos de un ArrayList mutable.
9. Limpiar datos con un stream
Enunciado. Convierte " Java ", "api", "JAVA", " " y "datos" en una lista ordenada, sin vacíos ni duplicados, usando minúsculas.
import java.util.List;
public class LimpiarDatos {
public static void main(String[] args) {
List<String> entrada = List.of(" Java ", "api", "JAVA", " ", "datos");
List<String> resultado = entrada.stream()
.map(String::trim)
.filter(texto -> !texto.isEmpty())
.map(String::toLowerCase)
.distinct()
.sorted()
.toList();
System.out.println(resultado);
}
}
El orden del pipeline importa: normalizar antes de distinct hace que Java y JAVA sean el mismo dato.
10. Reto integrado: importar movimientos
Enunciado. Un archivo contiene líneas categoria;importe. Ignora líneas mal formadas o importes no positivos y suma el total por categoría.
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ImportarMovimientos {
public static void main(String[] args) {
Path archivo = Path.of("movimientos.txt");
try {
if (!Files.exists(archivo)) {
Files.write(archivo, List.of("libros;20", "cafe;3.5", "incorrecta", "libros;12"), StandardCharsets.UTF_8);
}
Map<String, Double> totales = new HashMap<>();
for (String linea : Files.readAllLines(archivo, StandardCharsets.UTF_8)) {
String[] partes = linea.split(";");
if (partes.length != 2) continue;
try {
double importe = Double.parseDouble(partes[1]);
if (importe > 0) {
totales.put(partes[0], totales.getOrDefault(partes[0], 0.0) + importe);
}
} catch (NumberFormatException e) {
System.out.println("Importe ignorado: " + partes[1]);
}
}
totales.entrySet().stream().sorted(Map.Entry.comparingByKey())
.forEach(e -> System.out.println(e.getKey() + ": " + e.getValue()));
} catch (IOException e) {
System.out.println("No se pudo procesar el archivo: " + e.getMessage());
}
}
}
El try exterior trata el archivo y el interior descarta solo importes inválidos. El mapa agrupa; el stream se limita a ordenar y mostrar.
Diagnóstico de diseño
Una única List<String> con líneas como "libros;32.0" obligaría a dividir textos, buscar categorías y reconstruir líneas en cada cambio. Una solución más clara usa una clase para cada movimiento, una List<Movimiento> para conservar la importación y un Map<String, Double> para los totales. El formato del archivo no debería convertirse en el modelo interno de toda la aplicación.
En el proyecto final aplicaremos este criterio para separar modelo, lógica y persistencia local.