# Herencia Semestre 2, 2025 ## Usos Reutilización de código: evita duplicación. Modelado natural de jerarquías: ejemplo, **Animal → Perro → Pastor Alemán**. Extensibilidad: se pueden crear nuevas clases a partir de otras ya existentes. Permite **polimorfismo**. La palabra **polimorfismo** proviene del griego poly (muchos) y morphé (formas). En Java, significa que un mismo objeto puede adoptar múltiples formas dependiendo del contexto de uso. **Comparación con composición:** * **Herencia:** modela relación “es-un” (is-a). * **Composición:** modela relación “tiene-un” (has-a). ## Modificadores de visibilidad La herencia trae un modificador nuevo: `protected`, a medio camino entre `private` y `public`. * `private`: solo dentro de la misma clase. * `protected`: la clase y sus subclases. * `public`: cualquier clase del programa. * Sin modificador queda visible solo dentro del paquete. ```java[] public class Persona { private String dpi; // solo dentro de Persona protected String nombre; // Persona y sus subclases public int edad; // cualquier clase public void saludar() { System.out.println("Hola, soy " + nombre); } } ``` Lo que la subclase necesite tocar directamente va protected; lo demás se queda private. ## Extends "¡Tan linda tu hija! es igualita a ti" "Vos si no podes negar a tu hijo, es tu viva imágen" Una clase (subclase) puede **heredar atributos y métodos** de otra (superclase). Se establece con la palabra clave `extends`. ```java[] public class Persona { protected String nombre; public void saludar() { System.out.println("Hola, soy " + nombre); } } public class Estudiante extends Persona { protected String carnet; } ``` Aquí, `Estudiante` hereda `nombre` y `saludar()` de `Persona`. ## Jerarquía de clases En Java, todas las clases heredan directa o indirectamente de `Object`. Esto significa que cualquier objeto en Java tiene disponibles métodos como: * `toString()` – representación en texto. * `equals()` – comparación de igualdad. * `getClass()` – obtener el tipo en tiempo de ejecución. Ejemplo: `Object → Persona → Estudiante`. Proposito: * Organizar las clases en **niveles lógicos**. * Permitir que las clases compartan **atributos y comportamientos comunes**. * Facilitar el **polimorfismo**, al tratar objetos diferentes bajo un mismo tipo general. ## Acceso a miembros heredados Una subclase **hereda** los miembros `public` y `protected`. Los miembros `private` **no son heredados**, pero sí accesibles a través de getters/setters. ```java[] public class Estudiante extends Persona { public void mostrar() { // System.out.println(dpi); // private: no accesible System.out.println(nombre); // protected: heredado System.out.println(edad); // public: heredado } } ``` ## Override La **sobrescritura de métodos** ocurre cuando una subclase redefine el comportamiento de un método heredado de la superclase. El método mantinen la **misma firma** (nombre, parámetros y tipo de retorno compatible). Se utiliza para **personalizar o especializar** el comportamiento heredado. La anotación `@Override` no es obligatoria, pero ayuda al compilador a detectar errores. La resolución del método ocurre en **tiempo de ejecución** (polimorfismo dinámico). No se pueden sobrescribir métodos `final`, `static` o `private`. ```java[] class Persona { public void saludar() { System.out.println("Hola"); } } class Estudiante extends Persona { @Override public void saludar() { System.out.println("Hola, soy estudiante"); } } ``` ## Super La palabra clave **`super`** se utiliza en herencia para referirse explícitamente a la superclase. Permite invocar al **constructor de la superclase** desde la subclase. Permite acceder a **métodos o atributos** de la superclase que han sido sobrescritos o están ocultos. Útil para extender comportamientos heredados sin perder la lógica original. ```java[] public class Persona { protected String nombre; public Persona(String nombre) { this.nombre = nombre; } public void saludar() { System.out.println("Hola, soy " + nombre); } } public class Estudiante extends Persona { protected String carnet; public Estudiante(String nombre, String carnet) { super(nombre); // constructor de Persona this.carnet = carnet; } @Override public void saludar() { super.saludar(); // método de Persona System.out.println("Estudio con el carnet " + carnet); } } public class Main { public static void main(String[] args) { Estudiante e = new Estudiante("Ana", "24001"); e.saludar(); } } ``` Salida: ```text[] Hola, soy Ana Estudio con el carnet 24001 ``` `super(nombre)` va siempre en la primera línea del constructor: Persona se encarga del nombre y Estudiante solo agrega el carnet. `super.saludar()` ejecuta el saludo de Persona y después agrega lo suyo. Sin esa línea, el saludo de la superclase se perdería. ## Polimorfismo El **polimorfismo** es la capacidad de un objeto de adoptar múltiples formas. Esto permite que el mismo método tenga diferentes comportamientos según el tipo real del objeto en tiempo de ejecución. Una referencia de la **superclase** puede apuntar a un objeto de cualquier **subclase**. Permite escribir código más **genérico y extensible**, trabajando con la superclase pero obteniendo el comportamiento de la subclase. ```java[] class Persona { public void saludar() { System.out.println("Hola"); } } class Estudiante extends Persona { @Override public void saludar() { System.out.println("Hola, soy estudiante"); } } class Profesor extends Persona { @Override public void saludar() { System.out.println("Buenos días, soy profesor"); } } public class Main { public static void main(String[] args) { Persona p1 = new Estudiante(); Persona p2 = new Profesor(); p1.saludar(); // Hola, soy estudiante p2.saludar(); // Buenos días, soy profesor } } ``` ## Upcasting y Downcasting ```java[] Persona p = new Estudiante(); // upcasting implícito Estudiante e = (Estudiante) p; // downcasting explícito ``` * **Upcasting:** seguro, siempre válido. * **Downcasting:** puede lanzar `ClassCastException` si la referencia no es del tipo esperado. ## Arreglos y polimorfismo Una de las ventajas del polimorfismo es poder trabajar con colecciones de la superclase y almacenar objetos de distintas subclases. Esto facilita el manejo uniforme de diferentes tipos de objetos. ```java[] import java.util.ArrayList; public class Main { public static void main(String[] args) { ArrayList personas = new ArrayList<>(); personas.add(new Estudiante()); personas.add(new Profesor()); personas.add(new Estudiante()); for (Persona p : personas) { // Cada objeto ejecuta su propia versión p.saludar(); } } } ``` Solo un `ArrayList` maneja objetos de diferentes subclases gracias al polimorfismo. ## Clases abstractas y herencia Una clase abstracta **no puede instanciarse**. Puede contener métodos abstractos (sin implementación). Las subclases deben implementar los métodos abstractos. Es una especie de contato. Yo (subclase) me comprometo a implementar los métodos que se definen en la clase abastracta. ```java[] abstract class Animal { public abstract void hacerSonido(); } class Perro extends Animal { @Override public void hacerSonido() { System.out.println("Guau"); } } ``` ## Final `final class` → no puede ser extendida. `final` en un método → no puede ser sobreescrito. ```java[] final class Constante { // no se puede extender } class Base { public final void metodo() {} } class Derivada extends Base { // @Override metodo() ❌ error } ``` ## Ejemplo completo: la Pokédex Vamos a construir un equipo Pokémon aplicando todo lo anterior, paso a paso. ### El problema sin herencia Cada Pokémon como clase independiente repite los mismos datos una y otra vez. ```java[] public class Pikachu { String nombre; int nivel; int hp; public void mostrarFicha() { /* ... */ } } public class Squirtle { String nombre; // otra vez int nivel; // otra vez int hp; // otra vez public void mostrarFicha() { /* ... */ } } ``` Consecuencias de la duplicación: * Un mismo atributo vive en 150 archivos distintos. * Agregar experiencia obliga a editar todas las clases. * No existe un tipo común que los agrupe en una lista. La solución es una superclase `Pokemon` con lo que todos comparten. ### La superclase Pokemon ```java[] public class Pokemon { protected String nombre; protected int nivel; protected int hp; public Pokemon(String nombre, int nivel, int hp) { this.nombre = nombre; this.nivel = nivel; this.hp = hp; } public void atacar() { System.out.println(nombre + " usa Placaje."); } public void mostrarFicha() { System.out.println(nombre + " | Nv. " + nivel + " | HP " + hp); } } ``` Los atributos son `protected` para que las subclases puedan usarlos directamente, sin abrirlos al resto del programa. El constructor inicializa los datos comunes y las subclases lo reutilizan. `atacar()` es un ataque genérico que cada tipo va a sobrescribir. `mostrarFicha()` se hereda tal cual y ninguna subclase necesita reescribirlo. ### PokemonElectrico ```java[] public class PokemonElectrico extends Pokemon { private int voltaje; public PokemonElectrico(String nombre, int nivel, int hp, int voltaje) { super(nombre, nivel, hp); this.voltaje = voltaje; } @Override public void atacar() { System.out.println(nombre + " lanza " + voltaje + " V."); } } ``` Hereda `nombre`, `nivel` y `hp` sin escribirlos otra vez, y agrega el voltaje, que solo tiene sentido para un Pokémon eléctrico. `super(nombre, nivel, hp)` delega en el constructor de `Pokemon`. Después la subclase solo se encarga de lo suyo. El `@Override` mantiene la misma firma con otro comportamiento. Pikachu ya no usa Placaje. ### PokemonAgua ```java[] public class PokemonAgua extends Pokemon { private int presion; public PokemonAgua(String nombre, int nivel, int hp, int presion) { super(nombre, nivel, hp); this.presion = presion; } @Override public void atacar() { System.out.println(nombre + " ataca a " + presion + " psi."); } public void bucear() { System.out.println(nombre + " se sumerge y esquiva el ataque."); } } ``` La misma estructura que el Pokémon eléctrico, con otro ataque. La superclase no cambió ni una línea para que esto funcione. `bucear()` solo existe en esta subclase. Una variable declarada como `Pokemon` no lo puede invocar, aunque el objeto sea un `PokemonAgua`. ### Un equipo con polimorfismo ```java[] ArrayList equipo = new ArrayList<>(); equipo.add(new PokemonElectrico("Pikachu", 12, 35, 90)); equipo.add(new PokemonAgua("Squirtle", 10, 44, 60)); equipo.add(new PokemonElectrico("Jolteon", 30, 65, 400)); for (Pokemon p : equipo) { p.mostrarFicha(); p.atacar(); } ``` La lista es de `Pokemon`, pero guarda objetos de cualquier subclase. Eso es upcasting y ocurre solo. Una sola llamada a `p.atacar()`, tres comportamientos distintos. Java decide en tiempo de ejecución cuál versión corresponde a cada objeto. Salida: ```text[] Pikachu | Nv. 12 | HP 35 Pikachu lanza 90 V. Squirtle | Nv. 10 | HP 44 Squirtle ataca a 60 psi. Jolteon | Nv. 30 | HP 65 Jolteon lanza 400 V. ``` `mostrarFicha()` se heredó sin cambios y `atacar()` cambió en cada subclase. ### Pokemon como clase abstracta Un Pokémon genérico no existe en el juego. Nadie debería poder crear uno. ```java[] public abstract class Pokemon { protected String nombre; protected int nivel; protected int hp; public Pokemon(String nombre, int nivel, int hp) { /* ... */ } public abstract void atacar(); // cada tipo define el suyo public void mostrarFicha() { /* se hereda tal cual */ } } ``` Lo que gana el diseño: * `new Pokemon(...)` deja de compilar. * Toda subclase queda obligada a definir `atacar()`. * `mostrarFicha()` se sigue heredando con código real. * El polimorfismo del equipo funciona igual. ### El límite de la herencia Gengar es de fantasma y veneno. Dragonite es de dragón y volador. ¿De qué clase heredan? ```java[] // no compila: Java no permite heredar de dos clases class Gengar extends PokemonFantasma, PokemonVeneno { } ``` La salida forzada es copiar el método del veneno en cada clase que lo necesite, y volvemos a la duplicación del inicio. La salida correcta es separar el tipo (herencia) de la habilidad (contrato). La habilidad se declara en una interfaz. Cada tipo pasa a ser un contrato independiente, no una clase padre. ```java[] interface IFantasma { void atravesarMuros(); } interface IVeneno { void envenenar(); } class Gengar extends Pokemon implements IFantasma, IVeneno { @Override public void atravesarMuros() { System.out.println(nombre + " atraviesa el muro."); } @Override public void envenenar() { System.out.println(nombre + " envenena al rival."); } } ``` Gengar hereda de `Pokemon`, que es lo que todo Pokémon comparte, y los tipos se suman con `implements` sin límite. Dragonite resuelve igual con `IDragon` e `IVolador` sobre la misma clase `Pokemon`. Eso lo vemos en la próxima lección. ## Buenas prácticas Usen herencia solo si existe relación **is-a**. Prefieran **composición** si es un caso “tiene-un”. Eviten jerarquías profundas y complejas. Documenten las clases base para que sea claro qué debe heredarse. ## Limitaciones No hay herencia múltiple de clases en Java. Puede generar acoplamiento excesivo. Difícil de mantener en jerarquías muy profundas. **Alternativa:** interfaces y composición.