Saltar a contenido

UD2 · Sección 10 — Ejercicios y actividades

Dato Valor
Unidad Didáctica UD2 — Programación Orientada a Objetos con la API de Java
Resultado de Aprendizaje RA2 — Escribe y prueba programas sencillos, reconociendo y aplicando los fundamentos de la programación orientada a objetos.
Sección 10 de 10 — Ejercicios y actividades
Evaluación 1.ª Evaluación
Duración estimada 3 horas lectivas + trabajo autónomo
Criterios de evaluación cubiertos CE 2.1 a CE 2.9 (cierre global del RA2)
Prerrequisitos UD1 completa, UD2 Secciones 1 a 9
Paquete del código de ejemplo dam.programacion.ud2
Herramientas JDK 25, IntelliJ IDEA Community, Maven 3.9.x, JUnit 5

1. Punto de partida

Has llegado a la última sección de la unidad. A lo largo de las nueve anteriores has conocido los fundamentos de la POO, has aprendido a explorar el API de Java con Javadoc e IntelliJ, y has trabajado con las clases más usadas del JDK: String, Math, Random, java.time, StringBuilder, wrappers y Objects. También has dado tus primeros pasos con Maven y has escrito tus primeros tests con JUnit 5.

Esta sección no introduce teoría nueva: es el momento de poner en práctica todo lo aprendido en proyectos pequeños pero completos. Cada ejercicio se centra en una o varias clases del API y se resuelve combinando lo que ya sabes. Te presentamos el enunciado, una propuesta de solución totalmente funcional y, cuando procede, una pequeña variante para que la resuelvas tú.

La idea es que intentes cada ejercicio antes de mirar la solución. Es ahí, en el intento, donde realmente se consolidan los conceptos. La solución está para que la consultes cuando te atasques o para que compares tu enfoque con el nuestro.

Al final de la sección encontrarás la preparación para la prueba objetiva RA2, con el formato y los temas que entrarán.


2. Cómo abordar los ejercicios

Antes de empezar, unas pautas que te ahorrarán tiempo y frustración:

  1. Lee el enunciado completo antes de picar código. Asegúrate de entender qué se pide.
  2. Diseña antes de programar. Dedica uno o dos minutos a pensar qué clases del API vas a usar y en qué orden. Un diagrama mental o en papel ayuda.
  3. Escribe incrementalmente. No programes todo de golpe: haz una parte, ejecuta, verifica, y avanza. Es más fácil corregir un error en 5 líneas que en 50.
  4. Usa System.out.println para depurar. Imprime valores intermedios para ver qué pasa.
  5. Consulta el Javadoc cuando dudes. Ctrl+Q sobre cualquier clase o método te da la documentación.
  6. No mires la solución hasta haberlo intentado. Aunque no logres completarlo, el proceso de intentarlo es donde se aprende.
  7. Compara tu solución con la propuesta. No hay una única forma correcta: si la tuya funciona y es legible, está bien.

Analogía del gimnasio. Aprender a programar es como entrenar en un gimnasio: leer la teoría es como ver vídeos de ejercicios, te dan idea de la técnica, pero el músculo solo crece cuando coges las pesas. Esta sección es la sala de pesas. El dolor muscular (frustración al depurar) es parte del proceso.


3. Ejercicio 1 — Manipulación de Strings: normalizador de emails

Enunciado

Escribe un programa que reciba un email tal y como lo escribe un usuario descuidado (con espacios, mayúsculas, puntos en sitios raros) y lo normalice:

  1. Eliminar espacios al principio y al final.
  2. Convertir todo a minúsculas.
  3. Sustituir cualquier espacio interno por un punto.
  4. Comprobar que contiene una @ y que termina en .com, .es o .org.
  5. Devolver el email normalizado o un mensaje de error si no es válido.

Ejemplo:

  • Entrada: " Maria.Garcia @Example.COM "
  • Salida: "maria.garcia@example.com" (válido)
  • Entrada: "usuario#dominio.com"
  • Salida: "Email invalido: falta la @"

Solución propuesta

package dam.programacion.ud2;

/**
 * Ejercicio 1: Normalizador de emails.
 * Demuestra: String (strip, toLowerCase, replace, contains, endsWith, indexOf),
 * validacion sin expresiones regulares.
 */
public class NormalizadorEmail {

    public static void main(String[] args) {
        String[] pruebas = {
            "  Maria.Garcia @Example.COM ",
            "usuario#dominio.com",
            "  ana ruiz  example es  ",
            "carlos@example.net",
            "  eLena@EXAMPLE.ORG  "
        };

        for (String email : pruebas) {
            String resultado = normalizar(email);
            System.out.println("Entrada: '" + email + "'");
            System.out.println("Salida:  " + resultado);
            System.out.println();
        }
    }

    public static String normalizar(String email) {
        // 1. Eliminar espacios y pasar a minusculas
        String normalizado = email.strip().toLowerCase();

        // 2. Sustituir espacios internos por puntos
        normalizado = normalizado.replace(" ", ".");

        // 3. Validar que contiene @
        if (!normalizado.contains("@")) {
            return "Email invalido: falta la @";
        }

        // 4. Validar el dominio
        if (!normalizado.endsWith(".com")
                && !normalizado.endsWith(".es")
                && !normalizado.endsWith(".org")) {
            return "Email invalido: dominio no soportado";
        }

        return normalizado + " (valido)";
    }
}

Salida esperada:

Entrada: '  Maria.Garcia @Example.COM '
Salida:  maria.garcia@example.com (valido)

Entrada: 'usuario#dominio.com'
Salida:  Email invalido: falta la @

Entrada: '  ana ruiz  example es  '
Salida:  ana.ruiz..example.es (valido)

Entrada: 'carlos@example.net'
Salida:  Email invalido: dominio no soportado

Entrada: '  eLena@EXAMPLE.ORG  '
Salida:  elena@example.org (valido)

Observación. Fíjate en la entrada " ana ruiz example es ": como hay varios espacios seguidos, la salida queda con .. (dos puntos). Una versión más completa del normalizador debería eliminar esos dobles puntos; te lo proponemos como variante.

Variante para ti

Mejora el método normalizar para que, después de sustituir espacios por puntos, no queden secuencias de dos o más puntos seguidos (usa replaceAll("\\.+", "."), que reemplaza una secuencia de uno o más puntos por un solo punto).


4. Ejercicio 2 — Cálculos con Math: estadísticas básicas

Enunciado

Dado un array de números (por ejemplo, las notas de un alumno), calcula:

  1. La nota máxima.
  2. La nota mínima.
  3. La media (redondeada a 2 decimales con Math.round).
  4. La desviación respecto a la media de cada nota (valor absoluto de la diferencia).
  5. La nota "aprobada" si la media es >= 5, "suspenso" en caso contrario.

Ejemplo:

  • Entrada: [7.5, 6.0, 8.2, 4.5, 9.1]
  • Salida:
    1
    2
    3
    4
    5
    Nota maxima: 9.1
    Nota minima: 4.5
    Media: 7.06
    Desviaciones: 0.44, 1.06, 1.14, 2.56, 2.04
    Resultado: aprobada
    

Solución propuesta

package dam.programacion.ud2;

/**
 * Ejercicio 2: Estadisticas basicas con Math.
 * Demuestra: Math.max, Math.min, Math.abs, Math.round, bucle for-each.
 */
public class EstadisticasNotas {

    public static void main(String[] args) {
        double[] notas = {7.5, 6.0, 8.2, 4.5, 9.1};

        // 1. Maximo y minimo
        double max = notas[0];
        double min = notas[0];
        double suma = 0;
        for (double nota : notas) {
            max = Math.max(max, nota);
            min = Math.min(min, nota);
            suma += nota;
        }

        // 2. Media redondeada a 2 decimales
        // Truco: multiplicar por 100, round, dividir por 100.0
        double media = Math.round((suma / notas.length) * 100.0) / 100.0;

        // 3. Desviaciones
        System.out.println("Nota maxima: " + max);
        System.out.println("Nota minima: " + min);
        System.out.println("Media: " + media);

        System.out.print("Desviaciones: ");
        for (int i = 0; i < notas.length; i++) {
            double desviacion = Math.abs(notas[i] - media);
            System.out.printf("%.2f", desviacion);
            if (i < notas.length - 1) {
                System.out.print(", ");
            }
        }
        System.out.println();

        // 4. Aprobado o suspenso
        String resultado = (media >= 5) ? "aprobada" : "suspenso";
        System.out.println("Resultado: " + resultado);
    }
}

Truco del redondeo a 2 decimales. Java no tiene un método directo para redondear a N decimales. El patrón estándar es:

double redondeado = Math.round(valor * 100.0) / 100.0;   // 2 decimales
double redondeado3 = Math.round(valor * 1000.0) / 1000.0; // 3 decimales

Multiplicas por 10^N, redondeas al entero más cercano, y vuelves a dividir. Funciona porque Math.round elimina los decimales sobrantes.

Variante para ti

Añade al programa el cálculo de la desviación típica (raíz cuadrada de la media de los cuadrados de las desviaciones). Necesitarás Math.sqrt y otro bucle.


5. Ejercicio 3 — Generador de contraseñas con Random

Enunciado

Escribe un programa que genere contraseñas aleatorias cumpliendo estos requisitos:

  1. Longitud configurable (por defecto, 12 caracteres).
  2. Mezcla de mayúsculas, minúsculas, dígitos y símbolos especiales (!@#$%&*).
  3. Debe contener al menos una mayúscula, una minúscula, un dígito y un símbolo.

Utiliza ThreadLocalRandom por ser la mejor práctica moderna.

Solución propuesta

package dam.programacion.ud2;

import java.util.concurrent.ThreadLocalRandom;

/**
 * Ejercicio 3: Generador de contrasenas con ThreadLocalRandom.
 * Demuestra: ThreadLocalRandom.current().nextInt, StringBuilder,
 * validacion de requisitos.
 */
public class GeneradorContrasenas {

    // Conjuntos de caracteres
    private static final String MAYUSCULAS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    private static final String MINUSCULAS = "abcdefghijklmnopqrstuvwxyz";
    private static final String DIGITOS = "0123456789";
    private static final String SIMBOLOS = "!@#$%&*";
    private static final String TODOS = MAYUSCULAS + MINUSCULAS + DIGITOS + SIMBOLOS;

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            String pwd = generar(12);
            System.out.println(pwd);
        }
        System.out.println("---");
        System.out.println("Password de 20 caracteres: " + generar(20));
    }

    public static String generar(int longitud) {
        // Validacion minima
        if (longitud < 4) {
            throw new IllegalArgumentException("Longitud minima: 4 caracteres");
        }

        StringBuilder sb = new StringBuilder(longitud);
        ThreadLocalRandom r = ThreadLocalRandom.current();

        // Garantizar al menos uno de cada tipo
        sb.append(MAYUSCULAS.charAt(r.nextInt(MAYUSCULAS.length())));
        sb.append(MINUSCULAS.charAt(r.nextInt(MINUSCULAS.length())));
        sb.append(DIGITOS.charAt(r.nextInt(DIGITOS.length())));
        sb.append(SIMBOLOS.charAt(r.nextInt(SIMBOLOS.length())));

        // Completar el resto con caracteres aleatorios de TODOS
        for (int i = 4; i < longitud; i++) {
            sb.append(TODOS.charAt(r.nextInt(TODOS.length())));
        }

        // Mezclar los caracteres (otherwise los 4 primeros son predecibles)
        return mezclar(sb.toString());
    }

    private static String mezclar(String s) {
        StringBuilder sb = new StringBuilder(s);
        ThreadLocalRandom r = ThreadLocalRandom.current();
        for (int i = sb.length() - 1; i > 0; i--) {
            int j = r.nextInt(i + 1);
            char temp = sb.charAt(i);
            sb.setCharAt(i, sb.charAt(j));
            sb.setCharAt(j, temp);
        }
        return sb.toString();
    }
}

Salida (los valores cambian entre ejecuciones):

1
2
3
4
5
6
7
g7!TmP9@aK2#
3$fLpN&Xb1Qv
Z*8cA#tM5nD@
q2R!fH7vN*4P
9L$gY3&kTm#b
---
Password de 20 caracteres: Tp7@Nv3xQb9$LfRc&2!m

Detalles importantes:

  1. Uso de ThreadLocalRandom.current() en lugar de new Random() por la recomendación moderna.
  2. Garantía de requisitos: los 4 primeros caracteres ya cumplen los requisitos mínimos; el resto son aleatorios.
  3. Mezcla final: sin ella, los 4 primeros caracteres siempre serían MAY-min-dig-simb, lo que reduce la entropía. El algoritmo de mezcla es el Fisher-Yates, clásico para barajar.
  4. Uso de constantes private static final para los conjuntos de caracteres (idea que verás en UD4).

Variante para ti

Añade un parámetro boolean incluirSimbolos que permita generar contraseñas sin símbolos (útil para sistemas que no los admiten). Adapta las garantías mínimas en consecuencia.


6. Ejercicio 4 — Fechas con java.time: cálculo de edad y días hasta el cumpleaños

Enunciado

Escribe un programa que, dada una fecha de nacimiento, calcule:

  1. La edad actual en años, meses y días (usando Period).
  2. Los días exactos que faltan para el próximo cumpleaños.
  3. El día de la semana en que caerá el próximo cumpleaños.
  4. La fecha formateada como "14 de julio de 2026" en español.

Solución propuesta

package dam.programacion.ud2;

import java.time.LocalDate;
import java.time.Month;
import java.time.Period;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Locale;

/**
 * Ejercicio 4: Calculos con fechas usando java.time.
 * Demuestra: LocalDate, Period, ChronoUnit, DateTimeFormatter, Locale.
 */
public class CalculadoraEdad {

    public static void main(String[] args) {
        LocalDate nacimiento = LocalDate.of(2005, Month.JULY, 14);
        LocalDate hoy = LocalDate.now();

        // 1. Edad en anos, meses y dias
        Period edad = Period.between(nacimiento, hoy);
        System.out.println("Edad: " + edad.getYears() + " anios, "
                + edad.getMonths() + " meses, " + edad.getDays() + " dias");

        // 2. Proximo cumpleanos
        LocalDate proximoCumple = nacimiento.withYear(hoy.getYear());
        if (proximoCumple.isBefore(hoy) || proximoCumple.isEqual(hoy)) {
            proximoCumple = proximoCumple.plusYears(1);
        }

        long diasFaltan = ChronoUnit.DAYS.between(hoy, proximoCumple);
        System.out.println("Proximo cumpleanos: " + proximoCumple);
        System.out.println("Faltan " + diasFaltan + " dias");

        // 3. Dia de la semana del proximo cumpleanos
        DateTimeFormatter diaSemanaFmt = DateTimeFormatter
                .ofPattern("EEEE", new Locale("es", "ES"));
        System.out.println("Cae en: " + proximoCumple.format(diaSemanaFmt));

        // 4. Fecha formateada en espanol
        DateTimeFormatter fechaLarga = DateTimeFormatter
                .ofPattern("d 'de' MMMM 'de' yyyy", new Locale("es", "ES"));
        System.out.println("Fecha larga: " + proximoCumple.format(fechaLarga));
    }
}

Nota sobre ChronoUnit. Es una enumeración que permite calcular diferencias en unidades específicas (días, meses, años, horas...). ChronoUnit.DAYS.between(a, b) devuelve el número total de días entre dos fechas. Es una alternativa a Period cuando solo te interesa una unidad.

Salida (parcial; depende de la fecha actual):

1
2
3
4
5
Edad: 20 anios, 11 meses, 30 dias
Proximo cumpleanos: 2026-07-14
Faltan 1 dias
Cae en: martes
Fecha larga: 14 de julio de 2026

Variante para ti

Añade un método diasVividos que devuelva el número total de días desde el nacimiento hasta hoy. Pista: ChronoUnit.DAYS.between(nacimiento, hoy).


7. Ejercicio 5 — Informe con StringBuilder: reporte de ventas

Enunciado

Construye un informe de ventas a partir de un array de productos. Cada producto tiene nombre, unidades vendidas y precio unitario. El informe debe incluir:

  1. Cabecera con título y fecha.
  2. Línea separadora.
  3. Una línea por producto con el cálculo del subtotal (unidades × precio).
  4. Total general.
  5. Número de productos y media de subtotales.

Usa StringBuilder para construir el informe y String.join donde sea natural.

Solución propuesta

package dam.programacion.ud2;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

/**
 * Ejercicio 5: Informe de ventas con StringBuilder.
 * Demuestra: StringBuilder (append, encadenamiento), String.format,
 * LocalDate, DateTimeFormatter, Locale.
 */
public class InformeVentas {

    public static void main(String[] args) {
        // Datos de entrada: arrays paralelos
        // (Mas adelante, en UD4, usaremos una clase Producto)
        String[] nombres = {"Portatil", "Raton", "Teclado", "Monitor", "Auriculares"};
        int[] unidades = {3, 12, 8, 2, 15};
        double[] precios = {899.99, 19.95, 49.50, 249.00, 29.99};

        String informe = generarInforme(nombres, unidades, precios);
        System.out.println(informe);
    }

    public static String generarInforme(String[] nombres, int[] unidades, double[] precios) {
        StringBuilder sb = new StringBuilder(500);  // capacidad inicial estimada
        LocalDate hoy = LocalDate.now();
        DateTimeFormatter fmt = DateTimeFormatter
                .ofPattern("d 'de' MMMM 'de' yyyy", new Locale("es", "ES"));

        // Cabecera
        sb.append("=== INFORME DE VENTAS ===\n");
        sb.append("Fecha: ").append(hoy.format(fmt)).append("\n");
        sb.append(String.join("", "-", "".repeat(40))).append("\n");

        // Detalle por producto
        double totalGeneral = 0;
        for (int i = 0; i < nombres.length; i++) {
            double subtotal = unidades[i] * precios[i];
            totalGeneral += subtotal;
            sb.append(String.format("%-15s %3d x %8.2f = %10.2f%n",
                    nombres[i], unidades[i], precios[i], subtotal));
        }

        sb.append(String.join("", "-", "".repeat(40))).append("\n");

        // Resumen
        double media = totalGeneral / nombres.length;
        sb.append(String.format("Total general:        %10.2f%n", totalGeneral));
        sb.append(String.format("Numero de productos:  %10d%n", nombres.length));
        sb.append(String.format("Media por producto:   %10.2f%n", media));

        return sb.toString();
    }
}

Salida esperada:

=== INFORME DE VENTAS ===
Fecha: 14 de julio de 2026
----------------------------------------
Portatil          3 x   899.99 =    2699.97
Raton            12 x    19.95 =     239.40
Teclado           8 x    49.50 =     396.00
Monitor           2 x   249.00 =     498.00
Auriculares      15 x    29.99 =     449.85
----------------------------------------
Total general:          4283.22
Numero de productos:         5
Media por producto:     856.64

Patrones de String.format que se ven aquí:

Patrón Significado Ejemplo
%-15s String alineado a la izquierda, 15 caracteres de ancho "Portatil "
%3d Entero con 3 caracteres de ancho, rellenando con espacios " 3"
%8.2f Decimal con 8 caracteres de ancho, 2 decimales " 899.99"
%10.2f Decimal con 10 caracteres de ancho, 2 decimales " 2699.97"
%n Salto de línea portable (depende del SO)
"x".repeat(40) Repite la cadena 40 veces "xxxx...x"

Variante para ti

Añade al informe una columna "IVA (21%)" con el cálculo del IVA de cada producto, y una línea "Total con IVA" al final.


8. Ejercicio 6 — Mini-proyecto integrador: gestor de tareas

Enunciado

Combina todo lo aprendido en un pequeño gestor de tareas. Requisitos:

  1. Cada tarea tiene: descripción (String), fecha límite (LocalDate), prioridad (alta/media/baja, como String) y completada (boolean).
  2. Crea 5 tareas con datos diversos (alguna completada, alguna con fecha pasada, alguna con prioridad alta).
  3. Genera un informe que muestre:
  4. Tareas pendientes ordenadas por fecha límite (más urgente primero).
  5. Para cada tarea pendiente, los días que faltan (o "VENCIDA" si la fecha ya pasó).
  6. Estadísticas: total de tareas, completadas, pendientes, vencidas.
  7. Construye el informe con StringBuilder y formatea las fechas en español.

Nota. En este ejercicio usamos arrays paralelos para guardar las tareas. En UD4 aprenderás a crear una clase Tarea que agrupe todos los datos de una tarea en un objeto, lo que es mucho más limpio. Por ahora, los arrays paralelos nos sirven para no adelantar contenido.

Solución propuesta

package dam.programacion.ud2;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Locale;

/**
 * Ejercicio 6: Gestor de tareas integrador.
 * Combina: String, LocalDate, Period/ChronoUnit, StringBuilder,
 * Math, condicionales basicos (vistos en UD1).
 */
public class GestorTareas {

    public static void main(String[] args) {
        // Datos: 5 tareas en arrays paralelos
        String[] descripciones = {
            "Entregar proyecto Java",
            "Estudiar examen POO",
            "Comprar regalo cumple",
            "Renovar suscripcion",
            "Llamar al dentista"
        };
        LocalDate[] fechasLimite = {
            LocalDate.of(2026, 7, 20),
            LocalDate.of(2026, 7, 18),
            LocalDate.of(2026, 7, 25),
            LocalDate.of(2026, 6, 30),    // fecha pasada
            LocalDate.of(2026, 8, 1)
        };
        String[] prioridades = {"alta", "media", "baja", "media", "alta"};
        boolean[] completadas = {false, false, true, false, false};

        String informe = generarInforme(
                descripciones, fechasLimite, prioridades, completadas);
        System.out.println(informe);
    }

    public static String generarInforme(
            String[] descripciones,
            LocalDate[] fechasLimite,
            String[] prioridades,
            boolean[] completadas) {

        StringBuilder sb = new StringBuilder(800);
        LocalDate hoy = LocalDate.now();
        DateTimeFormatter fmt = DateTimeFormatter
                .ofPattern("d 'de' MMMM 'de' yyyy", new Locale("es", "ES"));

        // Cabecera
        sb.append("=== GESTOR DE TAREAS ===\n");
        sb.append("Hoy: ").append(hoy.format(fmt)).append("\n");
        sb.append("-".repeat(60)).append("\n");

        // Estadisticas
        int total = descripciones.length;
        int completadasCount = 0;
        int vencidas = 0;
        int pendientes = 0;

        for (int i = 0; i < total; i++) {
            if (completadas[i]) {
                completadasCount++;
            } else {
                pendientes++;
                if (fechasLimite[i].isBefore(hoy)) {
                    vencidas++;
                }
            }
        }

        // Listado de pendientes (ordenar por fecha: seleccion manual simple)
        // Creamos indices ordenados sin modificar los arrays originales
        int[] indicesPendientes = new int[pendientes];
        int idx = 0;
        for (int i = 0; i < total; i++) {
            if (!completadas[i]) {
                indicesPendientes[idx++] = i;
            }
        }

        // Ordenacion por seleccion (la mas sencilla de entender)
        for (int i = 0; i < indicesPendientes.length - 1; i++) {
            int menor = i;
            for (int j = i + 1; j < indicesPendientes.length; j++) {
                if (fechasLimite[indicesPendientes[j]]
                        .isBefore(fechasLimite[indicesPendientes[menor]])) {
                    menor = j;
                }
            }
            int temp = indicesPendientes[i];
            indicesPendientes[i] = indicesPendientes[menor];
            indicesPendientes[menor] = temp;
        }

        // Detalle
        sb.append("\nTAREAS PENDIENTES (ordenadas por urgencia):\n\n");
        for (int i = 0; i < indicesPendientes.length; i++) {
            int indice = indicesPendientes[i];
            long diasFaltan = ChronoUnit.DAYS.between(hoy, fechasLimite[indice]);
            String estado = (diasFaltan < 0) ? "VENCIDA" : diasFaltan + " dias";
            sb.append(String.format("[%s] %-25s | %s | %s%n",
                    prioridades[indice].toUpperCase(),
                    descripciones[indice],
                    fechasLimite[indice].format(fmt),
                    estado));
        }

        // Resumen
        sb.append("\n").append("-".repeat(60)).append("\n");
        sb.append("ESTADISTICAS\n");
        sb.append(String.format("Total de tareas: %d%n", total));
        sb.append(String.format("Completadas:     %d%n", completadasCount));
        sb.append(String.format("Pendientes:      %d%n", pendientes));
        sb.append(String.format("  - Vencidas:    %d%n", vencidas));
        sb.append(String.format("  - A tiempo:    %d%n", pendientes - vencidas));

        return sb.toString();
    }
}

Salida (parcial; depende de la fecha actual):

=== GESTOR DE TAREAS ===
Hoy: 14 de julio de 2026
------------------------------------------------------------

TAREAS PENDIENTES (ordenadas por urgencia):

[ALTA] Renovar suscripcion        | 30 de junio de 2026 | VENCIDA
[MEDIA] Estudiar examen POO       | 18 de julio de 2026 | 4 dias
[ALTA] Entregar proyecto Java     | 20 de julio de 2026 | 6 dias
[BAJA] Comprar regalo cumple      | 25 de julio de 2026 | 11 dias
[ALTA] Llamar al dentista         | 1 de agosto de 2026 | 18 dias

------------------------------------------------------------
ESTADISTICAS
Total de tareas: 5
Completadas:     1
Pendientes:      4
  - Vencidas:    1
  - A tiempo:    3

Observaciones:

  • Este ejercicio combina elementos de todas las secciones anteriores: String, LocalDate, ChronoUnit, StringBuilder, String.format, bucles, condicionales y ordenación.
  • La ordenación por selección es la más sencilla de entender para un principiante. En UD6 verás List.sort con comparadores, mucho más limpio.
  • Los arrays paralelos son un patrón provisional: en UD4 los sustituirás por una clase Tarea.

Variante para ti

Añade al informe una sección "Tareas completadas" que muestre las ya hechas, con la descripción y la fecha límite que tenían.


9. Ejercicio 7 — Tests con JUnit 5: validar el normalizador de emails

Enunciado

Recupera el método normalizar del Ejercicio 1 y escribe tests con JUnit 5 que verifiquen:

  1. Que un email válido con espacios y mayúsculas se normaliza correctamente.
  2. Que un email sin @ devuelve el mensaje de error esperado.
  3. Que un email con dominio no soportado devuelve el mensaje de error esperado.
  4. Que un email ya normalizado se mantiene igual.

Solución propuesta

package dam.programacion.ud2;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

/**
 * Ejercicio 7: Tests para NormalizadorEmail.
 * Demuestra: @Test, assertEquals, assertTrue, JUnit 5 basico.
 */
class NormalizadorEmailTest {

    @Test
    void normalizarEmailValidoConEspaciosYMayusculas() {
        String entrada = "  Maria.Garcia @Example.COM ";
        String resultado = NormalizadorEmail.normalizar(entrada);
        assertEquals("maria.garcia@example.com (valido)", resultado);
    }

    @Test
    void normalizarEmailSinArrobaDevuelveError() {
        String entrada = "usuario#dominio.com";
        String resultado = NormalizadorEmail.normalizar(entrada);
        assertEquals("Email invalido: falta la @", resultado);
    }

    @Test
    void normalizarEmailConDominioNoSoportadoDevuelveError() {
        String entrada = "carlos@example.net";
        String resultado = NormalizadorEmail.normalizar(entrada);
        assertEquals("Email invalido: dominio no soportado", resultado);
    }

    @Test
    void normalizarEmailYaNormalizadoQuedaIgual() {
        String entrada = "ana@example.es";
        String resultado = NormalizadorEmail.normalizar(entrada);
        assertEquals("ana@example.es (valido)", resultado);
    }

    @Test
    void normalizarEmailVacioDevuelveError() {
        String resultado = NormalizadorEmail.normalizar("");
        assertTrue(resultado.startsWith("Email invalido"));
    }
}

Cómo ejecutarlos. En IntelliJ, clic derecho sobre la clase → "Run 'NormalizadorEmailTest'". Si los 5 tests pasan, verás la barra verde. Si alguno falla, IntelliJ te muestra el valor esperado y el recibido.

Variante para ti

Añade tests para casos límite: null, solo espacios, varios emails separados por comas. Recuerda que null puede lanzar NullPointerException; esto te dará pie a manejarlo en el método normalizar.


10. Actividad integradora: tu primer mini-proyecto

Como actividad final, te proponemos un mini-proyecto que integra todos los conceptos de la unidad. Elige una de las siguientes opciones:

Opción A: Gestor de biblioteca personal

Crea un programa que permita gestionar una pequeña biblioteca personal. Funcionalidades:

  • Mantener un catálogo de libros (título, autor, fecha de compra, leído/no leído).
  • Mostrar listado de libros pendientes de leer, ordenados por antigüedad.
  • Calcular días desde la compra de cada libro.
  • Generar un informe con StringBuilder que incluya estadísticas (total, leídos, pendientes, media de días desde compra).
  • Tests con JUnit que validen el cálculo de días y las estadísticas.

Opción B: Simulador de citas médicas

Crea un programa que gestione citas médicas. Funcionalidades:

  • Mantener un listado de citas (paciente, fecha y hora, médico, motivo).
  • Mostrar próximas citas ordenadas por fecha.
  • Calcular minutos hasta la próxima cita.
  • Identificar citas pasadas (ya atendidas o no show).
  • Generar un informe con StringBuilder.
  • Tests con JUnit que validen el cálculo de tiempo hasta la próxima cita.

Opción C: Calculadora de presupuesto doméstico

Crea un programa que gestione gastos mensuales. Funcionalidades:

  • Mantener un listado de gastos (concepto, importe, fecha, categoría).
  • Mostrar gastos ordenados por importe descendente.
  • Calcular total gastado, gasto medio, gasto máximo y mínimo.
  • Calcular días desde el último gasto.
  • Generar un informe mensual con StringBuilder y String.format.
  • Tests con JUnit que validen los cálculos.

Cómo entregarlo

  1. Crea un proyecto Maven en IntelliJ con paquete dam.programacion.ud2.
  2. Implementa el programa en una clase principal (Main, GestorBiblioteca, etc.).
  3. Escribe al menos 5 tests con JUnit 5 que validen las funcionalidades clave.
  4. Sube el proyecto a Git (lo verás en el módulo de Entornos de Desarrollo) o entrégalo en un .zip según indique tu profesor.
  5. Asegúrate de que mvn test pasa todos los tests antes de entregar.

Criterios de evaluación del mini-proyecto:

Criterio Peso
Funcionalidad: cumple los requisitos 40%
Uso correcto de las clases del API (String, LocalDate, StringBuilder, etc.) 20%
Calidad del código: nombres claros, comentarios, indentación 15%
Tests: cubren los casos importantes y pasan 15%
Estructura Maven: pom.xml correcto, paquetes bien organizados 10%

11. Preparación para la prueba objetiva RA2

La prueba objetiva RA2 es un examen de 90 minutos, sin material, que combina teoría y práctica. Aquí tienes la guía de preparación.

11.1. Formato del examen

Bloque Tipo Peso aproximado
Bloque teórico 10-15 preguntas de elección múltiple y cortas 30%
Bloque práctico 2-3 ejercicios de código 70%

11.2. Temas que entran

Todo lo visto en las secciones 1 a 9 de UD2:

Tema Puntos clave
Pilares de la POO Clase vs. objeto, new, heap vs. stack, operador punto, encapsulación, abstracción, herencia, polimorfismo
API de Java Paquetes (java.lang, java.util, java.time), Javadoc, métodos de instancia vs. estáticos
String Inmutabilidad, pool, búsqueda, transformación, extracción, comparación, formatted
Math Constantes, abs, max, min, clamp, redondeo, potencia, trigonometría, random
Random y ThreadLocalRandom Semilla, reproducibilidad, nextInt, nextDouble, nextBoolean, nextGaussian
java.time LocalDate, LocalTime, LocalDateTime, Period, Duration, DateTimeFormatter
StringBuilder Mutabilidad, append, insert, delete, replace, reverse, StringJoiner, String.join
Wrappers y Objects Autoboxing/unboxing, constantes, parseXxx, Objects.requireNonNull, equals seguro
Maven pom.xml, coordenadas GAV, dependencias, ciclo de vida, estructura de carpetas

11.3. Temas que NO entran (por estar en unidades posteriores)

  • Clases propias, constructores, this, private/public (UD4)
  • Excepciones y try-catch (UD3)
  • Colecciones (List, Set, Map) y Streams (UD6)
  • Herencia con extends, interfaces (UD4, UD7)
  • JavaFX (UD5)

11.4. Consejos para el examen

  1. Lee todo el examen antes de empezar. Empieza por lo que te sepas mejor; deja lo difícil para el final.
  2. En los ejercicios prácticos, escribe el import siempre. Muchos puntos se pierden por olvidar el import.
  3. No uses == con strings. Usa .equals(). Es un error clásico que resta puntos.
  4. Recuerda la inmutabilidad. Si escribes s.toUpperCase(); sin asignar, no pasa nada. Hay que hacer s = s.toUpperCase();.
  5. En LocalDate, los meses van de 1 a 12. No de 0 a 11 como en Calendar.
  6. Si no recuerdas un método exacto, escribe el más plausible. Un método con nombre razonable y firma correcta suele tener penalización menor que dejarlo en blanco.
  7. Gestiona el tiempo. Si un ejercicio te lleva más de 25 minutos, déjalo y vuelve al final.
  8. Revisa al terminar. Comprueba que los import están, que los métodos se llaman correctamente y que no hay ; olvidados.

11.5. Ejemplo de pregunta teórica

Pregunta. ¿Cuál es la diferencia entre == y .equals() al comparar dos strings en Java? Pon un ejemplo donde == devuelva false pero .equals() devuelva true.

Respuesta modelo. == compara referencias (direcciones de memoria); .equals() compara contenidos. Por ejemplo:

1
2
3
4
String a = new String("hola");
String b = new String("hola");
System.out.println(a == b);        // false: dos objetos distintos en el heap
System.out.println(a.equals(b));   // true: mismo contenido

a y b son referencias distintas a dos objetos String distintos, aunque su contenido sea idéntico. Por eso == devuelve false, pero .equals(), que compara caracteres, devuelve true.

11.6. Ejemplo de ejercicio práctico

Enunciado. Escribe un programa que: 1. Cree un LocalDate con la fecha actual. 2. Calcule la fecha correspondiente a dentro de 30 días. 3. Genere un StringBuilder con el texto: "En 30 dias sera <fecha formateada como dd/MM/yyyy>". 4. Imprima el resultado.

Solución modelo.

package dam.programacion.ud2;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class EjemploExamen {
    public static void main(String[] args) {
        LocalDate hoy = LocalDate.now();
        LocalDate futura = hoy.plusDays(30);
        DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd/MM/yyyy");
        StringBuilder sb = new StringBuilder();
        sb.append("En 30 dias sera ").append(futura.format(fmt));
        System.out.println(sb);
    }
}

12. Cierre de la unidad

Has llegado al final de la UD2. A lo largo de diez secciones has pasado de "¿qué es una clase?" a manejar con soltura las clases más importantes del API estándar de Java, escribir tests automáticos y entender cómo se gestiona un proyecto profesional con Maven.

Lo que has aprendido:

  • Los cuatro pilares de la POO y cómo se materializan en el API de Java.
  • Leer y entender documentación Javadoc.
  • Trabajar con String (inmutable, métodos de búsqueda, transformación, comparación).
  • Hacer cálculos con Math y generar aleatorios con Random y ThreadLocalRandom.
  • Manejar fechas y horas con java.time.
  • Construir textos eficientemente con StringBuilder.
  • Usar wrappers y Objects para casos donde los primitivos no bastan.
  • Gestionar dependencias externas con Maven y escribir tests con JUnit 5.

Lo que viene en UD3:

La siguiente unidad, "Control de Flujo y Depuración", te dará las herramientas para que tus programas tomen decisiones y repitan acciones: if, switch expression, bucles for, while, do-while, gestión de excepciones con try-catch-finally y depuración con IntelliJ. Es una unidad esencial porque sin control de flujo, ningún programa hace nada realmente interesante. Hemos ido usando algo de if y de for a lo largo de la UD2 como aperitivo, pero ahora los estudiaremos a fondo.

Mensaje final. Aprender a programar es un proceso largo. Lo importante no es que recuerdes todos los métodos de String o que sepas escribir un StringBuilder perfecto en el primer intento. Lo importante es que entiendas la lógica subyacente y sepas buscar lo que no recuerdes. Cualquier programador profesional consulta el Javadoc a diario; tú ya sabes hacerlo. Esa es tu verdadera superpotencia.

Sigue practicando. La única forma de aprender a programar es programando. Nos vemos en la UD3.


13. Información ampliada y próximos pasos

Próximos pasos:

  1. Resuelve los 7 ejercicios propuestos sin mirar las soluciones. Si te atasques, consulta y vuelve a intentarlo.
  2. Elige una de las tres opciones de mini-proyecto y desarrollela completamente en IntelliJ con tests. Es la mejor forma de consolidar.
  3. Repasa el cuestionario de cada sección unos días antes del examen. Si fallas alguna pregunta, vuelve a la sección correspondiente.
  4. Practica en papel. En el examen no tendrás IntelliJ ni autocompletado. Escribe a mano pequeños programas para acostumbrarte.
  5. Avanza a la UD3 cuando hayas superado la prueba objetiva RA2. La UD3 te dará el control de flujo y las excepciones que necesitas para que tus programas empiecen a hacer cosas realmente interesantes.