Programación Orientada a Objetos
Clases y objetos
La plantilla, la instancia y la referencia: qué hace new, dónde vive el objeto y por qué dos nombres pueden mirar al mismo.
Desde Java 8
Este capítulo cubre Java 8. Estás leyendo en .
De valores sueltos a un objeto
La programación orientada a objetos (POO) parte de una idea simple: un programa son piezas que juntan estado y comportamiento, y se hablan entre ellas. Este capítulo construye la primera y enseña dónde vive.
El capítulo de variables acabó con las cajas: un Integer que puede ser null porque una caja ya
no es un número, es una referencia a un objeto. Aquí se ve qué hay al otro lado, con la memoria a la
vista.
Una clase agrupa los datos con las operaciones que los mantienen coherentes, y cada objeto es una copia con su propio estado: el saldo y el titular dejan de vivir sueltos.
public class Cuenta {
String titular;
long saldoEnCentimos;
Cuenta(String titular) {
this.titular = titular;
}
void ingresar(long centimos) { saldoEnCentimos += centimos; }
}La clase es la plantilla: no guarda ningún saldo, solo declara qué campos y qué métodos tendrá cada
cuenta. El objeto es la instancia, la que sí tiene titular y saldo. El saldo no aparece en el
constructor porque un campo nace con su valor por defecto, y el long por defecto es 0.
Cuenta primera = new Cuenta("Nuria");
Cuenta segunda = new Cuenta("Iker");
primera.ingresar(2500);
System.out.println(primera.saldoEnCentimos); // 2500
System.out.println(segunda.saldoEnCentimos); // 0Dos objetos, dos estados. Nada de lo que le pasa a uno se contagia al otro.
Los campos de esta primera versión no llevan modificador, así que cualquiera puede tocarlos
desde fuera. En los ejemplos siguientes aparecen ya private: el capítulo de encapsulamiento
explica por qué y qué se gana con ello.
new: reservar, rellenar, construir
La expresión new Cuenta("Nuria") hace tres cosas y devuelve una cuarta:
- Reserva en el montón (
heap) memoria para un objeto nuevo de esa clase. - Deja todos los campos con su valor por defecto:
0,falseonull. - Ejecuta el constructor con los argumentos que le has pasado.
- Devuelve una referencia al objeto, y ese es el resultado de la expresión.
La variable no guarda el objeto: guarda la referencia. Dos variables pueden guardar la misma, y
null es la que no apunta a ninguno.
La variable guarda la referencia
Cuenta a = new Cuenta("Nuria");
Cuenta b = a; // no copia la cuenta: copia la referencia
a.ingresar(2500);
System.out.println(b.saldoEnCentimos); // 2500, es la misma cuenta
Cuenta c = new Cuenta("Nuria");
System.out.println(a == b); // true, mismo objeto
System.out.println(a == c); // false, mismo titular, otra cuenta== pregunta si las dos referencias apuntan al mismo objeto; para el contenido está equals.
null es la referencia que no apunta a nada. Cualquier acceso a través de ella falla en ejecución.
Cuenta cuenta = null;
cuenta.ingresar(100); // NullPointerExceptionEl compilador no lo impide: null es legal en cualquier tipo referencia.
Pasar la referencia no mueve el objeto
Un método recibe una copia de la referencia. Con esa copia llega al mismo objeto, así que lo que mutas se ve fuera; reasignar el parámetro solo cambia la copia local.
static void ingresarMil(Cuenta cuenta) { cuenta.ingresar(1000); }
static void sustituir(Cuenta cuenta) { cuenta = new Cuenta("Otra"); }
Cuenta cuenta = new Cuenta("Nuria");
ingresarMil(cuenta);
System.out.println(cuenta.saldoEnCentimos); // 1000, la mutación viaja
sustituir(cuenta);
System.out.println(cuenta.titular); // Nuria, la reasignación noDos referencias, un objeto
-
01
Pila
cuentaLa variable del llamador guarda una referencia. -
02
Parámetro
cuentaOtra copia de la misma referencia, en el marco del método. -
03
Montón
Cuenta("Nuria")Un único objeto, con su saldo dentro. Los dos llegan aquí.
ingresar se ve fuera y cuenta = new Cuenta(...) no.Campos con valor por defecto, locales no
Un campo no necesita inicializador: cuando el objeto nace ya tiene 0, false o null según su
tipo. Una local no: el compilador comprueba que esté asignada en todos los caminos antes de leerla.
class Ejemplo {
int numero; // 0
boolean activo; // false
String texto; // null
void roto() {
int total;
total = total + 1; // no compila: no está inicializada
}
}El análisis es estático: el compilador mira el código, no lo ejecuta, y un if sin else que
asigna solo dentro deja el otro camino sin asignar. Inicializa las locales donde las declaras y
reserva el valor por defecto para los campos: uno de referencia que nadie asigna es un
NullPointerException esperando su turno.
this y la cadena de constructores
Dentro de un método de instancia, this es el objeto sobre el que se llamó. Desambigua cuando el
parámetro se llama igual que el campo, como en this.titular = titular.
En un constructor, this(...) llama a otro constructor de la misma clase. Tiene que ser la primera
instrucción, y así no se repite la inicialización.
public class Cuenta {
private final String titular;
private long saldoEnCentimos;
public Cuenta(String titular) {
this(titular, 0); // delega en el de abajo
}
public Cuenta(String titular, long saldoInicial) {
this.titular = titular;
this.saldoEnCentimos = saldoInicial;
}
public String titular() { return titular; }
}Dos restricciones: this(a); this(b); no compila, y this(...) no puede ir dentro de un if, así
que la inicialización condicional va en el constructor de destino.
static: de la clase, no del objeto
Un miembro static existe una sola vez, en la clase, y se comparte entre todas las instancias. Un
contexto estático no tiene this porque no hay un objeto concreto al que referirse.
public class Cuenta {
private static long totalCreadas;
private static final long LIMITE_DIARIO;
private final String titular;
static {
LIMITE_DIARIO = 1_000_000; // una vez, al inicializar la clase
}
Cuenta(String titular) {
this.titular = titular;
totalCreadas++;
}
static long cuentasCreadas() { return totalCreadas; }
}El bloque static { ... } corre una sola vez, cuando la clase se usa por primera vez, y antes de
que nadie lea sus miembros. Si falla, el fallo sale en ese primer uso, envuelto en
ExceptionInInitializerError (o tal cual, si era un Error), y la clase queda marcada como
errónea: los usos siguientes fallan con NoClassDefFoundError.
Un método static no puede leer un campo de instancia: recibe el objeto como parámetro.
Objetos que nadie apunta
Un objeto vive mientras haya una referencia alcanzable que llegue a él. Cuando se va la última, queda candidato a recogerse: no se libera en ese instante y la JVM no promete cuándo lo hará.
cuenta = null deja la cuenta inalcanzable si no hay otra referencia. Y System.gc() no recoge
nada: le pide a la JVM una recolección. La especificación no la obliga y algunas configuraciones la
ignoran, así que un programa que depende de ella está mal diseñado.
Los cuatro pilares
La programación orientada a objetos se apoya en cuatro propiedades, y esta sección las recorre en este orden. Cada una tiene su palabra clave, que es lo que vas a escribir en el código:
Las cuatro propiedades y su palabra clave
-
02
Encapsulamiento
privateCapítulo 2: el estado se protege dentro de la clase. -
03
Herencia
extendsCapítulo 3: una clase parte de lo que ya hace otra. -
03
Polimorfismo
@OverrideCapítulo 3: la misma llamada ejecuta el comportamiento del objeto real. -
04
Abstracción
interfaceCapítulo 4: programas contra el contrato, no contra la implementación.
Las cuatro van juntas y se explican mal por separado: sin encapsulamiento la herencia expone
estado que nadie debería tocar, y sin abstracción el polimorfismo se queda en un if disfrazado.
Los records del capítulo 6 no añaden una quinta: son una forma compacta de escribir el contenedor
de valores, y heredan equals, hashCode y toString del lenguaje.
Qué te llevas
- La clase es la plantilla; el objeto es la instancia, con su propio estado.
newreserva memoria, inicializa los campos, ejecuta el constructor y devuelve una referencia.- La variable guarda la referencia;
==compara referencias ynullno apunta a nada. - Usar una referencia
nulllanzaNullPointerExceptionen ejecución, no en compilación. - Al pasar un objeto se copia la referencia: la mutación se ve fuera y la reasignación no.
- Los campos llegan con valor por defecto; las locales deben estar asignadas antes de leerse.
thises el objeto actual ythis(...)encadena constructores en la primera línea.staticpertenece a la clase, no usathisy su inicializador corre una vez.- El recolector decide cuándo recoge lo inalcanzable;
System.gc()solo sugiere.