Saltar a contenido

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é null provoca NullPointerException.
  • 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; String como tipo de referencia; Integer.parseInt(); IO.println() de java.lang (JEP 512); la trampa == frente a .equals(); clases implícitas con void main().
  • De la UD2: arrays (new int[5], .length, Arrays.sort); excepciones con try-catch y la jerarquía Throwable; 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, Factura o Pedido del 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:

void main() {
    // Dos objetos de la clase String: cada uno, con su contenido propio
    String saludo = "Hola";   // objeto 1: la secuencia de caracteres H-o-l-a
    String alumno = "Ada";    // objeto 2: la secuencia A-d-a

    // El operador punto pide servicios a CADA objeto en particular
    IO.println(saludo.length());   // 4: la longitud de ESTE objeto
    IO.println(alumno.length());   // 3: la longitud de ESTE otro

    // El comportamiento es el mismo (el "manual" está en la clase);
    // el resultado depende del estado de cada objeto concreto
}

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:

import java.util.Random;

void main() {
    // La clase Random es la plantilla; new fabrica un objeto a partir de ella
    // (adelanto de la sección 5, donde la estudiaremos a fondo)
    Random dado = new Random();

    // El objeto guarda su propio estado interno y produce
    // una secuencia de números pseudoaleatorios
    IO.println(dado.nextInt(6) + 1);   // tirada de dado: entre 1 y 6
    IO.println(dado.nextInt(6) + 1);   // otra tirada, pedida al MISMO objeto
}

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:

Use mouse to pan and zoom
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:

  1. 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.
  2. 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.
  3. 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:

  1. 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).
  2. 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 a new; en la sección 2 lo manejaremos con naturalidad y en la UD4 lo escribirás tú mismo.
  3. Devuelve una referencia: new entrega la "flecha" que apunta al objeto recién creado, y esa flecha es lo que se guarda en la variable dado.

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:

int vidas = 3;               // variable PRIMITIVA: la caja contiene el valor 3
String frase = "Java";       // variable de REFERENCIA: la caja contiene una flecha

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).

Use mouse to pan and zoom
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:

1
2
3
4
5
6
7
8
9
void main() {
    int[] datos = { 5, 10, 15 };   // un objeto array en el montón (ya lo hacías en UD2)
    int[] otra = datos;            // NO copia el array: copia la REFERENCIA

    otra[0] = 99;                  // modifico "el de otra"... ¿o el de datos?

    IO.println(datos[0]);          // 99: ¡porque es el MISMO objeto!
    IO.println(datos == otra);     // true: ambas variables apuntan al mismo array
}

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.

1
2
3
4
5
6
7
void main() {
    String nombre = null;          // referencia en blanco: no apunta a ningún objeto

    IO.println(nombre.length());   // ¡NullPointerException en esta línea!
    // Le pedimos .length() a una etiqueta que no tiene estantería detrás:
    // es como pulsar los botones de un mando que no tiene televisor asignado
}

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:

1
2
3
4
5
6
7
8
void main() {
    String a = "java";
    String b = new String("java");   // SOLO para el experimento: fuerza un objeto NUEVO
                                     // (en código real no se hace; lo explica la sección 3)

    IO.println(a == b);       // false: a y b apuntan a objetos DISTINTOS (identidad)
    IO.println(a.equals(b));  // true:  ambos objetos contienen los mismos caracteres
}

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:

Use mouse to pan and zoom
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:

void main() {
    // Forma 1: miembro de INSTANCIA. Se pide a un OBJETO concreto.
    String frase = "hola mundo";
    IO.println(frase.length());           // 10 caracteres de ESTE objeto concreto

    // Forma 2: miembro ESTÁTICO. Se pide a la CLASE, sin objeto alguno.
    IO.println(Integer.parseInt("42"));   // 42: servicio de la clase Integer
    IO.println(Math.sqrt(2));             // 1.4142...: servicio de la clase Math
    IO.println(Math.max(3, 9));           // 9: otro servicio de la clase Math
}

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:

1
2
3
int[] numeros = new int[3];
IO.println(numeros.length);      // length SIN paréntesis: es un CAMPO (un dato)
IO.println("hola".length());     // length() CON paréntesis: es un MÉTODO (una acción)

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):

1
2
3
4
5
6
String texto = "programar es pensar";

int letras = texto.length();            // llamada SIN argumentos: respuesta 19
String grito = texto.toUpperCase();     // llamada SIN argumentos (adelanto de la sección 3)
boolean empiezaBien = texto.startsWith("pro");   // llamada CON un argumento: true
String cambiado = texto.replace('a', 'e');       // llamada CON dos argumentos: "progremer es penser"

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:

1
2
3
4
5
6
7
8
9
void main() {
    String frase = "  Java 25  ";

    // Cada punto habla con el objeto que devolvió la llamada anterior
    String resultado = frase.strip().toUpperCase();   // (adelanto de la sección 3)

    IO.println(resultado);            // "JAVA 25": sin espacios y en mayúsculas
    IO.println(resultado.length());   // 7
}

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:

Use mouse to pan and zoom
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:

  1. Un catch (Exception ex) atrapa cualquier subclase de Exception (NumberFormatException, ArithmeticException...). Lo usaste para el catch general de tus menús robustos: la variable ex, de tipo Exception, puede contener objetos de cualquier clase colgante de esa rama.
  2. El compilador te obligaba a ordenar los catch de específico a general (NumberFormatException antes que Exception). ¿Por qué era error ponerlos al revés? Porque el catch general, 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.
  3. El método ex.getMessage() que llamabas sobre cualquier excepción está escrito una sola vez, en Throwable, 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:

1
2
3
4
5
6
7
8
void main() {
    // Una MISMA variable, de tipo Exception, contiene objetos de clases DISTINTAS
    Exception fallo = new NumberFormatException("no es un número");
    IO.println(fallo.getClass().getName());   // java.lang.NumberFormatException

    fallo = new ArithmeticException("/ por cero");
    IO.println(fallo.getClass().getName());   // java.lang.ArithmeticException
}

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)
Use mouse to pan and zoom
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.

Use mouse to pan and zoom
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:

  1. Una clase es una plantilla (campos + métodos); un objeto es un ejemplar concreto con valores propios. Estado propio, comportamiento compartido.
  2. new hace tres cosas: reserva memoria en el montón, inicializa el objeto (constructor) y devuelve una referencia.
  3. 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.
  4. Asignar una referencia a otra variable no copia el objeto: crea un alias (dos etiquetas, un mismo objeto).
  5. null es la referencia en blanco; usar el punto sobre ella lanza NullPointerException.
  6. == compara referencias (identidad); .equals() compara contenido. Con primitivos, == es seguro.
  7. El operador punto da acceso a los miembros: campos (como array.length, sin paréntesis) y métodos (como "hola".length(), con paréntesis).
  8. Miembros de instancia: se piden a un objeto (frase.length()). Miembros estáticos: se piden a la clase (Math.sqrt(), Integer.parseInt()).
  9. 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.
  10. 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 new y 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 == otra despué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 length frente a length() (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.

¿Cuál es la diferencia correcta entre clase y objeto? Un objeto es una plantilla y una clase es un ejemplar de esa plantilla. La clase es la plantilla que define estado y comportamiento; el objeto es un ejemplar concreto creado a partir de ella. Clase y objeto son sinónimos en Java. Una clase es un tipo primitivo ampliado y un objeto es una variable local. Tras ejecutar Random a = new Random(); y Random b = a;, ¿cuántos objetos Random hay en memoria? Dos objetos idénticos e independientes. Uno solo: b copia la referencia de a, así que ambas variables apuntan al mismo objeto. Ninguno, hasta que se llame por primera vez a nextInt(). Dos objetos que comparten automáticamente su estado interno. ¿Qué guarda exactamente una variable declarada como String frase = "Java";? Los caracteres de la cadena, uno detrás de otro, dentro de la propia variable. Una referencia (una "flecha") al objeto String, que vive en el montón. Una copia completa del objeto String dentro de la pila. El nombre de la clase y la longitud del texto, nada más. Si s1 y s2 son dos variables String con el mismo texto, ¿qué compara cada operador? Ambos comparan el contenido de las cadenas. == compara referencias (si son el mismo objeto); s1.equals(s2) compara el contenido. equals compara referencias y == compara el contenido. El compilador prohíbe usar == con String, así que solo se puede usar equals. Cuando el programa ejecuta new int[5], ¿dónde queda el array? En la pila, junto a la variable que lo declara. En el montón; la variable guarda una referencia a él. En el propio archivo .class, junto al código. En el pool de cadenas, como los literales String. ¿Por qué datos.length se escribe sin paréntesis y "hola".length() con paréntesis? Porque los arrays son más antiguos que la clase String en el API. Por una decisión de estilo sin más trascendencia técnica. Porque length de los arrays es un campo (un dato) y length() de String es un método (una acción que se ejecuta). Porque length vive en la pila y length() en el montón. "Un cajero automático no te deja meter la mano en la caja fuerte: solo te ofrece botones y ranuras." ¿Qué pilar describe la frase? Abstracción. Herencia. Encapsulación. Polimorfismo. NumberFormatException desciende de RuntimeException, que desciende de Exception. Esa cadena de tipos es un ejemplo directo de... Encapsulación. Abstracción. Herencia. Polimorfismo. Una variable declarada como 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? Polimorfismo. La encapsulación del recolector de basura. El pool de cadenas. La sobrecarga de operadores, que Java sí permite. ¿Qué ocurre al ejecutar String s = null; IO.println(s.length());? Imprime 0, porque la cadena está vacía. Imprime la palabra null. Lanza NullPointerException: se usa el punto sobre una referencia que no apunta a ningún objeto. No compila, porque null no se puede asignar a un String.

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.