Unidad 6. Programación Orientada a Objetos Avanzada
Objetivos de la Unidad
- Modelar relaciones de composición entre clases (relación "TIENE-UN").
- Implementar jerarquías de herencia con
extendsy distinguir la relación "ES-UN". - Diseñar clases abstractas y definir contratos mediante métodos abstractos.
- Definir e implementar interfaces para modelar capacidades transversales.
- Aplicar el polimorfismo y la ligadura dinámica para escribir código genérico y extensible.
- Sobreescribir
equals/hashCodee implementarComparablepara ordenación natural.
1. Clases Compuestas (Composición)
Hasta ahora, hemos trabajado con objetos sencillos (como un Coche con velocidad o color). Sin embargo, en el mundo profesional, el software se parece más a un juego de LEGO.
¿Cómo construyes un castillo de Lego? No fabricas el plástico desde cero. Tomas bloques que ya existen (ventanas, puertas, ladrillos) y los "compones" para crear algo nuevo y más complejo.
La Composición es exactamente eso: crear clases complejas usando otras clases como "piezas".
1.1. La Relación "TIENE-UN" (Has-A)
Para saber si estás ante un caso de composición, usa la prueba del "TIENE-UN":
- Un
OrdenadorTIENE UNProcesador. - Un
PersonajeTIENE UNArma. - Una
CasaTIENE UNACocina.
(Si la frase fuera "ES UN", estaríamos hablando de Herencia, que veremos luego).
Consejo Pro
En Java, implementamos esto declarando atributos que son referencias a otros Objetos, en lugar de simples int o String.
1.2. Ventajas: Delegación
La clase contenedora (Personaje) no necesita saber cómo dispara el Arma. Solo le pide que dispare.
A esto se le llama Delegación: "Yo (Personaje) no calculo el daño, delego esa tarea a mi arma".
1.3. Ejemplo: Personaje y Arma
Vamos a ver cómo se traduce esto a código real.
1.4. Composición con Colecciones (Relación "TIENE-MUCHOS")
¿Y si nuestro personaje tiene una mochila? Una mochila no tiene un solo objeto, tiene muchos.
Aquí es donde combinamos Composición con ArrayList.
1.5. Visualización Completa
En este diagrama mostramos todos los atributos para ver la estructura completa de objetos en memoria.
classDiagram
class Personaje {
- String nombre
- int vida
- Arma armaEquipada
- Mochila mochila
+ Personaje(nombre, arma)
+ atacar()
+ cambiarArma(Arma)
+ cogerObjeto(Item)
}
class Arma {
- String nombre
- int dano
+ getDano()
}
class Mochila {
- ArrayList~Item~ items
- int capacidadMax
+ guardar(Item)
}
class Item {
- String nombre
- int durabilidad
}
Item --o Mochila : Contiene
Personaje o-- Arma : Agregación (Tiene una)
Personaje *-- Mochila : Composición (Parte de él)
💻 Momento de Práctica: El Ordenador Modular
Vamos a construir un PC modular. Esta vez no es una sola clase, será un sistema de piezas ensambladas.
1. Las Piezas (Clases Componentes)
- Clase
Procesador:- Atributos:
marca(String) ymodelo(String). - Constructor y método
toString()que devuelva algo como "Intel i7".
- Atributos:
- Clase
MemoriaRAM:- Atributos:
capacidad(int, en GB) ytecnologia(String, ej: "DDR4", "DDR5"). - Constructor y método
toString().
- Atributos:
2. La Torre (Clase Compuesta)
- Clase
Ordenador:- Atributos:
cpu: Objeto de tipoProcesador.bancosRAM:ArrayList<MemoriaRAM>(Aquí guardaremos los módulos).MAX_RAM_SLOTS: Constantefinal int(ej: 4).
- Constructor: Debe recibir solo el Procesador y crear la lista de RAM vacía.
- Atributos:
3. La Lógica de Ensamblaje
-
Método
void anadirMemoria(MemoriaRAM ram):- Comprueba si hay hueco libre (
size() < MAX_RAM_SLOTS). - Si hay hueco, añade la memoria a la lista.
- Si no, imprime: "¡No quedan slots libres!".
- Comprueba si hay hueco libre (
-
Método
void mostrarConfiguracion():- Imprime el procesador.
- Recorre la lista de RAM sumando sus capacidades.
- Muestra el total: "Ordenador con [Intel i7] y [32] GB de RAM instalada".
Reto Extra: Modifica anadirMemoria para que impida mezclar tecnologías. Si la primera RAM es "DDR4", no debe dejar añadir una "DDR5".
2. Introducción a la Herencia
La Herencia es el mecanismo que nos permite crear nuevas clases basadas en clases que ya existen.
Imagina que estás programando un videojuego con 50 tipos de enemigos distintos (Zombies, Aliens, Robots...). Todos tienen vida, todos se mueven, todos tienen coordenadas.
- ¿Vas a escribir el código de
moverse()50 veces? NO. - ¿Si descubres un fallo en el movimiento, vas a corregirlo en 50 archivos? NO.
Aquí entra la Herencia: Igual que tú heredas el ADN de tus padres (no tienes que "aprender" a tener ojos, ya te vienen de fábrica), en programación creamos una Clase Padre con lo común y las Clases Hijas heredan su código automáticamente.
2.1. La Regla del "ES-UN" (Is-A)
Para saber si debes usar herencia, la frase debe tener sentido:
- Un
GuerreroES UNPersonaje. (Correcto: Herencia) - Un
CocheES UNMotor. (Incorrecto: Es Composición, porque TIENE un motor)
2.2. Sintaxis (extends) y Visibilidad (protected)
Para heredar usamos extends. Pero tenemos un problema: los hijos no pueden ver los atributos private de sus padres.
Para solucionarlo, existe un modificador de acceso diseñado específicamente para la herencia: protected.
Este nivel de visibilidad actúa como un punto intermedio entre la privacidad total y la exposición pública:
| Modificador | Alcance y Descripción |
|---|---|
public |
Visible desde cualquier clase de la aplicación. |
protected |
Visible para las clases del mismo paquete y para sus subclases (hijas), independientemente de donde estén. |
private |
Visible únicamente dentro de la propia clase (ni siquiera los hijos pueden acceder). |
¿Cuándo usar protected?
Aunque protected es útil para la herencia, no abuses de él.
Por defecto, intenta mantener tus atributos private y usa Getters/Setters (públicos o protegidos) si los hijos necesitan acceder. Usa protected en atributos solo cuando sea estrictamente necesario para el diseño o rendimiento.
Importante: super()
La llamada a super() debe ser SIEMPRE la primera línea del constructor del hijo. No puedes decorar la casa si no has puesto los cimientos antes.
Curiosidad: ¿Herencia Múltiple?
En Java, una subclase SOLO puede tener un PADRE (extends). No existe la herencia múltiple de clases (como en C++ o Python).
Esto se hace para evitar conflictos (el "Problema del Diamante"). Sin embargo, Java compensa esto permitiendo implementar múltiples Interfaces.
2.3. Sobreescritura de Métodos (@Override)
¿Y si el hijo quiere hacer las cosas de forma diferente a su padre?
El Guerrero hereda saludar(), pero quiere saludar gritando.
Podemos reescribir el método usando la etiqueta @Override.
2.4. Diagrama de Herencia
En UML, la herencia se representa con una flecha de línea continua y punta triangular hueca que va del Hijo al Padre.
classDiagram
class Personaje {
# String nombre
# int vida
+ saludar()
}
class Guerrero {
- int furia
+ saludar()
}
class Mago {
- int mana
+ lanzarHechizo()
}
Personaje <|-- Guerrero : Es un
Personaje <|-- Mago : Es un
(Nota: El símbolo # representa protected en los diagramas).
💻 Momento de Práctica: El Reino Animal
Vamos a crear una pequeña jerarquía biológica.
- Crea una clase
Animalcon atributosprotected nombreyedad.- Método
hacerSonido()que imprima: "..." (sonido genérico).
- Método
- Crea la clase
Perroque hereda deAnimal.@OverridehacerSonido()-> "¡Guau!".
- Crea la clase
Gatoque hereda deAnimal.@OverridehacerSonido()-> "¡Miau!".
- En el Main:
- Crea un Perro y un Gato.
- Haz que ambos "hablen".
- Prueba Curiosa: Intenta hacer
Animal a = new Perro(...). ¿Funciona? (Spoiler: Sí, es el principio del Polimorfismo).
3. Clases Abstractas
Hasta ahora, nuestras clases servían para crear objetos (new Guerrero()). Pero a veces, necesitamos definir un concepto que es puramente estructural y no debería existir por sí solo.
Concepto Abstracto vs Implementación Concreta: Piensa en la ingeniería. Tú sabes lo que es un "Vehículo" (tiene motor, ruedas, transporta gente).
- Pero tú no puedes ir a un concesionario y decir: "Deme un Vehículo, a secas".
- El vendedor te dirá: "¿Quiere un Coche? ¿Una Moto? ¿Un Camión?".
"Vehículo" es un concepto abstracto. No se puede fabricar. Solo sirve de base para crear cosas reales como "Coche" o "Moto".
3.1. Definición (abstract)
En Java, usamos la palabra clave abstract para impedir que una clase se pueda instanciar.
- Efecto inmediato:
new Figura("Rojo")dará ERROR DE COMPILACIÓN. - Utilidad: Solo podemos usarla como padre (
extends) para crear clases útiles comoCirculooRectangulo.
3.2. Métodos Abstractos (El Contrato)
Si definimos una "Figura", sabemos que todas las figuras tienen un Área. Pero, ¿cómo se calcula el área de "una figura genérica"? ¡No se puede! Depende de si es redonda o cuadrada.
Aquí usamos un Método Abstracto: Definimos el QUÉ pero dejamos el CÓMO vacío.
La Regla de Oro: Si heredas de una clase abstracta, estás OBLIGADO a implementar sus métodos abstractos (darles código). Si no lo haces, tú también serás abstracto.
3.3. Ejemplo: Geometría
3.4. Diagrama UML
En los diagramas de clases, las clases abstractas y los métodos abstractos se representan en cursiva (o con la etiqueta <<abstract>>).
classDiagram
class Figura {
<<abstract>>
# String color
+ Figura(String color)
+ calcularArea()* double
}
class Circulo {
- double radio
+ calcularArea() double
}
class Rectangulo {
- double base
- double altura
+ calcularArea() double
}
Figura <|-- Circulo
Figura <|-- Rectangulo
(Nota: El asterisco * o la cursiva en calcularArea() indica que es abstracto).
3.5. Clases y Métodos Finales (final)
Justo lo opuesto a abstract es final.
- Clase
final: "Estéril". Nadie puede heredar de ella. (Ej:Stringesfinalpor seguridad). - Método
final: "Intocable". Nadie puede sobreescribirlo (@Override).
💻 Momento de Práctica: Gestión de Nóminas
Vamos a crear un sistema de pago para una empresa.
- Clase Abstracta
Empleado:- Atributos:
nombre,dni. - Método Abstracto:
double calcularSueldo(). - Método Normal:
toString()que devuelva el nombre.
- Atributos:
- Hija
EmpleadoAsalariado:- Tiene
sueldoBase(fijo) yantiguedad(años). - Sueldo =
sueldoBase+ (100 *antiguedad).
- Tiene
- Hija
Freelance:- Tiene
precioHorayhorasTrabajadas. - Sueldo =
precioHora*horasTrabajadas.
- Tiene
- Main (Polimorfismo puro):
- Crea un
ArrayList<Empleado>. - Añade un Asalariado y un Freelance.
- Recorre la lista y suma cuánto dinero total tiene que pagar la empresa este mes.
- Crea un
4. Interfaces
Las Interfaces llevan la abstracción al siguiente nivel. Si una Clase Abstracta define QUÉ ERES (Identidad), una Interfaz define QUÉ PUEDES HACER (Capacidad).
4.1. Metáfora: El Mando de la Consola
Imagina el mando de una videoconsola.
- Tiene unos botones definidos: X, O, Triángulo, Cuadrado.
- Si juegas al FIFA, al pulsar X haces un Pase.
- Si juegas al Call of Duty, al pulsar X haces un Salto.
El mando (Interfaz) impone qué botones existen, pero no qué hacen. La clase concreta (el juego) es quien decide cómo reaccionar a esos botones.
4.2. Diferencias Clave
| Característica | Clase Abstracta | Interfaz |
|---|---|---|
| Concepto | Identidad (ADN). "Soy un Animal". | Habilidad (Título). "Sé Nadar". |
| Relación | Padres/Hijos (Jerarquía Vertical). | Contrato (Conexión Horizontal). |
| Atributos | Puede tener variables normales. | Solo constantes (public static final). |
| Herencia | Simple (Solo un padre). | Múltiple (Puedes tener muchas habilidades). |
4.3. Ejemplo: GPS Universal (Localizable)
Imagina una App de mapas. Necesita mostrar cosas en el mapa. No solo coches. También personas, mascotas con collar GPS, o incluso edificios. No tienen NADA en común (no son familia), pero todos son Localizables.
Primero, vamos a definir una clase que almacene las coordenadas.
Ahora, vamos a definir la interfaz Localizable. Estará implementada por todas las clases que quieran ser localizables y usaremos el objeto Coordenadas para almacenar las coordenadas en cada clase.
Podemos usar la interfaz Localizable para implementar la localización en clases abstractas.
Podemos usar la interfaz Localizable para implementar la localización en clases concretas.
Aquí tenemos un ejemplo de uso en el main:
4.4. Diagrama UML
En UML, la relación "implementa una interfaz" (Realización) se dibuja con una línea discontinua y punta triangular hueca.
classDiagram
class Localizable {
<<interface>>
+ getLatitud()
+ getLongitud()
}
class Vehiculo {
<<abstract>>
+ getLatitud()
+ getLongitud()
}
class Usuario {
+ getLatitud()
+ getLongitud()
}
class Perro {
+ getLatitud()
+ getLongitud()
}
Localizable <|.. Vehiculo : Implements
Localizable <|.. Usuario : Implements
Localizable <|.. Perro : Implements
Poder Ilimitado: Implementar Múltiples Interfaces
A diferencia de la herencia de clases (extends), donde solo puedes tener un padre, en Java puedes implementar tantas interfaces como quieras.
Solo tienes que separarlas por comas:
public class SuperHeroe implements Volador, SuperFuerza, IdentidadSecreta { ... }
¡Eso sí! Tienes que escribir el código de TODOS los métodos de TODAS las interfaces.
💻 Momento de Práctica: La Casa Domótica
Vamos a crear los dispositivos inteligentes de una casa.
- La Interfaz: Crea
Controlablecon métodosvoid encender()yvoid apagar(). - Las Clases: Crea
Televisor,AireAcondicionadoyPersianaque implementenControlable.- Piensa en qué atributo necesitas para indicar si un dispositivo está encendido o apagado.
- Detalle: El televisor al encenderse dice: "Bienvenido a Netflix".
- Detalle: El aire al apagarse dice: "Cerrando aspas...".
- La Prueba: En el
main, crea un objeto de cada uno. Enciéndelos y apágalos. - Reflexión: Fíjate que aunque los métodos se llaman igual, cada objeto hace algo distinto (uno muestra logos, otro mueve aspas). Eso es gracias a la interfaz.
5. Polimorfismo
El Polimorfismo ("muchas formas") es la magia real de la POO. Nos permite tratar a objetos diferentes de forma genérica, pero conservando su comportamiento único.
5.1. Metáfora: La Caja Mágica
Imagina que tienes una caja etiquetada como "Personaje".
- Dentro de la caja metes a un Guerrero.
- Tú, desde fuera, solo ves la etiqueta "Personaje".
- Si le gritas a la caja "¡ATACA!", lo que saldrá de dentro es un ESPADAZO (comportamiento de Guerrero), aunque tú trataras la caja como algo genérico.
Concepto Clave:
- Tipo de la Variable (La Caja): Define qué métodos PUEDES llamar.
- Tipo del Objeto (El Contenido): Define CÓMO se ejecutan esos métodos.
5.2. Ligadura Dinámica (Dynamic Binding)
Java decide en tiempo de ejecución qué código ejecutar. Esto se llama Ligadura Dinámica.
5.3. Trasteando con la Herencia: Casting y instanceof
A veces, tienes un Personaje (caja genérica) pero sabes que dentro hay un Guerrero y quieres usar su método especial gritoDeGuerra() (que el Mago no tiene).
Intentarlo directamente da error:
Para solucionarlo, necesitamos hacer Casting (destapar la caja y confirmar qué es). Pero ¡cuidado! Si te equivocas, el programa explota. Para estar seguros, usamos el "escáner de rayos X": instanceof.
💻 Reto Final de Unidad: El Zoológico Interactivo 🦁🐬🐶
Vamos a crear un simulador de zoológico con gestión de energía.
- Clase Abstracta
Animal:- Atributos:
nombre,energia(empieza en 50). - Método
jugar(int gasto): Resta energía e imprime "[Nombre] está jugando... Energía restante: [X]". - Método Abstracto
comer(String comida): Cada animal recupera energía de forma distinta según lo que coma. Si no quiere la comida, muestra rechazo con un mensaje.
- Atributos:
- Interfaz
Trucos:- Método
hacerTruco(): Muestra un mensaje especial.
- Método
- Las Especies:
Leon: Solo come "Carne" (+100 de energía). Si le das otra cosa, se enfada y pierde 10. No hace trucos.Delfin: Come "Peces" (+50) y "Calamar" (+80). ImplementaTrucos: "¡Hace un salto mortal hacia atrás!" y resta 10 de energía.Perro: Come de todo ("Carne" +50, "Pienso" +30). ImplementaTrucos: "¡Da la patita y rueda!" y resta 5 de energía.
- El Show (Main):
- Crea un
ArrayList<Animal>con uno de cada. - Bucle del Show:
- Haz que todos jueguen (gastan 20 de energía).
- Aliméntalos a todos con "Carne". (Observa cómo el Delfín no la querrá).
- Si el animal sabe hacer trucos (
instanceof), ¡que actúe para el público!
- Crea un
5.4. Sobreescritura de Comparadores (Igualdad y Orden)
En Java, a menudo necesitamos saber si dos objetos son "iguales" o cuál va antes en una lista (ordenar).
Por defecto, == solo compara si son el mismo objeto en memoria, no si tienen los mismos datos.
5.4.1. Igualdad de Contenido (equals)
Para comparar el contenido, debemos sobreescribir el método equals y hashCode.
El contrato de equals y hashCode
equals(): Define cuándo dos objetos son lógicamente "el mismo".hashCode(): Devuelve un número entero que representa al objeto (como una huella dactilar rápida).
Regla de Oro: Siempre que sobreescribas equals, DEBES sobreescribir hashCode.
¿Por qué? Porque colecciones como HashSet o HashMap usan primero el hashCode para buscar rápido. Si dos objetos son iguales pero tienen hashCode distinto, estas colecciones fallarán y creerán que son objetos diferentes.
5.4.2. Orden Natural (Comparable)
Si queremos poder ordenar una lista de personajes con Collections.sort(lista), nuestra clase debe implementar la interfaz Comparable<T> y su método compareTo.
Ejemplo 1: Ordenar por Nombre (Texto)
Ejemplo de Uso:
Ejemplo 2: Ordenar por Nivel (Numérico)
Si preferimos ordenar por su poder (nivel), comparamos enteros.
!!! tip "Comparadores de tipos Integer y Double
Java ya tiene implementados los comparadores para estos tipos.
1 2 | |
6. Ejemplo Completo de Integración
A continuación, tienes un ejemplo de código completo que integra Herencia, Clases Abstractas, Interfaces y Polimorfismo. Puedes copiar cada bloque en un archivo separado para probarlo.
Escenario: Un pequeño juego RPG.
6.1. La Interfaz (Capacidad)
6.2. La Clase Abstracta (Identidad Base)
6.3. Las Clases Concretas (Implementación)
6.4. El Main (Polimorfismo en Acción)
6.5. Diagrama UML Completo del Juego de Rol
classDiagram
class Atacable {
<<interface>>
+ recibirDano(int cantidad)
}
class Personaje {
<<abstract>>
# String nombre
# int vida
# int nivel
+ Personaje(nombre, vida, nivel)
+ atacar(Personaje objetivo)*
+ recibirDano(int cantidad)
+ estaVivo() boolean
}
class Guerrero {
- int furia
+ Guerrero(nombre, vida, nivel, furia)
+ atacar(Personaje objetivo)
+ gritoDeGuerra()
}
class Mago {
- int mana
+ Mago(nombre, vida, nivel, mana)
+ atacar(Personaje objetivo)
}
Atacable <|.. Personaje : Implements
Personaje <|-- Guerrero : Extends
Personaje <|-- Mago : Extends