La lección anterior terminó con un cabo suelto incómodo: new Prestamo() produce un préstamo sin libro, sin empleado y con cero días transcurridos, y para dejarlo utilizable hay que rellenar sus campos uno a uno desde fuera, confiando en no olvidar ninguno. Ese objeto a medio hacer es una bomba de relojería: entre el new y la última asignación existe un intervalo en el que el objeto es inválido, y si alguien lo usa ahí, obtiene un NullPointerException o, peor, un resultado silenciosamente incorrecto. Los constructores resuelven exactamente ese problema. Un constructor es el bloque de código que se ejecuta al crear un objeto, y su misión es garantizar que ningún objeto nazca en un estado inválido. En esta lección aprenderás a escribirlos, a sobrecargarlos, a encadenarlos, a controlar el orden exacto en que se inicializa todo y a validar los datos de entrada. Al terminar, Libro, Empleado y Prestamo serán objetos que nacen completos y coherentes.
Contenido
- Qué es un constructor
- Constructor frente a método: siete diferencias
- El constructor por defecto y cuándo desaparece
- Sobrecarga de constructores
- Encadenamiento con
this(...)y sus reglas - Orden de inicialización, paso a paso
- Campos
finaly su inicialización obligatoria - El constructor telescópico y el patrón Builder
- Validar en el constructor
- El constructor de copia
- Bloques
staticde inicialización - BiblioTech:
Libro,EmpleadoyPrestamocon constructores completos - Errores Comunes y Consejos
- Ejercicios
- Qué es un constructor
Un constructor es un bloque con el mismo nombre que la clase y sin tipo de retorno, que se ejecuta automáticamente al crear un objeto con new:
public class Libro {
public String titulo;
public String autor;
public String isbn;
/** Constructor: se ejecuta al hacer new Libro(...). */
public Libro(String titulo, String autor, String isbn) {
this.titulo = titulo;
this.autor = autor;
this.isbn = isbn;
}
}Y desde ese momento, crear un libro es una sola línea que no puede quedarse a medias:
Compara con lo que hacías hasta ahora:
// ANTES: cuatro lineas, y nada te obliga a escribir las cuatro
Libro javaEfectivo = new Libro();
javaEfectivo.titulo = "Java Efectivo";
javaEfectivo.autor = "Joshua Bloch";
javaEfectivo.isbn = "978-0000000001";La diferencia no es de brevedad, es de garantías. Con el constructor, el compilador exige los tres datos: es literalmente imposible crear un Libro sin título. Ese principio tiene nombre y te acompañará toda la carrera: hacer que los estados inválidos sean irrepresentables.
Fíjate también en el uso de this (lección 03-03): los parámetros se llaman igual que los campos —convención universal en Java— y this.titulo distingue el campo del parámetro que lo ensombrece.
- Constructor frente a método: siete diferencias
| Aspecto | Constructor | Método |
|---|---|---|
| Nombre | Idéntico al de la clase, con mayúscula inicial | Cualquiera, en lowerCamelCase |
| Tipo de retorno | Ninguno (ni siquiera void) |
Obligatorio (void o un tipo) |
| Cuándo se ejecuta | Solo al crear el objeto con new |
Cuando se le llama explícitamente |
| Se hereda | No (03-05) | Sí |
Puede ser static |
No | Sí |
return |
Solo return; para salir antes |
return valor; según el tipo |
| Si no lo escribes | Java te da uno por defecto | No aparece nada |
El error más común de todos es este:
public class Libro {
public void Libro(String titulo) { // ¡NO es un constructor!
this.titulo = titulo;
}
}Poner void convierte el constructor en un método normal que casualmente se llama Libro. Compila sin protestar, new Libro("...") deja de compilar y el programador se vuelve loco buscando el motivo. Regla: un constructor no lleva tipo de retorno, nunca.
- El constructor por defecto y cuándo desaparece
Si una clase no declara ningún constructor, Java añade uno automáticamente: el constructor por defecto, sin parámetros y con cuerpo vacío.
Pero en cuanto declaras cualquier constructor, el regalo desaparece:
public class Libro {
public Libro(String titulo) { this.titulo = titulo; }
}
Libro l = new Libro(); // ERROR de compilacionerror: constructor Libro in class Libro cannot be applied to given types; required: String found: no arguments
Es un comportamiento deliberado: si has decidido que un libro necesita título, Java no va a permitir a nadie saltárselo. Si además quieres permitir la creación sin argumentos, debes declararla explícitamente:
- Sobrecarga de constructores
Los constructores se sobrecargan igual que los métodos: mismo nombre, distinta lista de parámetros. Sirve para ofrecer varias formas de crear el mismo objeto.
public class Libro {
public String titulo;
public String autor;
public String isbn;
public int anioPublicacion;
public boolean disponible;
/** Constructor completo. */
public Libro(String titulo, String autor, String isbn,
int anioPublicacion, boolean disponible) {
this.titulo = titulo;
this.autor = autor;
this.isbn = isbn;
this.anioPublicacion = anioPublicacion;
this.disponible = disponible;
}
/** Constructor habitual: un libro nuevo entra disponible. */
public Libro(String titulo, String autor, String isbn, int anioPublicacion) {
this(titulo, autor, isbn, anioPublicacion, true);
}
/** Constructor minimo: para altas rapidas del catalogo. */
public Libro(String titulo, String isbn) {
this(titulo, "Desconocido", isbn, 0, true);
}
}Tres formas de crear un libro, todas coherentes:
Libro a = new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018, true);
Libro b = new Libro("Patrones de Diseño", "Erich Gamma", "978-0000000002", 1994);
Libro c = new Libro("Refactorización", "978-0000000003");
- Encadenamiento con
this(...) y sus reglas
this(...) y sus reglasEn el ejemplo anterior aparece this(...): la llamada de un constructor a otro constructor de la misma clase. Es el mecanismo que evita duplicar código de inicialización.
flowchart LR
A["new Libro(titulo, isbn)"] --> B["Libro(String, String)"]
B -->|"this(...)"| C["Libro(String, String, String, int, boolean)
constructor completo:
asigna todos los campos"]
El patrón profesional es tener un único constructor que realmente asigna (llamado a veces constructor canónico) y que todos los demás desemboquen en él mediante this(...). Así la validación se escribe una sola vez.
Reglas estrictas que impone el compilador:
this(...)debe ser la primera sentencia del constructor. Ni unSystem.out.printlnantes.- No puede haber a la vez
this(...)ysuper(...)(lección 03-05): solo una de las dos, y siempre primera. - No se pueden formar ciclos: si
A()llama aB()yB()llama aA(), el compilador lo detecta y falla con "recursive constructor invocation".
public Libro(String titulo, String isbn) {
System.out.println("Creando libro..."); // ERROR si va antes
this(titulo, "Desconocido", isbn, 0, true);
}
- Orden de inicialización, paso a paso
Cuando ejecutas new, ocurren varias cosas en un orden fijo que conviene conocer, porque explica comportamientos aparentemente mágicos. Las etapas son:
- Se reserva memoria y todos los campos toman su valor por defecto (
0,false,null). - Se ejecutan los inicializadores de campo y los bloques de inicialización de instancia, en el orden textual en que aparecen en el fichero.
- Se ejecuta el cuerpo del constructor.
Un ejemplo trazado que lo demuestra:
public class DemoOrden {
public int a = mostrar("1. inicializador del campo a");
{
mostrar("2. bloque de inicializacion de instancia");
}
public int b = mostrar("3. inicializador del campo b");
public DemoOrden() {
mostrar("4. cuerpo del constructor");
}
private static int mostrar(String mensaje) {
System.out.println(mensaje);
return 0;
}
public static void main(String[] args) {
new DemoOrden();
}
}Salida:
1. inicializador del campo a 2. bloque de inicializacion de instancia 3. inicializador del campo b 4. cuerpo del constructor
Dos conclusiones importantes:
- Los inicializadores y bloques se ejecutan antes que el cuerpo del constructor, siempre, y en orden de arriba abajo. Si tienes tres constructores sobrecargados, ese código común se ejecuta en los tres.
- Los bloques de inicialización de instancia (las llaves sueltas) sirven para código común a todos los constructores, pero en la práctica se usan poco: es más claro un constructor canónico al que los demás encadenan con
this(...).
- Campos
final y su inicialización obligatoria
final y su inicialización obligatoriaYa sabes que final impide reasignar una variable (módulo 1). Aplicado a un campo, significa que su valor se fija una sola vez y no cambia durante la vida del objeto:
public class Libro {
public final String isbn; // no tiene inicializador
public Libro(String isbn) {
this.isbn = isbn; // obligatorio asignarlo aqui
}
}Un campo final debe quedar asignado exactamente una vez al terminar todo constructor, ya sea en su declaración, en un bloque de inicialización o en el cuerpo del constructor. El compilador verifica los tres caminos:
public class Libro {
public final String isbn;
public Libro() { } // ERROR: variable isbn might not have been initialized
}public final String isbn = "000";
public Libro(String isbn) {
this.isbn = isbn; // ERROR: cannot assign a value to final variable isbn
}Los campos final son una herramienta de diseño de primer orden: expresan en el propio código que un dato es identidad del objeto y no algo que se pueda cambiar. El ISBN de un libro nunca cambia; el título tampoco. Lo que sí cambia es disponible. Esa distinción, marcada con final, documenta el modelo mejor que cualquier comentario, y prepara el terreno para la inmutabilidad que estudiarás en 03-07.
- El constructor telescópico y el patrón Builder
Cuando una clase tiene muchos campos opcionales, la sobrecarga degenera en lo que se conoce como constructor telescópico:
public Libro(String titulo) { ... }
public Libro(String titulo, String autor) { ... }
public Libro(String titulo, String autor, String isbn) { ... }
public Libro(String titulo, String autor, String isbn, int anio) { ... }
public Libro(String titulo, String autor, String isbn, int anio, boolean disp) { ... }
public Libro(String titulo, String autor, String isbn, int anio, boolean disp, String editorial) { ... }Los problemas son reales y graves:
- Ilegibilidad en la llamada. ¿Qué significa
new Libro("Java Efectivo", "Bloch", "978-...", 2018, true, "Anaya")? Hay que abrir la clase para saberlo. - Errores silenciosos por orden. Si intercambias dos parámetros del mismo tipo —
tituloeisbn, ambosString—, compila perfectamente y el bug aparece en producción. - Explosión combinatoria. Con seis campos opcionales harían falta decenas de constructores.
Una mejora parcial y barata: nombrar las variables antes de llamar.
String titulo = "Java Efectivo";
String isbn = "978-0000000001";
Libro l = new Libro(titulo, "Joshua Bloch", isbn, 2018, true);La solución completa es el patrón Builder, que permite escribir algo como:
Libro libro = new Libro.Builder("Java Efectivo", "978-0000000001")
.autor("Joshua Bloch")
.anioPublicacion(2018)
.disponible(true)
.build();Cada dato queda nombrado en la llamada, el orden deja de importar y los opcionales se omiten. Requiere clases internas estáticas, que se estudian en 04-03, así que el patrón se desarrolla en la lección 12-02 (Patrones de Diseño). Por ahora, quédate con la regla práctica: si necesitas más de cuatro o cinco parámetros en un constructor, es señal de que falta agrupar datos en objetos o de que necesitas un Builder.
- Validar en el constructor
Si el constructor es el guardián de la entrada al objeto, debe rechazar los datos absurdos. Un año de publicación de -500, una tarifa negativa o un ISBN vacío no deben poder entrar.
Nota importante sobre el mecanismo. La forma profesional de rechazar un argumento inválido es lanzar una excepción (
throw new IllegalArgumentException(...)), lo que impide que el objeto llegue a existir. Pero las excepciones son el módulo 6 y todavía no las hemos estudiado. Por eso, durante este módulo, la validación consistirá en avisar por consola y aplicar un valor por defecto seguro. Es una solución provisional y deliberadamente imperfecta: recuérdalo, porque en el módulo 6 volveremos a estas mismas clases para hacerlo bien.
public Libro(String titulo, String autor, String isbn,
int anioPublicacion, boolean disponible) {
// --- Validacion del titulo ---
if (titulo == null || titulo.isBlank()) {
System.out.println("AVISO: titulo vacio. Se usara 'Sin titulo'.");
this.titulo = "Sin titulo";
} else {
this.titulo = titulo.trim();
}
// --- Validacion del autor ---
this.autor = (autor == null || autor.isBlank()) ? "Desconocido" : autor.trim();
// --- Validacion del ISBN ---
if (isbn == null || isbn.isBlank()) {
System.out.println("AVISO: ISBN vacio. Se usara '000-0000000000'.");
this.isbn = "000-0000000000";
} else {
this.isbn = isbn.trim();
}
// --- Validacion del anio ---
if (anioPublicacion < 1450 || anioPublicacion > 2100) {
System.out.println("AVISO: anio " + anioPublicacion + " fuera de rango. Se usara 0.");
this.anioPublicacion = 0;
} else {
this.anioPublicacion = anioPublicacion;
}
this.disponible = disponible;
librosCreados++;
}Observa tres detalles: isBlank() (Java 11+) es mejor que isEmpty() porque también detecta cadenas de solo espacios; la comprobación de null va antes que la de contenido, aprovechando el cortocircuito de || que estudiaste en el módulo 1; y trim() normaliza la entrada, evitando que "Java Efectivo " y "Java Efectivo" se traten como títulos distintos.
Y aquí está el otro gran beneficio: el contador librosCreados++ ahora vive dentro del constructor, no repartido por main. Es imposible crear un libro sin contarlo.
- El constructor de copia
Un constructor de copia recibe un objeto de la misma clase y crea uno nuevo con sus mismos valores. Es la solución al problema del aliasing de la lección 03-02: cuando de verdad quieres un objeto independiente.
/**
* Constructor de copia: crea un ejemplar nuevo con los mismos datos
* bibliograficos que otro. Util para registrar duplicados en el catalogo.
*/
public Libro(Libro otro) {
this(otro.titulo, otro.autor, otro.isbn, otro.anioPublicacion, otro.disponible);
}Uso:
Libro original = new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018);
Libro segundoEjemplar = new Libro(original);
segundoEjemplar.disponible = false;
System.out.println(original.disponible); // true <-- independiente
System.out.println(original == segundoEjemplar); // false <-- objetos distintosCompáralo con Libro alias = original;, que no copiaba nada. Aquí sí hay dos objetos en el heap.
Un matiz que reaparecerá en 03-07: esta copia es superficial (shallow). Copia los valores de los campos; si algún campo fuese una referencia a un objeto mutable, ambas copias compartirían ese objeto interno. Con String e int no hay problema, porque son inmutables o primitivos.
- Bloques
static de inicialización
static de inicializaciónIgual que existen bloques de inicialización de instancia, existen bloques static, que se ejecutan una sola vez, cuando la JVM carga la clase, antes de que se cree ningún objeto:
public class Prestamo {
public static final int DIAS_PRESTAMO = 15;
public static final double TARIFA_DIARIA = 0.25;
public static final double MULTA_MAXIMA = 20.0;
public static final int UMBRAL_LEVE = 7;
/** Dias de retraso a partir de los cuales se alcanza el tope de multa. */
public static final int DIAS_HASTA_TOPE;
static {
// Se calcula una sola vez, al cargar la clase.
DIAS_HASTA_TOPE = (int) Math.ceil(MULTA_MAXIMA / TARIFA_DIARIA);
System.out.println("[Prestamo] Reglas cargadas. Tope a los "
+ DIAS_HASTA_TOPE + " dias de retraso.");
}
}Al ejecutar cualquier cosa que toque Prestamo, verás una vez:
El orden global, resumido en una tabla:
| Momento | Qué se ejecuta | Cuántas veces |
|---|---|---|
| Carga de la clase | Inicializadores static y bloques static, en orden textual |
Una por ejecución |
new |
Campos por defecto → inicializadores de instancia y bloques { } → cuerpo del constructor |
Una por objeto |
Los bloques static se usan sobre todo para inicializar constantes calculadas o cargar configuración. Úsalos con moderación: código que se ejecuta "solo" es difícil de depurar.
- BiblioTech:
Libro, Empleado y Prestamo con constructores completos
Libro, Empleado y Prestamo con constructores completosAplicamos todo lo anterior a las tres clases del dominio.
Libro
package com.nexussoftware.bibliotech.dominio;
/**
* Libro del catalogo tecnico de Nexus Software.
* Los datos bibliograficos son inmutables (final); solo cambia la disponibilidad.
*/
public class Libro {
public static int librosCreados = 0;
public final String titulo;
public final String autor;
public final String isbn;
public final int anioPublicacion;
public boolean disponible; // unico campo mutable
/** Constructor canonico: unico que asigna y valida. */
public Libro(String titulo, String autor, String isbn,
int anioPublicacion, boolean disponible) {
this.titulo = (titulo == null || titulo.isBlank()) ? "Sin titulo" : titulo.trim();
this.autor = (autor == null || autor.isBlank()) ? "Desconocido" : autor.trim();
if (isbn == null || isbn.isBlank()) {
System.out.println("AVISO: ISBN vacio en '" + this.titulo + "'.");
this.isbn = "000-0000000000";
} else {
this.isbn = isbn.trim();
}
if (anioPublicacion < 1450 || anioPublicacion > 2100) {
System.out.println("AVISO: anio invalido (" + anioPublicacion + ") en '"
+ this.titulo + "'. Se registra como 0.");
this.anioPublicacion = 0;
} else {
this.anioPublicacion = anioPublicacion;
}
this.disponible = disponible;
librosCreados++;
}
/** Alta habitual: un libro nuevo entra disponible. */
public Libro(String titulo, String autor, String isbn, int anioPublicacion) {
this(titulo, autor, isbn, anioPublicacion, true);
}
/** Constructor de copia: nuevo ejemplar con los mismos datos. */
public Libro(Libro otro) {
this(otro.titulo, otro.autor, otro.isbn, otro.anioPublicacion, otro.disponible);
}
public boolean estaDisponible() { return disponible; }
public void prestar() {
if (!disponible) {
System.out.println("AVISO: '" + titulo + "' ya estaba prestado.");
return;
}
disponible = false;
}
public void devolver() {
if (disponible) {
System.out.println("AVISO: '" + titulo + "' ya estaba disponible.");
return;
}
disponible = true;
}
public String describir() {
return titulo + " - " + autor + " (" + anioPublicacion + "), ISBN " + isbn;
}
}Empleado
package com.nexussoftware.bibliotech.dominio;
/** Empleado de Nexus Software autorizado a tomar libros en prestamo. */
public class Empleado {
public static final int MAX_PRESTAMOS_SIMULTANEOS = 3;
public static int empleadosRegistrados = 0;
public final String nombre;
public final String identificador;
public int prestamosAcumulados;
public Empleado(String nombre, String identificador, int prestamosAcumulados) {
this.nombre = (nombre == null || nombre.isBlank()) ? "Empleado sin nombre"
: nombre.trim();
if (identificador == null || !identificador.startsWith("EMP-")) {
System.out.println("AVISO: identificador invalido para " + this.nombre
+ ". Se asigna EMP-000.");
this.identificador = "EMP-000";
} else {
this.identificador = identificador.trim();
}
if (prestamosAcumulados < 0) {
System.out.println("AVISO: prestamos negativos en " + this.nombre + ". Se usa 0.");
this.prestamosAcumulados = 0;
} else {
this.prestamosAcumulados = prestamosAcumulados;
}
empleadosRegistrados++;
}
/** Alta habitual: empleado nuevo, sin prestamos. */
public Empleado(String nombre, String identificador) {
this(nombre, identificador, 0);
}
public boolean puedeTomarPrestado() {
return prestamosAcumulados < MAX_PRESTAMOS_SIMULTANEOS;
}
public void registrarPrestamo() {
if (!puedeTomarPrestado()) {
System.out.printf("AVISO: %s ya tiene %d prestamos (maximo %d).%n",
nombre, prestamosAcumulados, MAX_PRESTAMOS_SIMULTANEOS);
return;
}
prestamosAcumulados++;
}
public void registrarDevolucion() {
prestamosAcumulados = Math.max(0, prestamosAcumulados - 1);
}
}Prestamo
Aquí aparece la novedad prometida: el préstamo calcula su propio día de vencimiento al nacer, a partir de DIAS_PRESTAMO.
Trabajamos con días enteros, como en todo el curso hasta ahora:
diaPrestamoes el día del calendario interno de BiblioTech en que se entregó el libro (por ejemplo, el día 100 de operación). Las fechas reales conLocalDatellegan en la lección 10-05.
package com.nexussoftware.bibliotech.dominio;
/**
* Registro de un prestamo de un libro a un empleado.
* Nace con libro, empleado y dia de prestamo, y calcula su propio vencimiento.
*/
public class Prestamo {
public static final int DIAS_PRESTAMO = 15;
public static final double TARIFA_DIARIA = 0.25;
public static final double MULTA_MAXIMA = 20.0;
public static final int UMBRAL_LEVE = 7;
/** Dias de retraso a partir de los cuales la multa alcanza el tope. */
public static final int DIAS_HASTA_TOPE;
static {
DIAS_HASTA_TOPE = (int) Math.ceil(MULTA_MAXIMA / TARIFA_DIARIA);
}
public static int prestamosCreados = 0;
public final Libro libro;
public final Empleado empleado;
public final int diaPrestamo;
public final int diaVencimiento; // derivado: no se pide, se calcula
public final String referencia; // identificador legible del prestamo
public int diasTranscurridos;
public boolean devuelto;
/** Constructor canonico. */
public Prestamo(Libro libro, Empleado empleado, int diaPrestamo, int diasTranscurridos) {
if (libro == null) {
System.out.println("AVISO: prestamo sin libro. Se registra un marcador.");
this.libro = new Libro("Libro desconocido", "Desconocido", "000-0000000000", 0, false);
} else {
this.libro = libro;
}
if (empleado == null) {
System.out.println("AVISO: prestamo sin empleado. Se registra un marcador.");
this.empleado = new Empleado("Empleado desconocido", "EMP-000");
} else {
this.empleado = empleado;
}
this.diaPrestamo = Math.max(0, diaPrestamo);
this.diaVencimiento = this.diaPrestamo + DIAS_PRESTAMO; // regla de negocio
if (diasTranscurridos < 0) {
System.out.println("AVISO: dias transcurridos negativos. Se usa 0.");
this.diasTranscurridos = 0;
} else {
this.diasTranscurridos = diasTranscurridos;
}
this.devuelto = false;
prestamosCreados++;
this.referencia = String.format("PR-%04d", prestamosCreados);
// Efectos sobre los objetos relacionados
this.libro.prestar();
this.empleado.registrarPrestamo();
}
/** Prestamo recien realizado: cero dias transcurridos. */
public Prestamo(Libro libro, Empleado empleado, int diaPrestamo) {
this(libro, empleado, diaPrestamo, 0);
}
// ---------- Comportamiento (leccion 03-03) ----------
public int calcularDiasRetraso() {
return Math.max(0, diasTranscurridos - DIAS_PRESTAMO);
}
public double calcularMulta() {
return Math.min(calcularDiasRetraso() * TARIFA_DIARIA, MULTA_MAXIMA);
}
public String clasificarGravedad() {
int retraso = calcularDiasRetraso();
if (retraso == 0) return "SIN RETRASO";
if (retraso <= UMBRAL_LEVE) return "LEVE";
return "GRAVE";
}
public boolean estaVencido() { return calcularDiasRetraso() > 0; }
public int diasRestantes(){ return Math.max(0, DIAS_PRESTAMO - diasTranscurridos); }
public int diaDevolucionPrevista() { return diaVencimiento; }
public static boolean esRetrasoLeve(int diasRetraso) {
return diasRetraso > 0 && diasRetraso <= UMBRAL_LEVE;
}
}Puntos que merecen atención:
diaVencimientoes un campo derivado: no se pide al llamador, lo calcula el constructor. Es imposible crear un préstamo con un vencimiento incoherente con su fecha de inicio.referenciase genera sola a partir del contador estático, conString.format(módulo 1):PR-0001,PR-0002... Nadie puede inventarse una referencia.- El constructor tiene efectos sobre otros objetos: marca el libro como prestado y suma un préstamo al empleado. Esto es discutible en diseño avanzado (un constructor "que hace cosas" sorprende), pero aquí garantiza la coherencia del sistema. Recuérdalo cuando en el módulo 12 aparezca la separación entre construir y ejecutar.
Uso en BiblioTechApp
package com.nexussoftware.bibliotech;
import com.nexussoftware.bibliotech.dominio.Empleado;
import com.nexussoftware.bibliotech.dominio.Libro;
import com.nexussoftware.bibliotech.dominio.Prestamo;
public class BiblioTechApp {
public static void main(String[] args) {
System.out.println("=== BiblioTech 3.1 - Objetos que nacen completos ===\n");
// Catalogo
Libro javaEfectivo = new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018);
Libro patrones = new Libro("Patrones de Diseño", "Erich Gamma", "978-0000000002", 1994);
Libro refactor = new Libro("Refactorización", "Martin Fowler", "978-0000000003", 2018);
// Plantilla
Empleado marta = new Empleado("Marta Ruiz", "EMP-001");
Empleado diego = new Empleado("Diego Alonso", "EMP-002");
Empleado nuria = new Empleado("Nuria Vidal", "EMP-003");
// Prestamos: dia 100 del calendario interno
Prestamo p1 = new Prestamo(javaEfectivo, marta, 100, 20);
Prestamo p2 = new Prestamo(patrones, diego, 100, 10);
Prestamo p3 = new Prestamo(refactor, nuria, 100, 95);
System.out.printf("%-8s %-22s %-14s %5s %5s %8s %9s %s%n",
"REF", "LIBRO", "EMPLEADO", "INI", "VENC", "TRANSC", "MULTA", "GRAVEDAD");
imprimir(p1);
imprimir(p2);
imprimir(p3);
System.out.printf("%nLibros creados: %d | Empleados: %d | Prestamos: %d%n",
Libro.librosCreados, Empleado.empleadosRegistrados,
Prestamo.prestamosCreados);
System.out.printf("Tope de multa alcanzado a los %d dias de retraso.%n",
Prestamo.DIAS_HASTA_TOPE);
}
private static void imprimir(Prestamo p) {
System.out.printf("%-8s %-22s %-14s %5d %5d %8d %8.2f %s%n",
p.referencia, p.libro.titulo, p.empleado.nombre,
p.diaPrestamo, p.diaVencimiento, p.diasTranscurridos,
p.calcularMulta(), p.clasificarGravedad());
}
}Salida:
=== BiblioTech 3.1 - Objetos que nacen completos === REF LIBRO EMPLEADO INI VENC TRANSC MULTA GRAVEDAD PR-0001 Java Efectivo Marta Ruiz 100 115 20 1,25 LEVE PR-0002 Patrones de Diseño Diego Alonso 100 115 10 0,00 SIN RETRASO PR-0003 Refactorización Nuria Vidal 100 115 95 20,00 GRAVE Libros creados: 3 | Empleados: 3 | Prestamos: 3 Tope de multa alcanzado a los 80 dias de retraso.
Compara el arranque de main con el de la lección 03-02: seis líneas de creación en lugar de treinta, y ni una sola posibilidad de olvidar un campo.
Errores Comunes y Consejos
- Poner tipo de retorno al constructor.
public void Libro(...)es un método, no un constructor. Es el error número uno, y el compilador no te ayuda: el fallo aparece más tarde, al usarnew. - Perder el constructor por defecto sin darse cuenta. En cuanto declaras uno con parámetros,
new Libro()deja de compilar. Si lo necesitas, decláralo tú. - Poner algo antes de
this(...). No compila. Si necesitas ejecutar código común antes, va dentro del constructor canónico. - Llamar a métodos sobrescribibles desde el constructor. Es un problema sutil que se entiende del todo en 03-05: si una subclase sobrescribe ese método, se ejecutará antes de que los campos de la subclase estén inicializados, viendo
nully ceros. Regla: desde un constructor, llama solo a métodosprivateofinal. - Asignar sin
thiscuando hay shadowing.titulo = titulo;no hace nada. El IDE lo marca; no ignores el aviso. - Duplicar la validación en cada constructor sobrecargado. Encadena con
this(...)hasta el constructor canónico, y valida allí una sola vez. - Constructores con efectos secundarios pesados. Leer un fichero o abrir una conexión en un constructor hace la clase imposible de probar. Un constructor debe inicializar, no trabajar.
- Consejo: haz
finaltodo campo que no deba cambiar. Es gratis, lo comprueba el compilador y documenta el diseño mejor que un comentario. - Consejo: deja que el IDE genere el constructor (
Alt+Insert→ Constructor en IntelliJ,Alt+Shift+S→ Generate Constructor using Fields en Eclipse) y luego añade a mano la validación.
Ejercicios
Ejercicio 1: constructores para una clase Reserva
Retomando el modelo de salas de la lección 03-01, crea una clase Reserva con campos final para sala (String), empleado (Empleado), horaInicio (int, 0-23), horaFin (int) y asistentes (int), más un campo mutable activa (boolean) y un codigo generado automáticamente con formato RES-0001.
Requisitos:
- Un constructor canónico que valide: las horas dentro de 0-23,
horaFinposterior ahoraInicioy asistentes mayor que cero. Aplica valores por defecto seguros y avisa por consola. - Un constructor sobrecargado que asuma una duración de una hora (recibe solo la hora de inicio).
- Un contador
staticde reservas creadas que alimente el código. - Un método
duracionHoras().
Crea tres reservas, una de ellas con datos inválidos, y muestra el resultado.
Ejercicio 2: trazar el orden de inicialización
Escribe una clase TrazaConstruccion que imprima un mensaje en cada uno de estos puntos: un bloque static, un inicializador de campo static, un inicializador de campo de instancia, un bloque de inicialización de instancia y dos constructores encadenados con this(...). Crea dos objetos desde main y predice la salida antes de ejecutarla. Después explica: ¿qué mensajes aparecen una sola vez y cuáles dos? ¿Por qué?
Ejercicio 3: constructor de copia y aliasing
Escribe un método static en BiblioTechApp llamado duplicarEjemplar(Libro original) que devuelva un nuevo Libro con los mismos datos pero marcado como disponible. Demuestra con salida por consola que:
- El duplicado y el original son objetos distintos (
==dafalse). - Cambiar la disponibilidad de uno no afecta al otro.
- En cambio,
Libro alias = original;sí produce interferencia.
Después responde: si Libro tuviera un campo Empleado ultimoLector (una referencia mutable), ¿bastaría el constructor de copia tal como está escrito? ¿Qué problema aparecería?
Soluciones
Solución 1
package com.nexussoftware.bibliotech.dominio;
/** Reserva de una sala de reuniones de Nexus Software. */
public class Reserva {
public static int reservasCreadas = 0;
public final String sala;
public final Empleado solicitante;
public final int horaInicio;
public final int horaFin;
public final int asistentes;
public final String codigo;
public boolean activa;
/** Constructor canonico: unico que asigna y valida. */
public Reserva(String sala, Empleado solicitante,
int horaInicio, int horaFin, int asistentes) {
this.sala = (sala == null || sala.isBlank()) ? "Sala sin nombre" : sala.trim();
if (solicitante == null) {
System.out.println("AVISO: reserva sin solicitante. Se registra un marcador.");
this.solicitante = new Empleado("Empleado desconocido", "EMP-000");
} else {
this.solicitante = solicitante;
}
// Hora de inicio dentro del rango del dia
if (horaInicio < 0 || horaInicio > 23) {
System.out.println("AVISO: hora de inicio invalida (" + horaInicio + "). Se usa 9.");
this.horaInicio = 9;
} else {
this.horaInicio = horaInicio;
}
// Hora de fin: valida y posterior al inicio
if (horaFin < 0 || horaFin > 23 || horaFin <= this.horaInicio) {
int propuesta = Math.min(23, this.horaInicio + 1);
System.out.println("AVISO: hora de fin invalida (" + horaFin
+ "). Se usa " + propuesta + ".");
this.horaFin = propuesta;
} else {
this.horaFin = horaFin;
}
if (asistentes <= 0) {
System.out.println("AVISO: numero de asistentes invalido. Se usa 1.");
this.asistentes = 1;
} else {
this.asistentes = asistentes;
}
this.activa = true;
reservasCreadas++;
this.codigo = String.format("RES-%04d", reservasCreadas);
}
/** Reserva de una hora: solo se indica el inicio. */
public Reserva(String sala, Empleado solicitante, int horaInicio, int asistentes) {
this(sala, solicitante, horaInicio, horaInicio + 1, asistentes);
}
public int duracionHoras() {
return horaFin - horaInicio;
}
public void cancelar() {
if (!activa) {
System.out.println("AVISO: la reserva " + codigo + " ya estaba cancelada.");
return;
}
activa = false;
}
}Prueba:
Empleado marta = new Empleado("Marta Ruiz", "EMP-001");
Reserva r1 = new Reserva("Sala Norte", marta, 9, 11, 6);
Reserva r2 = new Reserva("Sala Sur", marta, 16, 4); // una hora
Reserva r3 = new Reserva("Sala Este", marta, 25, 3, -2); // datos invalidos
System.out.printf("%-9s %-12s %5s %5s %6s %11s%n",
"CODIGO", "SALA", "INI", "FIN", "PERS.", "DURACION");
System.out.printf("%-9s %-12s %5d %5d %6d %8d h%n",
r1.codigo, r1.sala, r1.horaInicio, r1.horaFin, r1.asistentes, r1.duracionHoras());
System.out.printf("%-9s %-12s %5d %5d %6d %8d h%n",
r2.codigo, r2.sala, r2.horaInicio, r2.horaFin, r2.asistentes, r2.duracionHoras());
System.out.printf("%-9s %-12s %5d %5d %6d %8d h%n",
r3.codigo, r3.sala, r3.horaInicio, r3.horaFin, r3.asistentes, r3.duracionHoras());Salida:
AVISO: hora de inicio invalida (25). Se usa 9. AVISO: hora de fin invalida (3). Se usa 10. AVISO: numero de asistentes invalido. Se usa 1. CODIGO SALA INI FIN PERS. DURACION RES-0001 Sala Norte 9 11 6 2 h RES-0002 Sala Sur 16 17 4 1 h RES-0003 Sala Este 9 10 1 1 h
Un detalle sutil: la validación de horaFin compara contra this.horaInicio (el ya validado), no contra el parámetro horaInicio. Si comparara contra el parámetro, una hora de inicio corregida podría dejar un intervalo incoherente. El orden de las validaciones importa.
Otro detalle: el constructor de una hora encadena con this(...) pasando horaInicio + 1, así que si el inicio es inválido, la corrección se hace una sola vez, en el canónico.
Solución 2
package com.nexussoftware.bibliotech;
public class TrazaConstruccion {
static int contadorStatic = mostrar("A. inicializador de campo static");
static {
mostrar("B. bloque static");
}
int campoInstancia = mostrar("C. inicializador de campo de instancia");
{
mostrar("D. bloque de inicializacion de instancia");
}
public TrazaConstruccion() {
this(0); // debe ser la PRIMERA sentencia
mostrar("F. cuerpo del constructor sin argumentos");
}
public TrazaConstruccion(int valor) {
mostrar("E. cuerpo del constructor canonico");
}
private static int mostrar(String mensaje) {
System.out.println(mensaje);
return 0;
}
public static void main(String[] args) {
System.out.println("--- primer objeto ---");
new TrazaConstruccion();
System.out.println("--- segundo objeto ---");
new TrazaConstruccion();
}
}Salida:
A. inicializador de campo static B. bloque static --- primer objeto --- C. inicializador de campo de instancia D. bloque de inicializacion de instancia E. cuerpo del constructor canonico F. cuerpo del constructor sin argumentos --- segundo objeto --- C. inicializador de campo de instancia D. bloque de inicializacion de instancia E. cuerpo del constructor canonico F. cuerpo del constructor sin argumentos
Explicación. A y B aparecen una sola vez y antes de todo: son código de clase, y la JVM lo ejecuta al cargarla, que ocurre en la primera referencia a TrazaConstruccion (aquí, la propia ejecución de main). C, D, E y F aparecen dos veces, una por objeto: son código de instancia.
Dos matices que sorprenden y conviene interiorizar. Primero, C y D se ejecutan antes que cualquier cuerpo de constructor, en orden textual. Segundo, aunque el new sin argumentos llama al constructor sin parámetros, el mensaje E aparece antes que el F: this(0) transfiere el control al canónico, que se ejecuta entero, y solo después continúa el cuerpo del constructor original.
Solución 3
/**
* Crea un ejemplar nuevo con los mismos datos bibliograficos que el original,
* siempre disponible para prestamo.
*/
private static Libro duplicarEjemplar(Libro original) {
Libro copia = new Libro(original); // constructor de copia
copia.disponible = true; // el ejemplar nuevo entra en estanteria
return copia;
}Demostración:
Libro original = new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018);
original.prestar(); // original queda NO disponible
Libro duplicado = duplicarEjemplar(original);
Libro alias = original;
System.out.println("1) original == duplicado : " + (original == duplicado)); // false
System.out.println(" original == alias : " + (original == alias)); // true
System.out.println("2) antes -> original: " + original.estaDisponible()
+ " | duplicado: " + duplicado.estaDisponible());
duplicado.prestar();
System.out.println(" despues -> original: " + original.estaDisponible()
+ " | duplicado: " + duplicado.estaDisponible());
System.out.println("3) alias.devolver() ...");
alias.devolver();
System.out.println(" original.estaDisponible(): " + original.estaDisponible());Salida:
1) original == duplicado : false original == alias : true 2) antes -> original: false | duplicado: true despues -> original: false | duplicado: false 3) alias.devolver() ... original.estaDisponible(): true
Lectura: el duplicado es un objeto independiente (paso 1 y 2), mientras que el alias es la misma caja de memoria con otro nombre, así que alias.devolver() cambia el estado que ve original (paso 3). Es exactamente la distinción de la lección 03-02.
Respuesta a la pregunta final. No, no bastaría. El constructor de copia tal como está escrito hace una copia superficial: copia el valor de cada campo, y el valor de un campo de tipo referencia es la dirección. Si Libro tuviera Empleado ultimoLector, el original y el duplicado apuntarían al mismo objeto Empleado, de modo que duplicado.ultimoLector.registrarPrestamo() alteraría también el empleado que ve el original. Es aliasing colado por la puerta de atrás.
Las salidas posibles son tres, y las verás desarrolladas en 03-07:
- Copia profunda: crear también un
Empleadonuevo en el constructor de copia (this.ultimoLector = new Empleado(otro.ultimoLector);). Costoso, y duplica entidades que quizá deban ser únicas. - Compartir a propósito: si el
Empleadoes una entidad compartida por todo el sistema, compartir la referencia es lo correcto; lo que hay que evitar es que alguien la mute a través del libro. - Hacer inmutable la clase referenciada, con lo que compartir deja de ser peligroso porque nadie puede cambiarla.
Esa es la razón profunda por la que la inmutabilidad simplifica tanto el diseño: cuando nada puede mutar, la diferencia entre copiar y compartir desaparece.
Conclusión
Has cerrado la puerta a los objetos incompletos. Sabes qué es un constructor y en qué se diferencia de un método —empezando por el detalle que más quebraderos de cabeza da, que no lleva tipo de retorno—; conoces el constructor por defecto y el momento exacto en que Java deja de regalártelo. Sabes sobrecargar constructores para ofrecer varias formas de crear el mismo objeto y encadenarlos con this(...) hacia un constructor canónico que valida una sola vez, con sus reglas estrictas. Has trazado el orden completo de inicialización —bloques static al cargar la clase; después, por cada new, valores por defecto, inicializadores de instancia y por último el cuerpo del constructor— y entiendes por qué un campo final obliga al compilador a comprobar todos los caminos. Conoces el problema del constructor telescópico y sabes que el patrón Builder lo resuelve en la lección 12-02. Validas en el constructor, de forma provisional con avisos y valores por defecto, sabiendo que el módulo 6 traerá la solución correcta con excepciones. Y dominas el constructor de copia, la única forma real de duplicar un objeto, con la advertencia de que es superficial.
BiblioTech ha dado un salto: Libro, Empleado y Prestamo nacen completos, con sus datos identitarios marcados como final, con validación de entrada, con contadores que no se pueden olvidar y con un Prestamo que calcula su propio día de vencimiento y su referencia PR-0001 sin que nadie se los dicte.
En la lección siguiente, Herencia, el modelo crecerá hacia arriba. Nexus Software no solo presta libros: también revistas técnicas y DVD de formación, cada uno con su plazo y su tarifa. En lugar de duplicar Libro tres veces, crearás una clase base Material con lo común y tres especializaciones que solo declaren sus diferencias. Aprenderás extends, super, el orden de construcción en una jerarquía y la diferencia —fuente inagotable de confusión— entre sobrescribir y sobrecargar.
Curso de Programación en Java
Módulo 1: Introducción a Java
- Introducción a Java
- Configuración del Entorno de Desarrollo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Operadores
- Entrada y Salida por Consola
- Tu Primer Programa Completo: BiblioTech
Módulo 2: Flujo de Control
- Sentencias Condicionales
- Bucles
- Sentencias Switch
- Break y Continue
- Depuración y Trazas de Ejecución
- Proyecto: Menú Interactivo de BiblioTech
Módulo 3: Programación Orientada a Objetos
- Introducción a la POO
- Clases y Objetos
- Métodos
- Constructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- La Clase Object: equals, hashCode y toString
Módulo 4: Programación Orientada a Objetos Avanzada
- Interfaces
- Clases Abstractas
- Clases Internas
- Clases Anónimas
- Expresiones Lambda
- Interfaces Funcionales y Referencias a Métodos
- Enumeraciones y Registros
Módulo 5: Estructuras de Datos y Colecciones
- Arreglos
- El Framework de Colecciones
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cola y Deque
- Pila
- Ordenación y Búsqueda en Colecciones
Módulo 6: Manejo de Excepciones
- Introducción a las Excepciones
- Bloque Try-Catch
- Throw y Throws
- Excepciones Personalizadas
- Bloque Finally
- Try-with-resources y AutoCloseable
- Estrategias de Manejo de Errores y Logging
Módulo 7: Entrada/Salida de Archivos
- Lectura de Archivos
- Escritura de Archivos
- Flujos de Archivos
- BufferedReader y BufferedWriter
- Serialización
- La API NIO.2: Path y Files
- Formatos de Intercambio: CSV y Properties
Módulo 8: Multihilo y Concurrencia
- Introducción al Multihilo
- Creación de Hilos
- Ciclo de Vida de un Hilo
- Sincronización
- Utilidades de Concurrencia
- Colecciones Concurrentes y Variables Atómicas
- Tareas Asíncronas con CompletableFuture
Módulo 9: Redes
- Introducción a las Redes
- Sockets
- ServerSocket
- DatagramSocket y DatagramPacket
- URL y HttpURLConnection
- El Cliente HTTP Moderno
Módulo 10: Temas Avanzados
- Genéricos
- Anotaciones
- Reflexión
- Características de Java 8: Streams y Optional
- Fechas y Horas con java.time
- Java 9 y Más Allá
- Memoria, Recolección de Basura y Rendimiento
Módulo 11: Frameworks y Librerías de Java
- Introducción a los Frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Pruebas Avanzadas con Mockito
- Librerías Esenciales del Ecosistema
