====== 03.2 - POO II: Relaciones entre Clases, Herencia y Abstracción ======
===== 1. Herencia =====
La herencia es un mecanismo fundamental de la Programación Orientada a Objetos (POO) que permite crear nuevas clases a partir de clases existentes, reutilizando sus atributos y métodos.
==== 1.1 Sintaxis y Funcionamiento ====
* **Superclase (Clase Padre):** Clase base de la que se hereda.
* **Subclase (Clase Hija):** Clase derivada que extiende a la superclase mediante la palabra clave ''extends''.
public class Vehiculo {
protected String matricula;
protected String marca;
public Vehiculo(String matricula, String marca) {
this.matricula = matricula;
this.marca = marca;
}
public void acelerar() {
IO.println("El vehículo está acelerando.");
}
}
public class Coche extends Vehiculo {
private int numeroPuertas;
public Coche(String matricula, String marca, int numeroPuertas) {
super(matricula, marca); // Llamada obligatoria al constructor del padre
this.numeroPuertas = numeroPuertas;
}
}
==== 1.2 Reglas y Elementos Clave ====
* **Constructores y ''super''**: Las clases hijas no heredan los constructores del padre. La primera línea del constructor de una subclase debe invocar al constructor de la superclase mediante ''super(...)''.
* **Acceso y Visibilidad**: Los miembros declarados como ''private'' en el padre no son accesibles directamente en las hijas. Para permitir el acceso dentro de la jerarquía sin hacerlos públicos a clases externas, se utiliza el modificador ''protected''.
* **Sobrescritura de Métodos (''@Override'')**: Permite a una subclase proporcionar una implementación específica de un método ya definido en su superclase.
* **Modificador ''final''**:
* **Clase ''final''**: Impide que otras clases puedan heredar de ella (ej. ''public final class Coche'').
* **Método ''final''**: Impide que el método pueda ser sobrescrito en las clases hijas.
==== 1.3 La palabra clave super ====
La palabra clave ''super'' es una referencia directa a la superclase (clase padre) desde una subclase. Funciona de manera similar a ''this'' (que hace referencia a la propia instancia actual), pero apuntando a los miembros de la clase superior en la jerarquía de herencia.
Tiene dos usos fundamentales:
* Invocación del constructor del padre (''super(...)''):\\ \\
* Inicializa los atributos definidos en la superclase.\\ \\
* Regla estricta: Si se utiliza, debe ser obligatoriamente la primera línea dentro del constructor de la subclase.\\ \\
* Si la subclase no llama explícitamente a ''super(...)'', Java intentará insertar de forma implícita una llamada al constructor sin parámetros del padre (''super()''). Si el padre no dispone de un constructor por defecto, se producirá un error de compilación.
* Acceso a métodos y atributos del padre (''super.miembro''):\\ \\
* Permite invocar un método de la superclase que ha sido sobrescrito en la subclase, permitiendo extender o reutilizar la lógica original en lugar de reemplazarla por completo.\\ \\
* Permite acceder a un atributo del padre en caso de que la subclase haya declarado un atributo con el mismo nombre (sombra de atributos / attribute shadowing).\\ \\
public class Vehiculo {
public Vehiculo(String marca) {
this.marca = marca;
}
public void arrancar() {
IO.println("El vehículo está listo para circular.");
}
}
public class Coche extends Vehiculo {
private int puertas;
public Coche(String marca, int puertas) {
super(marca); // 1. Llama al constructor de Vehiculo
this.puertas = puertas;
}
@Override
public void arrancar() {
super.arrancar(); // 2. Ejecuta la lógica original de arrancar() del padre
IO.println("El coche ha comprobado el estado del motor y encendido las luces.");
}
}
===== 2. La Superclase Object y sus Métodos Fundamentales =====
En Java, toda clase hereda implícitamente de ''java.lang.Object'' si no especifica otra superclase mediante ''extends''. Por ello, cualquier objeto en Java hereda una serie de métodos base definidos en ''Object''.
Para garantizar un comportamiento correcto en nuestras aplicaciones, es imprescindible aprender a sobrescribir (''@Override'') tres de sus métodos clave.
==== 2.1 El método toString() ====
Devuelve una representación en formato de texto (''String'') del objeto.
* **Comportamiento por defecto:** Si no se sobrescribe, la clase `Object` devuelve una cadena con el nombre de la clase y el código ''hash'' en hexadecimal (ej. ''Coche@15db9742'').
* **Uso:** Se invoca automáticamente cuando pasamos un objeto a un método de impresión como ''IO.println(coche)'' o al concatenarlo con un ''String''.
public class Coche {
private String matricula;
private String marca;
public Coche(String matricula, String marca) {
this.matricula = matricula;
this.marca = marca;
}
@Override
public String toString() {
return "Coche{matricula='" + matricula + "', marca='" + marca + "'}";
}
}
==== 2.2 Comparación de Objetos: equals() y hashCode() ====
Por defecto, el operador de igualdad (''=='') entre objetos **compara referencias en memoria**, no su contenido. Dos objetos distintos en memoria con exactamente los mismos atributos devolverán ''false'' si se comparan con ''==''.
=== El método equals(Object obj) ===
Permite definir qué significa que dos objetos sean **conceptualmente iguales** (por ejemplo, dos coches son iguales si tienen la misma matrícula, independientemente de si están en posiciones de memoria distintas).
@Override
public boolean equals(Object obj) {
// 1. Si apuntan a la misma dirección de memoria, son iguales
if (this == obj) return true;
// 2. Si el objeto recibido es nulo o de una clase diferente, no son iguales
if (obj == null || getClass() != obj.getClass()) return false;
// 3. Conversión de tipo (Casting) y comparación de atributos clave
Coche otro = (Coche) obj;
return this.matricula != null && this.matricula.equals(otro.matricula);
}
=== El método hashCode() ===
Devuelve un número entero (''int'') que representa el código ''hash'' del objeto. Es utilizado por las estructuras de datos basadas en tablas ''hash'' (como ''HashSet'', ''HashMap'', etc.) para organizar y buscar elementos rápidamente.
@Override
public int hashCode() {
// Genera un hash único basado en el atributo identificador
return matricula != null ? matricula.hashCode() : 0;
}
**El Contrato entre ''equals'' y ''hashCode'':**
Si dos objetos son iguales según el método ''equals()'', **obligatoriamente deben devolver el mismo valor de ''hashCode()''**. Si no se cumple esta regla, las colecciones avanzadas (como conjuntos o mapas) fallarán al buscar o guardar los objetos.
===== 3. Sobrescritura de Métodos y Polimorfismo =====
==== 3.1 Sobrescritura de Métodos (@Override) ====
La **sobrescritura de métodos** es el mecanismo mediante el cual una subclase proporciona una implementación específica para un método que ya ha sido definido en su superclase.
* **Requisitos:** El método en la subclase debe mantener exactamente la **misma firma** (mismo nombre, misma lista de parámetros y mismo tipo de retorno) que el método original del padre.
*
* **La anotación ''@Override''**: Es una directiva para el compilador. Aunque es opcional, su uso es una **buena práctica imprescindible**:\\ \\
* Le indica explícitamente al compilador que la intención es sobrescribir un método heredado.\\ \\
* Si cometemos un error tipográfico en el nombre del método o en los parámetros, el compilador generará un error en lugar de crear un método nuevo por accidente.\\ \\
^ Concepto ^ Firma del Método ^ Ámbito ^ Objetivo ^
| **Sobrecarga** | Mismo nombre, **diferentes parámetros** | Misma clase (o heredado) | Ofrecer diferentes formas de invocar la misma acción |
| **Sobrescritura** | Mismo nombre, **mismos parámetros** | Entre Superclase y Subclase | Cambiar o personalizar el comportamiento heredado |
public class Vehiculo {
public void acelerar() {
IO.println("El vehículo está ganando velocidad.");
}
}
public class Coche extends Vehiculo {
@Override
public void acelerar() {
IO.println("El coche acelera progresivamente inyectando combustible.");
}
}
==== 3.2 Polimorfismo y Enlazado Dinámico ====
El **polimorfismo** es la capacidad de tratar a objetos de distintas subclases como si fuesen instancias de su superclase común, permitiendo que cada uno responda a la misma llamada ejecutando su propio comportamiento sobrescrito.
Se fundamenta en dos mecanismos:
* **Tipado Polimórfico (Tipo Referencia vs. Tipo Real):** Una variable de tipo superclase puede almacenar una referencia a cualquier objeto de sus subclases.\\ \\
Vehiculo v = new Coche(); // Referencia: Vehiculo | Objeto real en memoria: Coche
* **Enlazado Dinámico:** \\ \\
* En **tiempo de compilación**, Java verifica que el método exista en la clase de la referencia (''Vehiculo'').\\ \\
* En **tiempo de ejecución**, la Máquina Virtual de Java (JVM) analiza el tipo real del objeto en memoria (''Coche'') y ejecuta su versión **sobrescrita** del método.
==== 3.3 Colecciones Polimórficas ====
La verdadera potencia del polimorfismo se aprecia al gestionar grupos de objetos heterogéneos bajo un mismo tipo base (como un array de la superclase):
// Colección polimórfica: almacena distintos tipos de vehículos
Vehiculo[] parking = new Vehiculo[3];
parking[0] = new Coche("1234-ABC", "SEAT", 5);
parking[1] = new Moto("5678-DEF", "Yamaha", true);
parking[2] = new Coche("9012-GHI", "Ford", 3);
// Enlazado dinámico: cada objeto ejecuta la versión de acelerar() de su clase real
for (Vehiculo v : parking) {
v.acelerar();
}
===== 4. Herencia vs. Composición =====
El diseño orientado a objetos exige elegir adecuadamente cómo asociar y conectar las clases.
==== 4.1 Herencia: Relación "Es un" (Is-A) ====
* Se utiliza cuando la subclase es una especialización clara e inequívoca de la superclase.
* **Limitación en Java:** No existe la herencia múltiple de clases. Una clase solo puede extender de una única superclase (''extends'').
==== 4.2 Composición: Relación "Tiene un" (Has-A) ====
* Consiste en incluir instancias de otras clases como atributos para delegar responsabilidades.
* **Principio de Diseño:** **Favorecer la composición sobre la herencia**. Otorga mayor flexibilidad y reduce el acoplamiento rígido entre clases.
// Clase componente
public class Motor {
private String tipo;
public Motor(String tipo) {
this.tipo = tipo;
}
public void arrancar() {
IO.println("Motor " + tipo + " en marcha.");
}
}
// Composición: Coche "TIENE UN" Motor
public class Coche {
private String marca;
private Motor motor; // Atributo de tipo objeto Motor
public Coche(String marca, Motor motor) {
this.marca = marca;
this.motor = motor;
}
public void encender() {
IO.print(marca + ": ");
motor.arrancar(); // Delegación de responsabilidad
}
}
===== 5. Clases y Métodos Abstractos (abstract) =====
Una **clase abstracta** representa un concepto incompleto o modelo base. Sirve como plantilla para que otras subclases deriven de ella.
==== 5.1 Características Clave ====
* **Instanciación prohibida:** No se puede instanciar directamente con ''new ClaseAbstracta()''.
* **Métodos Abstractos:** Se declaran sin cuerpo (''public abstract void desplazar();'').
* **Contrato obligatorio:** Toda subclase concreta **debe** implementar obligatoriamente todos los métodos abstractos heredados.
* **Estado y funcionalidad:** Puede contener atributos, constructores, métodos concretos con código y métodos abstractos.
* Si una clase tiene un método abstracto, **la clase se debe definir como abstracta**.
public abstract class Vehiculo {
private String matricula;
public Vehiculo(String matricula) {
this.matricula = matricula;
}
public String getMatricula() {
return matricula;
}
// Método concreto común
public void mostrarFicha() {
System.out.println("Vehículo Matrícula: " + matricula);
}
// Método abstracto (sin cuerpo, de implementación obligatoria en hijas)
public abstract void desplazar();
}
===== 6. Interfaces (interface) =====
Una interface establece un contrato formal de comportamiento. Define qué capacidades debe tener una clase sin importar la jerarquía de herencia a la que pertenezca.
==== 6.1 Características Clave ====
* Modificadores Implícitos (Regla de Oro):\\ \\
* **Métodos estándar**: Por defecto son siempre ''public'' y ''abstract''. No es necesario (e incluso se considera redundante) escribir las palabras clave ''public'' ni ''abstract''.\\ \\
* **Atributos**: Por defecto son siempre ''public static final'' (constantes). No existen atributos de instancia en una interfaz.
* **Herencia Múltiple de Interfaces**: Una clase solo puede extender de una superclase (''extends''), pero puede implementar múltiples interfaces separadas por comas (''implements'').\\ \\
* Tipos de Métodos:\\ \\
* **Abstractos (por defecto)**: Sin cuerpo. Implícitamente ''public abstract''. Toda clase concreta que implemente la interfaz debe proporcionar su implementación.\\ \\
* **Default (Java 8+)**: Métodos concretos con una implementación por defecto que las clases pueden reutilizar directamente o sobrescribir.\\ \\
* **Static (Java 8+)**: Métodos de utilidad asociados a la interfaz que no se heredan ni se pueden sobrescribir.\\ \\
* **Private (Java 9+)**: Métodos auxiliares para reutilizar código internamente dentro de los métodos ''default'' de la propia interfaz.\\ \\
// Método abstracto implícitamente: public abstract
void cargarBateria(int porcentaje);
// Método con implementación por defecto
default void mostrarEstadoCarga() {
IO.println("Comprobando conector e inicio de carga...");
}
==== 6.2 Importancia en Arquitectura de Software y Testing ====
Las interfaces no son solo una herramienta sintáctica; **son el pilar maestro del diseño de software profesional** por tres razones clave:
* **Desacoplamiento (Principio de Inversión de Dependencias)**: Permite que los módulos de alto nivel no dependan de implementaciones concretas, sino de **contratos abstractos**.
* **Facilidad para Pruebas Unitarias**: Al realizar tests unitarios, no queremos cobrar dinero real ni enviar emails verdaderos a clientes. Al depender de una interfaz, durante las pruebas automáticas podemos sustituir la clase real (''SmtpEmailSender'') por una implementación falsa de prueba (''MockEmailSender'') en cuestión de segundos.
* **Sustitución y Mantenibilidad** en Sistemas Reales: Permite cambiar tecnologías completas de infraestructura (por ejemplo, cambiar la base de datos o el proveedor de almacenamiento en la nube) simplemente creando una nueva clase que implemente la interfaz existente, sin romper el resto de la aplicación.
===== 7. Clases de envoltorio =====
En Java existen dos categorías principales de tipos de datos: tipos primitivos (''int'', ''double'', ''char'', ''boolean'', etc.) y tipos por referencia (objetos).
Dado que los tipos primitivos no heredan de la superclase universal ''Object'' ni implementan interfaces, Java proporciona las clases de envoltorio (wrapper classes). Estas clases //envuelven// un valor primitivo dentro de un objeto, permitiendo que sea tratado como tal en colecciones, tipos genéricos y métodos que esperan un ''Object''.
==== 7.1 Correspondencia de tipos ====
Cada tipo de dato primitivo tiene su correspondiente clase de envoltorio en el paquete estándar ''java.lang'':
* ''byte'' $\rightarrow$ ''Byte'' \\ \\
* ''short'' $\rightarrow$ ''Short'' \\ \\
* ''int'' $\rightarrow$ ''Integer'' \\ \\
* ''long'' $\rightarrow$ ''Long'' \\ \\
* ''float'' $\rightarrow$ ''Float'' \\ \\
* ''double'' $\rightarrow$ ''Double'' \\ \\
* ''char'' $\rightarrow$ ''Character'' \\ \\
* ''boolean'' $\rightarrow$ ''Boolean'' \\ \\
==== 7.2 Autoboxing y Unboxing ====
Desde Java 5, la conversión entre tipos primitivos y sus clases de envoltorio asociadas se realiza de forma automática mediante el compilador:
* **Autoboxing**: Conversión automática de un tipo primitivo a su correspondiente objeto envoltorio.\\ \\
* **Unboxing**: Conversión automática de un objeto envoltorio a su tipo primitivo equivalente.
// Unboxing: el objeto Double se convierte automáticamente a double primitivo
Double precioObj = 19.99;
double precio = precioObj;
==== 7.3 Conversión de cadenas y métodos de utilidad ====
Las clases envoltorio proporcionan potentes métodos estáticos de utilidad, siendo la conversión desde cadenas de texto (''String'') una de las operaciones más habituales:
// Métodos de utilidad
int maximo = Integer.max(10, 20);
String binario = Integer.toBinaryString(15); // Devuelve "1111"
===== Ejercicios =====
=== Ejercicio 1 ===
Crea la superclase ''Vehicle'' y la subclase ''ElectricCar'':
* ''Vehicle'':\\ \\
* Atributos protegidos: ''licensePlate'' (''String'') y ''model'' (''String'').\\ \\
* Constructor completo para inicializar sus atributos.\\ \\
* Sobrescribe los métodos de ''Object'':\\ \\
* ''toString()'': Devuelve una cadena con el formato ''"Vehiculo {Matrícula=..., Modelo=...}"''.\\ \\
* ''equals(Object obj)'': Compara dos vehículos basándose exclusivamente en que tengan la misma matricula (''licensePlate'').\\ \\
* ''hashCode()'': Genera un código hash basado en la matricula.\\ \\
* ''ElectricCar'':\\ \\
* Extiende de ''Vehicle''.\\ \\
* Atributo privado: ''batteryCapacity'' (''int'' en kWh).\\ \\
* Constructor completo que haga uso explícito de ''super(...)'' para delegar la inicialización de los atributos heredados.\\ \\
* Sobrescribe el método ''toString()'' reutilizando la salida de la superclase mediante ''super.toString()'' y concatenando la capacidad de la batería.\\ \\
En la clase principal (''Main''), instancia dos vehículos con la misma matrícula para verificar el funcionamiento de ''equals()'' e imprime una instancia de ''ElectricCar'' para comprobar la salida de ''toString()''.
=== Ejercicio 2 ===
Implementa una jerarquía de empleados para calcular sueldos:
* ''Employee'' (clase abstracta):\\ \\
* Atributo protegido: ''name'' (''String'').\\ \\
* Constructor completo y método ''getNombre()''.\\ \\
* Método abstracto: ''public abstract double getSalary()''.\\ \\
* ''SalariedEmployee'' (subclase concreta de ''Employee''):
* Atributo privado: ''baseSalary'' (''double'').\\ \\
* Constructor completo.\\ \\
* Implementación de ''getSalary()'': Devuelve directamente el valor de ''baseSalary''.\\ \\
* ''EmpleadoPorComision'' (subclase concreta de Empleado):\\ \\
* Atributos privados: ''salesMade'' (''int'') y ''commissionPerSale'' (''double'').\\ \\
* Constructor completo.\\ \\
* Implementación de ''getSalary()'': Devuelve el resultado de multiplicar las ventas realizadas por la comisión por venta ($salesMade \times commissionPerSale$).\\ \\
En la clase principal (''Main''), crea un array de tipo ''Employee[]'' que contenga al menos una instancia de ''SalariedEmployee'' y otra de ''ComissionEmployee''. Recorre el array con un bucle e imprime el nombre y el sueldo de cada empleado.
=== Ejercicio 1c ===
Aplica el principio de composición frente a la herencia para modelar la relación "Tiene un" (//Has-A//) entre un ordenador y su procesador:
* ''Procesador'':
* Atributos privados: ''modelo'' (''String'') y ''frecuenciaGhz'' (''double'').\\ \\
* Constructor completo.\\ \\
* Método ''procesarData()'': Imprime por pantalla un mensaje indicando que se están procesando datos con el modelo y la frecuencia del procesador.\\ \\
* ''Ordenador'':
* Atributos privados: ''marca'' (''String'') y ''cpu'' (de tipo Procesador).\\ \\
* Constructor completo.\\ \\
* Método ''ejecutarTarea()'': Muestra un mensaje indicando el inicio de la tarea delegando el trabajo mediante la llamada al método ''procesarData()'' del atributo ''cpu''.
En la clase principal (''Main''), instancia un objeto ''Procesador'', pásalo como argumento al instanciar un objeto ''Ordenador'' y ejecuta el método ''ejecutarTarea()''.
=== Ejercicio 1d ===
Abstrae capacidades independientes mediante la implementación de múltiples interfaces en una misma clase:
* Interface ''Enviable'':\\ \\
* Método abstracto: ''void enviar(String destino)''.\\ \\
* Interface ''Reseteable'':
* Método abstracto: ''void resetear()''.\\ \\
* Método por defecto (''default''):
* ''void diagnostico()'' que imprima por pantalla el mensaje //"Ejecutando comprobación del sistema..."//.\\ \\
* ''DispositivoIot'':
* Atributo privado: ''idDispositivo'' (''String'').\\ \\
* Implementa las interfaces ''Enviable'' y ''Reseteable''.\\ \\
* Proporciona la implementación de los métodos ''enviar(String destino)'' y ''resetear()'', mostrando mensajes informativos que incluyan el identificador del dispositivo.
En la clase principal (''Main''), instancia un objeto de ''DispositivoIot'', invoca sus métodos ''enviar()'' y ''resetear()'', y ejecuta el método predeterminado ''diag