Módulo 20: Buenas prácticas

Objetivos del módulo

  • Aplicar las convenciones de código de Java: nombres, formato y organización de una clase
  • Escribir código limpio: nombres significativos, métodos cortos con una única responsabilidad, sin números mágicos y siguiendo DRY, KISS y YAGNI
  • Distinguir los comentarios útiles de los inútiles
  • Reconocer olores de código y aplicar refactorizaciones con IntelliJ IDEA: renombrar, extraer método, extraer constante, introducir variable y sustituir condicional por polimorfismo
  • Documentar el código con Javadoc y generar la documentación HTML con la herramienta javadoc
  • Entender el control de versiones y usar Git: repositorio, commit, ramas, fusión y repositorios remotos (GitHub), desde la consola y desde IntelliJ
  • Conocer Maven como gestor de proyectos: el pom.xml, las coordenadas y el ciclo de vida

1. ¿Por qué importan las buenas prácticas?

📘 Concepto: El código se escribe una vez, pero se lee muchas veces: lo leen tus compañeros, el profesor que lo corrige y tú mismo dentro de seis meses, cuando ya no recuerdes por qué hiciste las cosas así. Robert C. Martin lo resume en una frase: la proporción entre el tiempo que pasamos leyendo código y escribiéndolo es de más de 10 a 1.

Un programa que funciona pero nadie entiende es como una casa sin planos con la instalación eléctrica escondida: funciona hasta el día en que hay que cambiar un enchufe. Las buenas prácticas no hacen que el programa haga más cosas; hacen que sea fácil de leer, de cambiar y de ampliar sin romperlo. Este módulo reúne las que se esperan de cualquier programador en su primer trabajo:

Práctica Pregunta que responde
Convenciones ¿Mi código tiene el aspecto que esperan otros programadores Java?
Código limpio ¿Se entiende qué hace sin tener que ejecutarlo mentalmente?
Refactorización ¿Puedo mejorar el diseño sin cambiar el comportamiento?
Javadoc ¿Puede alguien usar mi clase sin leer su código?
Control de versiones (Git) ¿Puedo volver atrás, trabajar en equipo y saber quién cambió qué?
Gestión del proyecto (Maven) ¿Cualquiera puede compilar, probar y empaquetar mi proyecto con un comando?

2. Convenciones de código Java

Las convenciones de Java proceden de las Code Conventions for the Java Programming Language de Sun (1997) y hoy se siguen, con pequeñas variaciones, en guías como la de Google (Google Java Style Guide). Lo fundamental es común a todas.

2.1. Nombres

Elemento Estilo Ejemplos correctos Incorrectos
Clase, interfaz, enum, record UpperCamelCase (sustantivo) CuentaBancaria, Imprimible, EstadoPedido cuentabancaria, Cuenta_Bancaria
Método lowerCamelCase (verbo) calcularTotal(), esValido(), getNombre() CalcularTotal(), total_calc()
Variable y parámetro lowerCamelCase precioUnitario, numAlumnos PrecioUnitario, p, x1
Constante (static final) UPPER_SNAKE_CASE IVA_GENERAL, MAX_INTENTOS ivaGeneral, Max
Paquete minúsculas, dominio invertido es.curso.tienda.modelo Tienda.Modelo, es.curso.Tienda
Literales de un enum UPPER_SNAKE_CASE EN_REPARACION, PAGADO enReparacion
Variable genérica de tipo Una letra mayúscula T, E, K, V tipo

Más reglas sobre nombres:

  • Los métodos que devuelven boolean se leen como preguntas: esPar(), estaVacia(), tieneStock(), puedePrestar().
  • Getters y setters: getX() / setX(); para boolean, isX() o, en español, esX().
  • Usa el mismo idioma en todo el proyecto (en este curso, español sin tildes ni ñ en los identificadores: anadir, anio, tamano). Java admite año como identificador, pero muchas herramientas y teclados no se llevan bien con ello.
  • Evita abreviaturas salvo las universales (id, url, max, min, num).

2.2. Formato

Regla Recomendación
Sangría 4 espacios (no tabuladores mezclados)
Llaves Estilo K&R: la { al final de la línea, la } sola en su línea
Llaves en if/for/while Siempre, aunque el bloque tenga una sola sentencia
Longitud de línea 100-120 caracteres como máximo
Una sentencia por línea a = 1; b = 2; en dos líneas
Espacios Alrededor de operadores (a + b), después de comas y de if/for/while; nunca entre el nombre de un método y su paréntesis
Líneas en blanco Una entre métodos; dentro de un método, para separar bloques lógicos
import Uno por clase, sin comodines (import java.util.List; mejor que import java.util.*;)

💡 Consejo: No formatees a mano. En IntelliJ, Code | Reformat Code (Ctrl+Alt+L) aplica el estilo a todo el fichero y Code | Optimize Imports (Ctrl+Alt+O) elimina los import sin usar. Actívalos también al hacer commit (opciones Reformat code y Optimize imports de la ventana Commit).

2.3. Organización de una clase

Orden de los elementos dentro de un fichero Java Figura 1. Orden habitual de los elementos de una clase.

Este es un ejemplo completo que sigue ese orden:

import java.util.Objects;

/** Ejemplo de una clase organizada según las convenciones habituales de Java. */
public class Producto implements Comparable<Producto> {

    // 1. Constantes (static final)
    public static final double IVA = 0.21;
    private static final int LONGITUD_MAXIMA_NOMBRE = 50;

    // 2. Atributos estáticos
    private static int productosCreados = 0;

    // 3. Atributos de instancia (privados)
    private final String codigo;
    private String nombre;
    private double precio;

    // 4. Constructores
    public Producto(String codigo, String nombre, double precio) {
        this.codigo = Objects.requireNonNull(codigo, "codigo");
        setNombre(nombre);
        setPrecio(precio);
        productosCreados++;
    }

    // 5. Métodos de comportamiento (públicos primero, después los privados que usan)
    public double precioConIva() {
        return redondear(precio * (1 + IVA));
    }

    public void aplicarDescuento(int porcentaje) {
        validarPorcentaje(porcentaje);
        precio = redondear(precio * (100 - porcentaje) / 100.0);
    }

    private static void validarPorcentaje(int porcentaje) {
        if (porcentaje < 0 || porcentaje > 100) {
            throw new IllegalArgumentException("Porcentaje no válido: " + porcentaje);
        }
    }

    private static double redondear(double valor) {
        return Math.round(valor * 100) / 100.0;
    }

    // 6. Getters y setters
    public String getCodigo() { return codigo; }
    public String getNombre() { return nombre; }
    public double getPrecio() { return precio; }

    public void setNombre(String nombre) {
        if (nombre == null || nombre.isBlank() || nombre.length() > LONGITUD_MAXIMA_NOMBRE) {
            throw new IllegalArgumentException("Nombre no válido");
        }
        this.nombre = nombre;
    }

    public void setPrecio(double precio) {
        if (precio < 0) {
            throw new IllegalArgumentException("Precio negativo");
        }
        this.precio = precio;
    }

    public static int getProductosCreados() { return productosCreados; }

    // 7. Métodos de Object e interfaces (equals, hashCode, toString, compareTo)
    @Override
    public boolean equals(Object o) {
        return o instanceof Producto p && codigo.equals(p.codigo);
    }

    @Override
    public int hashCode() {
        return codigo.hashCode();
    }

    @Override
    public String toString() {
        return codigo + " - " + nombre + " (" + precio + " €)";
    }

    @Override
    public int compareTo(Producto otro) {
        return codigo.compareTo(otro.codigo);
    }

    // 8. main (solo en clases de ejemplo; en un proyecto real va en su propia clase)
    public static void main(String[] args) {
        Producto p = new Producto("P-001", "Auriculares", 49.90);
        p.aplicarDescuento(10);
        System.out.println(p + " -> con IVA: " + p.precioConIva() + " €");
        System.out.println("Productos creados: " + getProductosCreados());
    }
}
P-001 - Auriculares (44.91 €) -> con IVA: 54.34 €
Productos creados: 1

Fíjate en que el constructor reutiliza los setters para no duplicar las validaciones, y en que equals usa el pattern matching de instanceof (Java 16).


3. Código limpio

📘 Concepto: Código limpio (clean code, término popularizado por el libro homónimo de Robert C. Martin) es el que se lee como una prosa bien escrita: cada nombre dice lo que es, cada método hace una sola cosa y no hay sorpresas. Los siguientes ejemplos son pares antes/después que producen exactamente la misma salida: lo único que cambia es la calidad del código.

3.1. Nombres significativos, sin números mágicos y con comentarios útiles

// ANTES: nombres crípticos, números mágicos, comentarios que repiten el código
public class FacturaAntes {
    public static void main(String[] args) {
        double[] p = {19.99, 5.50, 120.00};
        int[] c = {2, 4, 1};
        System.out.printf("Total: %.2f €%n", calc(p, c, true));
        System.out.printf("Total: %.2f €%n", calc(p, c, false));
    }

    // calcula
    static double calc(double[] p, int[] c, boolean b) {
        double t = 0;                       // t = 0
        for (int i = 0; i < p.length; i++) {
            t = t + p[i] * c[i];            // suma p por c
        }
        if (b && t > 100) {
            t = t - t * 0.05;
        }
        return t + t * 0.21;
    }
}

¿Qué es b? ¿Por qué 100? ¿Qué es 0.05? ¿Y 0.21? Hay que adivinarlo. Después de limpiar:

// DESPUÉS: nombres que explican, constantes con nombre y comentarios que aportan el "por qué"
public class FacturaDespues {

    private static final double IVA_GENERAL = 0.21;
    private static final double DESCUENTO_SOCIO = 0.05;
    // Política comercial 2026: el descuento de socio solo se aplica a compras grandes
    private static final double MINIMO_PARA_DESCUENTO = 100.0;

    public static void main(String[] args) {
        double[] precios = {19.99, 5.50, 120.00};
        int[] cantidades = {2, 4, 1};
        System.out.printf("Total: %.2f €%n", calcularTotalConIva(precios, cantidades, true));
        System.out.printf("Total: %.2f €%n", calcularTotalConIva(precios, cantidades, false));
    }

    static double calcularTotalConIva(double[] precios, int[] cantidades, boolean esSocio) {
        double baseImponible = 0;
        for (int i = 0; i < precios.length; i++) {
            baseImponible += precios[i] * cantidades[i];
        }
        if (esSocio && baseImponible > MINIMO_PARA_DESCUENTO) {
            baseImponible -= baseImponible * DESCUENTO_SOCIO;
        }
        return baseImponible * (1 + IVA_GENERAL);
    }
}

Las dos versiones imprimen lo mismo:

Total: 209.19 €
Total: 220.20 €
Mejora Por qué
calc → calcularTotalConIva El nombre del método dice qué devuelve
p, c, b, t → precios, cantidades, esSocio, baseImponible Se entienden sin contexto. Las variables de una letra solo se aceptan como contador de un bucle corto (i, j)
0.21, 0.05, 100 → constantes Un número mágico es un literal sin explicación. Con nombre se entiende y, si cambia el IVA, se cambia en un solo sitio
Comentarios eliminados // t = 0 repite el código: ruido. El comentario que queda explica una decisión que el código no puede expresar

Comentarios útiles e inútiles:

Útil Inútil
Explica el porqué: // El banco rechaza importes con más de 2 decimales Repite el qué: i++; // incrementa i
Advierte de consecuencias: // No cambiar el orden: el fichero se lee secuencialmente Código comentado «por si acaso» (para eso está Git)
Documenta la API pública con Javadoc (sección 5) Comentarios desactualizados que contradicen el código
// TODO: con una tarea concreta y pendiente Comentarios que intentan explicar un código confuso: mejor reescribe el código

💡 Consejo: Antes de escribir un comentario, pregúntate si un nombre mejor lo haría innecesario. // comprueba si el usuario es mayor de edad sobre if (u.e >= 18) se arregla con if (usuario.esMayorDeEdad()).

3.2. Métodos cortos, una sola responsabilidad y DRY

// ANTES: un método largo que hace de todo y repite código
public class InformeAntes {
    public static void main(String[] args) {
        String[] nombres = {"Ana", "Luis", "Marta", "Pablo"};
        double[] notas = {7.5, 4.0, 9.2, 5.0};
        procesar(nombres, notas);
    }

    static void procesar(String[] nombres, double[] notas) {
        System.out.println("=== INFORME DE NOTAS ===");
        double suma = 0;
        for (int i = 0; i < notas.length; i++) {
            suma += notas[i];
        }
        double media = suma / notas.length;
        System.out.printf("Media: %.2f%n", media);
        System.out.println("--- Aprobados ---");
        for (int i = 0; i < notas.length; i++) {
            if (notas[i] >= 5) {
                String n = nombres[i].toUpperCase();
                System.out.printf("%-6s %5.2f%n", n, notas[i]);
            }
        }
        System.out.println("--- Suspensos ---");
        for (int i = 0; i < notas.length; i++) {
            if (notas[i] < 5) {
                String n = nombres[i].toUpperCase();
                System.out.printf("%-6s %5.2f%n", n, notas[i]);
            }
        }
    }
}
// DESPUÉS: métodos cortos, cada uno con una única responsabilidad y sin duplicados
public class InformeDespues {

    private static final double NOTA_APROBADO = 5.0;

    public static void main(String[] args) {
        String[] nombres = {"Ana", "Luis", "Marta", "Pablo"};
        double[] notas = {7.5, 4.0, 9.2, 5.0};
        imprimirInforme(nombres, notas);
    }

    static void imprimirInforme(String[] nombres, double[] notas) {
        System.out.println("=== INFORME DE NOTAS ===");
        System.out.printf("Media: %.2f%n", media(notas));
        imprimirAlumnos("Aprobados", nombres, notas, true);
        imprimirAlumnos("Suspensos", nombres, notas, false);
    }

    static double media(double[] valores) {
        double suma = 0;
        for (double v : valores) {
            suma += v;
        }
        return suma / valores.length;
    }

    static void imprimirAlumnos(String titulo, String[] nombres, double[] notas, boolean aprobados) {
        System.out.println("--- " + titulo + " ---");
        for (int i = 0; i < notas.length; i++) {
            if (estaAprobado(notas[i]) == aprobados) {
                System.out.printf("%-6s %5.2f%n", nombres[i].toUpperCase(), notas[i]);
            }
        }
    }

    static boolean estaAprobado(double nota) {
        return nota >= NOTA_APROBADO;
    }
}

Salida de ambas versiones:

=== INFORME DE NOTAS ===
Media: 6.43
--- Aprobados ---
ANA     7.50
MARTA   9.20
PABLO   5.00
--- Suspensos ---
LUIS    4.00
  • Métodos cortos: imprimirInforme se lee como un índice: título, media, aprobados, suspensos. Si quieres el detalle, entras en el método correspondiente. Una regla práctica: si un método no cabe en la pantalla o necesitas comentarios para separar sus «partes», divídelo.
  • Principio de responsabilidad única (Single Responsibility Principle, la S de SOLID): cada método (y cada clase) debe tener un solo motivo para cambiar. media solo cambia si cambia cómo se calcula la media; imprimirAlumnos, si cambia el formato.
  • DRY (Don’t Repeat Yourself, no te repitas): los dos bucles casi idénticos se han unido en imprimirAlumnos. Con código duplicado, cada corrección hay que hacerla en varios sitios… y siempre se olvida alguno.

3.3. KISS: mantenlo simple

KISS (Keep It Simple, Stupid) recuerda que la solución más sencilla que funciona suele ser la mejor.

// ANTES: soluciones enrevesadas para problemas sencillos
public class KissAntes {
    public static void main(String[] args) {
        System.out.println(esPar(10) + " " + esPar(7));
        System.out.println(contiene(new String[]{"rojo", "verde"}, "verde"));
    }

    static boolean esPar(int n) {
        if (n % 2 == 0) {
            if (true) {
                return true;
            }
        } else {
            return false;
        }
        return false;
    }

    static boolean contiene(String[] colores, String buscado) {
        boolean encontrado = false;
        int i = 0;
        while (i < colores.length && encontrado == false) {
            if (colores[i].equals(buscado) == true) {
                encontrado = true;
            } else {
                encontrado = false;
            }
            i = i + 1;
        }
        return encontrado == true ? true : false;
    }
}
import java.util.Arrays;

// DESPUÉS: lo más simple que funciona
public class KissDespues {
    public static void main(String[] args) {
        System.out.println(esPar(10) + " " + esPar(7));
        System.out.println(contiene(new String[]{"rojo", "verde"}, "verde"));
    }

    static boolean esPar(int n) {
        return n % 2 == 0;
    }

    static boolean contiene(String[] colores, String buscado) {
        return Arrays.asList(colores).contains(buscado);
    }
}
true false
true

⚠️ Importante: Señales de que algo no es KISS: comparar un boolean con true o false (if (encontrado == true)), if (cond) return true; else return false; (es return cond;), condiciones con muchos ! y paréntesis, y reinventar lo que ya ofrece la biblioteca estándar (contains, Math.max, String.join, Collections.sort…).

3.4. YAGNI: no lo vas a necesitar

YAGNI (You Aren’t Gonna Need It) aconseja no programar funcionalidades «por si acaso». Si el enunciado pide guardar contactos en un fichero CSV, no diseñes un sistema de plugins para soportar en el futuro XML, JSON, bases de datos y la nube. Ese código extra hay que escribirlo, probarlo, documentarlo y mantenerlo, y lo más probable es que, cuando de verdad haga falta, los requisitos sean distintos de los que imaginaste.

Con YAGNI Sin YAGNI
class Agenda que guarda en CSV interface Almacen, AlmacenFactory, AlmacenCsv, AlmacenXml (vacío), AlmacenNube (vacío)…
Un parámetro cuando hace falta uno Métodos con cinco parámetros opcionales que nadie usa
Ampliar cuando llega el requisito, con las pruebas como red de seguridad Adivinar el futuro

YAGNI no está reñido con el buen diseño: separar las responsabilidades (como harás en el proyecto final con modelo, servicio y persistencia) sí hace falta hoy, porque facilita probar y entender el código.


4. Olores de código y refactorización

📘 Concepto: Refactorizar es cambiar la estructura interna del código sin cambiar su comportamiento externo, para hacerlo más fácil de entender y de modificar. Un olor de código (code smell, término de Kent Beck y Martin Fowler) es un síntoma que suele indicar un problema de diseño. No es un error (el programa funciona), sino una pista de que conviene refactorizar.

4.1. Olores frecuentes y su refactorización

Olor Síntoma Refactorización
Método largo Decenas de líneas, comentarios que separan «fases» Extraer método
Código duplicado El mismo bloque en varios sitios Extraer método / extraer superclase
Nombre poco claro calc, datos2, aux, hacerCosas() Renombrar
Número mágico Literales sin explicación (0.21, 86400) Extraer constante
Expresión complicada Una condición o un cálculo que hay que leer tres veces Introducir variable explicativa / extraer método
Lista de parámetros larga Métodos con 5 o más parámetros Introducir un objeto o record que los agrupe
Clase grande («clase Dios») Una clase con decenas de atributos y métodos que lo hace todo Extraer clase
switch sobre un tipo El mismo switch (tipo) repetido en varios métodos Sustituir condicional por polimorfismo
Obsesión por los primitivos Teléfonos, DNI, dinero como String/double sueltos Crear clases o records pequeños (Dni, Dinero)
Código muerto Métodos, variables o import que nadie usa Eliminar (Safe Delete)

⚠️ Importante: Refactoriza solo con pruebas en verde (módulo 19). Haz un cambio pequeño, ejecuta las pruebas, y repite. Si algo se rompe, sabes exactamente qué cambio lo ha causado.

4.2. El menú Refactor de IntelliJ IDEA

IntelliJ automatiza las refactorizaciones de forma segura: si renombras un método, actualiza todas las llamadas del proyecto; si extraes un método, detecta qué variables necesita como parámetros y qué devuelve.

Esquema del menú contextual Refactor de IntelliJ IDEA con el submenú Extract/Introduce Figura 2. Esquema del menú Refactor: ① clic derecho → Refactor, ② Refactor This (Ctrl+Alt+Shift+T) muestra solo las opciones aplicables, ③ Rename y ④ el submenú Extract/Introduce.

Refactorización Atajo Qué hace
Refactor This Ctrl+Alt+Shift+T Menú con las refactorizaciones posibles donde está el cursor
Rename Shift+F6 Renombra variable, método, clase, paquete… y todos sus usos
Change Signature Ctrl+F6 Cambia nombre, parámetros o tipo de retorno de un método y actualiza las llamadas
Extract Method Ctrl+Alt+M Convierte las líneas seleccionadas en un método nuevo
Introduce Variable Ctrl+Alt+V Guarda la expresión seleccionada en una variable con nombre
Extract Constant Ctrl+Alt+C Convierte un literal en una constante static final
Extract Field Ctrl+Alt+F Convierte una variable local o expresión en un atributo
Extract Parameter Ctrl+Alt+P Convierte un valor del método en un parámetro
Inline Ctrl+Alt+N Lo contrario de extraer: sustituye la variable o el método por su contenido
Move F6 Mueve una clase a otro paquete (o un miembro estático a otra clase)
Safe Delete Alt+Delete Borra un elemento comprobando antes que nadie lo usa

Para aplicar a FacturaAntes los cambios de la sección 3.1 con el IDE:

  1. Cursor sobre calc → Shift+F6 → escribe calcularTotalConIva → Enter. Se renombran la declaración y las dos llamadas.
  2. Cursor sobre 0.21 → Ctrl+Alt+C → nombre IVA_GENERAL. IntelliJ pregunta si quiere sustituir también otras apariciones del mismo valor.
  3. Selecciona el bucle for → Ctrl+Alt+M → nombre calcularBase. IntelliJ crea private static double calcularBase(double[] p, int[] c) y deja la llamada en su lugar.
  4. Selecciona b && t > 100 → Ctrl+Alt+V → nombre aplicaDescuento: la condición queda con nombre propio.

4.3. Sustituir condicional por polimorfismo

Esta refactorización, más profunda, aprovecha lo que aprendiste en los módulos 13 y 14. El síntoma es un switch (o una cadena de if) sobre un «tipo» que se repite en varios métodos:

// ANTES: un switch sobre un "tipo" que se repetirá en cada método nuevo
public class EnviosAntes {
    public static void main(String[] args) {
        String[] tipos = {"ESTANDAR", "URGENTE", "INTERNACIONAL"};
        for (String t : tipos) {
            System.out.printf("%-14s %6.2f € entrega: %2d día(s)%n", t, coste(t, 3.0), dias(t));
        }
    }

    static double coste(String tipo, double kilos) {
        switch (tipo) {
            case "ESTANDAR":
                return 4.0 + kilos * 0.5;
            case "URGENTE":
                return 9.0 + kilos * 1.0;
            case "INTERNACIONAL":
                return 15.0 + kilos * 2.5;
            default:
                throw new IllegalArgumentException(tipo);
        }
    }

    static int dias(String tipo) {
        switch (tipo) {
            case "ESTANDAR":
                return 3;
            case "URGENTE":
                return 1;
            case "INTERNACIONAL":
                return 10;
            default:
                throw new IllegalArgumentException(tipo);
        }
    }
}

Para añadir un tipo «RECOGIDA_EN_TIENDA» habría que buscar y modificar todos los switch del programa. Con polimorfismo, cada tipo es una clase que sabe calcular lo suyo:

import java.util.List;

// DESPUÉS: sustituir condicional por polimorfismo. Cada tipo de envío sabe calcular lo suyo
public class EnviosDespues {
    public static void main(String[] args) {
        List<Envio> envios = List.of(new Estandar(), new Urgente(), new Internacional());
        for (Envio e : envios) {
            System.out.printf("%-14s %6.2f € entrega: %2d día(s)%n", e.nombre(), e.coste(3.0), e.dias());
        }
    }
}

sealed interface Envio permits Estandar, Urgente, Internacional {
    String nombre();
    double coste(double kilos);
    int dias();
}

final class Estandar implements Envio {
    public String nombre() { return "ESTANDAR"; }
    public double coste(double kilos) { return 4.0 + kilos * 0.5; }
    public int dias() { return 3; }
}

final class Urgente implements Envio {
    public String nombre() { return "URGENTE"; }
    public double coste(double kilos) { return 9.0 + kilos * 1.0; }
    public int dias() { return 1; }
}

final class Internacional implements Envio {
    public String nombre() { return "INTERNACIONAL"; }
    public double coste(double kilos) { return 15.0 + kilos * 2.5; }
    public int dias() { return 10; }
}
ESTANDAR         5.50 € entrega:  3 día(s)
URGENTE         12.00 € entrega:  1 día(s)
INTERNACIONAL   22.50 € entrega: 10 día(s)

Ahora un tipo nuevo es una clase nueva y no hay que tocar el código existente (principio abierto/cerrado, la O de SOLID). La interfaz es sellada (Java 17) para que el compilador conozca todos los tipos posibles.

💡 Consejo: Si los tipos solo se diferencian en datos (no en comportamiento), hay una alternativa todavía más sencilla: un enum con atributos (ESTANDAR(4.0, 0.5, 3), URGENTE(9.0, 1.0, 1)…), como viste en el módulo 14. Elige la opción más simple que resuelva el problema (KISS).


5. Documentar con Javadoc

📘 Concepto: Javadoc es el sistema estándar de documentación de Java. Los comentarios que empiezan por /** y se colocan justo antes de una clase, atributo, constructor o método forman parte de la documentación. La herramienta javadoc del JDK los convierte en páginas HTML como las de la documentación oficial de Java.

5.1. Estructura de un comentario Javadoc

/**
 * Primera frase: resumen breve, termina en punto. Aparece en las tablas de resumen.
 *
 * <p>Párrafos adicionales con detalles. Se puede usar HTML básico.</p>
 *
 * @param nombre   descripción del parámetro
 * @return descripción del valor devuelto
 * @throws IllegalArgumentException cuándo se lanza
 */
Etiqueta Dónde Significado
@param nombre descripción Métodos, constructores, records, genéricos Un parámetro (uno por parámetro, en orden)
@return descripción Métodos no void El valor devuelto
@throws Tipo descripción Métodos, constructores Excepción que puede lanzar y cuándo (@exception es sinónimo)
@author nombre Clases Autor (se muestra con la opción -author)
@version texto Clases Versión (se muestra con -version)
@since versión Cualquiera Desde qué versión existe
@see referencia Cualquiera Enlace «Véase también» a otra clase o método
@deprecated explicación Cualquiera Obsoleto; indica qué usar en su lugar (acompáñalo de la anotación @Deprecated)
{@link Clase#metodo()} Dentro del texto Enlace en línea a otro elemento
{@code texto} Dentro del texto Texto en fuente de código, sin interpretar HTML

5.2. Una clase documentada

package es.curso.tienda;

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

/**
 * Carrito de la compra de una tienda online.
 *
 * <p>Guarda las líneas añadidas por el cliente y calcula el importe total
 * aplicando, si procede, un cupón de descuento. Ejemplo de uso:</p>
 *
 * <pre>{@code
 * Carrito carrito = new Carrito();
 * carrito.anadir("Teclado", 49.90, 2);
 * double total = carrito.total();   // 99.8
 * }</pre>
 *
 * @author Equipo del curso
 * @version 1.2
 * @since 1.0
 * @see LineaCarrito
 */
public class Carrito {

    /** Número máximo de unidades de un mismo producto por línea. */
    public static final int MAX_UNIDADES = 10;

    private final List<LineaCarrito> lineas = new ArrayList<>();

    /**
     * Añade un producto al carrito.
     *
     * @param producto nombre del producto; no puede estar vacío
     * @param precio   precio unitario en euros, mayor o igual que 0
     * @param unidades número de unidades, entre 1 y {@link #MAX_UNIDADES}
     * @throws IllegalArgumentException si algún dato no es válido
     */
    public void anadir(String producto, double precio, int unidades) {
        if (producto == null || producto.isBlank()) {
            throw new IllegalArgumentException("Producto vacío");
        }
        if (precio < 0 || unidades < 1 || unidades > MAX_UNIDADES) {
            throw new IllegalArgumentException("Precio o unidades no válidos");
        }
        lineas.add(new LineaCarrito(producto, precio, unidades));
    }

    /**
     * Calcula el importe total del carrito sin descuentos.
     *
     * @return la suma de los subtotales de todas las líneas, o {@code 0} si está vacío
     */
    public double total() {
        double suma = 0;
        for (LineaCarrito l : lineas) {
            suma += l.subtotal();
        }
        return suma;
    }

    /// Calcula el total aplicando un cupón de descuento.
    ///
    /// Este comentario usa la sintaxis **Markdown** de Javadoc (Java 23, JEP 467).
    ///
    /// @param porcentaje descuento entre 0 y 100
    /// @return el total con el descuento aplicado
    /// @throws IllegalArgumentException si el porcentaje está fuera de rango
    public double totalConCupon(int porcentaje) {
        if (porcentaje < 0 || porcentaje > 100) {
            throw new IllegalArgumentException("Porcentaje fuera de rango: " + porcentaje);
        }
        return total() * (100 - porcentaje) / 100.0;
    }

    /**
     * Indica cuántas líneas tiene el carrito.
     *
     * @return el número de líneas (no de unidades)
     */
    public int numeroLineas() {
        return lineas.size();
    }

    /**
     * Calcula el total antiguo con IVA incluido.
     *
     * @return el total multiplicado por 1,21
     * @deprecated Los precios ya incluyen IVA; usa {@link #total()}.
     */
    @Deprecated(since = "1.2")
    public double totalConIva() {
        return total() * 1.21;
    }
}

📘 Concepto: Desde Java 23 (JEP 467) los comentarios de documentación también pueden escribirse en Markdown, con líneas que empiezan por /// en lugar de /** ... */. Se usa **negrita**, `código`, listas con - y enlaces como [Carrito#total()]. Las etiquetas @param, @return… funcionan igual. En el ejemplo, totalConCupon está documentado así. Las dos sintaxis pueden convivir.

El record LineaCarrito documenta sus componentes con @param en el comentario de la clase:

package es.curso.tienda;

/**
 * Una línea del carrito: un producto con su precio y sus unidades.
 *
 * @param producto nombre del producto
 * @param precio   precio unitario en euros
 * @param unidades número de unidades
 */
public record LineaCarrito(String producto, double precio, int unidades) {

    /**
     * Calcula el importe de la línea.
     *
     * @return {@code precio * unidades}
     */
    public double subtotal() {
        return precio * unidades;
    }
}

5.3. Generar la documentación con la herramienta javadoc

Desde la carpeta del proyecto:

javadoc -d docs -sourcepath src/main/java -subpackages es.curso.tienda \
        -encoding UTF-8 -docencoding UTF-8 -charset UTF-8 \
        -author -version -windowtitle "Tienda 1.0" -doctitle "Tienda 1.0 - API"
Opción Significado
-d docs Carpeta de destino del HTML
-sourcepath src/main/java Dónde están los fuentes
-subpackages es.curso.tienda Documenta ese paquete y sus subpaquetes
-encoding, -docencoding, -charset Codificación UTF-8 de fuentes y HTML (para que las tildes se vean bien)
-author, -version Incluye las etiquetas @author y @version, que por defecto se omiten
-private Documenta también los miembros privados (por defecto solo public y protected)

Salida real (JDK 25):

Loading source files for package es.curso.tienda...
Constructing Javadoc information...
Creating destination directory: "docs/"
Building index for all the packages and classes...
Standard Doclet version 25.0.4.1+1-LTS
Building tree for all the packages and classes...
Generating docs/es/curso/tienda/App.html...
src/main/java/es/curso/tienda/App.java:4: warning: use of default constructor, which does not provide a comment
public class App {
       ^
Generating docs/es/curso/tienda/Carrito.html...
src/main/java/es/curso/tienda/Carrito.java:23: warning: use of default constructor, which does not provide a comment
public class Carrito {
       ^
Generating docs/es/curso/tienda/LineaCarrito.html...
Generating docs/es/curso/tienda/package-summary.html...
[...]
Generating docs/index.html...
Generating docs/help-doc.html...
2 warnings

Los dos avisos son del verificador de documentación (doclint): las clases tienen un constructor por defecto implícito y, como es público, debería estar documentado. Se resuelve declarando el constructor y documentándolo, o haciéndolo privado si la clase no debe instanciarse.

Abre docs/index.html en el navegador. Esta es la página de la clase Carrito (los textos fijos de la plantilla, como Method Summary, salen en inglés porque el JDK no incluye traducción al español):

Página HTML de la clase Carrito generada por javadoc, abierta en el navegador Figura 3. Captura real de la documentación generada por javadoc 25 para la clase Carrito: descripción, ejemplo, etiquetas, resumen de atributos, constructores y métodos (el obsoleto aparece marcado como Deprecated).

Detalle del método anadir con sus parámetros y la excepción que lanza Figura 4. Detalle de los métodos: cada @param, @return y @throws se convierte en una sección; {@link #MAX_UNIDADES} es un enlace.

💡 Consejo: En IntelliJ, escribe /** encima de un método y pulsa Enter: el IDE genera el esqueleto con un @param por parámetro, @return y @throws. Con Ctrl+Q (Quick Documentation) ves la documentación de cualquier clase o método, tuyo o del JDK. Y Tools | Generate JavaDoc… ejecuta la herramienta desde el IDE. En un proyecto Maven también puedes usar mvn javadoc:javadoc (plugin maven-javadoc-plugin).

¿Qué documentar? Las clases y los métodos públicos (son el «contrato» que usan otros), con especial atención a qué hacen, qué significan los parámetros, qué devuelven, qué excepciones lanzan y cualquier condición especial (null, rangos válidos). No hace falta documentar getters triviales con frases como «Devuelve el nombre».


6. Control de versiones con Git y GitHub

6.1. ¿Qué es el control de versiones?

📘 Concepto: Un sistema de control de versiones registra los cambios de los ficheros de un proyecto a lo largo del tiempo, de forma que puedes volver a cualquier versión anterior, ver quién cambió qué y por qué, y trabajar en equipo sin pisaros el trabajo. Git, creado por Linus Torvalds en 2005 para desarrollar el núcleo Linux, es hoy el sistema que usa prácticamente toda la industria.

Si alguna vez has tenido carpetas llamadas practica_final, practica_final_v2, practica_final_ESTA_SI y practica_final_ESTA_SI_corregida, ya has hecho control de versiones… manual y propenso a errores. Git lo hace bien y sin duplicar carpetas.

Concepto Significado
Repositorio (repo) Carpeta del proyecto cuyo historial gestiona Git. El historial vive en la subcarpeta oculta .git
Commit Una instantánea del proyecto en un momento, con un mensaje, un autor, una fecha y un identificador (hash)
Rama (branch) Una línea de desarrollo independiente. La principal se llama main
HEAD Puntero al commit (y la rama) en el que estás trabajando
Fusión (merge) Unir el trabajo de una rama en otra
Remoto Copia del repositorio en un servidor (GitHub, GitLab, Bitbucket) para compartirlo
Git frente a GitHub Git es el programa que gestiona versiones en tu ordenador; GitHub es un servicio web que aloja repositorios Git y añade colaboración (pull requests, issues…)

6.2. Las cuatro zonas de Git

Directorio de trabajo, área de preparación, repositorio local y remoto con los comandos que mueven cambios entre ellos Figura 5. Los cambios viajan de tu directorio de trabajo al área de preparación (git add), al repositorio local (git commit) y al remoto (git push).

El área de preparación (staging area) es la clave para entender Git: te permite elegir qué cambios van en cada commit. Es como preparar un paquete antes de enviarlo: metes (add) solo lo que corresponde y luego lo cierras (commit) con una etiqueta que lo describe.

6.3. Instalación y configuración inicial

Descarga Git de git-scm.com (en Windows incluye Git Bash). La primera vez configura tu identidad, que quedará grabada en cada commit:

git config --global user.name "Ana García"
git config --global user.email "ana@ejemplo.es"
git config --global init.defaultBranch main

6.4. Comandos básicos paso a paso

Esta es una sesión real con un proyecto de agenda (ejecutada con Git 2.43; los hash serán distintos en tu ordenador).

Crear el repositorio en la carpeta del proyecto:

$ git init
Initialized empty Git repository in /tmp/agenda/.git/

Ver el estado después de crear Agenda.java, README.md y .gitignore (y de compilar, lo que crea target/):

$ git status
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	.gitignore
	README.md
	src/

nothing added to commit but untracked files present (use "git add" to track)

La carpeta target/ no aparece porque el .gitignore la excluye (sección 6.6).

Preparar los cambios y confirmarlos con un commit:

$ git add .

$ git status
On branch main

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
	new file:   .gitignore
	new file:   README.md
	new file:   src/main/java/es/curso/Agenda.java

$ git commit -m "Primera versión de la agenda"
[main (root-commit) b3f5708] Primera versión de la agenda
 3 files changed, 27 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 README.md
 create mode 100644 src/main/java/es/curso/Agenda.java

Modificar, ver las diferencias y hacer otro commit. Añadimos el método contiene a Agenda.java:

$ git status --short
 M src/main/java/es/curso/Agenda.java

$ git diff
diff --git a/src/main/java/es/curso/Agenda.java b/src/main/java/es/curso/Agenda.java
index 8fc0fd3..2305df8 100644
--- a/src/main/java/es/curso/Agenda.java
+++ b/src/main/java/es/curso/Agenda.java
@@ -9,4 +9,8 @@ public class Agenda {
     public void anadir(String nombre) {
         contactos.add(nombre);
     }
+
+    public boolean contiene(String nombre) {
+        return contactos.contains(nombre);
+    }
 }

$ git commit -am "Añade búsqueda de contactos"
[main c4891d5] Añade búsqueda de contactos
 1 file changed, 4 insertions(+)

$ git log --oneline
c4891d5 Añade búsqueda de contactos
b3f5708 Primera versión de la agenda

git commit -am hace add y commit a la vez, pero solo de ficheros que Git ya conoce (los nuevos hay que añadirlos con git add).

6.5. Ramas y fusión

Una rama te permite desarrollar una funcionalidad sin tocar el código estable de main. Si sale bien, la fusionas; si no, la borras y aquí no ha pasado nada.

$ git branch exportar-csv

$ git switch exportar-csv
Switched to branch 'exportar-csv'

$ git add src/main/java/es/curso/ExportadorCsv.java

$ git commit -m "Añade exportador CSV vacío"
[exportar-csv dc5f289] Añade exportador CSV vacío
 1 file changed, 4 insertions(+)
 create mode 100644 src/main/java/es/curso/ExportadorCsv.java

$ git switch main
Switched to branch 'main'

$ git commit -am "Documenta el uso en el README"
[main bb944d7] Documenta el uso en el README
 1 file changed, 4 insertions(+)

$ git merge exportar-csv -m "Fusiona la rama exportar-csv"
Merge made by the 'ort' strategy.
 src/main/java/es/curso/ExportadorCsv.java | 4 ++++
 1 file changed, 4 insertions(+)
 create mode 100644 src/main/java/es/curso/ExportadorCsv.java

$ git log --oneline --graph --all
*   3758591 Fusiona la rama exportar-csv
|\  
| * dc5f289 Añade exportador CSV vacío
* | bb944d7 Documenta el uso en el README
|/  
* c4891d5 Añade búsqueda de contactos
* b3f5708 Primera versión de la agenda

$ git branch -d exportar-csv
Deleted branch exportar-csv (was dc5f289).

Gráfico de commits de main y de la rama exportar-csv con su fusión Figura 6. El historial anterior dibujado: la rama exportar-csv sale de c4891d5 y se fusiona en 3758591.

⚠️ Importante: Si las dos ramas modifican las mismas líneas de un fichero, Git no puede decidir solo y se produce un conflicto. Git marca el fichero con <<<<<<<, ======= y >>>>>>>; tú eliges qué queda, borras las marcas, haces git add del fichero y terminas con git commit. IntelliJ ofrece un asistente de tres paneles (Resolve Conflicts) que lo hace mucho más cómodo.

💡 Consejo: git switch (para cambiar de rama) y git restore (para descartar cambios de un fichero) existen desde Git 2.23. En tutoriales antiguos verás git checkout, que hace ambas cosas.

6.6. El fichero .gitignore para Java

No todo debe ir al repositorio: lo que se genera (clases compiladas, el .jar, informes) se puede reconstruir, y los ficheros de configuración personal del IDE molestan a los compañeros. El fichero .gitignore de la raíz indica a Git qué ignorar:

# Compilados y Maven
target/
*.class

# IntelliJ IDEA
.idea/
*.iml
out/

# Sistema operativo
.DS_Store
Thumbs.db

💡 Consejo: GitHub mantiene plantillas de .gitignore para cada lenguaje (repositorio github/gitignore, fichero Java.gitignore y Maven.gitignore), y te ofrece añadir una al crear un repositorio. Nunca subas contraseñas ni claves: si se suben por error, quedan en el historial aunque luego borres el fichero.

6.7. Repositorios remotos y GitHub

  1. Crea una cuenta en github.com y un repositorio nuevo vacío (sin README) llamado agenda.
  2. Conecta tu repositorio local con él y sube la rama main:
git remote add origin https://github.com/tu-usuario/agenda.git
git push -u origin main

GitHub te pedirá autenticarte (desde el navegador o con un token; las contraseñas ya no se aceptan para git push). En nuestra sesión de ejemplo usamos un remoto local en lugar de GitHub, así que la salida es más corta, pero los comandos son los mismos:

$ git remote add origin /tmp/remoto.git

$ git push -u origin main
To /tmp/remoto.git
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.

-u deja configurado el seguimiento: a partir de ahora basta git push y git pull. Después, una compañera clonó el repositorio (git clone <url>), añadió un commit y lo subió. Para traer sus cambios:

$ git pull
From /tmp/remoto
   3758591..15ff1bf  main       -> origin/main
Updating 3758591..15ff1bf
Fast-forward
 README.md | 2 ++
 1 file changed, 2 insertions(+)

$ git log --oneline -3
15ff1bf Añade autores
3758591 Fusiona la rama exportar-csv
bb944d7 Documenta el uso en el README

Fast-forward significa que no hubo trabajo nuevo en tu copia, así que Git solo tuvo que avanzar el puntero de la rama.

Resumen de comandos:

Comando Para qué
git init Crear un repositorio en la carpeta actual
git clone <url> Descargar un repositorio remoto completo
git status Ver qué ha cambiado y en qué zona está
git add <fichero> / git add . Preparar cambios para el próximo commit
git commit -m "mensaje" Confirmar los cambios preparados
git log --oneline --graph Ver el historial
git diff Ver los cambios aún no preparados (--staged: los preparados)
git branch <rama> / git branch -d <rama> Crear / borrar una rama
git switch <rama> (-c para crearla a la vez) Cambiar de rama
git merge <rama> Fusionar otra rama en la actual
git remote add origin <url> Asociar un repositorio remoto
git push / git pull Subir / traer y fusionar cambios del remoto
git restore <fichero> Descartar los cambios de un fichero

💡 Consejo: Buenos mensajes de commit: en imperativo y describiendo qué y por qué («Añade búsqueda de contactos», «Corrige el cálculo del IVA en facturas rectificativas»), no «cambios», «arreglos» o «asdf». Haz commits pequeños y frecuentes, cada uno con un cambio coherente que compile y pase las pruebas.

6.8. Git desde IntelliJ IDEA

IntelliJ integra Git sin necesidad de abrir la consola: VCS | Create Git Repository (o Git en la barra principal si el proyecto ya es un repositorio), Git | Clone desde la pantalla de bienvenida, y Share Project on GitHub para publicarlo directamente.

Esquema de IntelliJ IDEA con el widget de rama, la ventana Commit y el editor con cambios marcados Figura 7. Esquema de la integración de Git en IntelliJ IDEA (interfaz nueva).

Nº Elemento Descripción
① Widget de rama en la barra principal Muestra la rama actual; al pulsarlo puedes crear ramas, cambiar entre ellas, fusionar, hacer push y pull
② Ventana Commit (Alt+0) Lista de ficheros modificados (azul), nuevos (verde) y sin versionar; marca con la casilla los que quieres incluir (equivale a git add)
③ Mensaje del commit Obligatorio; IntelliJ recuerda los anteriores
④ Commit / Commit and Push… Confirma (y opcionalmente sube) los cambios. Amend modifica el último commit
⑤ Marcas en el canal del editor Verde = líneas añadidas, azul = modificadas, gris = borradas. Clic en la marca para ver o revertir el cambio

Atajos útiles: Commit Ctrl+K, Push Ctrl+Shift+K, Update Project (pull) Ctrl+T, menú rápido de operaciones de Git Alt+` y ventana Git (Alt+9), cuya pestaña Log muestra el gráfico de ramas como git log --graph.


7. Introducción a Maven

📘 Concepto: Maven es una herramienta de gestión y construcción de proyectos Java. A partir de un fichero de configuración, el pom.xml (Project Object Model), sabe descargar las bibliotecas que necesitas (dependencias), compilar, ejecutar las pruebas, empaquetar en un .jar y mucho más, siempre con los mismos comandos y la misma estructura de carpetas. Ya lo usaste en el módulo 19 para JUnit.

Sin Maven, cada programador compila a su manera, descarga los .jar de las bibliotecas a mano y la configuración del IDE de uno no sirve al otro. Con Maven, cualquiera clona el repositorio y ejecuta mvn package.

7.1. El pom.xml mínimo

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- Coordenadas: identifican el proyecto de forma única -->
    <groupId>es.curso</groupId>
    <artifactId>tienda</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.release>25</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>6.1.3</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
Elemento Significado
groupId Organización, en formato de dominio invertido (como los paquetes)
artifactId Nombre del proyecto; da nombre al .jar
version Versión. SNAPSHOT indica que está en desarrollo
packaging Tipo de artefacto que se genera (jar por defecto)
maven.compiler.release Versión de Java con la que se compila
dependencies Bibliotecas que usa el proyecto, identificadas por sus coordenadas. Maven las descarga de Maven Central y las guarda en ~/.m2/repository

7.2. Estructura y ciclo de vida

Maven sigue el principio de convención sobre configuración: si colocas el código donde espera (src/main/java, src/test/java, src/main/resources), no hay que configurar nada. Todo lo generado va a target/.

Fases del ciclo de vida de Maven: validate, compile, test, package, verify, install y deploy Figura 8. Las fases del ciclo de vida se ejecutan en orden: pedir una ejecuta todas las anteriores.

Salida real de mvn package en el proyecto de la tienda:

[INFO] --------------------------< es.curso:tienda >---------------------------
[INFO] Building tienda 1.0-SNAPSHOT
[INFO]   from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- resources:3.4.0:resources (default-resources) @ tienda ---
[INFO] skip non existing resourceDirectory /tmp/p/src/main/resources
[INFO] 
[INFO] --- compiler:3.15.0:compile (default-compile) @ tienda ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 3 source files with javac [debug release 25] to target/classes
[INFO] 
[INFO] --- resources:3.4.0:testResources (default-testResources) @ tienda ---
[INFO] skip non existing resourceDirectory /tmp/p/src/test/resources
[INFO] 
[INFO] --- compiler:3.15.0:testCompile (default-testCompile) @ tienda ---
[INFO] No sources to compile
[INFO] 
[INFO] --- surefire:3.5.4:test (default-test) @ tienda ---
[INFO] No tests to run.
[INFO] 
[INFO] --- jar:3.5.0:jar (default-jar) @ tienda ---
[INFO] Building jar: /tmp/p/target/tienda-1.0-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

Cada línea --- plugin:versión:objetivo --- es un plugin trabajando en una fase. Como este pom.xml no fija las versiones de los plugins, Maven 3.9 usa las que trae por defecto (aquí, compiler 3.15.0 y surefire 3.5.4); en proyectos reales conviene fijarlas, como hicimos en el módulo 19. Para ejecutar la aplicación empaquetada:

java -cp target/tienda-1.0-SNAPSHOT.jar es.curso.tienda.App
Líneas: 2
Total: 109.75 €
Con cupón del 15 %: 93.29 €

💡 Consejo: En IntelliJ crea los proyectos con New Project → Java → Build system: Maven. La ventana Maven (lateral derecho) muestra las fases del ciclo de vida: doble clic en package equivale a mvn package. Si modificas el pom.xml, pulsa el icono Load Maven Changes que aparece en el editor para que el IDE descargue las nuevas dependencias. En el Curso de Java Avanzado profundizarás en Maven (plugins, perfiles, jar ejecutables, multimódulo) y verás Gradle.


8. Errores comunes

Error Consecuencia Solución
Nombres en varios idiomas mezclados (getName, calcularTotal, deleteCliente) Confusión Un idioma para todo el proyecto
Constantes «mágicas» repetidas Al cambiar una, se olvidan las demás static final con nombre
Comentar el código en lugar de borrarlo Ruido y dudas Bórralo: Git lo recuerda
Refactorizar sin pruebas Introducir errores sin darte cuenta Pruebas en verde antes y después de cada paso
Renombrar con «buscar y reemplazar» Cambia también textos y otros identificadores Shift+F6 (Rename) del IDE
Javadoc que repite el nombre del método («getNombre: obtiene el nombre») No aporta Documenta lo que no es obvio o no lo documentes
Subir target/, .idea/ o .class al repositorio Conflictos y repositorios enormes .gitignore desde el primer commit
Un único commit gigante al final del trabajo No se puede volver a un punto intermedio Commits pequeños y frecuentes
Trabajar directamente en main cambios arriesgados Rompes la versión estable Una rama por funcionalidad
Copiar .jar a mano en el proyecto Versiones incompatibles, «en mi máquina funciona» Dependencias en el pom.xml

9. Ejercicios

Ejercicio 1: Corrige los nombres

Indica qué convención incumple cada identificador y escribe el nombre correcto: class gestor_alumnos, int NumeroDeAlumnos, final static double pi = 3.14159, void Calcular(), package Es.Curso.App, boolean flag (indica si un alumno ha aprobado), String s (nombre del cliente), enum dia { lunes, martes }.

Ver solución
Original Problema Corrección
class gestor_alumnos Las clases van en UpperCamelCase sin guiones bajos class GestorAlumnos
int NumeroDeAlumnos Las variables empiezan en minúscula int numeroAlumnos
final static double pi Las constantes van en mayúsculas (y el orden habitual es static final) static final double PI (o mejor, usa Math.PI)
void Calcular() Los métodos empiezan en minúscula y deben decir qué calculan void calcularMedia()
package Es.Curso.App Los paquetes van en minúsculas package es.curso.app
boolean flag No dice nada boolean aprobado o estaAprobado
String s No dice nada String nombreCliente
enum dia { lunes, martes } El tipo en UpperCamelCase y los literales en mayúsculas enum Dia { LUNES, MARTES }

Ejercicio 2: Elimina los números mágicos

Limpia este fragmento: nombres, constantes y un método para cada responsabilidad. La salida debe ser la misma para las temperaturas 36.6, 38.2 y 37.5.

static void m(double[] t) {
    for (int i = 0; i < t.length; i++) {
        double f = t[i] * 1.8 + 32;
        System.out.printf("%.1f ºC = %.1f ºF%s%n", t[i], f, t[i] > 37.5 ? " (fiebre)" : "");
    }
}
Ver solución
public class Ej2Temperaturas {

    private static final double FACTOR_CELSIUS_A_FAHRENHEIT = 9.0 / 5.0;
    private static final double DESPLAZAMIENTO_FAHRENHEIT = 32.0;
    private static final double UMBRAL_FIEBRE_CELSIUS = 37.5;

    public static void main(String[] args) {
        double[] temperaturasCelsius = {36.6, 38.2, 37.5};
        for (double celsius : temperaturasCelsius) {
            System.out.printf("%.1f ºC = %.1f ºF%s%n", celsius, aFahrenheit(celsius),
                    tieneFiebre(celsius) ? " (fiebre)" : "");
        }
    }

    static double aFahrenheit(double celsius) {
        return celsius * FACTOR_CELSIUS_A_FAHRENHEIT + DESPLAZAMIENTO_FAHRENHEIT;
    }

    static boolean tieneFiebre(double celsius) {
        return celsius > UMBRAL_FIEBRE_CELSIUS;
    }
}
36.6 ºC = 97.9 ºF
38.2 ºC = 100.8 ºF (fiebre)
37.5 ºC = 99.5 ºF

9.0 / 5.0 deja más clara la fórmula que 1.8, y el umbral de fiebre ahora tiene nombre y se puede cambiar en un solo sitio. Además, aFahrenheit y tieneFiebre se pueden probar por separado con JUnit.

Ejercicio 3: Aplica DRY

Este método repite la misma comprobación tres veces. Refactorízalo extrayendo un método y comprueba que se comporta igual con un registro correcto, uno con el usuario vacío y otro con un email sin @.

static String registrar(String usuario, String email, String clave) {
    if (usuario == null || usuario.isBlank()) return "Error: el campo usuario es obligatorio";
    if (email == null || email.isBlank()) return "Error: el campo email es obligatorio";
    if (clave == null || clave.isBlank()) return "Error: el campo clave es obligatorio";
    if (!email.contains("@")) return "Error: email sin @";
    return "Registrado: " + usuario;
}
Ver solución
public class Ej3Registro {

    public static void main(String[] args) {
        System.out.println(registrar("ana", "ana@correo.es", "Ana1234!"));
        System.out.println(registrar("", "luis@correo.es", "secreta"));
        System.out.println(registrar("marta", "marta.correo.es", "Marta2026"));
    }

    static String registrar(String usuario, String email, String clave) {
        try {
            exigirTexto(usuario, "usuario");
            exigirTexto(email, "email");
            exigirTexto(clave, "clave");
            if (!email.contains("@")) {
                throw new IllegalArgumentException("email sin @");
            }
            return "Registrado: " + usuario;
        } catch (IllegalArgumentException e) {
            return "Error: " + e.getMessage();
        }
    }

    // La comprobación que antes estaba copiada tres veces, ahora en un solo sitio (DRY)
    private static void exigirTexto(String valor, String campo) {
        if (valor == null || valor.isBlank()) {
            throw new IllegalArgumentException("el campo " + campo + " es obligatorio");
        }
    }
}
Registrado: ana
Error: el campo usuario es obligatorio
Error: email sin @

Si mañana el criterio de «obligatorio» cambia (por ejemplo, un mínimo de 3 caracteres), se modifica solo exigirTexto.

Ejercicio 4: Documenta con Javadoc

Escribe el comentario Javadoc completo (clase y método, con @param, @return, @throws y @see) de un método long potencia(long base, int exponente) que lanza IllegalArgumentException si el exponente es negativo. Genera la documentación con -Xdoclint:all y comprueba que no da avisos.

Ver solución
/**
 * Utilidades matemáticas con enteros.
 *
 * @author Tu nombre
 * @version 1.0
 */
public final class Matematicas {

    private Matematicas() { }

    /**
     * Calcula la potencia entera {@code base} elevado a {@code exponente}.
     *
     * <p>Usa multiplicaciones sucesivas; no detecta el desbordamiento de {@code long}.</p>
     *
     * @param base      número que se multiplica
     * @param exponente número de veces que se multiplica la base; debe ser mayor o igual que 0
     * @return {@code base} elevado a {@code exponente}; {@code 1} si el exponente es 0
     * @throws IllegalArgumentException si {@code exponente} es negativo
     * @see Math#pow(double, double)
     */
    public static long potencia(long base, int exponente) {
        if (exponente < 0) {
            throw new IllegalArgumentException("Exponente negativo: " + exponente);
        }
        long resultado = 1;
        for (int i = 0; i < exponente; i++) {
            resultado *= base;
        }
        return resultado;
    }

    /**
     * Prueba rápida del método.
     *
     * @param args no se usan
     */
    public static void main(String[] args) {
        System.out.println(potencia(2, 10) + " " + potencia(5, 0) + " " + potencia(-3, 3));
    }
}

java Matematicas.java muestra 1024 1 -27, y javadoc -d doc -encoding UTF-8 -docencoding UTF-8 -charset UTF-8 -author -version -Xdoclint:all Matematicas.java termina sin ningún warning: el constructor privado evita el aviso del constructor por defecto y todos los elementos públicos están documentados.

Ejercicio 5: Una sesión de Git

Escribe los comandos para: crear un repositorio en la carpeta calculadora, añadir un .gitignore que excluya target/ y .idea/, hacer un primer commit con todo, crear una rama division, cambiar a ella, hacer un commit con el fichero Division.java, volver a main, fusionar la rama, borrarla y subir main a un repositorio de GitHub vacío.

Ver solución
cd calculadora
git init
printf 'target/\n.idea/\n' > .gitignore
git add .
git commit -m "Versión inicial de la calculadora"
git switch -c division                  # crea la rama y cambia a ella
git add src/main/java/es/curso/Division.java
git commit -m "Añade la operación de división"
git switch main
git merge division
git branch -d division
git remote add origin https://github.com/tu-usuario/calculadora.git
git push -u origin main

Como en main no hubo commits nuevos mientras trabajabas en la rama, el merge será un fast-forward (sin commit de fusión). Comprueba el resultado con git log --oneline --graph.

Ejercicio 6: Elige la refactorización

Indica qué olor de código hay en cada caso y qué refactorización (con su atajo de IntelliJ) aplicarías:

  1. if (usuario.getEdad() >= 18 && usuario.getPais().equals("ES") && !usuario.estaBloqueado()) {...} aparece en cinco sitios.
  2. double precio = base * 1.21; repetido por todo el programa.
  3. Un método procesar() de 120 líneas con comentarios // 1. leer, // 2. validar, // 3. guardar.
  4. Una variable llamada aux2.
  5. crearFactura(String nombre, String nif, String calle, String ciudad, String cp, double base, double iva).
  6. Un switch sobre tipoFigura para calcular el área y otro para el perímetro.
Ver solución
Nº Olor Refactorización
1 Código duplicado y expresión complicada Extract Method (Ctrl+Alt+M) a puedeComprar(usuario) o, mejor, un método usuario.puedeComprar() en la propia clase
2 Número mágico Extract Constant (Ctrl+Alt+C) a IVA_GENERAL
3 Método largo Extract Method tres veces: leer(), validar(), guardar()
4 Nombre poco claro Rename (Shift+F6) a un nombre que diga qué guarda
5 Lista de parámetros larga Agrupar en objetos: record Cliente(String nombre, String nif, Direccion direccion); con Change Signature (Ctrl+F6) se actualizan las llamadas
6 switch sobre un tipo Sustituir condicional por polimorfismo: interfaz sellada Figura con area() y perimetro() implementada por cada figura

Ejercicio 7 (propuesto): Refactoriza un ejercicio antiguo

Elige un programa largo que hicieras en los módulos 6 a 9 (por ejemplo, un menú con switch que lo hace todo en main). Sin cambiar su salida, aplica: nombres significativos, constantes, extraer métodos de menos de 15 líneas y eliminar duplicados. Usa solo las refactorizaciones del IDE y anota cuáles has usado.

Ejercicio 8 (propuesto): Documenta y publica la API

Documenta con Javadoc todas las clases públicas del proyecto de la agenda (o de cualquier proyecto con al menos tres clases), genera la documentación en docs/ con javadoc y corrige todos los avisos de -Xdoclint:all. Opcional: publica la carpeta docs/ con GitHub Pages.

Ejercicio 9 (propuesto): Resolver un conflicto

En un repositorio de prueba, crea dos ramas que modifiquen la misma línea de un fichero de forma distinta y fusiónalas. Observa el conflicto con git status, resuélvelo a mano editando el fichero y completa la fusión. Repite el ejercicio resolviéndolo con el asistente de IntelliJ (Git | Resolve Conflicts).

Ejercicio 10 (propuesto): Un repositorio en GitHub para el curso

Crea en GitHub un repositorio curso-java con un README.md que explique su contenido, un .gitignore para Java y Maven y una carpeta por módulo. Sube tus ejercicios con commits descriptivos (al menos uno por ejercicio) y comprueba en la web de GitHub el historial y las diferencias de cada commit.

Ejercicio 11 (propuesto): Del switch al enum

Toma EnviosAntes de la sección 4.3 y refactorízalo de nuevo, esta vez con un enum TipoEnvio cuyos literales guarden la tarifa base, el precio por kilo y los días. Compara con la versión de la interfaz sellada: ¿cuál elegirías si cada tipo tuviera además una forma distinta de calcular el seguro?

Ejercicio 12 (propuesto): Proyecto Maven desde cero

Crea con IntelliJ un proyecto Maven conversor con una clase ConversorUnidades (kilómetros a millas, kilos a libras…) documentada con Javadoc, sus pruebas JUnit (módulo 19) y un .gitignore. Ejecuta mvn clean package, comprueba que se generan el .jar y el informe de pruebas en target/surefire-reports y sube el proyecto a GitHub.


Resumen

Concepto Clave
Nombres Clases UpperCamelCase, métodos y variables lowerCamelCase, constantes UPPER_SNAKE_CASE, paquetes en minúsculas
Formato 4 espacios, llaves K&R siempre, líneas de 100-120 caracteres; Ctrl+Alt+L y Ctrl+Alt+O
Organización Constantes, atributos estáticos, de instancia, constructores, métodos, getters/setters, equals/hashCode/toString
Código limpio Nombres que explican, métodos cortos, una responsabilidad, sin números mágicos
DRY / KISS / YAGNI No repitas, hazlo simple, no programes lo que no se pide
Comentarios Explican el porqué; el qué lo dice el código
Refactorizar Mejorar la estructura sin cambiar el comportamiento, con pruebas en verde
IntelliJ Refactor Rename Shift+F6, Extract Method Ctrl+Alt+M, Constant Ctrl+Alt+C, Variable Ctrl+Alt+V, Refactor This Ctrl+Alt+Shift+T
Polimorfismo Sustituye los switch sobre tipos por clases que implementan una interfaz
Javadoc /** ... */ o /// (Java 23); @param, @return, @throws, @see, {@link}, {@code}; javadoc -d docs ...
Git init, status, add, commit, log, diff, branch, switch, merge, remote, push, pull, clone
.gitignore Excluye target/, *.class, .idea/…
GitHub Servicio que aloja repositorios Git remotos y facilita la colaboración
Maven pom.xml con coordenadas y dependencias; mvn clean, test, package