Apuntes DAM
Volver al inicio

Del diagrama de clases al código: una flota de alquiler

Ejercicio de JavaMedioUnos 70 minutos

Traduce a Java un diagrama de clases UML: una interfaz realizada, una clase abstracta con su método abstracto, dos subclases que redefinen métodos, una composición 1 a muchos y una asociación con multiplicidad; y úsalo para gestionar los alquileres y las revisiones de una flota de vehículos.

  • Leer un diagrama de clases
  • Realización de interfaces
  • Clases y métodos abstractos
  • Herencia y redefinición
  • Composición y asociación
  • Visibilidad (+ y -)

Enunciado

El diagrama de clases es el plano del programa: cada caja es una clase con sus atributos y sus métodos (- privado, + público, en cursiva o con {abstract} los abstractos) y cada flecha una relación. Pasar del diagrama al código es casi mecánico, y es lo que piden muchos exámenes de Entornos de Desarrollo y de Programación.

El diagrama de este ejercicio (en PlantUML, también en el código de partida; puedes pegarlo en el editor UML de la web para verlo dibujado) describe una empresa de alquiler: Revisable <|.. Vehiculo es una realización (implements), Vehiculo <|-- Coche una herencia (extends), Flota *-- Vehiculo una composición (la flota contiene sus vehículos, en una lista) y Alquiler --> Vehiculo una asociación navegable (el alquiler conoce su vehículo).

Qué tiene que hacer el programa

  1. Escribe la interfaz y las cinco clases del diagrama con los atributos, la visibilidad y los métodos que indica, y estas reglas: precioDia de un coche es 30 € + 4 € por plaza y el de una furgoneta 50 € + 0,02 € por kg de carga; precio(dias) es precioDia() × dias, salvo en la furgoneta, que desde 7 días tiene un 10 % de descuento sobre todo; un vehículo necesitaRevision si ha hecho 15.000 km o más desde la última revisión, y revisar la pone en sus km actuales.
  2. Alquiler.importe() es el precio de los días más 0,15 € por cada km por encima de los 200 incluidos por día. Flota.alta devuelve false si ya hay un vehículo con esa matrícula. toString de un coche: Coche de P plazas; de una furgoneta: Furgoneta de C kg.
  3. Órdenes: coche M P y furgoneta M C dan de alta (Alta: <toString> M o Ya existe un vehículo con matrícula M); alquilar M D K escribe Alquiler de M (<toString>): D días, K km → X (1 día) y suma los km al vehículo; revisar M escribe M revisado a los K km; las dos con No existe el vehículo M si no está.
  4. pendientes escribe Pendientes de revisión: y las matrículas separadas por , en el orden de alta, o ninguno. flota escribe Flota (N vehículos): (1 vehículo) y, por cada uno, M · <toString> · K km · X/día, más · revisión pendiente si la necesita.
  5. Una orden desconocida, con un número de datos distinto o con números no válidos (no enteros, plazas, carga o días de 0 o menos, km negativos): Orden no válida: línea. Al final: Facturado: X en N alquileres (1 alquiler). Los importes, con dos decimales, coma y €.

Entrada

Una orden por línea: coche M P, furgoneta M C, alquilar M D K, revisar M, pendientes o flota.

Ejemplos de ejecución

Tu programa debe escribir exactamente esta salida para estas entradas. Las pruebas del editor incluyen estos ejemplos y otros casos ocultos.

Una flota con dos vehículos

Entrada

coche 1234ABC 5
furgoneta 5678DEF 1200
coche 1234ABC 7
alquilar 1234ABC 3 750
alquilar 5678DEF 7 16000
pendientes
flota
revisar 5678DEF
pendientes

Salida por consola

Alta: Coche de 5 plazas 1234ABC
Alta: Furgoneta de 1200 kg 5678DEF
Ya existe un vehículo con matrícula 1234ABC
Alquiler de 1234ABC (Coche de 5 plazas): 3 días, 750 km → 172,50 €
Alquiler de 5678DEF (Furgoneta de 1200 kg): 7 días, 16000 km → 2656,20 €
Pendientes de revisión: 5678DEF
Flota (2 vehículos):
  1234ABC · Coche de 5 plazas · 750 km · 50,00 €/día
  5678DEF · Furgoneta de 1200 kg · 16000 km · 74,00 €/día · revisión pendiente
5678DEF revisado a los 16000 km
Pendientes de revisión: ninguno
Facturado: 2828,70 € en 2 alquileres

Órdenes erróneas

Entrada

coche 1111AAA cinco
furgoneta 2222BBB
alquilar 9999ZZZ 2 100
coche 3333CCC 4
alquilar 3333CCC 0 10
alquilar 3333CCC 1 150
volar 3333CCC
revisar 0000XXX
flota

Salida por consola

Orden no válida: coche 1111AAA cinco
Orden no válida: furgoneta 2222BBB
No existe el vehículo 9999ZZZ
Alta: Coche de 4 plazas 3333CCC
Orden no válida: alquilar 3333CCC 0 10
Alquiler de 3333CCC (Coche de 4 plazas): 1 día, 150 km → 46,00 €
Orden no válida: volar 3333CCC
No existe el vehículo 0000XXX
Flota (1 vehículo):
  3333CCC · Coche de 4 plazas · 150 km · 46,00 €/día
Facturado: 46,00 € en 1 alquiler

Guía paso a paso

Intenta resolverlo por tu cuenta y abre un paso solo cuando te atasques: cada uno te acerca a la solución sin dártela entera.

1. De la caja a la clase

Cada atributo con - es un private; cada método con + un public. {abstract} en un método obliga a declarar la clase abstract y a que cada subclase lo implemente.

java
abstract class Vehiculo implements Revisable {
    private final String matricula;
    private int km;
    private int kmUltimaRevision;

    public abstract double precioDia();
}
2. Las flechas

Triángulo hueco con línea discontinua: implements; con línea continua: extends. Rombo relleno con 0..*: un atributo List<Vehiculo> creado y gestionado por la flota. Flecha simple hacia Vehiculo con multiplicidad 1: un atributo Vehiculo en Alquiler.

3. Redefinir con super

La furgoneta redefine precio reutilizando el de su madre: super.precio(dias) y aplica el descuento encima.

4. El main

Un switch con flechas sobre la orden y un try que capture IllegalArgumentException (que incluye NumberFormatException) para todas las órdenes mal escritas.

Resuélvelo aquí

El editor trae el esqueleto del programa. Pulsa «Ejecutar» para comprobarlo con los ejemplos y con 2 casos ocultos que buscan los errores típicos.

☕JavaDel diagrama de clases al código: una flota de alquilerMedio

Ejemplo

Entrada (lo que se escribe por teclado)
coche 1234ABC 5
furgoneta 5678DEF 1200
coche 1234ABC 7
alquilar 1234ABC 3 750
alquilar 5678DEF 7 16000
pendientes
flota
revisar 5678DEF
pendientes
Salida esperada
Alta: Coche de 5 plazas 1234ABC
Alta: Furgoneta de 1200 kg 5678DEF
Ya existe un vehículo con matrícula 1234ABC
Alquiler de 1234ABC (Coche de 5 plazas): 3 días, 750 km → 172,50 €
Alquiler de 5678DEF (Furgoneta de 1200 kg): 7 días, 16000 km → 2656,20 €
Pendientes de revisión: 5678DEF
Flota (2 vehículos):
  1234ABC · Coche de 5 plazas · 750 km · 50,00 €/día
  5678DEF · Furgoneta de 1200 kg · 16000 km · 74,00 €/día · revisión pendiente
5678DEF revisado a los 16000 km
Pendientes de revisión: ninguno
Facturado: 2828,70 € en 2 alquileres
⏳
Test oculto #3
⏳
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada

Solución explicada

Ver la solución completa
java
1import java.util.ArrayList;
2import java.util.List;
3import java.util.Locale;
4import java.util.Scanner;
5
6/*
7@startuml
8interface Revisable {
9  +necesitaRevision(): boolean
10  +revisar(): void
11}
12abstract class Vehiculo {
13  -matricula: String
14  -km: int
15  -kmUltimaRevision: int
16  +Vehiculo(matricula: String)
17  +getMatricula(): String
18  +getKm(): int
19  +recorrer(km: int): void
20  +{abstract} precioDia(): double
21  +precio(dias: int): double
22  +necesitaRevision(): boolean
23  +revisar(): void
24}
25class Coche {
26  -plazas: int
27  +Coche(matricula: String, plazas: int)
28  +precioDia(): double
29  +toString(): String
30}
31class Furgoneta {
32  -cargaKg: int
33  +Furgoneta(matricula: String, cargaKg: int)
34  +precioDia(): double
35  +precio(dias: int): double
36  +toString(): String
37}
38class Flota {
39  -vehiculos: List<Vehiculo>
40  +alta(v: Vehiculo): boolean
41  +buscar(matricula: String): Vehiculo
42  +getVehiculos(): List<Vehiculo>
43  +pendientesDeRevision(): List<Vehiculo>
44}
45class Alquiler {
46  -dias: int
47  -km: int
48  +Alquiler(vehiculo: Vehiculo, dias: int, km: int)
49  +importe(): double
50}
51Revisable <|.. Vehiculo
52Vehiculo <|-- Coche
53Vehiculo <|-- Furgoneta
54Flota "1" *-- "0..*" Vehiculo
55Alquiler "0..*" --> "1" Vehiculo : vehiculo
56@enduml
57*/
58
59interface Revisable {
60    boolean necesitaRevision();
61    void revisar();
62}
63
64abstract class Vehiculo implements Revisable {
65    static final int KM_ENTRE_REVISIONES = 15000;
66    private final String matricula;
67    private int km;
68    private int kmUltimaRevision;
69
70    Vehiculo(String matricula) {
71        this.matricula = matricula;
72    }
73
74    public String getMatricula() { return matricula; }
75    public int getKm() { return km; }
76
77    public void recorrer(int km) {
78        this.km += km;
79    }
80
81    public abstract double precioDia();
82
83    public double precio(int dias) {
84        return precioDia() * dias;
85    }
86
87    @Override
88    public boolean necesitaRevision() {
89        return km - kmUltimaRevision >= KM_ENTRE_REVISIONES;
90    }
91
92    @Override
93    public void revisar() {
94        kmUltimaRevision = km;
95    }
96}
97
98class Coche extends Vehiculo {
99    private final int plazas;
100
101    Coche(String matricula, int plazas) {
102        super(matricula);
103        this.plazas = plazas;
104    }
105
106    @Override
107    public double precioDia() {
108        return 30 + 4 * plazas;
109    }
110
111    @Override
112    public String toString() {
113        return "Coche de " + plazas + " plazas";
114    }
115}
116
117class Furgoneta extends Vehiculo {
118    private final int cargaKg;
119
120    Furgoneta(String matricula, int cargaKg) {
121        super(matricula);
122        this.cargaKg = cargaKg;
123    }
124
125    @Override
126    public double precioDia() {
127        return 50 + 0.02 * cargaKg;
128    }
129
130    /** Desde 7 días, un 10 % de descuento sobre todo el alquiler. */
131    @Override
132    public double precio(int dias) {
133        double p = super.precio(dias);
134        return dias >= 7 ? p * 0.9 : p;
135    }
136
137    @Override
138    public String toString() {
139        return "Furgoneta de " + cargaKg + " kg";
140    }
141}
142
143class Flota {
144    private final List<Vehiculo> vehiculos = new ArrayList<>();
145
146    public boolean alta(Vehiculo v) {
147        if (buscar(v.getMatricula()) != null) return false;
148        vehiculos.add(v);
149        return true;
150    }
151
152    public Vehiculo buscar(String matricula) {
153        for (Vehiculo v : vehiculos) if (v.getMatricula().equals(matricula)) return v;
154        return null;
155    }
156
157    public List<Vehiculo> getVehiculos() { return vehiculos; }
158
159    public List<Vehiculo> pendientesDeRevision() {
160        List<Vehiculo> r = new ArrayList<>();
161        for (Vehiculo v : vehiculos) if (v.necesitaRevision()) r.add(v);
162        return r;
163    }
164}
165
166class Alquiler {
167    static final int KM_INCLUIDOS_DIA = 200;
168    static final double PRECIO_KM_EXTRA = 0.15;
169    private final Vehiculo vehiculo;
170    private final int dias;
171    private final int km;
172
173    Alquiler(Vehiculo vehiculo, int dias, int km) {
174        this.vehiculo = vehiculo;
175        this.dias = dias;
176        this.km = km;
177    }
178
179    public double importe() {
180        int extra = Math.max(0, km - KM_INCLUIDOS_DIA * dias);
181        return vehiculo.precio(dias) + extra * PRECIO_KM_EXTRA;
182    }
183}
184
185public class Main {
186    static String euros(double x) {
187        return String.format(Locale.ROOT, "%.2f", x).replace('.', ',') + " €";
188    }
189
190    public static void main(String[] args) {
191        Scanner sc = new Scanner(System.in);
192        Flota flota = new Flota();
193        double facturado = 0;
194        int alquileres = 0;
195        while (sc.hasNextLine()) {
196            String linea = sc.nextLine().trim();
197            if (linea.isEmpty()) continue;
198            String[] p = linea.split("\\s+");
199            try {
200                switch (p[0]) {
201                    case "coche", "furgoneta" -> {
202                        if (p.length != 3) throw new IllegalArgumentException();
203                        int n = Integer.parseInt(p[2]);
204                        if (n <= 0) throw new IllegalArgumentException();
205                        Vehiculo v = p[0].equals("coche") ? new Coche(p[1], n) : new Furgoneta(p[1], n);
206                        System.out.println(flota.alta(v) ? "Alta: " + v + " " + p[1] : "Ya existe un vehículo con matrícula " + p[1]);
207                    }
208                    case "alquilar" -> {
209                        if (p.length != 4) throw new IllegalArgumentException();
210                        int dias = Integer.parseInt(p[2]), km = Integer.parseInt(p[3]);
211                        if (dias <= 0 || km < 0) throw new IllegalArgumentException();
212                        Vehiculo v = flota.buscar(p[1]);
213                        if (v == null) {
214                            System.out.println("No existe el vehículo " + p[1]);
215                            break;
216                        }
217                        double importe = new Alquiler(v, dias, km).importe();
218                        v.recorrer(km);
219                        facturado += importe;
220                        alquileres++;
221                        System.out.println("Alquiler de " + p[1] + " (" + v + "): " + dias + (dias == 1 ? " día, " : " días, ") + km + " km → " + euros(importe));
222                    }
223                    case "revisar" -> {
224                        if (p.length != 2) throw new IllegalArgumentException();
225                        Vehiculo v = flota.buscar(p[1]);
226                        if (v == null) {
227                            System.out.println("No existe el vehículo " + p[1]);
228                            break;
229                        }
230                        v.revisar();
231                        System.out.println(p[1] + " revisado a los " + v.getKm() + " km");
232                    }
233                    case "pendientes" -> {
234                        List<String> m = new ArrayList<>();
235                        for (Vehiculo v : flota.pendientesDeRevision()) m.add(v.getMatricula());
236                        System.out.println("Pendientes de revisión: " + (m.isEmpty() ? "ninguno" : String.join(", ", m)));
237                    }
238                    case "flota" -> {
239                        int n = flota.getVehiculos().size();
240                        System.out.println("Flota (" + n + (n == 1 ? " vehículo):" : " vehículos):"));
241                        for (Vehiculo v : flota.getVehiculos()) {
242                            System.out.println("  " + v.getMatricula() + " · " + v + " · " + v.getKm() + " km · " + euros(v.precioDia()) + "/día"
243                                    + (v.necesitaRevision() ? " · revisión pendiente" : ""));
244                        }
245                    }
246                    default -> throw new IllegalArgumentException();
247                }
248            } catch (IllegalArgumentException e) {      // NumberFormatException también lo es
249                System.out.println("Orden no válida: " + linea);
250            }
251        }
252        System.out.println("Facturado: " + euros(facturado) + " en " + alquileres + (alquileres == 1 ? " alquiler" : " alquileres"));
253    }
254}

La interfaz Revisable la realiza Vehiculo, y por herencia también la cumplen Coche y Furgoneta: la flota pregunta a cada vehículo si necesita revisión sin saber de qué tipo es (polimorfismo).

La composición (rombo relleno) significa que los vehículos pertenecen a la flota: los crea, los guarda y los busca ella. La asociación de Alquiler es más débil: el alquiler solo apunta a un vehículo que existe por su cuenta.

El precio de la furgoneta muestra la redefinición: Alquiler.importe() llama a vehiculo.precio(dias) y Java ejecuta la versión de la clase real del objeto, con descuento o sin él.

Los atributos privados con métodos públicos que los usan (recorrer, revisar) son la encapsulación del diagrama: nadie puede bajar los km de un vehículo desde fuera.

Para ir más allá

  • Añade una clase Moto al diagrama y al código, sin tocar Flota ni Alquiler.
  • Dibuja el diagrama de secuencia de la orden alquilar.
  • Guarda el historial de alquileres de cada vehículo (una nueva asociación 1 -- 0..*) y muestra el más caro.

Dónde se explica