Los cuatro pilares de la POO — visión panorámica
Unidad didáctica (UD): UD3 — Programación Orientada a Objetos con la API de Java Resultado de aprendizaje (RA): RA2 — Escribe y prueba programas sencillos, reconociendo y aplicando los fundamentos de la programación orientada a objetos. Criterios de evaluación: CE 2.1 (se han identificado los fundamentos de la programación orientada a objetos) de forma directa; base conceptual de CE 2.3, CE 2.4, CE 2.5, CE 2.6 y CE 2.8 Sección: 1 de 11 · Evaluación: 1.ª · Unidad completa: 26 h en el IES + 2 h en empresa (FCT) Encaje en el roadmap: inmediatamente después de UD2 (Control de Flujo y Depuración · RA3) y antes de UD4 (Clases y Objetos Propios · RA4)
En esta sección aprenderás:
- Qué es una clase y qué es un objeto, y por qué en Java no existe lo uno sin lo otro.
- Qué ocurre exactamente cuando el programa ejecuta
new, y cómo se organiza la memoria en pila y montón. - Qué es una referencia, por qué asignar un objeto a otra variable no copia nada, y por qué
nullprovocaNullPointerException. - Por qué la trampa de
==frente a.equals()(que conoces de la UD1) ocurre lo que ocurre: por fin la verás con el modelo de memoria delante. - Cómo funciona el operador punto para pedir servicios a un objeto o a una clase.
- Los cuatro pilares de la POO —encapsulación, abstracción, herencia y polimorfismo— presentados con ejemplos del API de Java que ya conoces.
Necesitas tener fresco (de unidades anteriores):
- De la UD1: variables y tipos primitivos;
Stringcomo tipo de referencia;Integer.parseInt();IO.println()dejava.lang(JEP 512); la trampa==frente a.equals(); clases implícitas convoid main(). - De la UD2: arrays (
new int[5],.length,Arrays.sort); excepciones contry-catchy la jerarquíaThrowable; el depurador de IntelliJ (breakpoints y ventana Variables).
1.1 Llevas dos unidades usando objetos (sin saberlo)
Empiezo con una confesión: te he estado ocultando algo desde el primer día. Cada vez que escribiste "hola", o new int[5], o capturaste una NumberFormatException, estabas manejando objetos reales. Cada vez que llamaste a Integer.parseInt("42") o a IO.readln(), estabas hablando con clases. Has pasado dos unidades programando con objetos del API de Java sin que nadie te los hubiera presentado formalmente. Hoy vamos a arreglarlo, porque a partir de ahora esa ignorancia cómoda deja de ser gratuita.
La tabla siguiente repasa cosas que ya has escrito y traduce lo que había realmente debajo:
| Lo que escribiste | Dónde | Lo que había en realidad |
|---|---|---|
String saludo = "hola"; |
UD1, sección 7 | Un objeto de la clase String (un tipo de referencia, como ya avisamos entonces) |
int[] notas = new int[5]; |
UD2, sección 5 | Un objeto array, fabricado con new y guardado en el montón de memoria |
catch (NumberFormatException ex) |
UD2, sección 9 | La captura de un objeto de una clase que desciende de Exception |
Integer.parseInt("42") |
UD1, sección 8 | Una llamada a un método estático de la clase Integer |
IO.readln() |
UD1, sección 9 | Una llamada a un método estático de la clase IO, que vive en java.lang |
void main() { ... } |
UD1, sección 3 | Un método de instancia de una clase implícita que javac crea por ti (JEP 512) |
¿Y por qué nadie te lo explicó antes? Porque no hacía falta, y esa es la belleza del diseño del curso: para recorrer un array o validar un menú necesitas práctica, no teoría de objetos. Pero ahora sí importa, y por una razón muy práctica: el API de Java es una biblioteca gigante de clases listas para usar —miles de ellas—, y para sacarle partido tienes que saber cómo se leen, cómo se instancian y cómo se dialoga con sus objetos. Un programador profesional de Java pasa el día usando clases del API y clases escritas por sus compañeros de equipo, no reinventando ruedas.
Hay un detalle que vale la pena subrayar: Java es un lenguaje cien por cien orientado a objetos, hasta el punto de que todo código vive dentro de una clase. Cuando en la UD1 descubriste las clases implícitas de Java 25 (void main() sin clase visible ni static), no estabas escapando de las clases: javac te creaba una clase sin nombre por detrás. Ese atajo de la JEP 512 existe para que tus primeros programas no te asusten con ceremonia, no porque en Java haya código fuera del mundo orientado a objetos. Hoy que ya sabes programar, toca abrir el capó.
El plan de esta unidad (y de la siguiente)
En la UD3 aprendes a usar objetos de clases ya construidas por el API de Java: eres el conductor, no el mecánico. En la UD4 cruzarás al otro lado del taller y diseñarás tus propias clases, con encapsulación, constructores y herencia. Primero se conduce, luego se construye. Así que tranquilidad: en esta unidad nadie te va a pedir todavía que diseñes una clase.
1.2 Por qué existe la POO: el problema del código suelto
Para valorar la solución hay que entender el problema. Hasta ahora tus programas han sido pequeños: un menú, una calculadora, un juego de adivinar números. En ese tamaño, escribir una lista de instrucciones seguidas funciona perfectamente: lees datos, calculas y muestras resultados. Ese estilo se llama programación procedimental: el programa es una receta larga y los datos son ingredientes sueltos que la receta va tocando a su paso.
El drama llega cuando el programa crece. Imagina un videojuego gestionado solo con variables sueltas y arrays: la vida del jugador en un int, su inventario en un String[], y los enemigos en tres arrays paralelos (uno con los nombres, otro con la vida de cada uno, otro con su posición). Cada parte del programa necesita tocar esos mismos datos, y cualquier descuido los desincroniza: eliminas al enemigo 3 de un array y se te olvida quitarlo de los otros dos. A partir de ahí, el programa miente: los datos que muestra ya no cuadran entre sí. Los arrays paralelos son el antipatrón clásico del código sin clases; en la UD4 lo desmontaremos con una solución elegante (una clase Enemigo), pero la herida te la tienes que llevar hoy para entender por qué alguien inventó todo esto.
La programación orientada a objetos (en adelante, POO) propone lo contrario: en vez de datos por un lado y funciones por otro, cada unidad del programa empaqueta juntos sus datos y el comportamiento que opera sobre ellos. El jugador guarda su propia vida, su inventario y sus acciones; cada enemigo, las suyas. Nadie toca la vida del jugador "de rebote": se le pide al propio objeto que se dañe, que se cure o que informe de su estado. El dato y el código que lo mantiene coherente viven casados en la misma caja, y esa caja tiene cerradura.
Una analogía de cocina lo deja claro. Imagina el restaurante sin orden: cualquier camarero entra en la cocina, retira platos de cualquier mesa, rellena cualquier estante y apunta las cuentas en cualquier libreta. Con dos camareros y cuatro mesas, aún funciona. Con veinte camareros y cien mesas, es un motín: nadie sabe quién tocó qué. La POO es la mise en place del software: cada estación de trabajo lleva sus ingredientes, sus utensilios y sus reglas, y solo se comunica con las demás a través de pedidos concretos.
¿Qué gana la industria con esto? Mucho más que comodidad:
- Modelado directo del mundo real:
Cliente,FacturaoPedidodel problema se traducen en clases del programa casi tal cual, con los mismos datos y las mismas operaciones. - Reutilización: una clase escrita, probada y documentada se puede usar mil veces. El API de Java son miles de clases que la industria lleva puliendo desde hace casi tres décadas: tú solo las instancias y las usas.
- Trabajo en equipo: cada desarrollador construye clases distintas con fronteras claras; mientras respeten el contrato, el conjunto encaja.
- Mantenimiento: cuando algo falla o cambia, el cambio queda encerrado dentro de la clase responsable, sin regar código ajeno por todo el proyecto.
- Y la razón más inmediata para ti: prácticamente todo el código Java profesional que tocarás en la vida —y las preguntas técnicas de las entrevistas de trabajo— está organizado en clases. No es una moda: es la lengua materna del lenguaje que estás aprendiendo.
Como un edificio que se sostiene sobre cuatro columnas, la POO descansa sobre cuatro pilares: encapsulación, abstracción, herencia y polimorfismo. Te los presentaré con calma en la sección 1.6. Pero antes necesitamos vocabulario de obra: qué es una clase, qué es un objeto, qué hace new con la memoria y cómo se habla con un objeto mediante el operador punto. Empecemos por el cimiento.
1.3 Clase y objeto: el plano y el edificio
Una clase es una plantilla: una descripción que indica qué datos guarda algo y qué operaciones sabe hacer. Un objeto es un ejemplar concreto construido a partir de esa plantilla, con valores propios para sus datos, viviendo en memoria durante la ejecución del programa. La relación es exactamente la del plano de una vivienda con las viviendas construidas: el plano se dibuja una vez y define estructuras (habitaciones, puertas, instalaciones eléctricas); con él se construyen muchas casas, cada una con sus muebles, sus habitantes y su desgaste particular. El plano no se habita; las casas sí.
Si prefieres otra imagen: la clase es el molde de galletas y los objetos son las galletas. El molde define la forma; cada galleta acaba con su propia decoración, su propio grosor y su propio destino (la de chocolate se la come tu hermana). Nadie se come el molde.
Formalicemos el vocabulario, porque lo usarás durante todo el curso:
| Término | Significado |
|---|---|
| Clase | Plantilla que describe un tipo de cosa: sus datos (llamados campos o atributos) y su comportamiento (sus métodos) |
| Objeto | Ejemplar concreto de una clase, con valores propios en sus campos, creado en memoria durante la ejecución |
| Instancia | Sinónimo formal de objeto. "Instanciar" = crear un objeto a partir de una clase |
| Campo / atributo | Dato que forma parte del estado de cada objeto (la "longitud" de un String, la "semilla" de un Random) |
| Método | Acción que los objetos de una clase saben realizar (length(), nextInt(), equals()) |
| Miembro | Colectivo: los campos y los métodos de una clase son sus miembros |
Observa el programa siguiente. Usa solo cosas que ya conoces, pero ahora léelo con las gafas nuevas:
Aquí hay dos objetos distintos de una misma clase. Ambos responden al mismo mensaje —length(), "dime tu longitud"— porque el comportamiento está definido una sola vez, en la clase. Pero cada uno contesta según su propio estado: uno tiene cuatro caracteres y el otro, tres. Estado propio, comportamiento compartido: esa frase resume la relación entre objeto y clase, y la volverás a usar cuando en la UD4 escribas tus propias plantillas.
Vamos a instanciar algo más vistoso. La clase Random del API (de java.util, la misma familia de paquetes que Arrays, que ya usaste en la UD2) fabrica generadores de números aleatorios. La estudiaremos a fondo en la sección 5 de esta unidad; hoy nos sirve como cobaya perfecta de new:
Dos detalles nuevos en solo cinco líneas. Primero, el import java.util.Random; del principio: como ya hiciste con java.util.Arrays en la UD2, hay que decirle al compilador dónde vive la clase que vas a usar (String, Integer o IO no lo necesitaban porque java.lang se importa siempre de forma automática). Segundo, la palabra new, que merece sección propia dentro de esta sección —la 1.4, justo aquí debajo—.
El diagrama resume la idea central de clase frente a objetos:
flowchart LR
clase["Clase (la plantilla)\nCoche\nEstado: color, velocidad\nComportamiento: arrancar, acelerar, frenar"]
o1["Objeto 1 (una instancia)\nCoche de Ada\nrojo · 120 km/h"]
o2["Objeto 2 (otra instancia)\nCoche de Alan\nazul · 0 km/h"]
clase -- "new (instanciar)" --> o1
clase -- "new (instanciar)" --> o2
style clase fill:#2e7d32,color:#ffffff,stroke:#1b5e20
style o1 fill:#c8e6c9,color:#000000,stroke:#2e7d32
style o2 fill:#c8e6c9,color:#000000,stroke:#2e7d32
Tres matices que conviene fijar ya:
- Una clase puede tener cero o muchísimas instancias. El número de objetos vivos en cada momento depende del programa; la clase, en cambio, existe una sola vez, escrita en su fichero, y no se gasta por mucho que la uses.
- La clase permanece; los objetos nacen y mueren. Un objeto vive mientras alguna referencia lo mantenga accesible (lo verás con el recolector de basura en la sección 1.4). Cuando muere, la plantilla sigue ahí, lista para fabricar otro.
- Los ocho tipos primitivos de la UD1 son la gran excepción.
int,double,boolean... no son clases ni generan objetos: son valores puros y sin comportamiento, pensados para máxima velocidad. Todo lo demás en Java —String, arrays,Random, las excepciones— son objetos. En la sección 8 de esta unidad conocerás las clases envoltorio (Integer,Double...) que "empaquetan" a los primitivos cuando hacen falta versiones orientadas a objetos de ellos.
Leer la plantilla con tus propios ojos
En IntelliJ, haz Ctrl+clic (o Ctrl+B) sobre la palabra String de cualquiera de tus programas: saltarás al código fuente de la clase String, dentro del JDK. Una clase no es un ente abstracto: es un fichero Java de carne y hueso, con sus campos y sus métodos, que puedes leer (está en inglés, como casi toda la documentación de la industria; te acostumbrarás). Date dos minutos de paseo y vuelve: acabas de abrir la caja de herramientas de la que hablaremos el resto de la unidad.
¿Recuerdas el operador instanceof que se mencionó de pasada en la UD1, entre los operadores del lenguaje? Ahora puedes entenderlo: x instanceof String pregunta si el objeto referenciado por x es una instancia de la clase String, es decir, si "salió de esa plantilla". No lo usaremos por ahora (su forma moderna, el pattern matching, está reservada para la UD7), pero deja de ser un sonido raro: es una pregunta de parentesco entre objetos y clases.
1.4 new y la memoria: la pila y el montón
Vamos a responder con precisión una pregunta que hasta ahora habíamos esquivado: ¿qué significa exactamente new? Cuando el programa ejecuta Random dado = new Random();, ocurren tres cosas, en este orden:
- Reserva memoria en el montón: se aparta espacio suficiente para un objeto de la clase
Random(sus campos internos más una pequeña libreta administrativa del propio objeto). - Inicializa el objeto: se ejecuta el código de puesta a punto de la clase, llamado constructor. Es el código que deja el objeto "listo para usar": el que arranca la semilla interna del
Random. Lo reconoces porque son los paréntesis que escribes junto anew; en la sección 2 lo manejaremos con naturalidad y en la UD4 lo escribirás tú mismo. - Devuelve una referencia:
newentrega la "flecha" que apunta al objeto recién creado, y esa flecha es lo que se guarda en la variabledado.
La mejor noticia es que esto ya lo habías hecho. ¿Recuerdas int[] notas = new int[5]; en la UD2? Ese new hacía exactamente lo mismo: reservaba memoria en el montón para un array de cinco enteros, lo inicializaba (a ceros, como vimos entonces) y devolvía la referencia que guardabas en notas. Un array es un objeto: por eso tiene miembros como .length a los que accedes con el operador punto. Llevas desde la UD2 fabricando objetos sin saberlo.
Pila y montón: la mesa de trabajo y el almacén
Para entender el paso 3 (la referencia) hay que conocer los dos territorios de memoria que maneja la JVM:
- La pila (stack) es la mesa de trabajo de cada hilo del programa: pequeña, rapidísima y de acceso inmediato. Ahí viven las variables locales de
main(y, cuando en la UD4 escribas métodos propios, las de cada método, apiladas en marcos que se montan al llamar y se retiran al volver). Cuando el método termina, su mesa se limpia sola. - El montón (heap) es el almacén general: mucho más grande, compartido por todo el programa, y donde viven todos los objetos creados con
new. Nadie lo limpia al salir de un método; de eso se encarga el recolector de basura, del que hablamos más abajo.
| Pila (stack) | Montón (heap) | |
|---|---|---|
| Qué guarda | Variables locales: primitivos con su valor, referencias con su "flecha" | Los objetos completos, con todo su estado |
| Tamaño | Pequeño (megabytes) | Grande (gigabytes configurable) |
| Velocidad | Ultrarrápida, acceso directo | Almacenamiento gestionado |
| Vida de lo que guarda | La del bloque o método que la declara | Mientras exista alguna referencia al objeto |
| Quién la limpia | Automático, al terminar cada método | El recolector de basura (GC) |
La consecuencia de esta división es la distinción más importante de toda la sección. Fíjate en estas dos líneas:
En el primer caso, la variable vidas contiene el valor 3, directamente, en la pila. En el segundo, la variable frase no contiene el texto: contiene una referencia —una flecha— que apunta al objeto String que sí está en el montón. La variable es la etiqueta del estante, no el contenido del estante. Es la diferencia entre tener el billete en la mano (primitivo) y tener la llave de la taquilla donde está guardado (referencia).
flowchart LR
subgraph pila["Pila (stack): la mesa de trabajo"]
v1["variable: dado\ntipo: Random\ncontiene: una referencia"]
v2["variable: frase\ntipo: String\ncontiene: una referencia"]
v3["variable: vidas\ntipo: int\ncontiene: el valor 3"]
end
subgraph monton["Montón (heap): el almacén de objetos"]
o1["objeto Random\nestado interno oculto"]
o2["objeto String\n'Java'"]
end
v1 -- apunta a --> o1
v2 -- apunta a --> o2
style pila fill:#e3f2fd,stroke:#1565c0
style monton fill:#fff3e0,stroke:#e65100
Referencias: dos etiquetas, un mismo objeto
Si la variable guarda una flecha y no el objeto, entonces una asignación entre dos variables de tipo referencia copia la flecha, no el objeto. Compruébalo con material de la UD2:
Ahora hay dos etiquetas y un solo objeto. Cualquier cambio hecho a través de otra lo ves por datos, porque no hay "un array de datos y otro de otra": hay un array con dos nombres. Es el clásico "dos mandos a distancia para el mismo televisor": no importa qué mando pulses, el televisor que cambia de canal es el mismo.
¿Y ese texto raro que sale al imprimir un array entero?
Si alguna vez escribiste IO.println(datos) esperando ver [5, 10, 15] y obtuviste algo como [I@1b6d3586, ya puedes leerlo: [I es el nombre interno de la clase de los arrays de int (¡un array es un objeto, y tiene clase!) y @1b6d3586 es un identificador hexadecimal derivado del objeto concreto. No es la dirección exacta en memoria, pero sirve para lo que aquí importa: distingue a cada objeto de sus vecinos. Dos arrays con contenido idéntico muestran identificadores distintos, porque son dos objetos distintos.
null y la famosa NullPointerException
Una referencia puede estar "en blanco": no apuntar a ningún objeto. Ese estado se escribe null, y ya te cruzaste con él en la UD2 (los arrays sin inicializar valen null). La regla es simple y sin excepciones: si usas el operador punto sobre una referencia null, el programa lanza NullPointerException en ese instante.
Cuando en la UD2 viste tu primer NullPointerException, era un mensaje críptico. A partir de hoy es una frase con sentido: "alguien intentó usar una referencia que no apunta a ningún objeto". El 90 % de los NullPointerException de la vida profesional se leen así, y el depurador de IntelliJ te dice exactamente en qué línea ocurrió.
El recolector de basura: la limpieza automática del almacén
¿Qué pasa con un objeto cuando ya ninguna variable apunta a él? Se convierte en basura: ocupa sitio en el montón pero es inalcanzable. Java no te obliga a liberarlo a mano (no existe free ni delete, como en C o C++): el recolector de basura (Garbage Collector, GC) lo detecta y devuelve esa memoria al almacén por su cuenta, de vez en cuando, sin que tú lo llames. Por eso en los ejemplos nunca "borramos" objetos: simplemente dejamos de apuntarlos (por ejemplo, reutilizando la variable para otro objeto, o saliendo del método donde vivía su referencia). No es magia infalible —cuando conozcas las colecciones en la UD5 veremos que ciertos descuidos pueden retener objetos vivos sin querer—, pero a efectos de esta unidad la regla es: la memoria de los objetos la gestiona Java.
La trampa de la UD1, explicada por fin
En la UD1 te contamos la trampa clásica de == frente a .equals() con String, y te pedimos que confiaras. Hoy puedes verla entera, con el modelo de memoria en la mano:
Ahora el porqué es mecánico. == compara lo que hay dentro de las variables, y dentro hay flechas: ¿apuntan al mismo objeto? Como new String(...) fabricó un objeto nuevo, las flechas son distintas y == responde false, aunque el contenido sea idéntico. En cambio, .equals() envía un mensaje al objeto de a preguntándole: "¿eres igual que el de esta otra flecha?", y el objeto compara su contenido con el del otro. Identidad frente a igualdad de contenido:
flowchart LR
subgraph pila["Pila"]
va["a"]
vb["b"]
end
subgraph monton["Montón"]
s1["objeto 1\n'java'"]
s2["objeto 2\n'java'"]
end
va --> s1
vb --> s2
va -. "a == b es false: flechas distintas" .- vb
s1 -. "a.equals(b) es true: contenido igual" .- s2
Dos notas honestas para cerrar el tema. Primera: en la UD1 mencionamos el pool de cadenas: los literales idénticos suelen compartir un mismo objeto en una zona especial del montón, por eso a veces == "funciona" con literales y da la falsa sensación de ser válido. En la sección 3 abriremos esa caja con calma; hoy quédate con la regla de oro: para comparar contenido, .equals(); == solo para saber si dos referencias son el mismo objeto (y para primitivos, donde no hay trampa posible). Segunda: con tipos primitivos, == compara valores y funciona perfectamente, porque la variable contiene el valor directamente. No confundas los dos mundos:
Variable primitiva: int vidas = 3; |
Variable de referencia: String frase = "Java"; |
|
|---|---|---|
| La variable contiene | El valor (un 3) |
Una flecha hacia el objeto |
| El dato vive en | La propia pila | El objeto vive en el montón |
== compara |
Valores: sin sorpresas | Identidad: mismo objeto o no |
Asignar b = a |
Copia el valor | Copia la flecha: alias del mismo objeto |
Puede valer null |
No | Sí (y usarla lanza NullPointerException) |
Pruébalo en IntelliJ: mira la memoria con el depurador
Escribe un programa con int vidas = 3;, String frase = "Java"; e int[] datos = {5, 10, 15};. Pon un breakpoint en una línea posterior (clic en el margen izquierdo del editor) y ejecuta en modo depuración. En la ventana Variables, que ya conociste en la UD2, verás vidas con su valor suelto, y frase y datos como referencias que puedes desplegar para inspeccionar el objeto al que apuntan. Acabas de ver la diferencia entre pila y montón sin salir del IDE. El depurador deja de ser caza-errores y se convierte también en microscopio.
1.5 El operador punto .: hablar con los objetos
Ya sabes qué es una clase, qué es un objeto y dónde vive cada cosa. Falta el verbo de esta gramática: el operador punto, que es la única puerta de entrada a los miembros de un objeto o de una clase. Cada vez que escribes saludo.length() estás enviando un mensaje al objeto referenciado por saludo: "oye, dime tu longitud". El objeto recibe el mensaje, ejecuta el código del método length() de su clase y produce una respuesta. Esa metáfora —los objetos se comunican por mensajes— es el corazón conceptual de la POO y la volverás a encontrar en cualquier lenguaje orientado a objetos que toques en el futuro.
Hay dos formas de usar el punto, y llevas unidades alternándolas sin que nadie les hubiera puesto nombre. La diferencia exacta entre ambas será el protagonista de la sección 2 de esta unidad; aquí las presento para que las reconozcas:
La analogía que mejor me funciona: un método de instancia es preguntarle a un taxista concreto cuánto costaría un trayecto —necesitas un ejemplar, y la respuesta depende de su taxímetro—; un método estático es consultar el listado de tarifas del gremio: es información del colectivo (la clase), no de ningún coche en particular, y no necesitas que haya ningún taxi en la calle para leerla. Por eso Math.sqrt() funciona sin ningún "objeto raíz cuadrada" dando vueltas: la operación pertenece al concepto, no a un ejemplar.
Campos y métodos: los paréntesis no son decoración
Los miembros de un objeto son de dos familias, y ya conoces un ejemplo de cada:
numeros.length accede a un campo: un dato que el objeto guarda y que se lee tal cual, sin ejecutar nada. "hola".length() invoca un método: un bloque de código que se ejecuta y devuelve un resultado. La regla visual es mecánica: si lleva paréntesis, es una llamada y algo se ejecuta; si no los lleva, es un dato. Confundir ambos —escribir numeros.length() y no entender por qué no compila— es uno de los tres errores más frecuentes del primer mes de cualquier programador Java, así que estampa la diferencia a fuego hoy mismo y te ahorrarás tardes de frustración.
Llamadas con datos y con respuesta
Un método puede recibir información adicional para hacer su trabajo y puede devolver un resultado. Ya usaste esta mecánica en la UD1 con formatted(); ahora nombra las piezas (si algún método de String no te suena, no te preocupes: llegan en la sección 3; quédate hoy con la mecánica de las llamadas):
Los valores que van entre paréntesis se llaman argumentos y son los datos concretos que le pasas al método para que haga su faena ("cámbiame esta letra por esta otra"). Lo que entrega de vuelta es el valor de retorno, que puedes guardar en una variable, usar dentro de otra expresión o ignorar si no te interesa. Piensa en un método como un empleado diligente: le encargas un trabajo con instrucciones (argumentos) y te entrega un resultado (retorno); algunos trabajos no necesitan instrucciones (length()) y algunos no entregan nada tangible, solo realizan la acción (IO.println(...) se limita a pintar en consola).
Encadenado: la cadena de puntos
Como muchos métodos devuelven objetos (o valores sobre los que se puede seguir llamando), las llamadas pueden encadenarse con una cadena de puntos, ejecutándose de izquierda a derecha:
Lee la línea en voz alta como una cinta de producción: frase.strip() toma el objeto original y devuelve un String nuevo sin espacios; sobre ese objeto nuevo actúa .toUpperCase(), que devuelve otro String en mayúsculas; ese último se guarda en resultado. El encadenado es ubicuo en código Java profesional y también su peligro: si cualquier eslabón devuelve null, el siguiente punto lanza NullPointerException (ahora ya sabes exactamente por qué). Cuando un encadenado falla, parte la cadena en variables intermedias y depura cada eslabón: técnica de andamiaje que te salvará más de una tarde.
Pruébalo en IntelliJ: el experimento de los paréntesis
Escribe int[] n = new int[3]; y prueba IO.println(n.length);. Añade ahora los paréntesis: IO.println(n.length());. El compilador se quejará: cannot find symbol method length() o similar. Acabas de comprobar en tu propia piel la diferencia entre campo y método: el primero existe, el segundo no. Repite el experimento al contrario con "hola".length (sin paréntesis) y leerás el error espejo. Dos minutos de experimento valen más que una página de teoría.
1.6 Los cuatro pilares: visión panorámica
Ya tienes el vocabulario completo, así que subamos al balcón para ver el edificio entero. La POO se sostiene sobre cuatro pilares que trabajan en equipo: encapsulación, abstracción, herencia y polimorfismo. No son cuatro features sueltas que Java activa o desactiva: son decisiones de diseño que explican por qué las clases del API se comportan como se comportan, y serán las herramientas con las que construyas tus propias clases en la UD4 y las dominarás en la UD7.
Alcance de esta sección: mirar, no tocar
Los cuatro pilares se presentan aquí como visión panorámica: definiciones, analogías y ejemplos del API que ya conoces. No se te pedirá aplicarlos con clases propias hasta la UD4 (encapsulación y herencia básica) y la UD7 (polimorfismo completo y abstracción con interfaces). Lo que sí se te pedirá desde ahora mismo es reconocerlos: cuando uses una clase del API o leas código profesional, deberías saber qué pilar estás viendo en cada línea.
1.6.1 Encapsulación: la caja con cerradura
La idea. La encapsulación consiste en empaquetar los datos y el comportamiento en una misma unidad y proteger el estado interno de esa unidad, de modo que el único modo de tocarlo sea a través de la interfaz pública que la clase decide ofrecer. El dato deja de estar "suelto en la cocina" y pasa a vivir dentro de una caja con cerradura, cuyas únicas ranuras de acceso son los métodos que la clase publica.
La analogía. Un cajero automático. El dinero está dentro, pero no puedes abrir la caja fuerte ni tocar los billetes: solo puedes usar los botones y ranuras que el banco decidió instalarte (consultar saldo, sacar dinero con tu PIN). ¿Es una limitación molesta? Al contrario: es lo que garantiza que nadie (tú incluido) retire efectivo saltándose las reglas. La caja protege y las ranuras controlan.
El ejemplo que ya conoces. String es un objeto inmutable: una vez creado, no existe ninguna forma de cambiar los caracteres que contiene. Puedes pedirle transformaciones (toUpperCase(), replace()...), y cada una te devuelve un String nuevo; el original queda intacto. Ese blindaje es encapsulación llevada al extremo: ni siquiera el propio objeto se deja modificar. Random encapsula de otra forma: guarda en su interior la semilla y el estado del generador, y no hay manera directa de leerlos o retocarlos; solo te ofrece sus servicios (nextInt(), nextBoolean()...). Cuando escribas dado. en IntelliJ, el autocompletado solo mostrará la fachada pública: ninguna variable interna manipulable. Eso es la encapsulación funcionando en tu editor.
Para qué sirve. Para dos cosas que se agradecen con el tiempo. Primero, integridad: si el estado solo puede cambiar siguiendo los caminos que la clase vigila, es mucho más difícil que un objeto acabe en un estado absurdo (una vida negativa, un DNI con letras). Segundo, libertad de mantenimiento: como nadie toca las tripas, el fabricante puede cambiarlas (mejorar el algoritmo, optimizar la memoria) sin romper ningún programa que use la clase, siempre que respete la fachada. Así puede el JDK evolucionar cada seis meses sin que se caiga el software del planeta.
Dónde se profundiza. En la UD4, sección 2, con los modificadores de acceso (private), los getters y setters y la validación en los setters; y en la UD5, sección 2, cuando conozcas las colecciones inmutables (List.of() y compañía).
1.6.2 Abstracción: conducir sin entender el motor
La idea. La abstracción consiste en ofrecer una visión simplificada de algo complejo: quedarse con las operaciones que importan al usuario y ocultar el cómo. Frente a la encapsulación, que protege el estado, la abstracción simplifica el comportamiento: decide qué botones existen y qué hace cada uno, y sepulta la mecánica interna bajo una interfaz limpia.
La analogía. Conducir un coche. Para conducir necesitas el volante, dos pedales y el cambio: esa es la interfaz. No necesitas conocer la inyección electrónica, la relación de compresión ni la gestión de la bujía. De hecho, puedes pasarte la vida conduciendo sin abrir el capó, y cambiar de coche (eléctrico, diésel, híbrido) sin rehacer tu carné: la abstracción es tan buena que el qué se mantiene aunque el cómo cambie por completo.
El ejemplo que ya conoces. Integer.parseInt("42"): una llamada, una línea. Dentro hay un mundo: recorrer caracteres, validar signos, detectar desbordamientos, gestionar errores con NumberFormatException... y a ti te da exactamente igual, porque la abstracción te entrega un int y te olvida. Lo mismo hará LocalDate.now() cuando lo conozcas en la sección 6: te devuelve la fecha del sistema sin que consultes relojes del sistema operativo, zonas horarias ni calendarios. Cuando un método es una línea y debajo esconde un mundo, estás usando una abstrucción bien diseñada.
Encapsulación y abstracción no son lo mismo. Se confunden a menudo porque ambas "esconden" cosas, pero esconden cosas distintas:
| Encapsulación | Abstracción | |
|---|---|---|
| ¿Qué oculta? | El estado interno (los campos) | La complejidad del comportamiento (el cómo funciona) |
| Nivel en el que actúa | Control de acceso: qué se puede tocar | Diseño de la interfaz: qué se ofrece y qué se omite |
| Pregunta que responde | "¿Quién puede leer o cambiar este dato?" | "¿Qué operaciones necesita el usuario de esta clase?" |
| Metáfora | La cerradura de la caja fuerte | El panel de mandos del coche |
| Ejemplo cercano | No puedes modificar los caracteres de un String |
parseInt("42") oculta todo el parseo |
Van de la mano —diseña el panel (abstracción) y cierra el chasis (encapsulación)—, pero son decisiones distintas, y separarlas mentalmente te hará mejor diseñador de clases en la UD4.
Dónde se profundiza. En la UD4, al decidir qué miembros exponer de tus clases; y en la UD7, con las clases abstractas y las interfaces: contratos que declaran qué se debe hacer sin fijar cómo, que es la abstracción elevada a herramienta de arquitectura.
1.6.3 Herencia: hijos de una jerarquía
La idea. La herencia permite que una clase nazca a partir de otra: la subclase adquiere los campos y métodos de su superclase y puede añadir los suyos propios o redefinir algunos. Entre ambas se establece una relación es-un ("is-a"): un NumberFormatException es una RuntimeException, que es una Exception. Las clases se organizan así en jerarquías, como un árbol genealógico de tipos.
La analogía. La taxonomía biológica que estudiaste en el instituto: todo perro es un mamífero, y todo mamífero es un animal. Heredas del nivel superior un paquete de rasgos (respirar, alimentarse) y cada nivel añade los suyos (ladrar, amamantar). La herencia de clases funciona igual: lo común se escribe una sola vez arriba, y cada rama especializa. La reutilización es inmediata: nadie redefine "respirar" en cada especie.
El ejemplo que ya conoces (y es bueno). Llevas usando herencia desde la UD2 sin que te lo dijeran: toda la gestión de excepciones se apoya en una jerarquía:
classDiagram
Throwable <|-- Error
Throwable <|-- Exception
Exception <|-- RuntimeException
Exception <|-- IOException
RuntimeException <|-- NumberFormatException
RuntimeException <|-- ArithmeticException
RuntimeException <|-- NullPointerException
RuntimeException <|-- IllegalStateException
Tres consecuencias de ese árbol que ya viviste en la UD2, ahora con su nombre técnico:
- Un
catch (Exception ex)atrapa cualquier subclase deException(NumberFormatException,ArithmeticException...). Lo usaste para elcatchgeneral de tus menús robustos: la variableex, de tipoException, puede contener objetos de cualquier clase colgante de esa rama. - El compilador te obligaba a ordenar los
catchde específico a general (NumberFormatExceptionantes queException). ¿Por qué era error ponerlos al revés? Porque elcatchgeneral, al aceptar toda la familia, se traga también el específico y lo deja inalcanzable. Ese mensajito tan molesto era el compilador defendiendo la jerarquía de herencia. - El método
ex.getMessage()que llamabas sobre cualquier excepción está escrito una sola vez, enThrowable, la cima de la jerarquía: todas las subclases lo heredan. Unas lo redefinen a su manera, otras lo usan tal cual, pero ninguna ha tenido que copiarlo.
Un matiz importante para cuando llegues a la UD4: la herencia modela es-un ("un deportivo es un vehículo"), no tiene-un ("un coche tiene un motor"). Lo segundo es composición, y en la UD7 verás que a menudo es la mejor opción aunque la herencia parezca más tentadora.
Dónde se profundiza. En la UD4, sección 7 (extends, super, @Override, herencia básica) —donde además crearás tus propias excepciones, el caso de uso más didáctico de herencia que existe— y en la UD7, sección 1, con herencia avanzada y diseño de jerarquías.
1.6.4 Polimorfismo: un mensaje, muchas respuestas
La idea. El polimorfismo ("muchas formas") permite que la misma llamada se comporte de forma distinta según el objeto que la reciba. El mensaje es uno; la respuesta depende de quién lo escuche. Es el pilar que hace que las jerarquías de la herencia sirvan para algo más que ahorrar código: convierten las ramas del árbol en piezas intercambiables.
La analogía. El mensaje "habla" dirigido a distintos animales: el perro ladra, el gato maúlla, el pájaro canta, el humano suelta un discurso. Petición idéntica, respuestas radicalmente distintas, y —esto es lo bonito— quien pide no necesita saber a qué animal se lo pide: el mensaje es el mismo y cada receptor lo ejecuta a su manera.
El ejemplo que ya conoces. Míralo con la jerarquía de excepciones y el getClass().getName() que ya usaste en la UD2:
La variable fallo tiene un tipo declarado (Exception), pero en cada momento contiene un objeto de una subclase distinta, y ese objeto se comporta como lo que es, no como lo que la variable declara. Amplía esa imagen a un programa real: un bloque catch (Exception ex) puede recibir hoy una NumberFormatException y mañana una ArithmeticException, y el código que escribiste una sola vez funciona con ambas sin distinguir. Eso, en esencia, es polimorfismo: escribir código genérico que funciona para toda una familia de tipos.
Dos matices que verás desarrollarse más adelante, para que no te pillen de nuevas:
- Sobrescritura (override): una subclase redefine el método heredado con su propia versión. Es el corazón del polimorfismo en ejecución: el perro "habla" ladrando porque su clase redefine
hablar(). Se despliega por completo en la UD7 (secciones 4 y 5), con el nombre técnico de enlace dinámico. - Sobrecarga (overload): mismo nombre de método con distintos parámetros en la misma clase. Ya te has cruzado con ella sin saberlo:
IO.println()admite lo que le eches. La escribirás tú en la UD4, sección 5.
Dónde se profundiza. En la UD7, secciones 4 y 5: polimorfismo completo, tipo aparente frente a tipo real, y pattern matching para interrogar objetos con elegancia. Es, con permiso de la encapsulación, el pilar más potente del lenguaje.
Los cuatro pilares, en una tabla
| Pilar | Responde a | Analogía | Ejemplo que ya conoces | Se domina en |
|---|---|---|---|---|
| Encapsulación | "¿Quién puede tocar el estado?" | El cajero automático | String inmutable; estado oculto de Random |
UD4 (private, getters, validación) |
| Abstracción | "¿Qué se ofrece y qué se oculta del comportamiento?" | Conducir sin abrir el capó | Integer.parseInt("42") |
UD4 (diseño) y UD7 (interfaces, abstractas) |
| Herencia | "¿Cómo se reutiliza y se organiza en jerarquías?" | Taxonomía de seres vivos | Throwable → Exception → NumberFormatException |
UD4 (extends básico) y UD7 (avanzada) |
| Polimorfismo | "¿Y si el mismo mensaje funciona para muchos tipos?" | "Habla": ladra, maúlla, canta | Exception ex conteniendo subclases distintas |
UD7 (enlace dinámico, pattern matching) |
flowchart LR
POO["Los cuatro pilares\n(hoy: visión panorámica)"]
E["Encapsulación"]
A["Abstracción"]
H["Herencia"]
P["Polimorfismo"]
UD4["UD4 — Clases y Objetos Propios\nprivate, getters, constructores,\nherencia básica, records"]
UD7["UD7 — POO Avanzada\ninterfaces, clases abstractas,\npolimorfismo completo, pattern matching"]
POO --> E
POO --> A
POO --> H
POO --> P
E --> UD4
H --> UD4
A --> UD7
P --> UD7
style POO fill:#2e7d32,color:#ffffff,stroke:#1b5e20
style UD4 fill:#6a1b9a,color:#ffffff,stroke:#4a148c
style UD7 fill:#00695c,color:#ffffff,stroke:#004d40
1.7 Tu asiento en el mapa del curso
Esta sección es la puerta de la unidad, y conviene que sepas exactamente qué hay detrás. La UD3 tiene once secciones; esta es la primera y la única puramente conceptual: el resto es práctica constante sobre clases del API, hasta que manejar objetos te salga tan natural como escribir un if.
flowchart LR
S1["1. Pilares de la POO\n(esta sección)"]
S2["2. Clases y objetos\nen la API"]
S3["3. String\nen profundidad"]
S4["4. Math"]
S5["5. Random"]
S6["6. java.time"]
S7["7. StringBuilder"]
S8["8. Wrappers\ny Objects"]
S9["9. Maven y\ndependencias"]
S10["10. Test"]
S11["11. Ejercicios"]
S1 --> S2 --> S3 --> S4 --> S5 --> S6 --> S7 --> S8 --> S9 --> S10 --> S11
style S1 fill:#2e7d32,color:#ffffff,stroke:#1b5e20
El recorrido tiene una lógica que ahora ya puedes apreciar: en la sección 2 aprenderás a leer la documentación del API y a distinguir con precisión miembros de instancia y estáticos (la distinción que hoy hemos presentado de pasada); después, sección a sección, irás ganando kilometraje con las clases más usadas de Java (String, Math, Random, java.time, StringBuilder, los wrappers); y al final, con Maven, incorporarás la primera dependencia externa (JUnit 5) a tu proyecto. Todo ello consolidado con el test de la sección 10 y los ejercicios de la 11.
Y en el mapa global del curso, la escalera de la POO tiene tres peldaños que ya conoces:
- UD3 (aquí y ahora): usar. Conduces clases ajenas con conciencia de clase, objeto, referencia y pilares.
- UD4: construir. Encapsulación con
private, constructores, herencia básica, records, enums. De conductor a ingeniero. - UD7: dominar. Polimorfismo completo, interfaces, clases abstractas, pattern matching, principios de diseño.
Para cerrar, la tabla de promesas: todo lo que esta sección ha mencionado de pasada tiene ya su destino asignado en el roadmap. Si algo te ha quedado con ganas, ahí está el duelo:
| Tema mencionado hoy | Dónde se verá |
|---|---|
| Diferencia exacta entre miembros de instancia y estáticos | Sección 2 de esta unidad |
| Inmutabilidad y pool de cadenas a fondo | Sección 3 de esta unidad |
Uso práctico de constructores y fábricas estáticas (LocalDate.now()) |
Secciones 5 y 6 de esta unidad; UD4, sección 3 |
Random, semillas y reproducibilidad |
Sección 5 de esta unidad |
| Clases envoltorio y autoboxing | Sección 8 de esta unidad |
Colecciones inmutables (List.of()) |
UD5, sección 2 |
Diseñar tus propias clases, constructores y private |
UD4, secciones 1-4 |
| Creación de excepciones propias | UD4, sección 10 |
Pattern matching con instanceof y switch con tipos |
UD7 (única entrada en el curso) |
1.8 Resumen, glosario y checklist
Si tuvieras que quedarte con diez ideas de esta sección, que fueran estas:
- Una clase es una plantilla (campos + métodos); un objeto es un ejemplar concreto con valores propios. Estado propio, comportamiento compartido.
newhace tres cosas: reserva memoria en el montón, inicializa el objeto (constructor) y devuelve una referencia.- La pila guarda las variables locales (primitivos con su valor; referencias con su flecha); el montón guarda los objetos. Mesa de trabajo frente a almacén.
- Asignar una referencia a otra variable no copia el objeto: crea un alias (dos etiquetas, un mismo objeto).
nulles la referencia en blanco; usar el punto sobre ella lanzaNullPointerException.==compara referencias (identidad);.equals()compara contenido. Con primitivos,==es seguro.- El operador punto da acceso a los miembros: campos (como
array.length, sin paréntesis) y métodos (como"hola".length(), con paréntesis). - Miembros de instancia: se piden a un objeto (
frase.length()). Miembros estáticos: se piden a la clase (Math.sqrt(),Integer.parseInt()). - Las llamadas se pueden encadenar porque cada método devuelve un valor sobre el que seguir llamando; si un eslabón es
null, la cadena revienta. - Los cuatro pilares: encapsulación (protege el estado), abstracción (oculta el cómo), herencia (jerarquías es-un), polimorfismo (un mensaje, muchas respuestas). Hoy se presentan; se aplican en la UD4 y se dominan en la UD7.
Glosario de la sección
| Término | Definición en una línea |
|---|---|
| Clase | Plantilla que define campos y métodos para un tipo de cosas |
| Objeto / instancia | Ejemplar concreto de una clase, con estado propio, vivo en memoria |
| Instanciar | Crear un objeto a partir de una clase, con new |
| Campo (atributo) | Dato que forma parte del estado de cada objeto |
| Método | Bloque de comportamiento que los objetos de una clase saben ejecutar |
| Miembro | Un campo o un método de una clase |
new |
Operador que reserva memoria, inicializa y devuelve la referencia de un objeto |
| Constructor | Código de inicialización que new ejecuta sobre el objeto recién creado |
| Referencia | Valor que una variable guarda para apuntar a un objeto del montón |
| Pila (stack) | Memoria rápida por hilo donde viven las variables locales |
| Montón (heap) | Memoria compartida donde viven todos los objetos |
null |
Referencia que no apunta a ningún objeto |
NullPointerException |
Excepción al usar el punto sobre una referencia null |
| Recolector de basura (GC) | Mecanismo de la JVM que libera los objetos sin referencias |
| Método de instancia | Miembro que se invoca sobre un objeto concreto |
| Método estático | Miembro que se invoca sobre la clase, sin objeto |
| Encapsulación | Pilar: el estado interno solo se toca por la interfaz pública |
| Abstracción | Pilar: se ofrece una visión simplificada que oculta el cómo |
| Herencia | Pilar: una subclase adquiere los miembros de su superclase (es-un) |
| Polimorfismo | Pilar: la misma llamada se comporta según el objeto que la recibe |
Checklist: al terminar, debes ser capaz de...
- [ ] Explicar con tus palabras la diferencia entre clase y objeto, y poner un ejemplo del API que no sea
String(CE 2.1). - [ ] Narrar los tres pasos que ejecuta
newy decir dónde queda cada cosa (pila y montón) (CE 2.1, base de CE 2.3 y CE 2.8). - [ ] Predecir el resultado de
datos == otradespués de una asignación entre variables de referencia (CE 2.1). - [ ] Explicar por qué
==falla con objetos y.equals()no, usando las palabras "referencia" e "identidad" (CE 2.1, CE 2.4). - [ ] Distinguir un campo de un método por los paréntesis y poner el ejemplo
lengthfrente alength()(CE 2.4). - [ ] Identificar cuál de los cuatro pilares está presente en un escenario dado (CE 2.1).
- [ ] Usar el depurador de IntelliJ para inspeccionar las referencias y los objetos de un programa (CE 2.9).
1.9 Autoevaluación de la sección
¿Esto no era la sección 10?
En efecto: el test oficial de repaso de la unidad vive en la sección 10, dividido por secciones. Este bloque es una autoevaluación rápida para hacer justo después de leer, sin material y sin presión: si fallas algo, vuelve a la subsección correspondiente antes de continuar. El orden de las preguntas sigue el orden de la sección.
Random a = new Random(); y Random b = a;, ¿cuántos objetos Random hay en memoria?
String frase = "Java";?
s1 y s2 son dos variables String con el mismo texto, ¿qué compara cada operador?
== compara referencias (si son el mismo objeto); s1.equals(s2) compara el contenido.
equals compara referencias y == compara el contenido.
== con String, así que solo se puede usar equals.
new int[5], ¿dónde queda el array?
datos.length se escribe sin paréntesis y "hola".length() con paréntesis?
length de los arrays es un campo (un dato) y length() de String es un método (una acción que se ejecuta).
length vive en la pila y length() en el montón.
NumberFormatException desciende de RuntimeException, que desciende de Exception. Esa cadena de tipos es un ejemplo directo de...
Exception fallo ha contenido primero un NumberFormatException y luego un ArithmeticException, comportándose cada uno según su clase real. ¿Qué pilar anticipa la escena?
String s = null; IO.println(s.length());?
Cuando las diez preguntas te salgan sin dudar, estás listo para la sección 2 — Clases y objetos en la API de Java, donde aprenderás a leer la documentación oficial del JDK con criterio y a dominar la diferencia entre miembros de instancia y estáticos, el detalle que hoy hemos dejado presentado de pasada.