Módulo 18: Diagramas UML de clases

Objetivos del módulo

  • Saber qué es UML, para qué sirve y qué tipos de diagramas define
  • Leer y dibujar clases UML con sus atributos, operaciones, visibilidad, tipos y multiplicidad
  • Representar miembros estáticos, abstractos, interfaces, enumeraciones y estereotipos
  • Distinguir y usar correctamente las relaciones: asociación, agregación, composición, generalización, realización y dependencia
  • Modelar clases asociación y relaciones reflexivas
  • Traducir un diagrama de clases a Java (ingeniería directa) y un programa Java a un diagrama (ingeniería inversa)
  • Interpretar diagramas de objetos y tener una primera idea de los diagramas de casos de uso y de secuencia
  • Conocer las herramientas habituales (draw.io, PlantUML, StarUML, IntelliJ IDEA) y escribir diagramas como texto con PlantUML
  • Diseñar e implementar casos completos: biblioteca, tienda online y centro educativo

1. ¿Qué es UML y para qué sirve?

📘 Concepto: UML (Unified Modeling Language, Lenguaje Unificado de Modelado) es un lenguaje gráfico estándar para especificar, visualizar, construir y documentar sistemas de software. No es un lenguaje de programación ni una metodología: es una notación, un conjunto de símbolos con un significado preciso que todo el mundo entiende igual.

Piensa en los planos de un arquitecto. Antes de construir una casa nadie coloca ladrillos al azar: se dibujan planos que el fontanero, el electricista y el cliente pueden leer porque todos conocen los símbolos. UML cumple esa función en el software: antes (o después) de escribir miles de líneas de Java, dibujas las clases y sus relaciones en un diagrama que cualquier programador, en cualquier empresa y en cualquier lenguaje orientado a objetos, sabe interpretar.

Un poco de historia

Año Hito
Años 90 Conviven decenas de notaciones: Booch, OMT (Rumbaugh), OOSE (Jacobson)…
1997 Booch, Rumbaugh y Jacobson («los tres amigos») unifican sus métodos y el OMG (Object Management Group) adopta UML 1.1 como estándar
2005 UML 2.0: revisión profunda, se pasa a 13 tipos de diagramas
2017 UML 2.5.1, la versión vigente, con 14 tipos de diagramas. También es la norma ISO/IEC 19505

¿Para qué se usa en la práctica?

  • Diseñar antes de programar: descubres errores de diseño cuando cuestan poco (borrar una línea en un dibujo) y no cuando cuestan mucho (rehacer cinco clases).
  • Comunicar: en una reunión, un diagrama de una página explica más que veinte ficheros de código.
  • Documentar un sistema existente para quien llegue después (o para ti dentro de seis meses).
  • Aprobar exámenes: en DAM/DAW y en la universidad es habitual que te den un enunciado y te pidan el diagrama de clases, o que te den el diagrama y te pidan el código Java.

💡 Consejo: UML no exige dibujarlo todo. Un buen diagrama muestra lo necesario para el propósito que tiene. Un boceto en una pizarra para discutir una idea puede omitir atributos y tipos; un diagrama de diseño detallado para un examen debe incluirlos todos.


2. Tipos de diagramas UML

UML 2.5 define 14 diagramas agrupados en dos familias. Los estructurales describen qué hay en el sistema (vista estática); los de comportamiento describen qué hace y cómo cambia con el tiempo (vista dinámica). Dentro del comportamiento hay una subfamilia, los diagramas de interacción, centrados en el intercambio de mensajes entre objetos.

Árbol con los 14 diagramas de UML 2.5 divididos en estructura y comportamiento Figura 1. Clasificación de los diagramas de UML 2.5. Destacados, los que trabajarás en este módulo.

Familia Diagrama ¿Qué muestra? Uso típico
Estructura Clases Clases, atributos, operaciones y relaciones Diseño del código orientado a objetos (el más usado)
Estructura Objetos Objetos concretos y sus enlaces en un instante Ejemplos y depuración de un diseño
Estructura Paquetes Agrupación de elementos y dependencias entre paquetes Organización de un proyecto grande
Estructura Componentes Piezas desplegables y sus interfaces Arquitectura de aplicaciones
Estructura Despliegue Nodos físicos (servidores, dispositivos) y qué se ejecuta en cada uno Infraestructura
Estructura Estructura compuesta Estructura interna de una clase compleja Diseño detallado
Estructura Perfiles Extensiones de UML con estereotipos propios Adaptar UML a un dominio
Comportamiento Casos de uso Qué funcionalidades ofrece el sistema y a quién Requisitos
Comportamiento Actividad Flujo de pasos, decisiones y paralelismo Procesos, algoritmos (parecido a un diagrama de flujo)
Comportamiento Máquina de estados Estados de un objeto y transiciones Objetos con ciclo de vida (pedido, semáforo)
Interacción Secuencia Mensajes entre objetos ordenados en el tiempo Detallar un caso de uso o un método
Interacción Comunicación Los mismos mensajes, organizados por enlaces Alternativa a la secuencia
Interacción Tiempos Cambios de estado sobre un eje temporal Sistemas de tiempo real
Interacción Visión global de la interacción Un diagrama de actividad cuyos nodos son interacciones Visión general de escenarios

⚠️ Importante: El diagrama de clases es, con diferencia, el más importante para un programador Java y el que más aparece en exámenes. Le dedicamos casi todo el módulo.


3. La clase en UML

Una clase se dibuja como un rectángulo dividido en tres compartimentos: nombre, atributos y operaciones.

Clase CuentaBancaria con sus tres compartimentos y anotaciones numeradas Figura 2. Anatomía de una clase UML: ① nombre, ② atributos, ③ operaciones, ④ miembros estáticos subrayados y ⑤ visibilidad.

  1. Nombre (①): sustantivo en singular, en UpperCamelCase, centrado y en negrita: CuentaBancaria, no cuentas_bancarias.
  2. Atributos (②): el estado que guarda cada objeto.
  3. Operaciones (③): el comportamiento, es decir, los métodos (incluidos los constructores).

Los compartimentos de atributos y operaciones son opcionales: en un diagrama de análisis puedes dibujar solo el nombre, y en un diagrama grande es habitual ocultar los getters y setters triviales para no saturarlo.

Este es el código Java que corresponde exactamente a la figura 2:

// Traducción a Java de la clase UML CuentaBancaria (figura 2)
public class CuentaBancaria {

    // ---- Atributos ----
    private static int numCuentas = 0;          // - numCuentas: int = 0   (subrayado: estático)
    private final String iban;                  // - iban: String {readOnly}
    private String titular;                     // - titular: String
    private double saldo;                       // - saldo: double = 0.0
    protected String[] autorizados = new String[3]; // # autorizados: String [0..3]

    // ---- Operaciones ----
    public CuentaBancaria(String iban, String titular) { // + CuentaBancaria(iban: String, titular: String)
        this.iban = iban;
        this.titular = titular;
        this.saldo = 0.0;
        numCuentas++;
    }

    public void ingresar(double cantidad) {     // + ingresar(cantidad: double): void
        if (cantidad <= 0) {
            throw new IllegalArgumentException("La cantidad debe ser positiva");
        }
        saldo += cantidad;
    }

    public boolean retirar(double cantidad) {   // + retirar(cantidad: double): boolean
        if (cantidad <= 0 || cantidad > saldo) {
            return false;
        }
        saldo -= cantidad;
        return true;
    }

    public double getSaldo() {                  // + getSaldo(): double
        return saldo;
    }

    public String getIban() {                   // + getIban(): String
        return iban;
    }

    public static int getNumCuentas() {         // + getNumCuentas(): int   (subrayado: estático)
        return numCuentas;
    }

    String resumen() {                          // ~ resumen(): String   (visibilidad de paquete)
        return iban + " | " + titular + " | " + saldo + " €";
    }

    public static void main(String[] args) {
        CuentaBancaria c1 = new CuentaBancaria("ES21 0049 0001", "Ana Ruiz");
        CuentaBancaria c2 = new CuentaBancaria("ES33 2100 0002", "Luis Gil");
        c1.ingresar(500);
        boolean ok = c1.retirar(120.5);
        boolean ko = c2.retirar(50);
        System.out.println(c1.resumen());
        System.out.println(c2.resumen());
        System.out.println("Retirada c1: " + ok + ", retirada c2: " + ko);
        System.out.println("Cuentas creadas: " + CuentaBancaria.getNumCuentas());
    }
}

Salida (JDK 25, java CuentaBancaria.java):

ES21 0049 0001 | Ana Ruiz | 379.5 €
ES33 2100 0002 | Luis Gil | 0.0 €
Retirada c1: true, retirada c2: false
Cuentas creadas: 2

3.1. Visibilidad

El primer carácter de cada miembro indica su visibilidad, que se corresponde directamente con los modificadores de acceso de Java que viste en el módulo 11.

Tabla de visibilidades UML y ámbitos concéntricos de acceso Figura 3. Los cuatro símbolos de visibilidad y su equivalente en Java.

UML Nombre Java Accesible desde
+ público public Cualquier clase
- privado private Solo la propia clase
# protegido protected Las clases del paquete y las subclases (aunque estén en otro paquete)
~ paquete sin modificador Solo las clases del mismo paquete

⚠️ Importante: Un error típico de examen es poner + a los atributos. Salvo constantes, los atributos van con - (encapsulación) y se accede a ellos mediante operaciones +.

3.2. Sintaxis completa de atributos y operaciones

Desglose de la sintaxis de un atributo y una operación UML Figura 4. Partes de un atributo y de una operación. Solo el nombre es obligatorio; lo demás se añade según el nivel de detalle.

Atributo: visibilidad nombre : Tipo [multiplicidad] = valorInicial {propiedades}

Operación: visibilidad nombre(param1: Tipo1, param2: Tipo2) : TipoRetorno

Fíjate en que UML escribe el tipo después del nombre (saldo: double), al revés que Java (double saldo). Algunos detalles más:

Elemento UML Significado En Java
- telefonos: String [0..*] Multiplicidad del atributo: de 0 a muchos valores private List<String> telefonos;
- notas: double [3] Exactamente 3 valores private double[] notas = new double[3];
- email: String [0..1] Opcional private String email; // puede ser null
- iban: String {readOnly} No cambia tras inicializarse private final String iban;
- /edad: int Atributo derivado (/): se calcula a partir de otros Un método getEdad() que usa fechaNacimiento
+ retirar(cantidad: double): boolean Operación con un parámetro y retorno public boolean retirar(double cantidad)
+ ingresar(cantidad: double) Sin tipo de retorno = void public void ingresar(double cantidad)

💡 Consejo: En los diagramas usa los tipos de Java (int, double, String, LocalDate, List<Producto>) si el diagrama es para implementarlo en Java. UML tiene sus tipos primitivos propios (Integer, Real, Boolean, String), pero en un proyecto Java lo natural es escribir los de Java.

3.3. Miembros estáticos y abstractos

  • Un miembro estático (static) se dibuja subrayado.
  • Una clase abstracta escribe su nombre en cursiva (o añade {abstract} o el estereotipo «abstract», útil cuando escribes a mano y la cursiva no se distingue).
  • Una operación abstracta también va en cursiva.

Clase abstracta Figura con miembro estático y operación abstracta, y sus subclases Figura 5. Figura es abstracta (nombre en cursiva), area() es abstracta y contador y getContador() son estáticos (subrayados).

import java.util.List;

// Clase abstracta (nombre en cursiva), operación abstracta (cursiva) y miembros estáticos (subrayados)
public class Figuras {
    public static void main(String[] args) {
        List<Figura> figuras = List.of(new Circulo(1.5), new Rectangulo(2, 3), new Circulo(0.5));
        for (Figura f : figuras) {
            System.out.printf("%-10s área = %6.2f%n", f.getNombre(), f.area());
        }
        System.out.println("Figuras creadas: " + Figura.getContador());
    }
}

abstract class Figura {
    private static int contador = 0;      // estático -> subrayado en UML
    protected String nombre;

    protected Figura(String nombre) {
        this.nombre = nombre;
        contador++;
    }

    public abstract double area();        // abstracto -> cursiva en UML

    public String getNombre() {
        return nombre;
    }

    public static int getContador() {     // estático -> subrayado en UML
        return contador;
    }
}

class Circulo extends Figura {
    private double radio;

    Circulo(double radio) {
        super("Círculo");
        this.radio = radio;
    }

    @Override
    public double area() {
        return Math.PI * radio * radio;
    }
}

class Rectangulo extends Figura {
    private double base, altura;

    Rectangulo(double base, double altura) {
        super("Rectángulo");
        this.base = base;
        this.altura = altura;
    }

    @Override
    public double area() {
        return base * altura;
    }
}
Círculo    área =   7.07
Rectángulo área =   6.00
Círculo    área =   0.79
Figuras creadas: 3

3.4. Interfaces, enumeraciones y estereotipos

📘 Concepto: Un estereotipo es una etiqueta entre comillas angulares «...» que amplía el significado de un elemento UML. Los más habituales en Java son «interface», «enumeration» y «abstract». También puedes crear los tuyos, como «record» o «entity», siempre que el equipo sepa qué significan.

Interfaz Pagable, enumeración EstadoPedido, record Punto y notación de piruleta Figura 6. Una interfaz, una enumeración y un record, con la clase Pedido que los usa. Abajo, la notación abreviada «piruleta» para indicar que Pedido implementa Pagable.

  • Interfaz: rectángulo con «interface» encima del nombre. Sus operaciones son abstractas (cursiva). Las constantes son estáticas (subrayadas). También existe la notación piruleta (lollipop): un círculo unido a la clase que la implementa.
  • Enumeración: «enumeration» y los literales en el compartimento de atributos, sin visibilidad ni tipo. Si el enum tiene atributos y métodos (como en Java), se añaden debajo.
  • Record (Java 16): UML no tiene un símbolo específico; lo más claro es marcarlo con «record» y listar sus componentes.
// «interface», «enumeration» y «record» en Java
public class Estereotipos {
    public static void main(String[] args) {
        Pedido p = new Pedido(new Punto(40.4168, -3.7038), 59.90);
        System.out.println("Estado inicial: " + p.getEstado());
        p.avanzar();
        p.avanzar();
        System.out.println("Estado actual: " + p.getEstado() + " (" + p.getEstado().getDescripcion() + ")");
        System.out.println("Destino: " + p.getDestino());
        System.out.printf("Importe a pagar: %.2f €%n", p.importe());
    }
}

interface Pagable {                       // «interface»
    double IVA = 0.21;                    // constante (public static final implícito)
    double importe();                     // operación abstracta
}

enum EstadoPedido {                       // «enumeration»
    PENDIENTE("Pendiente de pago"),
    ENVIADO("En camino"),
    ENTREGADO("Entregado al cliente");

    private final String descripcion;

    EstadoPedido(String descripcion) {
        this.descripcion = descripcion;
    }

    public String getDescripcion() {
        return descripcion;
    }
}

record Punto(double latitud, double longitud) { }   // «record»: atributos inmutables

class Pedido implements Pagable {
    private final Punto destino;
    private final double base;
    private EstadoPedido estado = EstadoPedido.PENDIENTE;

    Pedido(Punto destino, double base) {
        this.destino = destino;
        this.base = base;
    }

    public void avanzar() {
        estado = switch (estado) {
            case PENDIENTE -> EstadoPedido.ENVIADO;
            case ENVIADO, ENTREGADO -> EstadoPedido.ENTREGADO;
        };
    }

    @Override
    public double importe() {
        return base * (1 + IVA);
    }

    public EstadoPedido getEstado() { return estado; }
    public Punto getDestino() { return destino; }
}
Estado inicial: PENDIENTE
Estado actual: ENTREGADO (Entregado al cliente)
Destino: Punto[latitud=40.4168, longitud=-3.7038]
Importe a pagar: 72.48 €

💡 Consejo: Fíjate en que en la figura 6 los atributos estado y destino de Pedido no aparecen en su compartimento: se representan como el rol de la flecha que va a EstadoPedido y a Punto. Es la convención recomendada: si el tipo de un atributo es otra clase de tu diagrama, dibújalo como asociación, no como texto (y nunca de las dos formas a la vez).


4. Relaciones entre clases

Las clases no viven aisladas: un pedido pertenece a un cliente, un coche tiene un motor, un asalariado es un empleado… UML distingue seis relaciones principales, cada una con su notación.

Tabla con la notación de las seis relaciones UML Figura 7. Resumen de notaciones. Apréndete esta figura: es la base de todo el módulo.

4.1. Asociación

📘 Concepto: Una asociación indica que los objetos de una clase están conectados con objetos de otra de forma duradera: «un pedido conoce a su cliente». En Java se traduce en un atributo cuyo tipo es la otra clase (o una colección de ella).

Una asociación puede llevar varios adornos:

Asociación unidireccional, bidireccional con nombre y reflexiva, con resaltes numerados Figura 8. Elementos de una asociación: ① nombre con su sentido de lectura, ② multiplicidad, ③ rol y ④ navegabilidad.

Adorno Dónde se escribe Qué significa
Nombre (①) En el centro de la línea, con un triángulo ▶ opcional Verbo que describe la relación: «Profesor imparte ▶ Asignatura»
Multiplicidad (②) En cada extremo Cuántos objetos de esa clase se relacionan con uno de la otra
Rol (③) En cada extremo Papel que juega esa clase; en Java suele ser el nombre del atributo
Navegabilidad (④) Punta de flecha abierta en un extremo Quién conoce a quién: Pedido → Cliente = el pedido tiene una referencia al cliente

Sin flechas, la asociación se considera bidireccional (o no especificada): ambos extremos se conocen. Con una flecha es unidireccional. Una asociación de una clase consigo misma se llama reflexiva (un empleado y su jefe, una persona y sus amigos).

4.2. Multiplicidad

Seis multiplicidades con su lectura y su traducción a Java Figura 9. Multiplicidades más habituales y cómo se implementan.

La multiplicidad se lee siempre desde el otro extremo: en Pedido "0..*" —— "1" Cliente, el 1 junto a Cliente dice que cada pedido tiene exactamente un cliente, y el 0..* junto a Pedido dice que cada cliente tiene de cero a muchos pedidos.

Multiplicidad Se lee Implementación Java típica
1 exactamente uno Atributo obligatorio, se recibe en el constructor y se valida que no sea null
0..1 cero o uno Atributo que puede ser null (o un Optional al devolverlo)
* o 0..* cero o muchos List, Set o Map, inicialmente vacía
1..* uno o muchos Colección que se valida para que no quede vacía
n..m (p. ej. 2..4) entre n y m Colección con control de tamaño en los métodos que añaden/quitan
n (p. ej. 3) exactamente n Array de tamaño fijo o colección validada

4.3. Asociación en Java: tres ejemplos

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

// Asociaciones: unidireccional (Pedido -> Cliente), bidireccional (Profesor <-> Asignatura) y reflexiva (Empleado)
public class Asociacion {
    public static void main(String[] args) {
        // 1) Unidireccional: el pedido conoce a su cliente, el cliente no conoce sus pedidos
        Cliente ana = new Cliente("Ana");
        Pedido p1 = new Pedido(1, ana);
        Pedido p2 = new Pedido(2, ana);
        System.out.println("Pedido " + p1.getNumero() + " de " + p1.getCliente().getNombre());
        System.out.println("Pedido " + p2.getNumero() + " de " + p2.getCliente().getNombre());

        // 2) Bidireccional: ambos extremos se conocen y hay que mantenerlos coherentes
        Profesor marta = new Profesor("Marta");
        Asignatura prog = new Asignatura("Programación");
        Asignatura bd = new Asignatura("Bases de Datos");
        marta.impartir(prog);
        marta.impartir(bd);
        System.out.println(marta.getNombre() + " imparte " + marta.getAsignaturas().size() + " asignaturas");
        System.out.println(bd.getNombre() + " la imparte " + bd.getProfesor().getNombre());

        // 3) Reflexiva: un empleado puede tener un jefe que también es empleado
        Empleado jefa = new Empleado("Elena", null);
        Empleado e1 = new Empleado("Pablo", jefa);
        Empleado e2 = new Empleado("Sara", jefa);
        System.out.println(e1.getNombre() + " y " + e2.getNombre() + " dependen de " + e1.getJefe().getNombre());
        System.out.println(jefa.getNombre() + " tiene " + jefa.getSubordinados().size() + " subordinados");
    }
}

class Cliente {
    private final String nombre;
    Cliente(String nombre) { this.nombre = nombre; }
    String getNombre() { return nombre; }
}

class Pedido {
    private final int numero;
    private final Cliente cliente;          // multiplicidad 1: siempre hay un cliente

    Pedido(int numero, Cliente cliente) {
        if (cliente == null) {
            throw new IllegalArgumentException("Un pedido necesita un cliente");
        }
        this.numero = numero;
        this.cliente = cliente;
    }
    int getNumero() { return numero; }
    Cliente getCliente() { return cliente; }
}

class Profesor {
    private final String nombre;
    private final List<Asignatura> asignaturas = new ArrayList<>(); // 0..*

    Profesor(String nombre) { this.nombre = nombre; }

    void impartir(Asignatura a) {
        if (!asignaturas.contains(a)) {
            asignaturas.add(a);
            a.setProfesor(this);            // mantenemos el otro extremo
        }
    }
    String getNombre() { return nombre; }
    List<Asignatura> getAsignaturas() { return List.copyOf(asignaturas); }
}

class Asignatura {
    private final String nombre;
    private Profesor profesor;              // 0..1

    Asignatura(String nombre) { this.nombre = nombre; }

    void setProfesor(Profesor p) {
        if (this.profesor != p) {
            this.profesor = p;
            p.impartir(this);               // mantenemos el otro extremo
        }
    }
    String getNombre() { return nombre; }
    Profesor getProfesor() { return profesor; }
}

class Empleado {
    private final String nombre;
    private final Empleado jefe;                                  // rol "jefe" 0..1
    private final List<Empleado> subordinados = new ArrayList<>(); // rol "subordinados" 0..*

    Empleado(String nombre, Empleado jefe) {
        this.nombre = nombre;
        this.jefe = jefe;
        if (jefe != null) {
            jefe.subordinados.add(this);
        }
    }
    String getNombre() { return nombre; }
    Empleado getJefe() { return jefe; }
    List<Empleado> getSubordinados() { return List.copyOf(subordinados); }
}
Pedido 1 de Ana
Pedido 2 de Ana
Marta imparte 2 asignaturas
Bases de Datos la imparte Marta
Pablo y Sara dependen de Elena
Elena tiene 2 subordinados

⚠️ Importante: Una asociación bidireccional obliga a mantener los dos extremos coherentes: si Marta imparte Bases de Datos, Bases de Datos debe tener a Marta como profesora. Por eso impartir() y setProfesor() se llaman mutuamente con una comprobación que evita el bucle infinito. Siempre que puedas, prefiere asociaciones unidireccionales: son más sencillas y generan menos errores.

4.4. Agregación

📘 Concepto: La agregación es una asociación especial de tipo todo-parte «débil»: el todo tiene partes, pero las partes existen por sí mismas y pueden compartirse entre varios todos. Se dibuja con un rombo vacío (◇) en el extremo del todo.

Equipo agrega Jugador con rombo vacío Figura 10. Un equipo tiene jugadores, pero los jugadores existen antes y después del equipo y pueden estar en varios.

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

// Agregación: el equipo "tiene" jugadores, pero los jugadores existen por sí mismos
public class Agregacion {
    public static void main(String[] args) {
        // Los jugadores se crean FUERA del equipo
        Jugador j1 = new Jugador("Lucía", 9);
        Jugador j2 = new Jugador("Marcos", 4);

        Equipo titular = new Equipo("Titular");
        Equipo seleccion = new Equipo("Selección");
        titular.fichar(j1);
        titular.fichar(j2);
        seleccion.fichar(j1);              // un jugador puede estar en varios equipos

        System.out.println(titular);
        System.out.println(seleccion);

        titular = null;                    // el equipo desaparece...
        System.out.println("Sin equipo titular, " + j2 + " sigue existiendo");
    }
}

class Jugador {
    private final String nombre;
    private final int dorsal;

    Jugador(String nombre, int dorsal) {
        this.nombre = nombre;
        this.dorsal = dorsal;
    }

    @Override
    public String toString() {
        return nombre + " (" + dorsal + ")";
    }
}

class Equipo {
    private final String nombre;
    private final List<Jugador> jugadores = new ArrayList<>();   // 0..*

    Equipo(String nombre) { this.nombre = nombre; }

    void fichar(Jugador j) {               // recibe un objeto que ya existe
        jugadores.add(j);
    }

    @Override
    public String toString() {
        return "Equipo " + nombre + ": " + jugadores;
    }
}
Equipo Titular: [Lucía (9), Marcos (4)]
Equipo Selección: [Lucía (9)]
Sin equipo titular, Marcos (4) sigue existiendo

4.5. Composición

📘 Concepto: La composición es la relación todo-parte fuerte: la parte no tiene sentido sin el todo, pertenece a un único todo y nace y muere con él. Se dibuja con un rombo relleno (◆) en el extremo del todo, cuya multiplicidad es siempre 1 (o 0..1).

Casa compuesta de Habitacion y Pedido compuesto de LineaPedido Figura 11. Las habitaciones no existen sin su casa; las líneas de pedido no existen sin su pedido.

En Java, la composición se reconoce porque el todo crea las partes (normalmente en su constructor o en un método anadir...) y no las expone al exterior: nadie más guarda referencias a ellas.

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

// Composición: las habitaciones nacen y mueren con su casa
public class Composicion {
    public static void main(String[] args) {
        Casa casa = new Casa("C/ Mayor, 7");
        casa.anadirHabitacion("Salón", 25.0);       // la casa CREA sus habitaciones
        casa.anadirHabitacion("Cocina", 12.5);
        casa.anadirHabitacion("Dormitorio", 14.0);
        System.out.println(casa);
        System.out.printf("Superficie total: %.1f m²%n", casa.superficie());
        casa = null; // al desaparecer la casa, nadie más referencia a sus habitaciones:
                     // el recolector de basura las eliminará con ella
    }
}

class Casa {
    private final String direccion;
    private final List<Habitacion> habitaciones = new ArrayList<>();   // 1..*

    Casa(String direccion) {
        this.direccion = direccion;
    }

    void anadirHabitacion(String nombre, double metros) {
        habitaciones.add(new Habitacion(nombre, metros));   // se crea dentro
    }

    double superficie() {
        double total = 0;
        for (Habitacion h : habitaciones) {
            total += h.metros();
        }
        return total;
    }

    @Override
    public String toString() {
        return "Casa en " + direccion + " con " + habitaciones;
    }

    // La parte se declara dentro del todo y no se expone al exterior
    private record Habitacion(String nombre, double metros) {
        @Override
        public String toString() {
            return nombre + " " + metros + " m²";
        }
    }
}
Casa en C/ Mayor, 7 con [Salón 25.0 m², Cocina 12.5 m², Dormitorio 14.0 m²]
Superficie total: 51.5 m²

4.6. Agregación frente a composición

Universidad compuesta de facultades que agregan profesores Figura 12. En el mismo modelo pueden convivir ambas: la facultad no existe sin su universidad, el profesor sí existe sin su facultad.

Criterio Agregación ◇ Composición ◆
Fuerza del vínculo Débil Fuerte
¿La parte existe sin el todo? Sí No
¿La parte puede estar en varios todos? Sí (multiplicidad del todo 0..*) No (multiplicidad del todo 1 o 0..1)
¿Quién crea la parte? Normalmente alguien de fuera, que la pasa al todo El propio todo
Al destruir el todo… Las partes siguen vivas Las partes se destruyen con él
Ejemplos Equipo-Jugador, Playlist-Canción, Departamento-Profesor, Biblioteca-Socio Casa-Habitación, Pedido-LineaPedido, Libro-Capítulo, Tablero-Casilla

💡 Consejo: Para decidir, hazte la pregunta clave: «si destruyo el todo, ¿tiene sentido que la parte siga existiendo?». Si la respuesta es no, es composición. Si dudas entre agregación y asociación simple, usa asociación: incluso los autores de UML reconocen que la agregación aporta poco significado extra, y muchos equipos ni la usan.

4.7. Generalización (herencia)

📘 Concepto: La generalización expresa la relación «es un» entre una clase general (superclase) y otra más específica (subclase). Se dibuja con una línea continua terminada en un triángulo hueco que apunta a la superclase. En Java es extends.

Empleado abstracto con subclases Asalariado y PorHoras Figura 13. Generalización. Las subclases solo muestran lo que añaden o redefinen; lo heredado no se repite.

import java.util.List;

// Generalización: Asalariado y PorHoras SON empleados
public class Generalizacion {
    public static void main(String[] args) {
        List<Empleado> plantilla = List.of(
                new Asalariado("Irene", 2100.0),
                new PorHoras("Óscar", 80, 15.5),
                new Asalariado("Raúl", 1850.0));
        double total = 0;
        for (Empleado e : plantilla) {
            System.out.printf("%-6s %8.2f €%n", e.getNombre(), e.calcularSueldo());
            total += e.calcularSueldo();
        }
        System.out.printf("Total  %8.2f €%n", total);
    }
}

abstract class Empleado {
    private final String nombre;

    protected Empleado(String nombre) { this.nombre = nombre; }

    public String getNombre() { return nombre; }

    public abstract double calcularSueldo();
}

class Asalariado extends Empleado {
    private final double sueldoMensual;

    Asalariado(String nombre, double sueldoMensual) {
        super(nombre);
        this.sueldoMensual = sueldoMensual;
    }

    @Override
    public double calcularSueldo() { return sueldoMensual; }
}

class PorHoras extends Empleado {
    private final int horas;
    private final double precioHora;

    PorHoras(String nombre, int horas, double precioHora) {
        super(nombre);
        this.horas = horas;
        this.precioHora = precioHora;
    }

    @Override
    public double calcularSueldo() { return horas * precioHora; }
}
Irene   2100.00 €
Óscar   1240.00 €
Raúl    1850.00 €
Total   5190.00 €

⚠️ Importante: No uses herencia solo para reutilizar código. Comprueba que la frase «un X es un Y» es verdad siempre. «Un asalariado es un empleado» es correcto; «una pila es una lista» no lo es (una pila no debe permitir insertar en medio), así que ahí conviene una asociación o composición.

4.8. Realización (implementación de interfaces)

📘 Concepto: La realización indica que una clase cumple el contrato definido por una interfaz. Se dibuja con una línea discontinua terminada en triángulo hueco hacia la interfaz. En Java es implements.

Interfaz Imprimible realizada por Factura e Informe Figura 14. Factura e Informe realizan Imprimible. Una clase puede realizar varias interfaces.

// Realización: Factura e Informe REALIZAN (implementan) el contrato Imprimible
public class Realizacion {
    public static void main(String[] args) {
        Imprimible[] documentos = {
                new Factura("F-2026-001", 145.20),
                new Informe("Ventas de septiembre", 12)
        };
        for (Imprimible d : documentos) {
            System.out.println(d.imprimir());
            System.out.println("  páginas: " + d.paginas());
        }
    }
}

interface Imprimible {
    String imprimir();

    default int paginas() {          // método por defecto (Java 8)
        return 1;
    }
}

class Factura implements Imprimible {
    private final String numero;
    private final double total;

    Factura(String numero, double total) {
        this.numero = numero;
        this.total = total;
    }

    @Override
    public String imprimir() {
        return "FACTURA " + numero + " - Total: " + total + " €";
    }
}

class Informe implements Imprimible {
    private final String titulo;
    private final int numPaginas;

    Informe(String titulo, int numPaginas) {
        this.titulo = titulo;
        this.numPaginas = numPaginas;
    }

    @Override
    public String imprimir() {
        return "INFORME: " + titulo;
    }

    @Override
    public int paginas() {
        return numPaginas;
    }
}
FACTURA F-2026-001 - Total: 145.2 €
  páginas: 1
INFORME: Ventas de septiembre
  páginas: 12

Si una interfaz hereda de otra (interface Ordenable extends Comparable<...>), entre ellas se dibuja generalización (línea continua), no realización.

4.9. Dependencia

📘 Concepto: La dependencia es la relación más débil: una clase usa a otra de forma temporal, sin guardarla en un atributo. Aparece cuando la otra clase es el tipo de un parámetro, de una variable local, de un valor de retorno o cuando se crea dentro de un método. Se dibuja con una flecha discontinua de punta abierta, opcionalmente con un estereotipo («use», «create»…).

CalculadoraEnvio depende de Paquete y de Etiqueta Figura 15. Si Paquete o Etiqueta cambian, puede que CalculadoraEnvio tenga que cambiar; pero no los guarda.

import java.time.LocalDate;

// Dependencia: CalculadoraEnvio USA Paquete y crea Etiqueta, pero no los guarda como atributos
public class Dependencia {
    public static void main(String[] args) {
        CalculadoraEnvio calc = new CalculadoraEnvio();
        Paquete ligero = new Paquete("Valencia", 1.2);
        Paquete pesado = new Paquete("Bilbao", 7.8);
        System.out.printf("Envío a %s: %.2f €%n", ligero.destino(), calc.calcularCoste(ligero));
        System.out.printf("Envío a %s: %.2f €%n", pesado.destino(), calc.calcularCoste(pesado));
        System.out.println(calc.generarEtiqueta(pesado, LocalDate.of(2026, 10, 5)));
    }
}

record Paquete(String destino, double kilos) { }

record Etiqueta(String texto) { }

class CalculadoraEnvio {
    private static final double TARIFA_BASE = 4.50;
    private static final double PRECIO_KILO_EXTRA = 1.25;

    // Paquete aparece solo como parámetro: dependencia «use»
    double calcularCoste(Paquete p) {
        double extra = Math.max(0, p.kilos() - 2) * PRECIO_KILO_EXTRA;
        return TARIFA_BASE + extra;
    }

    // Etiqueta se crea y se devuelve, pero no se guarda: dependencia «create»
    Etiqueta generarEtiqueta(Paquete p, LocalDate fecha) {
        return new Etiqueta("[" + fecha + "] " + p.destino() + " - " + p.kilos() + " kg");
    }
}
Envío a Valencia: 4.50 €
Envío a Bilbao: 11.75 €
Etiqueta[texto=[2026-10-05] Bilbao - 7.8 kg]

💡 Consejo: No dibujes una dependencia hacia cada clase de la biblioteca estándar que uses (String, Math, LocalDate…): el diagrama se volvería ilegible. Dibuja solo las dependencias entre tus clases que aporten información.

4.10. ¿Qué relación elijo?

Escala de relaciones de menor a mayor acoplamiento Figura 16. Cuanto más fuerte es la relación, más depende una clase de la otra.

Sigue este razonamiento, en orden:

  1. ¿«A es un B» siempre? → Generalización (si B es una interfaz, realización).
  2. ¿A guarda una referencia a B en un atributo?
    • No, solo la usa en un método → Dependencia.
    • Sí. ¿Es una relación todo-parte?
      • No → Asociación.
      • Sí, y la parte no tiene sentido sin el todo → Composición.
      • Sí, pero la parte vive por su cuenta → Agregación (o asociación simple).

5. Clases asociación

A veces un dato no pertenece a ninguna de las dos clases, sino a la relación entre ellas. La nota de un alumno en una asignatura no es del alumno (tiene una nota por asignatura) ni de la asignatura (tiene una nota por alumno): es de ese alumno en esa asignatura.

📘 Concepto: Una clase asociación es una asociación que además es una clase, con sus propios atributos y operaciones. Se dibuja como una clase normal unida con una línea discontinua a la mitad de la asociación.

Alumno y Asignatura con la clase asociación Matricula Figura 17. Matricula guarda el curso y la nota de cada pareja alumno-asignatura.

Java no tiene clases asociación: se implementan como una clase normal con una referencia a cada extremo, y cada extremo guarda la lista de objetos de la clase asociación.

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

// Clase asociación: Matricula guarda los datos del vínculo Alumno-Asignatura (curso y nota)
public class ClaseAsociacion {
    public static void main(String[] args) {
        Alumno laura = new Alumno("Laura");
        Alumno diego = new Alumno("Diego");
        Asignatura prog = new Asignatura("Programación");
        Asignatura sist = new Asignatura("Sistemas");

        laura.matricular(prog, "2026/27").setNota(8.5);
        laura.matricular(sist, "2026/27").setNota(6.0);
        diego.matricular(prog, "2026/27").setNota(4.25);

        for (Alumno a : List.of(laura, diego)) {
            for (Matricula m : a.getMatriculas()) {
                System.out.println(m);
            }
        }
        System.out.println("Matriculados en " + prog.getNombre() + ": " + prog.getMatriculas().size());
    }
}

class Alumno {
    private final String nombre;
    private final List<Matricula> matriculas = new ArrayList<>();

    Alumno(String nombre) { this.nombre = nombre; }

    Matricula matricular(Asignatura asig, String curso) {
        Matricula m = new Matricula(this, asig, curso);
        matriculas.add(m);
        asig.registrar(m);
        return m;
    }
    String getNombre() { return nombre; }
    List<Matricula> getMatriculas() { return List.copyOf(matriculas); }
}

class Asignatura {
    private final String nombre;
    private final List<Matricula> matriculas = new ArrayList<>();

    Asignatura(String nombre) { this.nombre = nombre; }

    void registrar(Matricula m) { matriculas.add(m); }
    String getNombre() { return nombre; }
    List<Matricula> getMatriculas() { return List.copyOf(matriculas); }
}

class Matricula {                       // la clase asociación
    private final Alumno alumno;
    private final Asignatura asignatura;
    private final String curso;
    private double nota = -1;           // -1 = sin calificar

    Matricula(Alumno alumno, Asignatura asignatura, String curso) {
        this.alumno = alumno;
        this.asignatura = asignatura;
        this.curso = curso;
    }

    void setNota(double nota) {
        if (nota < 0 || nota > 10) {
            throw new IllegalArgumentException("Nota fuera de rango: " + nota);
        }
        this.nota = nota;
    }

    @Override
    public String toString() {
        return alumno.getNombre() + " - " + asignatura.getNombre() + " (" + curso + "): " + nota;
    }
}
Laura - Programación (2026/27): 8.5
Laura - Sistemas (2026/27): 6.0
Diego - Programación (2026/27): 4.25
Matriculados en Programación: 2

💡 Consejo: Las clases asociación aparecen casi siempre en relaciones muchos a muchos (* — *) con datos propios: Alumno-Asignatura (nota), Actor-Película (personaje), Cliente-Hotel (reserva con fechas). Si ya cursaste bases de datos, es exactamente la tabla intermedia de una relación N:M con atributos.


6. De UML a Java y de Java a UML

Pasar de un diagrama a código se llama ingeniería directa; obtener el diagrama a partir del código, ingeniería inversa. Ambas son preguntas de examen clásicas y las herramientas CASE las automatizan.

Clase Producto en UML y su código Java, con flechas de ingeniería directa e inversa Figura 18. La misma clase vista en UML y en Java.

6.1. Tabla de traducción

UML Java
Clase Nombre class Nombre { }
Clase en cursiva / {abstract} abstract class Nombre
«interface» interface Nombre
«enumeration» con literales enum Nombre { A, B, C }
- x: int = 0 private int x = 0;
Atributo subrayado static
{readOnly} final
+ op(a: int): String public String op(int a)
Operación en cursiva método abstract (o método de interfaz sin cuerpo)
Asociación navegable A → B, multiplicidad 1 o 0..1 En A: private B b;
Asociación A → B, multiplicidad * En A: private List<B> bs = new ArrayList<>();
Asociación bidireccional Atributo en ambas clases y métodos que mantienen la coherencia
Agregación A ◇— B Colección de B en A; los B se reciben desde fuera
Composición A ◆— B A crea los B (constructor o método anadir) y no los expone
Generalización B ——▷ A class B extends A
Realización B - - ▷ I class B implements I
Dependencia A - - > B B como parámetro, variable local, retorno o new B() dentro de un método
Clase asociación C entre A y B class C { A a; B b; ...datos... } y listas de C en A y/o B

6.2. Ingeniería inversa paso a paso

Dado un código, construye el diagrama siguiendo siempre el mismo orden:

  1. Una caja por cada clase, interfaz, enum o record, con su nombre (cursiva si es abstracta, estereotipo si procede).
  2. Atributos cuyo tipo es primitivo, String, fecha u otra clase que no esté en el diagrama: al compartimento de atributos, con su visibilidad, tipo y multiplicidad.
  3. Atributos cuyo tipo es otra clase del diagrama (o una colección de ella): no van al compartimento, se convierten en asociación, agregación o composición, con el nombre del atributo como rol y la multiplicidad en ese extremo.
  4. Operaciones públicas relevantes (puedes omitir getters/setters triviales si el enunciado lo permite).
  5. extends → generalización; implements → realización.
  6. Parámetros, variables locales y new de otras clases del diagrama → dependencias.

✏️ Ejercicio: Aplica estos pasos al código de la sección 4.4 (Equipo y Jugador) y comprueba que obtienes la figura 10.


7. Diagramas de objetos

Un diagrama de clases describe todos los objetos posibles; un diagrama de objetos es una fotografía de los objetos concretos que existen en un instante de la ejecución, con los valores de sus atributos.

Objeto ana de tipo Cliente enlazado a dos pedidos con sus líneas Figura 19. Diagrama de objetos: la cliente Ana con dos pedidos y sus líneas en un momento concreto.

Notación:

  • Cada objeto es un rectángulo con nombre : Clase subrayado. Si el nombre no importa, se escribe solo : Clase (objeto anónimo).
  • En lugar de declaraciones, los atributos muestran valores: numero = 7.
  • Las líneas son enlaces (links), instancias concretas de las asociaciones. No llevan multiplicidad (siempre conectan un objeto con otro).

Este programa crea exactamente los objetos de la figura 19 (la clase Pedido tiene una lista privada de líneas, como en la composición de la sección 4.5):

public static void main(String[] args) {
    Cliente ana = new Cliente("C1", "Ana");
    Pedido p7 = new Pedido(7, ana);
    Pedido p8 = new Pedido(8, ana);
    p7.agregarLinea("Teclado", 1, 35.0);
    p7.agregarLinea("Ratón", 2, 12.5);
    p8.agregarLinea("Monitor", 1, 189.0);

    for (Pedido p : List.of(p7, p8)) {
        System.out.println(p);
    }
}
p7:Pedido cliente=Ana lineas=2 total=60.0
p8:Pedido cliente=Ana lineas=1 total=189.0

💡 Consejo: Los diagramas de objetos son muy útiles para comprobar un diagrama de clases: si intentas dibujar un ejemplo real y no encaja (por ejemplo, un pedido que necesita dos clientes), es que la multiplicidad está mal. También son la versión «UML» de los dibujos de pila y montón que hiciste en el módulo 10.


8. Otros diagramas: casos de uso y secuencia

8.1. Diagrama de casos de uso

Describe qué puede hacer el sistema y quién lo usa, sin entrar en cómo está programado. Es el primer diagrama que se suele hacer al recoger requisitos con un cliente.

Diagrama de casos de uso de un sistema de préstamos de biblioteca Figura 20. Casos de uso del sistema de préstamos de una biblioteca.

Elemento Notación Significado
Actor Muñeco con nombre Un rol externo que usa el sistema (persona, otro sistema). No es una persona concreta: Marta puede ser socia y bibliotecaria
Caso de uso Elipse con un verbo + complemento Una funcionalidad completa que aporta valor al actor: «Prestar libro»
Límite del sistema Rectángulo con nombre Lo que queda dentro es lo que hay que construir
Asociación Línea actor-caso El actor participa en ese caso de uso
«include» Flecha discontinua del caso base al incluido El caso base siempre ejecuta el incluido: prestar un libro siempre comprueba el socio
«extend» Flecha discontinua del caso que extiende al base Comportamiento opcional que se añade en ciertas condiciones: al devolver, solo si hay retraso, se calcula una multa

8.2. Diagrama de secuencia

Muestra cómo colaboran los objetos para realizar una tarea, ordenando los mensajes de arriba abajo en el tiempo. Es el puente perfecto entre el diagrama de clases y el código de un método.

Diagrama de secuencia del préstamo de un libro con fragmento alt Figura 21. Secuencia de mensajes para prestar un libro.

Elemento Notación
Línea de vida Rectángulo objeto : Clase con una línea discontinua vertical
Activación Rectángulo estrecho sobre la línea de vida: el objeto está ejecutando un método
Mensaje síncrono Flecha continua con punta rellena: una llamada a método, numerada (1, 1.1…)
Respuesta Flecha discontinua: el valor de retorno
Creación Mensaje «create» que llega a la caja del nuevo objeto, dibujada a la altura en la que se crea
Fragmento combinado Rectángulo con una etiqueta: alt (if/else), opt (if sin else), loop (bucle)

El método que implementa ese diagrama (los comentarios llevan la numeración de los mensajes):

Prestamo prestar(String isbn, Socio socio) {
    Libro libro = buscar(isbn);                       // 1.1: buscar(isbn)
    if (!libro.estaDisponible()) {                    // 1.2: estaDisponible()
        throw new IllegalStateException("El libro " + isbn + " ya está prestado");
    }
    Prestamo p = new Prestamo(libro, socio);          // 1.3: «create»
    libro.marcarPrestado();                           // 1.4: marcarPrestado()
    return p;                                         // respuesta: el préstamo
}

Con un main que presta dos veces el mismo libro, la salida real es:

Préstamo creado: El Quijote -> Carmen
Error: El libro 978-84-1 ya está prestado

9. Herramientas para dibujar UML

Herramienta Tipo Ventajas Inconvenientes
draw.io (diagrams.net) Web y escritorio, gratuita Muy fácil, tiene biblioteca de formas UML, guarda en tu disco, Google Drive o GitHub Es solo dibujo: no comprueba que el diagrama sea UML correcto ni genera código
PlantUML Texto → imagen, libre El diagrama es un fichero de texto: se versiona con Git, se compara y se genera automáticamente Hay que aprender su sintaxis; el diseño automático no siempre queda bonito
StarUML Escritorio, de pago con evaluación Herramienta CASE completa, valida el modelo, genera código Java Licencia de pago para uso continuado
IntelliJ IDEA IDE El plugin Diagrams (incluido en el IDE) genera diagramas a partir del código real: clic derecho en un paquete → Diagrams | Show Diagram (Ctrl+Alt+Shift+U). Con el plugin PlantUML Integration del Marketplace editas y previsualizas ficheros .puml El plugin Diagrams es tradicionalmente una función de la versión Ultimate; comprueba si tu licencia la incluye
Papel y lápiz Analógica Lo que tendrás en el examen No se puede deshacer

💡 Consejo: Para estudiar, empieza con draw.io (busca la forma «Class» en la sección UML) y pasa a PlantUML cuando domines la notación: escribir el diagrama como texto es más rápido y, como el fichero vive junto al código en el repositorio, el diagrama no se queda desactualizado en una carpeta olvidada.

9.1. PlantUML: diagramas como texto

En PlantUML describes el diagrama en un fichero .puml y la herramienta lo dibuja. Este es el código completo de un pequeño modelo de nóminas:

@startuml
' Diagrama de clases escrito como texto con PlantUML
skinparam classAttributeIconSize 0
skinparam classBackgroundColor #E8EAF6
skinparam classBorderColor #283593
skinparam classHeaderBackgroundColor #C5CAE9
skinparam arrowColor #37474F
hide empty members

abstract class Empleado {
  - nombre: String
  + getNombre(): String
  + {abstract} calcularSueldo(): double
}

class Asalariado {
  - sueldoMensual: double
  + calcularSueldo(): double
}

class PorHoras {
  - horas: int
  - precioHora: double
  + calcularSueldo(): double
}

class Departamento {
  - nombre: String
  {static} + MAX_EMPLEADOS: int = 50
}

interface Imprimible {
  + imprimir(): String
}

class Nomina {
  - mes: int
  + imprimir(): String
}

enum TipoContrato {
  INDEFINIDO
  TEMPORAL
  PRACTICAS
}

Empleado <|-- Asalariado
Empleado <|-- PorHoras
Departamento "1" o-- "1..*" Empleado : empleados >
Imprimible <|.. Nomina
Nomina "0..*" --> "1" Empleado : empleado
Empleado --> "1" TipoContrato : contrato
Nomina ..> Departamento : <<use>>
@enduml

Y esta es la imagen que genera PlantUML 1.2026.8 a partir de ese texto (plantuml -tsvg nominas.puml):

Diagrama generado por PlantUML con Empleado, subclases, Departamento, Nomina e interfaz Figura 22. Diagrama generado automáticamente por PlantUML. Los círculos C, A, I y E indican clase, clase abstracta, interfaz y enumeración.

Chuleta de la sintaxis de relaciones de PlantUML:

PlantUML Relación
A --> B Asociación navegable de A a B
A -- B Asociación sin navegabilidad
A o-- B Agregación (rombo vacío en A)
A *-- B Composición (rombo relleno en A)
A <\|-- B Generalización: B hereda de A
A <\|.. B Realización: B implementa A
A ..> B Dependencia
A "1" --> "0..*" B : rol Multiplicidades y etiqueta
{static} / {abstract} Miembro subrayado / en cursiva
(A, B) .. C C es clase asociación entre A y B

💡 Consejo: Puedes probar PlantUML sin instalar nada en su servidor web oficial (plantuml.com) o, si usas Docker, con docker run --rm -v "%cd%:/data" -w /data plantuml/plantuml -tsvg fichero.puml. En IntelliJ, instala el plugin PlantUML Integration desde Settings | Plugins | Marketplace y abre cualquier fichero .puml para ver la vista previa en vivo.


10. Casos completos

En esta sección aplicas todo lo anterior a tres enunciados típicos. Para cada uno: lee el enunciado, intenta dibujar tú el diagrama antes de mirar la figura y después compara con la implementación.

10.1. Caso 1: biblioteca

Enunciado. Una biblioteca cataloga libros (ISBN, título) escritos por uno o varios autores (nombre, nacionalidad). De cada libro tiene uno o más ejemplares físicos, cada uno con un código y un estado (disponible, prestado, en reparación). Los socios (número, nombre) pueden tener a la vez como máximo 3 préstamos; cada préstamo es de un ejemplar a un socio, con fecha de inicio y de fin (15 días).

Diagrama de clases del caso de la biblioteca Figura 23. Diagrama de clases de la biblioteca.

Decisiones de diseño:

  • Libro ◆ Ejemplar: un ejemplar no existe sin la obra a la que pertenece y el libro los crea en su constructor.
  • Libro → Autor (1..*): un autor existe por sí mismo y puede tener muchos libros; como no necesitamos navegar del autor a sus libros, es unidireccional.
  • Biblioteca ◇ Socio: los socios existen como personas aunque la biblioteca cierre.
  • Socio — Prestamo 0..3: la multiplicidad máxima del enunciado aparece en el diagrama y se comprueba en el código con MAX_PRESTAMOS.
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

// Caso 1: biblioteca (figura 23)
public class CasoBiblioteca {
    public static void main(String[] args) {
        Biblioteca b = new Biblioteca("Biblioteca Municipal");
        Autor cervantes = new Autor("Miguel de Cervantes", "española");
        Autor gaiman = new Autor("Neil Gaiman", "británica");
        Autor pratchett = new Autor("Terry Pratchett", "británica");
        Libro quijote = b.catalogar("978-84-376-0494-7", "Don Quijote de la Mancha", List.of(cervantes), 2);
        Libro presagios = b.catalogar("978-84-450-7601-2", "Buenos presagios", List.of(gaiman, pratchett), 1);
        Socio s1 = b.darDeAlta("Marta López");
        Socio s2 = b.darDeAlta("Jorge Sanz");

        LocalDate hoy = LocalDate.of(2026, 10, 5);
        b.prestar(s1, quijote, hoy);
        b.prestar(s2, quijote, hoy);
        b.prestar(s1, presagios, hoy);
        System.out.println(quijote);
        System.out.println(presagios);
        for (Prestamo p : b.getPrestamos()) {
            System.out.println("  " + p);
        }
        try {
            b.prestar(s2, presagios, hoy);
        } catch (IllegalStateException e) {
            System.out.println("No se pudo prestar: " + e.getMessage());
        }
    }
}

record Autor(String nombre, String nacionalidad) { }

enum EstadoEjemplar { DISPONIBLE, PRESTADO, EN_REPARACION }

class Libro {
    private final String isbn;
    private final String titulo;
    private final List<Autor> autores;                           // 1..*  (asociación)
    private final List<Ejemplar> ejemplares = new ArrayList<>(); // 1..*  (composición)

    Libro(String isbn, String titulo, List<Autor> autores, int numEjemplares) {
        if (autores.isEmpty() || numEjemplares < 1) {
            throw new IllegalArgumentException("Un libro necesita autor y al menos un ejemplar");
        }
        this.isbn = isbn;
        this.titulo = titulo;
        this.autores = List.copyOf(autores);
        for (int i = 1; i <= numEjemplares; i++) {
            ejemplares.add(new Ejemplar(isbn + "#" + i, this));   // el libro crea sus ejemplares
        }
    }

    Ejemplar ejemplarDisponible() {
        for (Ejemplar e : ejemplares) {
            if (e.getEstado() == EstadoEjemplar.DISPONIBLE) return e;
        }
        return null;
    }

    String getTitulo() { return titulo; }

    @Override
    public String toString() {
        int libres = 0;
        for (Ejemplar e : ejemplares) {
            if (e.getEstado() == EstadoEjemplar.DISPONIBLE) libres++;
        }
        return titulo + " (" + autores.size() + " autor/es) - disponibles " + libres + "/" + ejemplares.size();
    }
}

class Ejemplar {
    private final String codigo;
    private final Libro libro;
    private EstadoEjemplar estado = EstadoEjemplar.DISPONIBLE;

    Ejemplar(String codigo, Libro libro) { this.codigo = codigo; this.libro = libro; }
    String getCodigo() { return codigo; }
    Libro getLibro() { return libro; }
    EstadoEjemplar getEstado() { return estado; }
    void setEstado(EstadoEjemplar estado) { this.estado = estado; }
}

class Socio {
    static final int MAX_PRESTAMOS = 3;
    private static int siguienteNumero = 1;
    private final int numero = siguienteNumero++;
    private final String nombre;
    private final List<Prestamo> prestamos = new ArrayList<>();   // 0..3

    Socio(String nombre) { this.nombre = nombre; }
    boolean puedePedirPrestado() { return prestamos.size() < MAX_PRESTAMOS; }
    void anotar(Prestamo p) { prestamos.add(p); }
    String getNombre() { return nombre; }
    int getNumero() { return numero; }
}

class Prestamo {
    private final Socio socio;
    private final Ejemplar ejemplar;
    private final LocalDate fechaInicio;
    private final LocalDate fechaFin;

    Prestamo(Socio socio, Ejemplar ejemplar, LocalDate inicio) {
        this.socio = socio;
        this.ejemplar = ejemplar;
        this.fechaInicio = inicio;
        this.fechaFin = inicio.plusDays(15);
    }

    @Override
    public String toString() {
        return ejemplar.getCodigo() + " (" + ejemplar.getLibro().getTitulo() + ") -> socio "
                + socio.getNumero() + " " + socio.getNombre() + ", del " + fechaInicio + " al " + fechaFin;
    }
}

class Biblioteca {
    private final String nombre;
    private final Map<String, Libro> catalogo = new LinkedHashMap<>(); // composición
    private final List<Socio> socios = new ArrayList<>();              // agregación
    private final List<Prestamo> prestamos = new ArrayList<>();        // composición

    Biblioteca(String nombre) { this.nombre = nombre; }

    Libro catalogar(String isbn, String titulo, List<Autor> autores, int ejemplares) {
        Libro l = new Libro(isbn, titulo, autores, ejemplares);
        catalogo.put(isbn, l);
        return l;
    }

    Socio darDeAlta(String nombre) {
        Socio s = new Socio(nombre);
        socios.add(s);
        return s;
    }

    Prestamo prestar(Socio socio, Libro libro, LocalDate hoy) {
        if (!socio.puedePedirPrestado()) {
            throw new IllegalStateException(socio.getNombre() + " ha alcanzado el máximo de préstamos");
        }
        Ejemplar e = libro.ejemplarDisponible();
        if (e == null) {
            throw new IllegalStateException("no quedan ejemplares de " + libro.getTitulo());
        }
        e.setEstado(EstadoEjemplar.PRESTADO);
        Prestamo p = new Prestamo(socio, e, hoy);
        socio.anotar(p);
        prestamos.add(p);
        return p;
    }

    List<Prestamo> getPrestamos() { return List.copyOf(prestamos); }
}
Don Quijote de la Mancha (1 autor/es) - disponibles 0/2
Buenos presagios (2 autor/es) - disponibles 0/1
  978-84-376-0494-7#1 (Don Quijote de la Mancha) -> socio 1 Marta López, del 2026-10-05 al 2026-10-20
  978-84-376-0494-7#2 (Don Quijote de la Mancha) -> socio 2 Jorge Sanz, del 2026-10-05 al 2026-10-20
  978-84-450-7601-2#1 (Buenos presagios) -> socio 1 Marta López, del 2026-10-05 al 2026-10-20
No se pudo prestar: no quedan ejemplares de Buenos presagios

💡 Consejo: En el módulo 21 retomarás este dominio para construir una aplicación de consola completa, con persistencia en CSV, menú y pruebas.

10.2. Caso 2: tienda online

Enunciado. Los clientes (email, nombre) tienen una o varias direcciones y realizan pedidos. Un pedido tiene un estado (carrito, pagado, enviado, entregado) y una o más líneas; cada línea indica la cantidad de un producto y congela su precio en el momento de la compra. Los productos (código, nombre, precio) pertenecen a una categoría. Los pedidos se pagan con tarjeta o con Bizum, y el coste de envío lo calcula una calculadora aparte (gratis desde 150 €; 3,95 € en la provincia 46 y 5,95 € en el resto).

Diagrama de clases del caso de la tienda online Figura 24. Diagrama de clases de la tienda online.

Decisiones de diseño:

  • Pedido ◆ LineaPedido y Cliente ◆ Direccion: partes que no tienen sentido solas.
  • LineaPedido → Producto: asociación unidireccional. El producto no debe conocer las líneas donde aparece.
  • MetodoPago es una interfaz realizada por TarjetaCredito y Bizum: mañana puedes añadir PayPal sin tocar Pedido (polimorfismo).
  • Pedido ⇢ CalculadoraEnvio: dependencia, porque la recibe como parámetro en total().

A continuación tienes el main y las clases que expresan las relaciones del diagrama. Categoria, Producto, Cliente, TarjetaCredito y el record Direccion siguen el patrón de siempre (atributos privados, constructor y getters; Producto se añade a su categoría en el constructor y Cliente crea sus direcciones y sus pedidos): escríbelas tú como práctica a partir del diagrama.

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

// Caso 2: tienda online (figura 24)
public class CasoTienda {
    public static void main(String[] args) {
        Categoria perifericos = new Categoria("Periféricos");
        Producto teclado = new Producto("P01", "Teclado mecánico", 79.90, perifericos);
        Producto raton = new Producto("P02", "Ratón inalámbrico", 24.50, perifericos);
        Cliente cli = new Cliente("nuria@correo.es", "Nuria Vidal");
        cli.anadirDireccion("C/ Colón, 12", "Valencia", "46004");

        Pedido ped = cli.realizarPedido();
        ped.agregar(teclado, 1);
        ped.agregar(raton, 2);
        System.out.println(ped.resumen());
        double total = ped.total(new CalculadoraEnvio());
        System.out.printf("Total con envío: %.2f €%n", total);
        ped.pagar(new Bizum("600111222"), total);
        System.out.println("Estado: " + ped.getEstado());
        System.out.println(perifericos.getNombre() + " tiene " + perifericos.getProductos().size() + " productos");
    }
}

interface MetodoPago {
    boolean pagar(double importe);
}

class Bizum implements MetodoPago {
    private final String telefono;
    Bizum(String telefono) { this.telefono = telefono; }
    @Override public boolean pagar(double importe) {
        System.out.printf("Bizum de %.2f € desde el %s%n", importe, telefono);
        return true;
    }
}

class LineaPedido {
    private final Producto producto;   // navegable: la línea conoce el producto
    private final int cantidad;
    private final double precioUnitario;

    LineaPedido(Producto producto, int cantidad) {
        this.producto = producto;
        this.cantidad = cantidad;
        this.precioUnitario = producto.getPrecio();   // se congela el precio del momento
    }
    double subtotal() { return cantidad * precioUnitario; }
    @Override public String toString() {
        return String.format("%d x %-18s %7.2f €", cantidad, producto.getNombre(), subtotal());
    }
}

class Pedido {
    private final int numero;
    private final Cliente cliente;
    private final List<LineaPedido> lineas = new ArrayList<>();   // composición 1..*
    private EstadoPedido estado = EstadoPedido.CARRITO;
    private MetodoPago metodoPago;                                // 0..1

    Pedido(int numero, Cliente cliente) { this.numero = numero; this.cliente = cliente; }
    void agregar(Producto p, int cantidad) { lineas.add(new LineaPedido(p, cantidad)); }
    EstadoPedido getEstado() { return estado; }

    double importeProductos() {
        double t = 0;
        for (LineaPedido l : lineas) t += l.subtotal();
        return t;
    }

    double total(CalculadoraEnvio calc) {                         // dependencia
        return importeProductos() + calc.coste(this, cliente.direccionPrincipal());
    }

    void pagar(MetodoPago metodo, double importe) {
        if (metodo.pagar(importe)) {
            this.metodoPago = metodo;
            this.estado = EstadoPedido.PAGADO;
        }
    }

    String resumen() {
        StringBuilder sb = new StringBuilder("Pedido " + numero + " de " + cliente.getNombre() + "\n");
        for (LineaPedido l : lineas) sb.append("  ").append(l).append('\n');
        return sb.append(String.format("  Productos: %.2f €", importeProductos())).toString();
    }
}

class CalculadoraEnvio {
    double coste(Pedido p, Direccion d) {
        if (p.importeProductos() >= 150) return 0.0;       // envío gratis
        return d.cp().startsWith("46") ? 3.95 : 5.95;      // provincia de la tienda o resto
    }
}
Pedido 1 de Nuria Vidal
  1 x Teclado mecánico     79.90 €
  2 x Ratón inalámbrico    49.00 €
  Productos: 128.90 €
Total con envío: 132.85 €
Bizum de 132.85 € desde el 600111222
Estado: PAGADO
Periféricos tiene 2 productos

10.3. Caso 3: centro educativo

Enunciado. Un centro educativo se organiza en departamentos y grupos. Todas las personas tienen DNI y nombre; pueden ser profesores (con especialidad) o alumnos (con número de expediente). Cada departamento tiene uno o más profesores y uno de ellos es el jefe. Cada grupo (máximo 30 alumnos) tiene un profesor tutor. Los alumnos se matriculan en asignaturas (código, nombre, horas) y obtienen una nota en cada una.

Diagrama de clases del caso del centro educativo Figura 25. Diagrama de clases del centro educativo.

Decisiones de diseño:

  • Persona es abstracta: no existen personas que no sean ni profesor ni alumno.
  • Centro ◆ Departamento y Centro ◆ Grupo: sin centro no hay departamentos ni grupos.
  • Departamento ◇ Profesor: el profesor puede cambiar de departamento. Además, otra asociación distinta con el rol jefe: entre dos clases puede haber varias asociaciones con significados diferentes.
  • Matricula es la clase asociación que guarda la nota de cada alumno en cada asignatura.

Se muestran el main y las clases con relaciones interesantes; Profesor (subclase de Persona con especialidad), el record Asignatura y Centro (que crea departamentos y grupos, igual que Biblioteca creaba libros) quedan como práctica.

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

// Caso 3: centro educativo (figura 25)
public class CasoCentro {
    public static void main(String[] args) {
        Centro ies = new Centro("IES La Costa");
        Departamento info = ies.crearDepartamento("Informática");
        Profesor pedro = new Profesor("11111111H", "Pedro Martín", "Programación");
        Profesor rosa = new Profesor("22222222J", "Rosa Navarro", "Sistemas");
        info.incorporar(pedro);
        info.incorporar(rosa);
        info.setJefe(rosa);

        Grupo dam1 = ies.crearGrupo("1DAM-A", pedro);
        Alumno a1 = new Alumno("33333333P", "Iván Gómez", "EXP-001");
        Alumno a2 = new Alumno("44444444A", "Lola Ortega", "EXP-002");
        dam1.inscribir(a1);
        dam1.inscribir(a2);
        Asignatura prog = new Asignatura("PRO", "Programación", 256);
        Asignatura sis = new Asignatura("SIS", "Sistemas Informáticos", 192);
        a1.matricular(prog).calificar(7.5);
        a1.matricular(sis).calificar(5.0);
        a2.matricular(prog).calificar(9.25);

        System.out.println(ies);
        System.out.println(info);
        System.out.println(dam1);
        for (Alumno a : dam1.getAlumnos()) {
            System.out.printf("%s -> media %.2f%n", a.presentarse(), a.notaMedia());
        }
    }
}

abstract class Persona {
    protected final String dni;
    protected final String nombre;
    protected Persona(String dni, String nombre) { this.dni = dni; this.nombre = nombre; }
    public abstract String presentarse();
    public String getNombre() { return nombre; }
}

class Alumno extends Persona {
    private final String expediente;
    private final List<Matricula> matriculas = new ArrayList<>();

    Alumno(String dni, String nombre, String expediente) {
        super(dni, nombre);
        this.expediente = expediente;
    }

    Matricula matricular(Asignatura a) {
        Matricula m = new Matricula(this, a);
        matriculas.add(m);
        return m;
    }

    double notaMedia() {
        double suma = 0;
        for (Matricula m : matriculas) suma += m.getNota();
        return matriculas.isEmpty() ? 0 : suma / matriculas.size();
    }

    @Override public String presentarse() { return "Alumno " + nombre + " (" + expediente + ")"; }
}

class Matricula {                                   // clase asociación Alumno-Asignatura
    private final Alumno alumno;
    private final Asignatura asignatura;
    private double nota;
    Matricula(Alumno alumno, Asignatura asignatura) { this.alumno = alumno; this.asignatura = asignatura; }
    Matricula calificar(double nota) { this.nota = nota; return this; }
    double getNota() { return nota; }
}

class Departamento {
    private final String nombre;
    private final List<Profesor> profesores = new ArrayList<>();  // agregación 1..*
    private Profesor jefe;                                        // asociación, rol "jefe"

    Departamento(String nombre) { this.nombre = nombre; }
    void incorporar(Profesor p) { profesores.add(p); }

    void setJefe(Profesor p) {
        if (!profesores.contains(p)) {
            throw new IllegalArgumentException("El jefe debe pertenecer al departamento");
        }
        jefe = p;
    }

    @Override public String toString() {
        return "Dpto. " + nombre + ": " + profesores.size() + " profesores, jefa/e " + jefe.getNombre();
    }
}

class Grupo {
    static final int MAX_ALUMNOS = 30;
    private final String codigo;
    private final Profesor tutor;                                 // 1
    private final List<Alumno> alumnos = new ArrayList<>();       // agregación 0..30

    Grupo(String codigo, Profesor tutor) { this.codigo = codigo; this.tutor = tutor; }

    void inscribir(Alumno a) {
        if (alumnos.size() == MAX_ALUMNOS) throw new IllegalStateException("Grupo completo");
        alumnos.add(a);
    }

    List<Alumno> getAlumnos() { return List.copyOf(alumnos); }

    @Override public String toString() {
        return "Grupo " + codigo + " (tutor: " + tutor.getNombre() + ", " + alumnos.size() + " alumnos)";
    }
}
IES La Costa: 1 departamento(s), 1 grupo(s)
Dpto. Informática: 2 profesores, jefa/e Rosa Navarro
Grupo 1DAM-A (tutor: Pedro Martín, 2 alumnos)
Alumno Iván Gómez (EXP-001) -> media 6.25
Alumno Lola Ortega (EXP-002) -> media 9.25

11. Errores comunes al hacer diagramas de clases

Error Por qué está mal Solución
Poner el atributo cliente: Cliente dentro de Pedido y además dibujar la asociación Es información duplicada Si Cliente está en el diagrama, solo la asociación con el rol cliente
Usar herencia para «reutilizar» (Coche extends Motor) Un coche no es un motor Composición: Coche ◆— Motor
Rombo en el extremo equivocado El rombo va en el todo, no en la parte Casa ◆— Habitacion, nunca al revés
Flecha de herencia apuntando a la subclase El triángulo apunta a la superclase Asalariado ——▷ Empleado
Olvidar la multiplicidad El diagrama no dice si un pedido tiene uno o varios productos Escribe la multiplicidad en los dos extremos
Leer la multiplicidad al revés Cliente 1 —— 0..* Pedido: el 0..* está junto a Pedido porque cada cliente tiene 0..* pedidos Lee siempre «cada objeto del otro extremo tiene…»
Atributos públicos + Rompe la encapsulación - para atributos, + para operaciones
Clases con nombre en plural (Productos) Una clase describe un objeto Producto; la colección es la multiplicidad *
Dibujar dependencias hacia String, List, Math… Ruido que oculta lo importante Solo dependencias entre tus clases
Clase «Dios» con 40 atributos Hace demasiado Repartir responsabilidades entre varias clases

12. Ejercicios

Ejercicio 1: De Java a UML

Dibuja (en papel, draw.io o PlantUML) el diagrama de clases de este código:

// Cuenta.java, CuentaAhorro.java y Titular.java (este último importa java.util.ArrayList y java.util.List)
public abstract class Cuenta {
    private static int contador;
    protected String numero;
    private double saldo;
    public Cuenta(String numero) { this.numero = numero; contador++; }
    public abstract double comision();
    public double getSaldo() { return saldo; }
    public static int getContador() { return contador; }
}

public class CuentaAhorro extends Cuenta {
    private double interes;
    public CuentaAhorro(String numero, double interes) { super(numero); this.interes = interes; }
    @Override public double comision() { return 0; }
}

public class Titular {
    private String nif;
    private List<Cuenta> cuentas = new ArrayList<>();
    public void abrir(Cuenta c) { cuentas.add(c); }
}
Ver solución
@startuml
skinparam classAttributeIconSize 0
abstract class Cuenta {
  {static} - contador: int
  # numero: String
  - saldo: double
  + Cuenta(numero: String)
  + {abstract} comision(): double
  + getSaldo(): double
  {static} + getContador(): int
}
class CuentaAhorro {
  - interes: double
  + CuentaAhorro(numero: String, interes: double)
  + comision(): double
}
class Titular {
  - nif: String
  + abrir(c: Cuenta)
}
Cuenta <|-- CuentaAhorro
Titular "0..*" o--> "0..*" Cuenta : cuentas
@enduml
  • Cuenta en cursiva (abstracta), comision() en cursiva y contador/getContador() subrayados.
  • El atributo cuentas no se escribe en Titular: se convierte en una asociación con rol cuentas y multiplicidad 0..*.
  • Las cuentas se reciben desde fuera (abrir(Cuenta c)) y una cuenta puede tener varios titulares, así que es agregación (también sería correcto una asociación simple). La flecha indica que el titular conoce sus cuentas, pero no al revés.

Ejercicio 2: Identifica la relación

Indica qué relación (asociación, agregación, composición, generalización, realización o dependencia) hay entre cada pareja y en qué extremo va el adorno:

  1. Factura y LineaFactura
  2. Gato y Animal
  3. Profesor y Asignatura
  4. Playlist y Cancion
  5. ArrayList y List
  6. Conversor con un método double aEuros(Moneda m)
  7. Persona y Persona (amistad)
  8. Edificio y Planta
Ver solución
Pareja Relación Explicación
Factura - LineaFactura Composición, ◆ en Factura La línea no existe sin su factura
Gato - Animal Generalización, triángulo en Animal Un gato es un animal
Profesor - Asignatura Asociación Se conocen, pero no hay todo-parte
Playlist - Cancion Agregación, ◇ en Playlist La canción existe sin la playlist y está en muchas
ArrayList - List Realización, triángulo en List ArrayList implements List
Conversor - Moneda Dependencia, flecha hacia Moneda Moneda solo es un parámetro
Persona - Persona Asociación reflexiva 0..* — 0..* Rol amigos
Edificio - Planta Composición, ◆ en Edificio Si se derriba el edificio, no quedan plantas

Ejercicio 3: De UML a Java

Implementa en Java este diagrama (descrito en PlantUML) y escribe un main que cree un coche con conductor y una moto, y muestre su descripción y la prima del seguro del coche (300 € + 0,1 € por cc):

@startuml
interface Asegurable {
  + calcularPrima(): double
}
abstract class Vehiculo {
  - matricula: String
  + {abstract} describir(): String
}
class Coche
class Moto {
  - tieneBaul: boolean
}
class Motor {
  - cilindrada: int
}
class Conductor {
  - nombre: String
  - permiso: String
}
Vehiculo <|-- Coche
Vehiculo <|-- Moto
Asegurable <|.. Coche
Coche "1" *-- "1" Motor : motor
Coche "0..*" --> "0..1" Conductor : conductor
@enduml
Ver solución
import java.util.ArrayList;
import java.util.List;

public class Ej3Vehiculos {
    public static void main(String[] args) {
        Conductor eva = new Conductor("Eva", "B");
        Coche c = new Coche("1234-LMN", 1600);
        Moto m = new Moto("5678-XYZ", true);
        c.asignarConductor(eva);
        List<Vehiculo> flota = new ArrayList<>(List.of(c, m));
        for (Vehiculo v : flota) {
            System.out.println(v.describir());
        }
        System.out.printf("Seguro del coche: %.2f €%n", c.calcularPrima());
    }
}

interface Asegurable {
    double calcularPrima();
}

abstract class Vehiculo {
    private final String matricula;

    protected Vehiculo(String matricula) { this.matricula = matricula; }

    public String getMatricula() { return matricula; }

    public abstract String describir();
}

class Motor {
    private final int cilindrada;
    Motor(int cilindrada) { this.cilindrada = cilindrada; }
    int getCilindrada() { return cilindrada; }
}

class Conductor {
    private final String nombre;
    private final String permiso;
    Conductor(String nombre, String permiso) { this.nombre = nombre; this.permiso = permiso; }
    String getNombre() { return nombre; }
}

class Coche extends Vehiculo implements Asegurable {
    private final Motor motor;          // composición 1: lo crea el coche
    private Conductor conductor;        // asociación 0..1

    Coche(String matricula, int cilindrada) {
        super(matricula);
        this.motor = new Motor(cilindrada);
    }

    void asignarConductor(Conductor c) { this.conductor = c; }

    @Override
    public double calcularPrima() {
        return 300 + motor.getCilindrada() * 0.1;
    }

    @Override
    public String describir() {
        String quien = (conductor == null) ? "sin conductor" : "conducido por " + conductor.getNombre();
        return "Coche " + getMatricula() + " (" + motor.getCilindrada() + " cc) " + quien;
    }
}

class Moto extends Vehiculo {
    private final boolean tieneBaul;

    Moto(String matricula, boolean tieneBaul) {
        super(matricula);
        this.tieneBaul = tieneBaul;
    }

    @Override
    public String describir() {
        return "Moto " + getMatricula() + (tieneBaul ? " con baúl" : " sin baúl");
    }
}
Coche 1234-LMN (1600 cc) conducido por Eva
Moto 5678-XYZ con baúl
Seguro del coche: 460.00 €

La composición se nota en que el coche crea su motor en el constructor (no lo recibe); la asociación 0..1 con el conductor se traduce en un atributo que puede ser null, y por eso describir() lo comprueba.

Ejercicio 4: Multiplicidades

Escribe la multiplicidad de cada extremo:

  1. Un país tiene una capital; una capital lo es de un país.
  2. Un alumno puede estar matriculado en ninguna o varias asignaturas; en una asignatura hay entre 5 y 30 alumnos.
  3. Un pedido tiene al menos una línea; cada línea es de un pedido.
  4. Una persona puede tener pasaporte o no; un pasaporte es de una persona.
  5. Una partida de parchís tiene entre 2 y 4 jugadores; un jugador juega muchas partidas.
Ver solución
Relación Extremo izquierdo Extremo derecho
País — Capital 1 (junto a País) 1 (junto a Capital)
Alumno — Asignatura 5..30 (junto a Alumno) 0..* (junto a Asignatura)
Pedido ◆— Linea 1 (junto a Pedido) 1..* (junto a Linea)
Persona — Pasaporte 1 (junto a Persona) 0..1 (junto a Pasaporte)
Partida — Jugador 0..* (junto a Partida) 2..4 (junto a Jugador)

Recuerda: lo que se dice de cada alumno («en ninguna o varias asignaturas») se escribe junto a Asignatura.

Ejercicio 5: Modela una clínica veterinaria

Una clínica tiene veterinarios. Los dueños (nombre, teléfono) tienen una o más mascotas, que pueden ser perros (con raza) o gatos (esterilizados o no). Un veterinario atiende citas: cada cita es de una mascota con un veterinario, en una fecha y hora y con un motivo. Dibuja el diagrama e impleméntalo con un main de prueba.

Ver solución
@startuml
skinparam classAttributeIconSize 0
class Clinica {
  - nombre: String
  + contratar(nombre: String): Veterinario
}
class Veterinario {
  - nombre: String
  + citar(m: Mascota, cuando: LocalDateTime, motivo: String): Cita
}
class Dueno {
  - nombre: String
  - telefono: String
}
abstract class Mascota {
  # nombre: String
  + {abstract} especie(): String
}
class Perro {
  - raza: String
}
class Gato {
  - esterilizado: boolean
}
class Cita {
  - fechaHora: LocalDateTime
  - motivo: String
}
Clinica "1" o-- "0..*" Veterinario : plantilla
Dueno "1" -- "1..*" Mascota : mascotas
Mascota <|-- Perro
Mascota <|-- Gato
(Veterinario, Mascota) .. Cita
@enduml

Clases principales (Dueno, Gato y Clinica siguen el mismo patrón; Clinica crea sus veterinarios en contratar(nombre)):

import java.time.LocalDateTime;

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

abstract class Mascota {
    protected final String nombre;
    private final Dueno dueno;                                  // 1
    protected Mascota(String nombre, Dueno dueno) {
        this.nombre = nombre;
        this.dueno = dueno;
        dueno.anadir(this);                                     // coherencia bidireccional
    }
    public abstract String especie();
    public String getNombre() { return nombre; }
}

class Perro extends Mascota {
    private final String raza;
    Perro(String nombre, Dueno dueno, String raza) { super(nombre, dueno); this.raza = raza; }
    @Override public String especie() { return "perro (" + raza + ")"; }
}

// Clase asociación entre Veterinario y Mascota
record Cita(Veterinario veterinario, Mascota mascota, LocalDateTime fechaHora, String motivo) {
    @Override public String toString() {
        return fechaHora + " " + mascota.getNombre() + ", " + mascota.especie() + ": " + motivo;
    }
}

class Veterinario {
    private final String nombre;
    private final List<Cita> citas = new ArrayList<>();
    Veterinario(String nombre) { this.nombre = nombre; }
    Cita citar(Mascota m, LocalDateTime cuando, String motivo) {
        Cita c = new Cita(this, m, cuando, motivo);
        citas.add(c);
        return c;
    }
    String getNombre() { return nombre; }
    List<Cita> getCitas() { return List.copyOf(citas); }
}
Clínica Patitas - agenda de Dra. Soler:
  2026-10-07T10:30 Toby, perro (Beagle): Vacuna anual
  2026-10-07T11:00 Misi, gato esterilizado: Revisión
Alberto tiene 2 mascotas

Cita es una clase asociación: guarda datos (fecha, motivo) que no son ni del veterinario ni de la mascota, sino de su encuentro.

Ejercicio 6: Encuentra los errores

Un compañero ha entregado este diagrama de una tienda. Encuentra al menos cinco errores y escribe la versión corregida:

@startuml
class Productos {
  + nombre: String
  + precio: double
  + categoria: Categoria
}
class Categoria {
  + nombre: String
}
class Pedido {
  - numero: int
}
class LineaPedido {
  - cantidad: int
}
Productos --|> Categoria
LineaPedido *-- Pedido
LineaPedido --> Productos
@enduml
Ver solución
Error Corrección
Nombre en plural (Productos) Producto: una clase describe un solo objeto
Atributos públicos (+) Privados (-)
El atributo categoria: Categoria aparece como texto Se quita del compartimento y se representa como asociación
Productos --\|> Categoria dice que un producto es una categoría Un producto pertenece a una categoría: agregación Categoria o-- Producto
LineaPedido *-- Pedido pone el rombo en la línea El rombo va en el todo: Pedido *-- LineaPedido
Faltan multiplicidades 1 / 1..* en la composición, 0..* / 1 en las demás
@startuml
skinparam classAttributeIconSize 0
class Producto {
  - nombre: String
  - precio: double
}
class Categoria {
  - nombre: String
}
class Pedido {
  - numero: int
}
class LineaPedido {
  - cantidad: int
}
Categoria "1" o-- "0..*" Producto : productos
Pedido "1" *-- "1..*" LineaPedido : lineas
LineaPedido "0..*" --> "1" Producto : producto
@enduml

Ejercicio 7 (propuesto): Hotel

Un hotel tiene habitaciones (número, tipo: individual, doble o suite, precio por noche). Los clientes hacen reservas de una o varias habitaciones para un rango de fechas; cada reserva tiene un estado (confirmada, cancelada, finalizada). Dibuja el diagrama de clases decidiendo qué es composición, qué es enumeración y dónde va el precio total, e impleméntalo con un método que calcule el importe de una reserva.

Ejercicio 8 (propuesto): Red social

Modela usuarios que siguen a otros usuarios (asociación reflexiva con roles seguidores y seguidos), publican mensajes (composición: si se borra el usuario se borran sus mensajes) y dan «me gusta» a mensajes de otros. Implementa seguir(Usuario u) manteniendo la coherencia en ambos sentidos.

Ejercicio 9 (propuesto): Ajedrez

Un tablero se compone de 64 casillas (multiplicidad exacta 64). Cada casilla puede contener 0..1 piezas. Las piezas son abstractas (color, movimientoValido(origen, destino)) y hay seis tipos concretos. Dibuja el diagrama y escribe el esqueleto Java con al menos dos piezas implementadas.

Ejercicio 10 (propuesto): Ingeniería inversa de tu propio código

Toma el programa de gestión de alumnos o de agenda que hiciste en el módulo 16 o 17 y dibuja su diagrama de clases con PlantUML. Comprueba con el plugin de IntelliJ (o con docker run ... plantuml/plantuml) que el texto genera la imagen sin errores.

Ejercicio 11 (propuesto): Diagrama de secuencia de un cajero

Dibuja el diagrama de secuencia de «sacar dinero»: el Cliente introduce la tarjeta en el Cajero, el cajero pide al Banco que valide el PIN, comprueba el saldo de la Cuenta y, en un fragmento alt, entrega el dinero o muestra «saldo insuficiente». Escribe después el método Java de Cajero que corresponde al diagrama, numerando los mensajes en comentarios.

Ejercicio 12 (propuesto): Casos de uso de una plataforma de cursos

Dibuja el diagrama de casos de uso de una plataforma en la que los alumnos se inscriben en cursos y entregan tareas, los profesores crean cursos y corrigen tareas, y el administrador gestiona usuarios. Usa al menos un «include» (por ejemplo, «Iniciar sesión») y un «extend» (por ejemplo, «Pagar curso premium»).


Resumen

Concepto Notación UML En Java
Clase Rectángulo con nombre, atributos y operaciones class
Visibilidad + - # ~ public private protected (paquete)
Atributo - nombre: Tipo [mult] = valor {props} private Tipo nombre = valor;
Operación + nombre(p: Tipo): Retorno public Retorno nombre(Tipo p)
Estático Subrayado static
Abstracto Cursiva o {abstract} abstract
Interfaz / enum / record «interface», «enumeration», «record» interface, enum, record
Asociación Línea continua (flecha abierta = navegable) Atributo de tipo de la otra clase
Multiplicidad 1, 0..1, *, 1..*, n..m Atributo simple, null permitido o colección
Agregación Rombo vacío ◇ en el todo Colección de objetos recibidos desde fuera
Composición Rombo relleno ◆ en el todo El todo crea y posee las partes
Generalización Línea continua + triángulo hueco extends
Realización Línea discontinua + triángulo hueco implements
Dependencia Línea discontinua + flecha abierta Parámetro, variable local, retorno o new
Clase asociación Clase unida con discontinua a la asociación Clase con referencias a ambos extremos
Diagrama de objetos nombre : Clase subrayado, valores Estado de la memoria en un instante
Casos de uso Actores, elipses, «include», «extend» Requisitos (no se traduce directamente)
Secuencia Líneas de vida, activaciones, mensajes, alt/loop Llamadas dentro de un método
PlantUML A <\|-- B, A *-- B, A o-- B, A --> B, A ..> B Diagramas como texto versionable