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
booleanse leen como preguntas:esPar(),estaVacia(),tieneStock(),puedePrestar(). - Getters y setters:
getX()/setX(); paraboolean,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 admiteañocomo 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 losimportsin 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
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 edadsobreif (u.e >= 18)se arregla conif (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:
imprimirInformese 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.
mediasolo 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
booleancontrueofalse(if (encontrado == true)),if (cond) return true; else return false;(esreturn 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.
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:
- Cursor sobre
calc→Shift+F6→ escribecalcularTotalConIva→Enter. Se renombran la declaración y las dos llamadas. - Cursor sobre
0.21→Ctrl+Alt+C→ nombreIVA_GENERAL. IntelliJ pregunta si quiere sustituir también otras apariciones del mismo valor. - Selecciona el bucle
for→Ctrl+Alt+M→ nombrecalcularBase. IntelliJ creaprivate static double calcularBase(double[] p, int[] c)y deja la llamada en su lugar. - Selecciona
b && t > 100→Ctrl+Alt+V→ nombreaplicaDescuento: 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
enumcon 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 herramientajavadocdel 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,totalConCuponestá 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):
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).
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 pulsaEnter: el IDE genera el esqueleto con un@parampor parámetro,@returny@throws. ConCtrl+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 usarmvn javadoc:javadoc(pluginmaven-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
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).
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, hacesgit adddel fichero y terminas congit 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) ygit restore(para descartar cambios de un fichero) existen desde Git 2.23. En tutoriales antiguos verásgit 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
.gitignorepara cada lenguaje (repositoriogithub/gitignore, ficheroJava.gitignoreyMaven.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
- Crea una cuenta en github.com y un repositorio nuevo vacío (sin README) llamado
agenda. - 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.
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.jary 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/.
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
packageequivale amvn package. Si modificas elpom.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:
if (usuario.getEdad() >= 18 && usuario.getPais().equals("ES") && !usuario.estaBloqueado()) {...}aparece en cinco sitios.double precio = base * 1.21;repetido por todo el programa.- Un método
procesar()de 120 líneas con comentarios// 1. leer,// 2. validar,// 3. guardar. - Una variable llamada
aux2. crearFactura(String nombre, String nif, String calle, String ciudad, String cp, double base, double iva).- Un
switchsobretipoFigurapara 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 |