Saltar a contenido

UD2 · Sección 1 — Los cuatro pilares de la POO: visión panorámica

Dato Valor
Unidad Didáctica UD2 — Programación Orientada a Objetos con la API de Java
Resultado de Aprendizaje RA2 — Escribe y prueba programas sencillos, reconociendo y aplicando los fundamentos de la programación orientada a objetos.
Sección 1 de 10 — Los cuatro pilares de la POO (visión panorámica)
Evaluación 1.ª Evaluación
Duración estimada 2,5 horas lectivas
Criterios de evaluación cubiertos CE 2.1 (identifica los fundamentos de la POO), CE 2.3 (instancia objetos a partir de clases predefinidas — nivel conceptual), CE 2.4 (utiliza métodos y propiedades — nivel introductorio), CE 2.5 (llamadas a métodos estáticos — nivel introductorio)
Prerrequisitos UD1 completa
Paquete del código de ejemplo dam.programacion.ud2
Herramientas JDK 25, IntelliJ IDEA Community, Maven 3.9.x

1. Punto de partida

En la Unidad 1 escribisteis vuestro primer programa Java, instalasteis IntelliJ con Maven, dominasteis los tipos primitivos, jugasteis con String y hasta os atrevisteis con un if como aperitivo. Pero hay algo que seguramente os ha estado rondando la cabeza: la primera línea que escribisteis fue public class Main { ... } y aceptasteis que esa envoltura era necesaria sin entender del todo qué significaba. La palabra class estaba ahí, presente pero muda. Va a cobrar vida en esta unidad.

El cambio que vamos a dar ahora marca un antes y un después en cómo piensas cuando programas. Hasta ahora trabajamos con mentalidad procedimental: piensa un paso, escribe una instrucción, ejecútala. Funciona, pero cuando un programa crece, esa aproximación se vuelve un infierno de variables sueltas y funciones dispersas. La Programación Orientada a Objetos (POO) propone organizar el código alrededor de "quién lo hace" en lugar de "qué hay que hacer". Los protagonistas dejan de ser los verbos y pasan a ser los sustantivos.

Esta sección es deliberadamente panorámica. No crearemos clases propias (eso llega en UD4), ni profundizaremos en herencia (UD4 y UD7). Aquí sentamos las bases conceptuales: el plano del centro comercial antes de entrar en cada tienda.


2. POO: de verbos a sustantivos

Imagina que tienes que modelar el proceso de pedir un café en una cafetería. En estilo procedimental escribirías algo así:

1
2
3
4
5
1. Leer tipo de café.
2. Calcular precio.
3. Restar precio del monedero.
4. Encender la cafetera.
5. Calentar agua, moler grano, extraer, servir, entregar.

Todo son verbos. Funciona, pero ¿qué pasa si tienes varios clientes a la vez? ¿Y si el monedero cambia de cash a tarjeta? Tocas varias funciones sueltas.

En POO identificas los sustantivos y les das responsabilidades:

1
2
3
4
- Cliente: sabe qué café quiere y cuánto dinero tiene.
- Cafetera: sabe encenderse, calentar agua, moler, extraer.
- Vaso: sabe llenarse y entregarse.
- Cajero: sabe cobrar.

Cada sustantivo será un objeto. Si mañana cambia la lógica del cobro, solo tocas el cajero. El resto del sistema sigue intacto.

Analogía del restaurante que usaremos a lo largo de la sección:

Elemento del restaurante Equivalente en POO
La receta escrita del risotto La clase
Cada plato de risotto que sale al comedor Un objeto (instancia)
Los ingredientes del plato El estado del objeto (atributos)
Las operaciones que el chef aplica al plato Los métodos del objeto
El comensal pide "un risotto" sin saber qué pasa en la cocina Abstracción
El chef esconde los detalles de cocción tras la barra Encapsulación
El risotto hereda características del plato "arroz" Herencia
"Una ración" significa cosas distintas según pidas risotto, paella o macarrones Polimorfismo

Java es un lenguaje POO puro: todo vive dentro de una clase. No hay funciones sueltas como en C o Python. Cada función es un método de alguna clase, y cada dato que no es primitivo es un objeto.


3. Clase y objeto: el molde y la galleta

Esta es la distinción más importante de la sección. Si la entiendes, el 80% del trabajo está hecho.

  • Clase: plantilla, molde o plano que define qué atributos y qué métodos tendrán todas las instancias de un cierto tipo. No ocupa memoria para datos concretos, solo define la estructura.
  • Objeto: instancia concreta de una clase, creada en memoria, con valores específicos para sus atributos. Ocupa memoria y tiene identidad propia.

Analogía del molde de galletas

Tienes un molde con forma de estrella. El molde es la clase: define que todas las galletas tendrán forma de estrella, cinco puntas. Pero el molde no es comestible. Cada vez que estiras la masa y presionas, obtienes una galleta. Esa galleta es un objeto. Puedes hacer cien galletas con el mismo molde: todas comparten la forma, pero cada una es independiente. Modificar el molde afecta a las galletas futuras, no a las ya hechas.

Use mouse to pan and zoom
flowchart LR
    M["MOLDE\n(clase)"] -->|presionas| G1["Galleta 1\n(objeto)"]
    M -->|presionas| G2["Galleta 2\n(objeto)"]
    M -->|presionas| G3["Galleta 3\n(objeto)"]
Aspecto Clase Objeto
Naturaleza Plantilla, definición Instancia concreta
Memoria No ocupa datos Ocupa memoria en el heap
Cantidad Una por tipo Infinitos posibles
Cuándo se crea En tiempo de compilación En tiempo de ejecución, con new
Analogía Molde de galletas Cada galleta
Ejemplo Java String "Maria", "Carlos", "Java 25"

Lo que ya sabes sin saberlo: String es una clase

Desde la UD1 usas objetos sin saberlo:

1
2
3
String nombre = "Maria";
int longitud = nombre.length();
System.out.println("Hola, " + nombre);

Sin darte cuenta estabas: declarando una variable de tipo String (la clase), asignándole una instancia (el objeto "Maria"), llamando a un método (length()) y usando el operador + sobrecargado para concatenar. Java hace que los literales entre comillas dobles sean automáticamente instancias de String. Conceptualmente, "Maria" y "Carlos" son dos objetos distintos de la misma clase.

La palabra new permite crear objetos a propósito:

String nombre = new String("Maria");   // legal, pero no recomendable con String

Para otras clases, sin embargo, new será la única manera de crear instancias. Lo verás con Random, StringBuilder y otras en secciones posteriores.


4. Los cuatro pilares de la POO

La POO se sostiene sobre cuatro ideas fuerza. Aquí los presentamos como visión panorámica: definitions, analogías y un ejemplo de cada uno con elementos del API de Java que ya conoces.

Use mouse to pan and zoom
graph TD
    POO["Programación Orientada a Objetos"]
    POO --> E["Encapsulación<br/>Esconde el estado, expone interfaz"]
    POO --> A["Abstracción<br/>Oculta la complejidad"]
    POO --> H["Herencia<br/>Una clase hereda de otra"]
    POO --> P["Polimorfismo<br/>Una operación, varias formas"]

    style POO fill:#1565c0,color:#fff,stroke:#0d47a1
    style E fill:#2e7d32,color:#fff,stroke:#1b5e20
    style A fill:#ef6c00,color:#fff,stroke:#e65100
    style H fill:#6a1b9a,color:#fff,stroke:#4a148c
    style P fill:#c62828,color:#fff,stroke:#b71c1c

Encapsulación

Agrupar datos y métodos en una misma unidad (la clase) y proteger los datos del acceso externo directo, exponiendo solo lo necesario.

Analogía del cajero automático. No abres el cajero con destornillador para sacar dinero: pulsas unos botones y el cajero, por dentro, hace lo que tenga que hacer. Te da una interfaz limpia y esconde el resto.

Ejemplo. String encapsula su estado interno. Sabemos que guarda los caracteres en un array (desde Java 9, en un byte[] con codec). ¿Puedes acceder a ese array? No. Solo puedes usar métodos públicos como length() o charAt(int). Si los desarrolladores de Java cambiaran mañana la implementación interna, tu código seguiría funcionando porque solo dependes de la interfaz pública.

Abstracción

Centrarse en los aspectos esenciales, ignorando los detalles irrelevantes. Mientras la encapsulación es una técnica (cómo se esconde), la abstracción es una actitud mental (qué elegimos modelar).

Analogía del coche. Cuando aprendes a conducir, nadie te explica el ciclo termodinámico del motor. Aprendes que hay un pedal de acelerador, uno de freno, un volante. Toda la complejidad mecánica queda oculta.

Ejemplo. System.out.println("Hola, mundo"). No necesitas saber que hay un PrintStream, ni buffers, ni llamadas al sistema operativo. La abstracción es "dame un texto y yo me encargo de mostrarlo por consola".

Herencia

Mecanismo por el cual una clase (subclase) hereda atributos y métodos de otra (superclase), pudiendo añadir los suyos o redefinir los heredados.

Analogía. Un plato base llamado "arroz cocido". A partir de él, mediante especialización, surgen el risotto (con caldo y queso), la paella (con azafrán y marisco) y el arroz con leche (con azúcar y leche). Todos comparten la base, cada uno añade o modifica algo.

Ejemplo. La clase Integer (wrapper que viste en UD1) hereda de la clase abstracta Number. Esto significa que Integer tiene todo lo que tiene Number (por ejemplo, intValue()) más sus propios métodos (parseInt, toBinaryString). Aquí solo presentamos el concepto; en la UD4 crearás tus propias jerarquías con extends.

Polimorfismo

Capacidad de que una misma operación se comporte de manera distinta según el objeto sobre el que se invoque. Del griego "muchas formas".

Analogía de "imprimir". Si te digo "imprime esto", el verbo es el mismo, pero la acción concreta depende del contexto: si te lo digo a ti, lo escribes a mano; a una impresora láser, sale por la bandeja; a una impresora 3D, fabrica una pieza de plástico. Una palabra, cuatro comportamientos.

Ejemplo. System.out.println es polimórfico por sobrecarga. Si escribes println(42), println(3.14), println("hola") o println(true), cada llamada usa un println distinto (uno recibe int, otro double, otro String, otro boolean). El nombre es siempre println, el comportamiento interno varía según el tipo.

Pilar Idea central Ejemplo Java ya visto Se profundiza en
Encapsulación Estado protegido, interfaz pública String oculta su array interno UD4 (private/public)
Abstracción Modelo solo lo esencial System.out.println() UD4 (clases abstractas)
Herencia Una clase hereda de otra Integer extends Number UD4, UD7
Polimorfismo Una operación, muchas formas println(int) vs println(String) UD7

5. new y la memoria: heap vs. stack

La Máquina Virtual de Java divide la memoria en varias zonas; nos interesan dos: la pila (stack) y el montículo (heap).

Use mouse to pan and zoom
flowchart TB
    subgraph STACK["STACK (Pila)"]
        S1["Frame main()<br/>args, nombre, edad"]
        S2["Frame saludar()<br/>saludo"]
    end
    subgraph HEAP["HEAP (Montículo)"]
        H1["Objeto String 'Maria'<br/>@0x1a4f"]
        H2["Objeto String 'Carlos'<br/>@0x1b8c"]
    end
    S1 -.referencia.-> H1
    S1 -.referencia.-> H2

La pila (stack) es una zona de memoria por hilo, privada, que funciona como una pila de platos: el último en entrar es el primero en salir. Cada vez que se llama a un método, la JVM reserva un frame con los parámetros, las variables locales y la dirección de retorno. Cuando el método termina, el frame se destruye. Aquí viven los primitivos locales y las referencias a objetos.

El montículo (heap) es una zona compartida por todos los hilos. Aquí viven los objetos. Cada vez que haces new, la JVM reserva espacio en el heap, inicializa el objeto y te devuelve una referencia que se guarda en la pila. Los objetos pueden vivir más allá del método que los creó.

Característica Stack Heap
Qué guarda Primitivos locales, referencias, frames Objetos
Ámbito Por hilo, privado Compartido por todos los hilos
Ciclo de vida Automático, ligado a la llamada al método Gestionado por el garbage collector
Velocidad Muy rápida Más lenta
Errores típicos StackOverflowError OutOfMemoryError, NullPointerException

Analogía del polideportivo. La pila es como las taquillas del vestuario: cada usuario tiene la suya, con su llave; se asignan al entrar y se liberan al salir; son pequeñas y privadas. El heap es como el almacén grande del polideportivo: un espacio compartido donde se guardan colchonetas, redes, pelotas; cualquier monitor puede meter o sacar cosas, y pueden quedarse mucho tiempo. Cuando una pelota ya no se usa y nadie sabe dónde está, eventualmente alguien la tira (eso es el garbage collector).

En la pila, una variable local int es como una toalla en tu taquilla. Una referencia a un objeto es como un papelito en tu taquilla con la dirección de dónde está la pelota en el almacén. Si pierdes el papelito, la pelota sigue en el almacén pero ya no sabes dónde está.

Caso especial: String y el pool de strings. Cuando escribes un literal como "Maria", Java no crea necesariamente un objeto nuevo en el heap: primero mira si ya existe un String con ese contenido en una zona especial llamada string pool. Si existe, te devuelve la referencia al existente. Por eso la comparación con == puede dar resultados sorprendentes: == compara referencias, no contenidos. De ahí que en UD1 insistiéramos en usar .equals() para comparar strings.

1
2
3
4
5
6
7
String a = "Maria";              // el literal "Maria" se crea en el pool
String b = "Maria";              // b apunta al mismo objeto del pool que a
String c = new String("Maria");  // c es un objeto nuevo, fuera del pool

System.out.println(a == b);        // true: misma referencia
System.out.println(a == c);        // false: referencias distintas
System.out.println(a.equals(c));   // true: mismo contenido

Implicaciones prácticas que recordarás:

  1. Pasar un objeto a un método pasa la referencia, no el objeto. Si el método lo modifica, el cambio se ve fuera.
  2. Pasar un primitivo pasa una copia del valor. El llamador no ve los cambios.
  3. Asignar un objeto a otra variable crea otra referencia al mismo objeto, no una copia.
  4. Llamar a un método sobre null provoca NullPointerException, el error más común de Java.

6. El operador punto .: tu mejor amigo

El operador . es probablemente el más utilizado en Java después del punto y coma. Su misión es acceder a miembros de un objeto o de una clase: atributos y métodos. La sintaxis general es referencia.miembro.

Métodos de instancia vs. métodos estáticos

  • Método de instancia: pertenece a un objeto concreto. Se llama con objeto.metodo(...).
  • Método estático: pertenece a la clase, no necesita instancia. Se llama con Clase.metodo(...).
1
2
3
4
5
6
7
// Método de instancia: depende del objeto concreto
String nombre = "Maria";
int n = nombre.length();      // 5: longitud DE ESTE string

// Método estático: utilidad de la clase, no necesita objeto
int max = Math.max(3, 7);     // max DE LA CLASE Math
int parsed = Integer.parseInt("42");  // parseInt DE LA CLASE Integer

La regla mental para decidir: "¿Necesito un objeto concreto para que este método tenga sentido?". Si la respuesta es sí, es de instancia. Si es no, es estático.

Encadenamiento

Si un método devuelve un objeto, puedes inmediatamente llamar a un método de ese objeto devuelto:

1
2
3
4
String texto = "  Hola, Mundo  ";
String resultado = texto.trim().toUpperCase().substring(0, 4);
// trim() -> "Hola, Mundo" -> toUpperCase() -> "HOLA, MUNDO"
// -> substring(0,4) -> "HOLA"

Se evalúa de izquierda a derecha. Es elegante, pero cuidado: si una etapa devuelve null, la siguiente lanza NullPointerException.

Desmenuzando System.out.println()

La llamada que más veces has escrito contiene, en una sola línea, toda la teoría de esta sección:

Fragmento Qué es
System Una clase del paquete java.lang
System.out Un atributo estático y público; referencia a un objeto de tipo PrintStream
System.out.println(...) Llamada al método de instancia println sobre ese PrintStream

Una sola línea usa miembros estáticos, miembros de instancia, referencias y el operador punto. Si la entiendes, tienes los fundamentos de POO asentados.


7. Ejemplo integrador

Programa que usa exclusivamente elementos ya vistos en UD1 y los conceptos nuevos de esta sección. No introduce nada de secciones posteriores.

package dam.programacion.ud2;

/**
 * Programa integrador de la Seccion 1 de UD2.
 * Demuestra los conceptos fundamentales de POO usando solo lo visto hasta ahora:
 * - La clase String como ejemplo de clase predefinida
 * - Variables locales (en la pila) y objetos String (en el heap)
 * - El operador punto para invocar metodos
 * - Metodos de instancia (length, charAt) y estaticos (Integer.parseInt)
 * - Los cuatro pilares ilustrados con la API de Java
 */
public class SintesisPilaresPOO {

    public static void main(String[] args) {

        // 1. Clase y objeto: "Maria" y "Carlos" son dos objetos de la clase String
        String cliente1 = "Maria";
        String cliente2 = "Carlos";

        // 2. Cada objeto tiene su propio estado (sus caracteres)
        // 3. El operador punto nos permite invocar metodos de instancia
        System.out.println("Cliente 1: " + cliente1 + " (longitud " + cliente1.length() + ")");
        System.out.println("Cliente 2: " + cliente2 + " (longitud " + cliente2.length() + ")");

        // 4. Encadenamiento: llamamos a varios metodos en secuencia
        char inicial = cliente1.toUpperCase().charAt(0);
        System.out.println("Inicial mayuscula de cliente1: " + inicial);

        // 5. Metodo estatico: parseInt pertenece a la clase Integer, no a un objeto
        int edad = Integer.parseInt("23");
        System.out.println("Edad parseada: " + edad);

        // 6. Polimorfismo por sobrecarga: println acepta int, String, char, double...
        System.out.println(edad);          // println(int)
        System.out.println(cliente1);      // println(String)
        System.out.println(inicial);       // println(char)
        System.out.println(3.14);          // println(double)

        // 7. Comparacion correcta: .equals() compara contenido, == compara referencias
        String otroCliente1 = "Maria";  // reutiliza el objeto del pool
        String cliente1ConNew = new String("Maria"); // objeto nuevo fuera del pool

        System.out.println("cliente1 == otroCliente1: " + (cliente1 == otroCliente1));          // true
        System.out.println("cliente1.equals(otroCliente1): " + cliente1.equals(otroCliente1));  // true
        System.out.println("cliente1 == cliente1ConNew: " + (cliente1 == cliente1ConNew));      // false
        System.out.println("cliente1.equals(cliente1ConNew): " + cliente1.equals(cliente1ConNew)); // true
    }
}

Salida esperada:

Cliente 1: Maria (longitud 5)
Cliente 2: Carlos (longitud 6)
Inicial mayuscula de cliente1: M
Edad parseada: 23
23
Maria
M
3.14
cliente1 == otroCliente1: true
cliente1.equals(otroCliente1): true
cliente1 == cliente1ConNew: false
cliente1.equals(cliente1ConNew): true

Antes de ejecutarlo, intenta predecir la salida. Si aciertas, has entendido la sección.


8. Errores comunes

Error Qué pasa Cómo evitarlo
Confundir clase con objeto Pensar que String y "Maria" son lo mismo String es la plantilla; "Maria" es una instancia concreta
Usar == con objetos Compara referencias, no contenidos Usa .equals() por defecto para comparar strings u objetos
Olvidar new cuando es necesario Error de compilación Para clases que no son String, normalmente hace falta new o un método factoría
Llamar a un método sobre null NullPointerException Inicializa las referencias y comprueba null cuando haya duda
Pensar que asignar un objeto lo copia Crea otra referencia al mismo objeto Recuerda: las referencias son como papelitos con la dirección del objeto en el heap

9. Resumen

Concepto Idea clave Ejemplo
Clase Plantilla que define estructura y comportamiento String
Objeto Instancia concreta, con su propio estado "Maria"
new Crea un objeto en el heap new String("Maria")
Heap Memoria compartida donde viven los objetos El objeto "Maria" vive aquí
Stack Memoria por hilo con variables locales y referencias La variable nombre vive aquí
Operador . Acceso a miembros nombre.length()
Método de instancia Pertenece a un objeto concreto nombre.length()
Método estático Pertenece a la clase Integer.parseInt("42")
Encapsulación Estado interno protegido String oculta su array
Abstracción Modelo solo lo esencial System.out.println()
Herencia Una clase hereda de otra Integer extends Number
Polimorfismo Una operación, varias formas println(int) vs println(String)
== vs .equals() Referencias vs. contenido a == b / a.equals(b)

10. Información ampliada y próximos pasos

Si te ha sabido a poco y quieres profundizar en algún concepto antes de seguir avanzando, aquí tienes recursos seleccionados:

Próximos pasos sugeridos:

  1. Probar el código de ejemplo en IntelliJ. Crea un proyecto Maven con paquete dam.programacion.ud2, copia SintesisPilaresPOO, ejecútalo y comprueba que la salida coincide con la predicha.
  2. Experimentar con == y .equals(). Crea un pequeño programa que declare varias variables String (unas con literales, otras con new String(...)) y compáralas. Predice el resultado antes de ejecutar.
  3. Avanzar a la Sección 2 de UD2. Te enseñará a leer la documentación oficial (Javadoc) y a explorar el API de Java (java.lang, java.util, java.math, java.time). Es el puente natural entre la teoría que acabas de aprender y las clases concretas que verás en las secciones 3 a 8.