Saltar al contenido principal

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.