Conceptos clave de POO de Java

Por qué la POO es más fácil de lo que parece

Cuando empiezas, oyes palabras como encapsulamiento o polimorfismo y suenan a examen. Pero la POO no es magia: es una forma de organizar código imitando objetos del mundo real. Si entiendes la analogía de la máquina expendedora o la cafetería, todo lo demás es memorizar reglas con ejemplos.

Imagina una cafetería: hay una receta (la clase), cafés servidos (los objetos), cada café tiene tamaño, azúcar y leche (atributos) y puede prepararse y servirse (métodos). A lo largo del artículo usaremos ese ejemplo para que sea memorable.

1. Clase (y 1.1 Clase abstracta)

Definición memorizable: la clase es la plantilla, molde o prototipo universal que define los atributos (variables) y métodos (comportamiento) que tendrán los objetos creados a partir de ella.

Realista: la clase Cafe no es un café real; es la receta general de todos los cafés.

public class Cafe {
    String tamano;      // atributo
    int azucar;         // atributo

    void preparar() {   // método
        System.out.println("Preparando " + tamano);
    }
}

1.1 Clase abstracta: cuando la plantilla es tan general que no tiene sentido instanciarla sola (nadie pide "un café genérico"), se declara abstracta y se usa como base para otras clases.

2. Objeto (y 2.1 Instancia de un objeto)

Definición memorizable: es una instancia concreta de una clase. Representa una entidad real en la memoria, con un estado propio y comportamientos definidos.

Realista: Cafe es el molde; miCafeDeHoy es el café que el barista te entregó.

Cafe miCafeDeHoy = new Cafe();

2.1 Instancia de un objeto: instanciar es "crear una copia tangible usando el molde". Cada new produce un objeto distinto con su propio estado.

3. Instanciación

Definición memorizable: crear un objeto real en memoria usando la clase como molde con new.

Cafe cafeDeLaManana = new Cafe();

Cada new genera un objeto nuevo, independiente y con su propio estado.

4. equals() y hashCode()

Definición memorizable: equals() decide CUÁNDO dos objetos son "iguales" (por contenido o el criterio que definas) y hashCode() genera un número que los agrupa en colecciones.

Realista: dos Cafe("mediano") creados con new son objetos distintos para ==, pero al comparar su contenido deberían ser iguales.

Cafe a = new Cafe("mediano");
Cafe b = new Cafe("mediano");

System.out.println(a == b);      // false: son objetos diferentes
System.out.println(a.equals(b)); // true: mismo contenido (si sobrescribes equals)

@Override
public boolean equals(Object o) {
    if (o instanceof Cafe otro) {
        return tamano.equals(otro.tamano);
    }
    return false;
}

Regla de oro: si sobrescribes equals(), sobrescribe también hashCode(); objetos iguales deben dar el mismo hashCode() o fallarán HashMap y HashSet.

5. static y miembros de instancia

Definición memorizable: static marca un miembro como PERTENECIENTE A LA CLASE (una sola copia compartida). Sin static, es de instancia (una copia por objeto).

Realista: la "receta de la cafetería" es de la clase; el "azúcar de tu café" es de la instancia.

public class Cafe {
    static String marca = "Café Perfetto"; // de la clase: Cafe.marca
    String tamano;                         // de la instancia: cafe.tamano
}

Cafe.marca = "Otra marca";   // cambia para TODAS las instancias
  • static se accede con el nombre de la clase: Cafe.marca.
  • Los miembros de instancia se acceden con el objeto: cafe.tamano.
  • En un método static no puedes usar this ni los miembros de instancia directamente.

6. Wrappers

Definición memorizable: clases que envuelven un tipo primitivo para tratarlo como objeto.

Realista: int tiene su versión objeto Integer, double tiene Double, boolean tiene Boolean y char tiene Character. Sirven para usar primitivos en colecciones, para poder tener null como "aún no definido" y para aprovechar métodos de utilidad.

int numero = 5;
Integer numeroObj = Integer.valueOf(numero); // boxing: primitivo -> objeto
int deVuelta = numeroObj.intValue();         // unboxing: objeto -> primitivo

Integer automatico = numero;   // autoboxing, el compilador lo hace solo
int autoBack = automatico;     // unboxing automático

Diferencia clave: int vale 0 por defecto; Integer puede ser null, es decir "no se ha definido".

7. Arrays

Definición memorizable: estructura que guarda varios valores del mismo tipo bajo un mismo nombre, en posiciones contiguas de memoria.

Realista: los tamaños disponibles de café en una cafetería.

String[] tamanos = { "chico", "mediano", "grande" };

System.out.println(tamanos[0]);     // "chico" (el índice comienza en 0)
System.out.println(tamanos.length); // 3

for (String tamano : tamanos) {
    System.out.println(tamano);
}

Regla: el primer elemento está en el índice 0 y el último en length - 1. Pedir un índice fuera de ese rango lanza una excepción.

8. String, StringBuilder y StringBuffer

Definición memorizable: String para texto que no cambia; StringBuilder para concatenar mucho texto en una sola hebra; StringBuffer igual pero seguro para múltiples hebras (hilos).

Realista: String es el cartel fijo; StringBuilder es la pizarra donde escribes y borras el mismo sitio.

String s = "cafe";                    // inmutable: "cafe" siempre será "cafe"
StringBuilder sb = new StringBuilder("cafe");
sb.append(" con leche");              // muta el MISMO objeto, sin crear copias
System.out.println(sb);               // "cafe con leche"

Regla: concadenar miles de veces con + crea objetos intermedios en memoria; con StringBuilder no. StringBuffer se usa cuando varios hilos modifican la misma cadena.

9. ¿String es inmutable?

Definición memorizable: sí, una vez creado, el texto de un String nunca cambia.

String nombre = "cafe";
nombre += " expresso";   // NO modifica "cafe": crea un NUEVO String

En el ejemplo, nombre pasa a apuntar a un objeto nuevo; el "cafe" original sigue en memoria sin cambios. Por eso compartir la misma referencia es seguro, pero concatenar en bucles es costoso.

10. Casting

Definición memorizable: convertir un objeto o un tipo a otro tipo compatible.

Realista: tienes un Cafe pero lo necesitas como CafeExpreso para acceder a doble, o lo subes a su tipo padre para pasarlo a un método genérico.

CafeExpreso expreso = new CafeExpreso();
Cafe cafe = expreso;                       // upcasting: subir al tipo padre, seguro
CafeExpreso deVuelta = (CafeExpreso) cafe; // downcasting: bajar al tipo real

Regla: subir es seguro, bajar exige que el objeto realmente sea de ese tipo. Si no lo es, lanzas una excepción en tiempo de ejecución.

11. Upcasting / downcasting

Definición memorizable: upcasting es SUBIR por la jerarquía (el objeto es el mismo, pero se ve como su superclase). Downcasting es BAJAR (recuperar el tipo concreto) y es la operación riesgosa.

CafeExpreso exp = new CafeExpreso();
Cafe cafe = exp;                            // upcasting: implícito y seguro
CafeExpreso vuelta = (CafeExpreso) cafe;    // downcasting: caste explícito

if (cafe instanceof CafeExpreso) {          // siempre verifica antes de bajar
    vuelta = (CafeExpreso) cafe;
}

Regla: subir siempre es seguro; bajar solo si el objeto realmente es de ese tipo (compruébalo con instanceof).

12. Atributo

Definición memorizable: son las variables internas de una clase que definen el estado, las características o los datos que puede almacenar un objeto.

Realista: el tamaño, la cantidad de azúcar y si lleva leche son atributos del café.

public class Cafe {
    private String tamano;   // atributo
    private boolean leche;   // atributo
}

Son el "qué es y qué sabe" el objeto en un momento dado.

13. Método (y 13.1 Métodos abstractos)

Definición memorizable: es una función definida dentro de una clase que determina el comportamiento o las acciones que puede realizar un objeto.

Realista: preparar(), servir() y calcularPrecio() son métodos del café.

public class Cafe {
    void preparar() {
        System.out.println("Café listo");
    }
}

14.1 Métodos abstractos: un método sin cuerpo (solo la firma) que declara qué debe hacer cada subclase, pero no cómo. Quien hereda está obligado a implementarlo.

14. Encapsulamiento

Definición memorizable: ocultar el estado interno de un objeto (con private) y permitir interactuar con él solo mediante métodos controlados (getters y setters).

Realista: el barista no te deja tocar los granos; tú pides y él te entrega el resultado. Tu código tampoco toca los atributos directamente.

public class Cuenta {
    private double saldo;   // nadie lo toca directamente

    public double getSaldo() {
        return saldo;
    }

    public void depositar(double monto) {
        if (monto > 0) {
            saldo += monto;
        }
    }
}

Regla de oro: atributos privados y acceso controlado. Es la "seguridad" de tu código.

15. Herencia

Definición memorizable: es el mecanismo por el cual una clase hija adquiere atributos y métodos de una clase padre, favoreciendo la reutilización de código.

Realista: CafeExpreso hereda todo lo de Cafe y solo agrega lo suyo: doble.

public class CafeExpreso extends Cafe {
    private boolean doble;
}

El hijo es un padre más algo extra. Reutilizas sin copiar y pegar.

16. Polimorfismo

Definición memorizable: significa "muchas formas": un mismo método se comporta distinto según el objeto que lo ejecuta.

Realista: preparar() es distinto para un expreso (a presión) y para un café de olla (hervido), pero ambos se invocan igual.

Cafe cafe = new CafeExpreso();
cafe.preparar(); // se ejecuta la versión del expreso

Misma llamada, comportamiento distinto. Permite tratar a todos como Cafe sin conocer el tipo exacto.

17. Abstracción

Definición memorizable: enfocarte solo en lo relevante del mundo real, ignorando los detalles complejos o irrelevantes para el sistema.

Realista: para la app de la cafetería, un Cafe es nombre, precio y tamaño; no te importa el color del empaque del grano.

public class Cafe {
    private String nombre;
    private double precio;
    private String tamano;
}

Simplificas la realidad hasta lo que el sistema necesita.

18. Interfaz (interface) e implements

Definición memorizable: es un contrato formal: un conjunto de métodos obligatorios que una clase debe implementar, logrando un desacoplamiento estricto.

Realista: "todas las máquinas que sirven bebidas" deben servir(). Tu código solo conoce el contrato, no la máquina concreta.

public interface Servible {
    void servir();
}

La palabra clave implements declara que una clase acepta ese contrato y queda obligada a implementar todos sus métodos.

public class MaquinaDeCafe implements Servible {
    @Override
    public void servir() {
        System.out.println("Sirviendo café");
    }
}

Obliga a cumplir el contrato y permite cambiar la implementación sin romper a quien la usa.

19. Clase abstracta

Definición memorizable: clase genérica que NO se puede instanciar directamente. Sirve como base común y puede tener métodos con código y métodos abstractos.

Realista: Bebida es abstracta; no creas "una bebida", creas un Refresco o un Cafe.

public abstract class Bebida {
    abstract void servir();           // obligatorio en los hijos
    void saludar() {                  // código real y compartido
        System.out.println("Disfruta tu bebida");
    }
}

Diferencia clave con la interfaz: la clase abstracta puede tener código o lógica compartida; la interfaz solo declara el contrato.

20. Clases abstractas vs interfaces

Definición memorizable: una clase abstracta es una base semi-terminada (puede tener código hecho y estado). Una interfaz es un contrato puro de métodos.

abstract class Maquina {
    String nombre;
    void encender() { System.out.println("Encendiendo"); }  // con implementación
    abstract void servir();
}

interface Servible {
    void servir();  // solo la firma
}

Cuándo usar cada una: si compartes código y estado, clase abstracta (extends). Si solo defines capacidades, interfaz (implements). Una clase solo extiende una clase, pero implementa varias interfaces.

21. Constructor

Definición memorizable: método especial que se ejecuta automáticamente al instanciar y que normalmente inicializa los atributos.

Realista: al crear tu café en la caja, la app inicializa su tamaño, precio y azúcar de una sola vez.

public class Cafe {
    private final String tamano;

    public Cafe(String tamano) {
        this.tamano = tamano;   // estado inicial
    }
}

Si no defines ninguno, existe el constructor por defecto. En cuanto defines uno, ese pasa a ser el tuyo.

22. Modificadores de acceso

Definición memorizable: palabras reservadas (public, private, protected) que controlan la visibilidad y el nivel de seguridad de atributos, métodos y clases.

Realista:

  • public: todos pueden verlo (el menú).
  • private: solo la clase (la receta secreta).
  • protected: la clase y sus hijas (la capacitación interna).
public class Cafe {
    private String recetaSecreta;   // solo aquí
    protected double margen;        // aquí y en subclases
    public String nombre;            // todos
}

23. final en variables, métodos y clases

Definición memorizable: final significa "no cambia". En variable: no puedes reasignarla. En método: no puedes sobrescribirlo. En clase: no puedes heredarla.

final int MAX_TAMANO = 5;      // constante: no se puede reasignar
final class Cafetera { }       // nadie puede heredar de Cafetera

class Maquina {
    final void encender() { }  // las subclases NO pueden sobrescribirlo
}

Por convención, las constantes final se nombran en MAYÚSCULAS con guiones bajos: MAX_TAMANO.

24. Sobrecarga (Overloading)

Definición memorizable: varios métodos con el MISMO nombre en la misma clase, pero diferente cantidad, orden o tipo de parámetros.

Realista: pedirCafe() sin argumentos, pedirCafe(tamano) y pedirCafe(tamano, azucar).

public class Cafeteria {
    void pedirCafe() {}
    void pedirCafe(String tamano) {}
    void pedirCafe(String tamano, int azucar) {}
}

El compilador decide cuál usar según los argumentos que le pasas.

25. Sobrescritura (Overriding)

Definición memorizable: una clase hija redefine o modifica un método que ya heredó de su padre, adaptándolo a su propio comportamiento.

public class CafeExpreso extends Cafe {
    @Override
    void preparar() {
        System.out.println("Apto a 9 bares de presión");
    }
}

Misma firma, nueva implementación. La anotación @Override avisa que estás cambiando lo que vino de la clase padre.

26. Sobrecarga vs sobrescritura

Definición memorizable: sobrecarga (overloading) = varios métodos con el mismo nombre pero DISTINTA firma en la misma clase. Sobrescritura (overriding) = reimplementar con la MISMA firma un método heredado.

class Cafe {
    void servir() { }
    void servir(boolean conLeche) { }   // SOBRECARGA: misma clase, distinta firma
}

class CafeConLeche extends Cafe {
    @Override
    void servir() { }                   // SOBRESCRITURA: igual firma, nueva lógica
}

Regla rápida: mismo nombre + distintos parámetros = overloading. Mismo nombre + iguales parámetros + @Override = overriding.

27. Acoplamiento

Definición memorizable: mide el grado de dependencia e interconexión entre clases o capas de un sistema. Lo ideal es buscar un acoplamiento DÉBIL.

Realista: si cambiar la máquina de café obliga a cambiar toda la app, estás acoplado. Si solo cambias una parte, estás desacoplado.

Se logra con interfaces, inyección de dependencias y evitando que las capas se conozcan entre sí.

28. Spring

Spring es el ecosistema de Java para construir aplicaciones empresariales. Trae el famoso contenedor de dependencias: tú defines los objetos (beans) y Spring se encarga de crearlos, conectarlos y administrarlos (Inversión de Control e Inyección de Dependencias).

29. Spring Boot

Spring Boot es Spring con "pilas incluidas": configuración automática, servidor embebido y arranque rápido. Creas una API en minutos sin pelear con configuraciones complejas.

@RestController
public class CafeteriaController {

    @GetMapping("/cafes")
    public List<Cafe> listar() {
        // ...
    }
}

30. Hibernate ORM

Hibernate ORM es el mapeador objeto-relacional que convierte tus clases Java en tablas de base de datos y viceversa. Trabaja con la JPA para gestionar el mapeo, el ciclo de vida de las entidades y las transacciones.

Realista: tu clase Cafe se convierte en una tabla cafe; un Cafe guardado corresponde a una fila y cada atributo es una columna.

@Entity
@Table(name = "cafe")
public class Cafe {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String tamano;
}

Dos conceptos que siempre aparecen en entrevistas:

  • Entidad: la clase anotada con @Entity que representa una tabla.
  • EntityManager: el objeto que gestiona las operaciones (guardar, buscar, eliminar) contra la base de datos.

Hibernate te permite trabajar con la base de datos usando objetos en lugar de escribir SQL a mano, aunque siempre puedes combinar ambos.

31. API Criteria en Java

La Criteria API es una forma de consultar bases de datos con objetos tipados en lugar de cadenas SQL. Como el código es tipado, los errores se detectan antes de ejecutarlo.

CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Cafe> query = cb.createQuery(Cafe.class);
Root<Cafe> root = query.from(Cafe.class);

query.where(cb.equal(root.get("tamano"), "grande"));

List<Cafe> cafes = entityManager.createQuery(query).getResultList();

Es muy útil para filtros dinámicos, por ejemplo "muéstrame cafés grandes sin leche si el usuario lo pide".

32. Arquitectura por capas

Una de las bases de los proyectos reales: separar responsabilidades en capas.

  • Controller (presentación): recibe las peticiones HTTP.
  • Service (negocio): contiene la lógica y las reglas del negocio.
  • Repository (persistencia): habla con la base de datos.
Controller -> Service -> Repository -> BD

Cada capa conoce solo a la siguiente. Eso mantiene el acoplamiento débil y permite reemplazar una parte sin romper las demás.

Resumen de un vistazo

Clase = molde, Objeto = copia, Atributo = dato, Método = acción, Encapsulamiento = esconder y controlar, Herencia = heredar, Polimorfismo = muchas formas, Abstracción = simplificar, Interfaz = contrato, Instanciar = crear con new, Constructor = inicializar, Modificadores = visibilidad, Overloading = mismo nombre con parámetros distintos, Overriding = redefinir lo heredado, Acoplamiento = dependencia.

© Arturo Juárez Monroy