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.
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;
}
}
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(…).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.@Override): Permite a una subclase proporcionar una implementación específica de un método ya definido en su superclase.final:final: Impide que otras clases puedan heredar de ella (ej. public final class Coche).final: Impide que el método pueda ser sobrescrito en las clases hijas.
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:
super(…)):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.super.miembro):
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.");
}
}
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.
Devuelve una representación en formato de texto (String) del objeto.
hash en hexadecimal (ej. Coche@15db9742).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 + "'}";
}
}
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 ==.
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);
}
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;
}
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.
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.
@Override: Es una directiva para el compilador. Aunque es opcional, su uso es una buena práctica imprescindible:| 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.");
}
}
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:
Vehiculo v = new Coche(); // Referencia: Vehiculo | Objeto real en memoria: Coche
Vehiculo).Coche) y ejecuta su versión sobrescrita del método.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();
}
El diseño orientado a objetos exige elegir adecuadamente cómo asociar y conectar las clases.
extends).
// 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
}
}
Una clase abstracta representa un concepto incompleto o modelo base. Sirve como plantilla para que otras subclases deriven de ella.
new ClaseAbstracta().public abstract void desplazar();).
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();
}
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.
public y abstract. No es necesario (e incluso se considera redundante) escribir las palabras clave public ni abstract.public static final (constantes). No existen atributos de instancia en una interfaz.extends), pero puede implementar múltiples interfaces separadas por comas (implements).public abstract. Toda clase concreta que implemente la interfaz debe proporcionar su implementación.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...");
}
Las interfaces no son solo una herramienta sintáctica; son el pilar maestro del diseño de software profesional por tres razones clave:
SmtpEmailSender) por una implementación falsa de prueba (MockEmailSender) en cuestión de segundos.
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.
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 Desde Java 5, la conversión entre tipos primitivos y sus clases de envoltorio asociadas se realiza de forma automática mediante el compilador:
// Unboxing: el objeto Double se convierte automáticamente a double primitivo Double precioObj = 19.99; double precio = precioObj;
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"
Crea la superclase Vehicle y la subclase ElectricCar:
Vehicle:licensePlate (String) y model (String).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:Vehicle.batteryCapacity (int en kWh).super(…) para delegar la inicialización de los atributos heredados.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().
Implementa una jerarquía de empleados para calcular sueldos:
Employee (clase abstracta):name (String).getNombre().public abstract double getSalary().SalariedEmployee (subclase concreta de Employee):baseSalary (double).getSalary(): Devuelve directamente el valor de baseSalary.EmpleadoPorComision (subclase concreta de Empleado):salesMade (int) y commissionPerSale (double).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.
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:modelo (String) y frecuenciaGhz (double).procesarData(): Imprime por pantalla un mensaje indicando que se están procesando datos con el modelo y la frecuencia del procesador.Ordenador:marca (String) y cpu (de tipo Procesador).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().
Abstrae capacidades independientes mediante la implementación de múltiples interfaces en una misma clase:
Enviable:void enviar(String destino).Reseteable:void resetear().default): void diagnostico() que imprima por pantalla el mensaje “Ejecutando comprobación del sistema…”.DispositivoIot:idDispositivo (String).Enviable y Reseteable.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