En la lección anterior dibujaste el plano: tres clases, Libro, Empleado y Prestamo, con sus atributos y sus responsabilidades. Ahora toca construir. Esta lección es la que convierte el diagrama en código Java real: aprenderás la sintaxis exacta para declarar una clase, qué valor tienen sus campos antes de que nadie los asigne, cómo crear objetos con new y qué ocurre exactamente en la memoria cuando lo haces. Verás por qué null no es "vacío" sino "ninguna dirección", qué significa realmente el NullPointerException que tarde o temprano te va a saltar, y una de las sorpresas más desagradables para quien empieza: dos variables pueden apuntar al mismo objeto, de modo que modificarlo por una lo modifica también para la otra. Al terminar, "Java Efectivo", "Patrones de Diseño" y "Refactorización" existirán en tu programa como tres objetos con vida propia.
Contenido
- Declarar una clase: sintaxis mínima
- Campos de instancia y valores por defecto
- Un fichero por clase pública y organización en paquetes
- Crear objetos con
new - Qué ocurre en memoria: pila y heap
- Acceder a los campos con el punto
- La referencia
nully elNullPointerException - Aliasing: varias referencias al mismo objeto
- Campos
staticfrente a campos de instancia - Constantes
static final - Ciclo de vida de un objeto y recolección de basura
- BiblioTech: primera versión de
Libroen funcionamiento - Errores Comunes y Consejos
- Ejercicios
- Declarar una clase: sintaxis mínima
Una clase es, en su forma más simple, un nombre y un bloque:
Diseccionémoslo con la misma calma con la que en el módulo 1 diseccionaste main:
publices un modificador de acceso: indica que la clase es visible desde cualquier paquete. Si lo omites, la clase solo será visible dentro de su propio paquete (visibilidad default, o "de paquete"). Los modificadores se estudian a fondo en la lección 03-07.classes la palabra reservada que declara una clase.Libroes el nombre. Por convención (módulo 1): sustantivo, singular, enUpperCamelCase.- Las llaves delimitan el cuerpo de la clase, donde viven campos, constructores y métodos.
Ojo a una diferencia fundamental respecto a todo lo que has escrito hasta ahora: dentro del cuerpo de una clase no puedes poner sentencias sueltas. Esto no compila:
El cuerpo de una clase solo admite declaraciones: campos, constructores, métodos, bloques de inicialización y otras clases. El código ejecutable vive dentro de métodos.
- Campos de instancia y valores por defecto
Un campo (o atributo, o variable de instancia) se declara igual que una variable local, pero directamente en el cuerpo de la clase:
package com.nexussoftware.bibliotech.dominio;
public class Libro {
public String titulo;
public String autor;
public String isbn;
public int anioPublicacion;
public boolean disponible;
}Nota importante: estos campos son
publicsolo de forma provisional, para que puedas manipularlos con el punto mientras aprendes el mecanismo. En la lección 03-07 los harásprivatey los expondrás mediante operaciones controladas, que es como se escribe código profesional. Considera estepublicun andamio.
Y aquí llega la primera diferencia importante con las variables locales que ya conoces. En el módulo 1 aprendiste que una variable local debe inicializarse antes de usarse o el compilador se queja. Los campos, en cambio, se inicializan automáticamente con un valor por defecto:
| Tipo del campo | Valor por defecto | Comentario |
|---|---|---|
byte, short, int, long |
0 |
El 0L en el caso de long |
float, double |
0.0 |
|
char |
'�' |
Carácter nulo, no visible al imprimir |
boolean |
false |
|
Cualquier referencia (String, Libro...) |
null |
No cadena vacía: literalmente "ninguna dirección" |
Libro vacio = new Libro();
System.out.println(vacio.titulo); // null (no "" ni "vacio")
System.out.println(vacio.anioPublicacion); // 0
System.out.println(vacio.disponible); // falseEsa inicialización automática es una comodidad, pero también una trampa: un objeto recién creado con new Libro() es un libro sin título, sin autor y no disponible, es decir, un objeto en estado inválido. La solución definitiva son los constructores, que estudiarás en la lección 03-04 y que garantizan que ningún objeto nazca incompleto. Por ahora, asignaremos los campos a mano.
- Un fichero por clase pública y organización en paquetes
Reglas del lenguaje, no de estilo:
- Un fichero
.javapuede contener como máximo una clasepublic. - Si contiene una clase
public, el fichero debe llamarse igual que la clase, con la misma capitalización:Librova enLibro.java. - El
packagedeclarado en la primera línea debe corresponder con la ruta de directorios (módulo 1).
Aplicado al proyecto bibliotech, la estructura queda así:
bibliotech/
└── src/main/java/
└── com/nexussoftware/bibliotech/
├── BiblioTechApp.java (paquete com.nexussoftware.bibliotech)
└── dominio/
├── Libro.java (paquete ...bibliotech.dominio)
├── Empleado.java
└── Prestamo.javaEl subpaquete dominio no es un capricho: separa las clases que representan el negocio del código que ejecuta la aplicación. Es la primera decisión de arquitectura del curso, y la retomarás en 03-07 (encapsulamiento a nivel de paquete) y en el módulo 12.
Como BiblioTechApp y Libro están ahora en paquetes distintos, tu clase de arranque necesita un import:
package com.nexussoftware.bibliotech;
import com.nexussoftware.bibliotech.dominio.Libro;
public class BiblioTechApp {
public static void main(String[] args) {
// ...
}
}Es exactamente el mismo mecanismo que usabas con import java.util.Scanner;. La diferencia es que ahora la clase importada la has escrito tú.
- Crear objetos con
new
newLa clase es el plano; new es la constructora que levanta la casa:
Tres partes que conviene separar mentalmente:
| Fragmento | Qué es | Qué hace |
|---|---|---|
Libro javaEfectivo |
Declaración de una variable de tipo referencia | Reserva un hueco en la pila para guardar una dirección |
new Libro() |
Operador de creación | Reserva memoria en el heap e inicializa los campos a sus valores por defecto |
= |
Asignación | Copia en la variable la dirección del objeto recién creado |
Los paréntesis de new Libro() son una llamada al constructor. Como todavía no has escrito ninguno, Java te proporciona un constructor por defecto sin parámetros que no hace nada más que crear el objeto. En 03-04 verás cuándo desaparece ese regalo.
Nada te obliga a guardar la referencia:
Ese objeto existe durante un instante y queda inalcanzable: no hay ninguna variable que lleve a él, así que el recolector de basura acabará eliminándolo (apartado 11).
- Qué ocurre en memoria: pila y heap
Recupera del módulo 1 la distinción entre pila (stack) y montículo (heap):
- La pila guarda las variables locales y los parámetros de cada método en ejecución. Es pequeña, rapidísima y se libera sola al terminar el método.
- El heap guarda todos los objetos. Es grande, lo gestiona la JVM y lo limpia el recolector de basura.
Ejecuta mentalmente este fragmento:
Libro a = new Libro();
a.titulo = "Java Efectivo";
a.isbn = "978-0000000001";
Libro b = new Libro();
b.titulo = "Patrones de Diseño";
b.isbn = "978-0000000002";flowchart LR
subgraph PILA["Pila (main)"]
A["a = 0x1A"]
B["b = 0x2B"]
end
subgraph HEAP["Heap"]
O1["0x1A : Libro
titulo = Java Efectivo
isbn = 978-0000000001
disponible = false"]
O2["0x2B : Libro
titulo = Patrones de Diseño
isbn = 978-0000000002
disponible = false"]
end
A --> O1
B --> O2
Cuatro lecturas que debes extraer del diagrama:
- La variable no contiene el objeto, contiene una flecha hacia él. Las direcciones (
0x1A) son ficticias; nunca las verás ni las necesitas. - Cada
newproduce un objeto distinto, aunque los valores fueran idénticos. - Los campos viven dentro del objeto, en el heap, no en la pila.
- Cuando
maintermine,aybdesaparecen de la pila; los objetos del heap quedan sin referencias y se convierten en basura.
- Acceder a los campos con el punto
El operador . sirve para llegar al interior de un objeto a través de una referencia:
Libro javaEfectivo = new Libro();
// Escritura
javaEfectivo.titulo = "Java Efectivo";
javaEfectivo.autor = "Joshua Bloch";
javaEfectivo.isbn = "978-0000000001";
javaEfectivo.anioPublicacion = 2018;
javaEfectivo.disponible = true;
// Lectura
System.out.println(javaEfectivo.titulo); // Java Efectivo
System.out.printf("%s (%d)%n", javaEfectivo.autor, javaEfectivo.anioPublicacion);El punto se lee como un genitivo: javaEfectivo.titulo es "el título de javaEfectivo". Y se puede encadenar cuando un campo es a su vez un objeto:
Funciona, pero anota mentalmente que encadenar puntos es un olor de diseño llamado tren de mensajes; la lección 03-07 lo formaliza con la Ley de Demeter.
Los campos también se pueden usar en cualquier expresión de las que ya dominas:
if (javaEfectivo.disponible && javaEfectivo.anioPublicacion >= 2015) {
System.out.println("Disponible y reciente");
}
- La referencia
null y el NullPointerException
null y el NullPointerExceptionnull es un valor especial que significa "esta referencia no apunta a ningún objeto". No es cero, no es cadena vacía, no es un objeto "en blanco": es la ausencia de dirección.
Imprimir una referencia nula es inofensivo. Lo que no es inofensivo es intentar usar el punto sobre ella:
Libro sinAsignar = null;
System.out.println(sinAsignar.titulo); // NullPointerException en ejecucionEl programa se detiene con una traza como esta:
Exception in thread "main" java.lang.NullPointerException:
Cannot read field "titulo" because "sinAsignar" is null
at com.nexussoftware.bibliotech.BiblioTechApp.main(BiblioTechApp.java:14)Léela con el método sistemático de la lección 02-05:
| Parte del mensaje | Qué te dice |
|---|---|
NullPointerException |
Se ha usado el punto sobre una referencia nula |
Cannot read field "titulo" |
El campo al que se intentaba acceder |
because "sinAsignar" is null |
Qué referencia era nula (mensaje detallado de Java 14+) |
at ...BiblioTechApp.main(BiblioTechApp.java:14) |
Fichero y línea exactos |
Ese "because ... is null" es una mejora enorme de Java moderno: en versiones antiguas solo veías el nombre de la excepción y la línea, y con a.b.c.d había que adivinar cuál de los cuatro era nulo.
Un NullPointerException no se resuelve con try/catch (módulo 6): se resuelve evitando que exista la referencia nula. Las herramientas para ello son las guardas de nulidad que ya usaste en el módulo 2 y, sobre todo, los constructores que obligan a dar valores (03-04):
if (libro != null) {
System.out.println(libro.titulo);
} else {
System.out.println("No hay libro asociado.");
}Recuerda del módulo 1 la regla de comparar cadenas: con un posible null de por medio, "Java Efectivo".equals(libro.titulo) es más seguro que libro.titulo.equals("Java Efectivo"), porque el literal nunca es nulo.
- Aliasing: varias referencias al mismo objeto
Este es el concepto que más sorpresas causa. Observa:
Libro original = new Libro();
original.titulo = "Refactorización";
original.disponible = true;
Libro copia = original; // ¿copia?
copia.disponible = false; // se modifica "copia"
System.out.println(original.disponible); // false <-- ¡no era una copia!Libro copia = original; no duplica el objeto. Copia la referencia, es decir, la dirección. El resultado son dos variables apuntando al mismo objeto del heap:
flowchart LR
subgraph PILA["Pila"]
V1["original = 0x3C"]
V2["copia = 0x3C"]
end
subgraph HEAP["Heap"]
OBJ["0x3C : Libro
titulo = Refactorizacion
disponible = false"]
end
V1 --> OBJ
V2 --> OBJ
A este fenómeno se le llama aliasing (dos nombres para la misma cosa). Consecuencias prácticas:
| Operación | Efecto |
|---|---|
copia.disponible = false; |
Modifica el objeto; se ve desde original |
copia = new Libro(); |
Reapunta la variable copia; original no se entera |
copia == original |
true si apuntan al mismo objeto (identidad) |
copia = null; |
Solo copia queda a nulo; el objeto sigue vivo por original |
Comprueba la diferencia entre mutar y reapuntar:
Libro a = new Libro();
a.titulo = "Java Efectivo";
Libro b = a;
b.titulo = "Patrones de Diseño"; // MUTAR el objeto compartido
System.out.println(a.titulo); // Patrones de Diseño
b = new Libro(); // REAPUNTAR la variable b
b.titulo = "Refactorización";
System.out.println(a.titulo); // Patrones de Diseño (a no cambia)Y compara la identidad con la igualdad, retomando el == del módulo 1:
Libro x = new Libro();
x.isbn = "978-0000000001";
Libro y = new Libro();
y.isbn = "978-0000000001";
System.out.println(x == y); // false: son dos objetos distintos
System.out.println(x == x); // trueDos libros con el mismo ISBN son objetos diferentes y == lo dice claramente. ¿Y si quieres que se consideren iguales por su ISBN? Eso exige sobrescribir equals, y es el tema de la lección 03-09.
Si de verdad quieres duplicar un objeto, hay que hacerlo campo a campo (o con un constructor de copia, lección 03-04):
Libro duplicado = new Libro();
duplicado.titulo = original.titulo;
duplicado.autor = original.autor;
duplicado.isbn = original.isbn;
duplicado.anioPublicacion = original.anioPublicacion;
duplicado.disponible = original.disponible;
- Campos
static frente a campos de instancia
static frente a campos de instanciaTodos los campos vistos hasta ahora son de instancia: cada objeto tiene su propia copia. Un campo static es distinto: pertenece a la clase, y existe una sola copia compartida por todos los objetos.
El caso de uso clásico es un contador. Nexus Software quiere saber cuántos préstamos se han creado durante la sesión:
package com.nexussoftware.bibliotech.dominio;
public class Prestamo {
// Campo de clase: uno solo, compartido por todos los prestamos
public static int prestamosCreados = 0;
// Campos de instancia: uno por objeto
public Libro libro;
public Empleado empleado;
public int diasTranscurridos;
}De momento el contador hay que incrementarlo a mano al crear cada préstamo (en 03-04 lo hará el constructor, que es su sitio natural):
Prestamo p1 = new Prestamo();
Prestamo.prestamosCreados++;
Prestamo p2 = new Prestamo();
Prestamo.prestamosCreados++;
System.out.println(Prestamo.prestamosCreados); // 2Diferencias esenciales:
| Aspecto | Campo de instancia | Campo static |
|---|---|---|
| Cuántas copias hay | Una por objeto | Una por clase |
| Cómo se accede | objeto.campo |
Clase.campo (recomendado) |
| Cuándo existe | Desde new hasta que se recolecta |
Desde que se carga la clase hasta que termina el programa |
| Ejemplo | libro.titulo |
Prestamo.prestamosCreados |
| Sirve para | Estado propio de cada objeto | Estado o datos comunes a todos |
Java permite acceder a un campo static a través de una instancia (p1.prestamosCreados), pero es muy mala práctica: da la falsa impresión de que el valor pertenece a ese objeto. Usa siempre el nombre de la clase.
Y una advertencia: los campos static mutables son estado global disfrazado, con los mismos riesgos que las veinte variables de main. Úsalos para contadores, constantes y poco más.
- Constantes
static final
static finalCombinando static (una sola copia) con final (no se puede reasignar, módulo 1) se obtiene la forma canónica de declarar una constante:
public class Prestamo {
/** Duracion estandar de un prestamo, en dias. */
public static final int DIAS_PRESTAMO = 15;
/** Importe cobrado por cada dia de retraso, en euros. */
public static final double TARIFA_DIARIA = 0.25;
/** Importe maximo que puede alcanzar una multa, en euros. */
public static final double MULTA_MAXIMA = 20.0;
/** Retraso maximo (en dias) que se considera leve. */
public static final int UMBRAL_LEVE = 7;
public static int prestamosCreados = 0;
public Libro libro;
public Empleado empleado;
public int diasTranscurridos;
}Esto resuelve un problema real de tu código actual: hasta ahora las cuatro reglas de negocio eran final locales dentro de main, invisibles para cualquier otra parte del programa. Ahora viven en la clase que las gobierna y se usan desde cualquier sitio:
System.out.printf("Plazo: %d dias, tarifa %.2f EUR/dia%n",
Prestamo.DIAS_PRESTAMO, Prestamo.TARIFA_DIARIA);Convenciones para constantes (módulo 1): MAYUSCULAS_CON_GUION_BAJO. Y una precaución: final impide reasignar la variable, no modificar el objeto al que apunta. public static final Libro CATALOGO_BASE = new Libro(); te impediría hacer CATALOGO_BASE = otroLibro;, pero no CATALOGO_BASE.titulo = "Otro";. Por eso las constantes se declaran de tipos inmutables (primitivos, String).
- Ciclo de vida de un objeto y recolección de basura
Un objeto Java atraviesa tres fases:
flowchart LR
A["1. Creacion
new reserva memoria
e inicializa campos"] --> B["2. Uso
hay al menos
una referencia viva"]
B --> C["3. Inalcanzable
ninguna referencia
llega a el"]
C --> D["4. Recolectado
el GC libera
la memoria"]
En Java no existe un operador para destruir objetos (no hay delete como en C++). Cuando un objeto deja de ser alcanzable desde cualquier referencia viva, se convierte en candidato a ser eliminado por el recolector de basura (garbage collector, GC), un proceso de la JVM que libera memoria automáticamente.
Libro temporal = new Libro();
temporal.titulo = "Refactorización";
temporal = null; // el objeto queda inalcanzable: sera recolectadoTres cosas que debes saber ya, sin entrar en detalles:
- No controlas cuándo se ejecuta el GC. Puede ser inmediatamente o mucho después.
System.gc()es una sugerencia que la JVM puede ignorar; no lo uses. - El GC elimina la mayor parte de las fugas de memoria, pero no todas: si mantienes referencias vivas a objetos que ya no necesitas (por ejemplo, en una estructura global que crece sin límite), la memoria no se libera.
- No pongas lógica de limpieza en
finalize(): está obsoleto desde Java 9 y no debe usarse (lección 03-09).
El funcionamiento interno del GC, las generaciones y el ajuste de rendimiento se estudian en la lección 10-07.
- BiblioTech: primera versión de
Libro en funcionamiento
Libro en funcionamientoReunamos todo. Fichero src/main/java/com/nexussoftware/bibliotech/dominio/Libro.java:
package com.nexussoftware.bibliotech.dominio;
/**
* Representa un libro del catalogo tecnico de Nexus Software.
*
* Version 1: los campos son publicos de forma provisional para poder
* asignarlos con el operador punto. En la leccion 03-07 pasaran a ser
* privados y se accedera a ellos mediante operaciones controladas.
*/
public class Libro {
/** Numero total de libros creados durante la ejecucion. */
public static int librosCreados = 0;
public String titulo;
public String autor;
public String isbn;
public int anioPublicacion;
public boolean disponible;
}Fichero Prestamo.java, todavía mínimo (sus operaciones llegan en 03-03 y su constructor en 03-04):
package com.nexussoftware.bibliotech.dominio;
/** Registro de un prestamo de un libro a un empleado. */
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;
public static int prestamosCreados = 0;
public Libro libro;
public String empleado; // pasara a ser un objeto Empleado en 03-04
public int diasTranscurridos;
}Y el arranque, BiblioTechApp.java:
package com.nexussoftware.bibliotech;
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.0 - Catalogo de objetos ===");
// --- Creacion de los tres libros del catalogo ---
Libro javaEfectivo = new Libro();
javaEfectivo.titulo = "Java Efectivo";
javaEfectivo.autor = "Joshua Bloch";
javaEfectivo.isbn = "978-0000000001";
javaEfectivo.anioPublicacion = 2018;
javaEfectivo.disponible = true;
Libro.librosCreados++;
Libro patrones = new Libro();
patrones.titulo = "Patrones de Diseño";
patrones.autor = "Erich Gamma";
patrones.isbn = "978-0000000002";
patrones.anioPublicacion = 1994;
patrones.disponible = true;
Libro.librosCreados++;
Libro refactorizacion = new Libro();
refactorizacion.titulo = "Refactorización";
refactorizacion.autor = "Martin Fowler";
refactorizacion.isbn = "978-0000000003";
refactorizacion.anioPublicacion = 2018;
refactorizacion.disponible = false;
Libro.librosCreados++;
// --- Listado ---
System.out.println("\nCatalogo:");
System.out.printf(" %-22s %-16s %-16s %4s %s%n",
"TITULO", "AUTOR", "ISBN", "ANIO", "ESTADO");
imprimir(javaEfectivo);
imprimir(patrones);
imprimir(refactorizacion);
System.out.printf("%nLibros creados: %d%n", Libro.librosCreados);
// --- Un prestamo que referencia a uno de esos libros ---
Prestamo p = new Prestamo();
p.libro = refactorizacion; // referencia al MISMO objeto
p.empleado = "Marta Ruiz";
p.diasTranscurridos = 20;
Prestamo.prestamosCreados++;
System.out.printf("%nPrestamo de '%s' a %s (%d dias transcurridos)%n",
p.libro.titulo, p.empleado, p.diasTranscurridos);
System.out.printf("Reglas vigentes: %d dias de plazo, %.2f EUR/dia, tope %.2f EUR%n",
Prestamo.DIAS_PRESTAMO, Prestamo.TARIFA_DIARIA,
Prestamo.MULTA_MAXIMA);
}
/** Imprime una fila del catalogo. Metodo auxiliar; los metodos se explican en 03-03. */
private static void imprimir(Libro libro) {
String estado = libro.disponible ? "Disponible" : "Prestado";
System.out.printf(" %-22s %-16s %-16s %4d %s%n",
libro.titulo, libro.autor, libro.isbn,
libro.anioPublicacion, estado);
}
}Salida:
=== BiblioTech 3.0 - Catalogo de objetos === Catalogo: TITULO AUTOR ISBN ANIO ESTADO Java Efectivo Joshua Bloch 978-0000000001 2018 Disponible Patrones de Diseño Erich Gamma 978-0000000002 1994 Disponible Refactorización Martin Fowler 978-0000000003 2018 Prestado Libros creados: 3 Prestamo de 'Refactorización' a Marta Ruiz (20 dias transcurridos) Reglas vigentes: 15 dias de plazo, 0,25 EUR/dia, tope 20,00 EUR
Compara este código con el del módulo 2. Los datos de un libro ya no están dispersos: viajan juntos dentro de un objeto, y puedes pasar el libro entero a un método con un solo parámetro (imprimir(javaEfectivo)) en lugar de cinco. Y p.libro = refactorizacion; es aliasing intencionado: el préstamo apunta al mismo ejemplar del catálogo, así que cuando en la próxima lección marques p.libro.disponible = false, el catálogo se enterará solo.
Lo que todavía falta salta a la vista: los objetos nacen vacíos y hay que rellenarlos a mano campo a campo (constructores, 03-04), no saben hacer nada (métodos, 03-03), y con más de tres libros el código se vuelve inviable porque no hay dónde guardarlos: eso lo resuelve el módulo 5 con las colecciones.
Errores Comunes y Consejos
- Olvidar el
new.Libro libro;declara una referencia, no crea nada. Si la usas, obtienes unNullPointerException(si es campo) o un error de compilación por variable no inicializada (si es local). - Creer que
=copia el objeto. Es el error del apartado 8.b = a;crea un alias, no una copia. Si necesitas un objeto independiente, hay que construirlo. - Confundir
nullcon""o con0.nullsignifica "sin objeto". Una cadena vacía es un objetoStringperfectamente válido, sobre el que puedes llamar métodos; sobrenullno puedes llamar nada. - Poner el nombre del fichero distinto al de la clase pública.
class Libroenlibro.javano compila en sistemas sensibles a mayúsculas (todos los Linux, y la JVM en general). Deja que el IDE cree las clases por ti. - Declarar campos como
static"porque así se accede más fácil". Es la vía rápida a un estado global compartido por todos los objetos: sititulofuesestatic, ¡los tres libros tendrían el mismo título! Un campo esstaticsolo si su valor es genuinamente común a toda la clase. - Acceder a un campo
staticmediante una instancia.p1.prestamosCreadoscompila, pero engaña al lector. Escribe siemprePrestamo.prestamosCreados. - Consejo: ante un
NullPointerException, no busques dónde falló, busca dónde faltó asignar. El mensaje "because X is null" te da el nombre exacto de la referencia; retrocede hasta el punto donde debería haberse asignado. - Consejo: acostúmbrate ya a
Alt+Insert/ "Generate" del IDE. Generará constructores, getters ytoStringen cuanto los estudies. Pero escríbelos a mano las primeras veces: solo así entiendes qué genera.
Ejercicios
Ejercicio 1: la clase Empleado
Crea la clase Empleado en el paquete com.nexussoftware.bibliotech.dominio con:
- Campos públicos
nombre(String),identificador(String) yprestamosAcumulados(int). - Un campo
staticempleadosRegistradosque cuente cuántos se han creado. - Una constante
static finalMAX_PRESTAMOS_SIMULTANEOScon valor 3.
Después, en BiblioTechApp, crea los tres empleados de Nexus Software (Marta Ruiz con identificador EMP-001, Diego Alonso con EMP-002 y Nuria Vidal con EMP-003), asígnales 2, 0 y 4 préstamos acumulados respectivamente, imprime una tabla con printf y señala con un asterisco a quienes superen el máximo permitido. Muestra al final el contador de empleados registrados.
Ejercicio 2: demostrar el aliasing
Escribe un programa que demuestre, con salida por consola, las cuatro situaciones de la tabla del apartado 8, usando dos referencias Libro:
- Mutar el objeto a través de una referencia y ver el cambio desde la otra.
- Reapuntar una referencia a un objeto nuevo y comprobar que la otra no cambia.
- Comparar con
==dos referencias al mismo objeto y dos objetos distintos con idéntico ISBN. - Poner una referencia a
nully comprobar que el objeto sigue accesible desde la otra.
Explica en comentarios qué ocurre en memoria en cada paso.
Ejercicio 3: provocar y leer un NullPointerException
Escribe un fragmento que provoque deliberadamente un NullPointerException sobre un objeto Prestamo cuyo campo libro no se ha asignado. Copia la traza completa que obtienes, identifica en ella la referencia nula, la línea culpable y el campo al que se intentaba acceder. Después, corrige el programa sin usar try/catch, de dos maneras distintas: (a) con una guarda de nulidad, y (b) asignando el campo antes de usarlo. Razona cuál de las dos es mejor solución y por qué.
Soluciones
Solución 1
Empleado.java:
package com.nexussoftware.bibliotech.dominio;
/**
* Empleado de Nexus Software autorizado a tomar libros en prestamo.
* Campos publicos de forma provisional (ver leccion 03-07).
*/
public class Empleado {
/** Numero maximo de prestamos simultaneos permitidos por la politica interna. */
public static final int MAX_PRESTAMOS_SIMULTANEOS = 3;
/** Cuantos empleados se han registrado en esta ejecucion. */
public static int empleadosRegistrados = 0;
public String nombre;
public String identificador;
public int prestamosAcumulados;
}Uso en BiblioTechApp:
Empleado marta = new Empleado();
marta.nombre = "Marta Ruiz";
marta.identificador = "EMP-001";
marta.prestamosAcumulados = 2;
Empleado.empleadosRegistrados++;
Empleado diego = new Empleado();
diego.nombre = "Diego Alonso";
diego.identificador = "EMP-002";
diego.prestamosAcumulados = 0;
Empleado.empleadosRegistrados++;
Empleado nuria = new Empleado();
nuria.nombre = "Nuria Vidal";
nuria.identificador = "EMP-003";
nuria.prestamosAcumulados = 4;
Empleado.empleadosRegistrados++;
System.out.printf("%n%-14s %-9s %-10s %s%n", "EMPLEADO", "ID", "PRESTAMOS", "AVISO");
imprimirEmpleado(marta);
imprimirEmpleado(diego);
imprimirEmpleado(nuria);
System.out.printf("%nEmpleados registrados: %d (maximo %d prestamos por persona)%n",
Empleado.empleadosRegistrados,
Empleado.MAX_PRESTAMOS_SIMULTANEOS);private static void imprimirEmpleado(Empleado e) {
// El asterisco marca a quien supera el limite de la politica interna.
String aviso = e.prestamosAcumulados > Empleado.MAX_PRESTAMOS_SIMULTANEOS ? "*" : "";
System.out.printf("%-14s %-9s %-10d %s%n",
e.nombre, e.identificador, e.prestamosAcumulados, aviso);
}Salida:
EMPLEADO ID PRESTAMOS AVISO Marta Ruiz EMP-001 2 Diego Alonso EMP-002 0 Nuria Vidal EMP-003 4 * Empleados registrados: 3 (maximo 3 prestamos por persona)
Puntos clave: MAX_PRESTAMOS_SIMULTANEOS es static final porque la política es de la empresa, no de cada persona; empleadosRegistrados es static (uno para todos) mientras que prestamosAcumulados es de instancia (uno por empleado). Si confundieras ambos, los tres empleados compartirían el mismo número de préstamos, que es justamente el error del apartado de consejos.
Solución 2
package com.nexussoftware.bibliotech;
import com.nexussoftware.bibliotech.dominio.Libro;
public class DemoAliasing {
public static void main(String[] args) {
// --- Situacion 1: mutar a traves de un alias ---
Libro a = new Libro();
a.titulo = "Java Efectivo";
a.isbn = "978-0000000001";
a.disponible = true;
Libro b = a; // b y a apuntan al MISMO objeto del heap
b.disponible = false; // se muta el objeto compartido
System.out.println("1) a.disponible = " + a.disponible); // false
System.out.println(" b.disponible = " + b.disponible); // false
// En memoria: una sola caja Libro con dos flechas apuntando a ella.
// --- Situacion 2: reapuntar una referencia ---
b = new Libro(); // b pasa a apuntar a un objeto NUEVO
b.titulo = "Patrones de Diseño";
b.isbn = "978-0000000002";
System.out.println("2) a.titulo = " + a.titulo); // Java Efectivo
System.out.println(" b.titulo = " + b.titulo); // Patrones de Diseño
// En memoria: dos cajas distintas; la flecha de b se ha movido,
// la de a no se ha tocado.
// --- Situacion 3: identidad con == ---
Libro c = a; // alias de a
Libro d = new Libro();
d.isbn = a.isbn; // mismo ISBN, objeto distinto
System.out.println("3) a == c : " + (a == c)); // true
System.out.println(" a == d : " + (a == d)); // false
System.out.println(" mismo ISBN: " + a.isbn.equals(d.isbn)); // true
// == compara direcciones, no contenidos: a y d son dos ejemplares
// distintos aunque describan el mismo titulo. Para que se consideren
// iguales por ISBN habra que sobrescribir equals (leccion 03-09).
// --- Situacion 4: anular una referencia ---
c = null; // solo se borra la flecha de c
System.out.println("4) c es null: " + (c == null)); // true
System.out.println(" a.titulo sigue siendo: " + a.titulo); // Java Efectivo
// El objeto no se recolecta porque a sigue apuntando a el.
}
}Salida:
1) a.disponible = false b.disponible = false 2) a.titulo = Java Efectivo b.titulo = Patrones de Diseño 3) a == c : true a == d : false mismo ISBN: true 4) c es null: true a.titulo sigue siendo: Java Efectivo
La regla que hay que memorizar: el punto muta el objeto y se ve desde todos los alias; el = mueve la flecha de una sola variable.
Solución 3
Código que provoca el fallo:
Prestamo p = new Prestamo();
p.empleado = "Diego Alonso";
p.diasTranscurridos = 18;
// p.libro NO se ha asignado: vale null por defecto
System.out.println("Libro prestado: " + p.libro.titulo); // linea 15Traza obtenida:
Exception in thread "main" java.lang.NullPointerException:
Cannot read field "titulo" because "p.libro" is null
at com.nexussoftware.bibliotech.BiblioTechApp.main(BiblioTechApp.java:15)Lectura de la traza:
| Dato | Valor |
|---|---|
| Referencia nula | p.libro (no p, que sí está creado) |
| Campo accedido | titulo |
| Línea culpable | 15 de BiblioTechApp.java |
| Causa raíz | El campo libro nunca se asignó, así que conserva su valor por defecto null |
(a) Guarda de nulidad, con el patrón del módulo 2:
if (p.libro != null) {
System.out.println("Libro prestado: " + p.libro.titulo);
} else {
System.out.println("Prestamo sin libro asociado (registro incompleto).");
}(b) Asignar el campo antes de usarlo:
Prestamo p = new Prestamo();
p.libro = javaEfectivo; // ahora el prestamo tiene libro
p.empleado = "Diego Alonso";
p.diasTranscurridos = 18;
System.out.println("Libro prestado: " + p.libro.titulo); // funciona¿Cuál es mejor? La (b), sin ninguna duda. La opción (a) trata el síntoma: acepta que puedan existir préstamos sin libro y añade una comprobación que habría que repetir en cada punto donde se use p.libro. La opción (b) ataca la causa: un préstamo sin libro no es un préstamo, es un objeto en estado inválido que nunca debió existir.
La versión definitiva de esa idea llega en la lección 03-04: un constructor que exija el libro como parámetro hace que sea imposible crear un Prestamo incompleto, y entonces la guarda de nulidad sobra porque el caso no puede darse. Esa es la diferencia entre programar defensivamente y diseñar bien: el mejor NullPointerException es el que no puede ocurrir.
Conclusión
Has dado el salto de dibujar clases a escribirlas. Sabes declarar una clase y sus campos, y conoces la regla —a menudo olvidada— de que los campos se inicializan solos con 0, false o null, lo que hace que un objeto recién creado pueda estar perfectamente vacío y perfectamente inválido. Has visto qué hace exactamente new: reservar memoria en el heap, inicializar los campos y devolver una referencia que guardas en la pila; y has trazado ese reparto de memoria en diagramas. Manejas el operador punto para leer y escribir campos, entiendes null como ausencia de dirección y sabes leer un NullPointerException hasta identificar qué referencia falló y por qué. Has comprendido el aliasing, que es el origen de una parte enorme de los errores de quien empieza: b = a no copia, comparte. Distingues campos de instancia de campos static, has llevado las cuatro reglas de negocio de BiblioTech a constantes static final dentro de Prestamo, donde por fin están en su sitio, y sabes que la memoria la libera el recolector de basura sin que tú intervengas. Tu catálogo ya son objetos: Libro, Empleado y un Prestamo que los relaciona.
Pero tus objetos todavía no saben hacer nada: son cajas de datos que alguien manipula desde fuera. Falta la mitad viva de la POO. En la lección siguiente, Métodos, darás comportamiento a tus clases: aprenderás la anatomía completa de un método, descubrirás por qué en Java todo se pasa por valor —y qué implica eso, sorprendentemente, cuando el argumento es un objeto—, dominarás la sobrecarga, this, los varargs y la diferencia entre métodos static y de instancia. Y harás la primera gran refactorización del módulo: sacar de main los cálculos de días de retraso, multa y gravedad, y mudarlos a Prestamo, que es su dueño legítimo.
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
