Ir al contenido
wannacode

Herencia

En resumen: La herencia es cuando una clase (la hija) recibe los campos y los métodos de otra clase (la clase padre), así que la parte común se escribe en un solo lugar.

Problema

Dos copias del mismo código

El programa de nóminas tiene una clase Employee con un nombre, un salario y un método para un aumento. Después hizo falta también un gerente, que además recibe un bono. Lo más rápido fue copiar Employee en una clase nueva Manager y añadir el bono.

Un mes después alguien introduce un aumento del -10 % en lugar del 10 %. Así que añades una comprobación a Employee. Te olvidas de ella en Manager:

// en Employee
public void raise(int percent) {
if (percent > 0) {
salary += salary * percent / 100;
}
}
// en Manager: copia de antes de la comprobación
public void raise(int percent) {
salary += salary * percent / 100;
}
Employee ivan = new Employee("Juan", 2000);
Manager maria = new Manager("María", 3000, 500);
ivan.raise(-10);
maria.raise(-10);
System.out.println(ivan);
System.out.println(maria);
Salida
Juan: 2000 €
María: 2700 €

El salario de Juan está protegido, pero el de María bajó 300 €. Las dos clases son iguales en un 90 %, y aun así cada corrección hay que hacerla dos veces.

La idea

La parte común va en la clase padre

Un gerente es un empleado, solo que con algo más. Dilo en el código: class Manager extends Employee. En Manager escribes solo lo nuevo, el bono:

class Manager extends Employee {
private int bonus;
public Manager(String name, int salary, int bonus) {
super(name, salary);
this.bonus = bonus;
}
public int getBonus() {
return bonus;
}
}
Manager maria = new Manager("María", 3000, 500);
maria.raise(-10);
maria.raise(10);
System.out.println(maria);
System.out.println("Bono: " + maria.getBonus() + " €");
Salida
María: 3300 €
Bono: 500 €

Ahora Manager tiene todo lo que tiene Employee: un nombre, un salario, raise() con la comprobación, toString(). Por eso el aumento del -10 % no pasó. La comprobación está escrita una vez y funciona para las dos clases.

Esto se llama herencia. Employee es la clase padre (clase base), y Manager es la clase hija. La hija lo toma todo de la clase padre y añade lo suyo.

Para comprobar si la herencia tiene sentido, usa la frase «X es un Y». «Un gerente es un empleado» es cierto. «Una factura es un empleado» no lo es, así que la factura no hereda de Employee, aunque tenga un importe y un nombre.

Pruébalo

¿Qué recibe el becario?

Intern no tiene nada propio salvo un constructor:

¿Qué imprimirá el programa?

class Intern extends Employee {
public Intern(String name, int salary) {
super(name, salary);
}
}
Intern ana = new Intern("Ana", 1200);
ana.raise(5);
System.out.println(ana);
Respuesta

La idea

Primero la clase padre

name y salary son private en Employee. La hija los tiene, pero no los ve: para la hija están ocultos, igual que para cualquier otra clase. Entonces, ¿cómo les da un valor Manager?

Llamando al constructor de la clase padre:

public Manager(String name, int salary, int bonus) {
super(name, salary);
this.bonus = bonus;
}

super(name, salary) llama al constructor de Employee con estos argumentos. Prepara el nombre y el salario, y después Manager prepara el bono. En Java, super(...) tiene que ser la primera línea del constructor.

Hay un solo objeto. Contiene tanto la parte de la clase padre como la de la hija, y cada parte la prepara su propio constructor.

public Manager(String name, int salary, int bonus) {
super(name, salary);
this.bonus = bonus;
}
Manager maria = new Manager("María", 3000, 500);
System.out.println(maria + ", bono " + maria.getBonus() + " €");
Paso a paso, en palabras
  1. new reserva un objeto. Contiene la parte de Employee y la parte de Manager.
  2. Empieza el constructor de Manager con los tres argumentos.
  3. Primero la clase padre: super(name, salary) llama al constructor de Employee.
  4. El constructor de Employee prepara su parte: nombre y salario.
  5. Solo entonces el cuerpo de Manager prepara el bono.
  6. El objeto es uno solo, y cada parte la prepara su propio constructor.
Más precisamente, para curiosos
  • Los constructores no se heredan. Manager no tiene un constructor Manager(String name, int salary) a menos que lo escribas.
  • Si la clase padre solo tiene un constructor con parámetros y la hija no le pasa nada, el programa no compila. El compilador no sabe qué nombre ni qué salario dar.

Pruébalo

¿Qué constructor se ejecuta primero?

Aquí la hija no llama a la clase padre de forma explícita:

¿Qué imprimirá el programa?

class Account {
public Account() {
System.out.println("Cuenta abierta");
}
}
class SavingsAccount extends Account {
public SavingsAccount() {
System.out.println("Interés: 3%");
}
}
SavingsAccount savings = new SavingsAccount();
Respuesta

Pruébalo

Encuentra el error

María debería tener un salario de 3000 € y un bono de 500 €. Pero el programa imprime:

Salida
María: 500 €

¿En qué línea está el error?

Elige la línea con el error.

La idea

protected: solo para las hijas

A fin de año, el bono del gerente se suma al salario. El método está en Manager, pero el salario es private en Employee. Un getter no basta: hay que cambiarlo.

Entre private y public hay un tercer nivel de acceso: protected. La clase y sus hijas ven ese miembro, y todos los demás no:

class Employee {
private String name;
protected int salary;
// ... el resto, como antes
}
class Manager extends Employee {
// ... bonus y el constructor
public void mergeBonus() {
salary += bonus;
bonus = 0;
}
}
Manager maria = new Manager("María", 3000, 500);
maria.mergeBonus();
System.out.println(maria);
System.out.println("Bono: " + maria.getBonus() + " €");
Salida
María: 3500 €
Bono: 0 €

protected debilita la protección: cualquier hija puede cambiar el salario sin pasar por la comprobación de raise(). Por eso los campos siguen siendo private por defecto. protected es para los casos en los que la hija realmente tiene que tocar el campo.

Más precisamente, para curiosos
  • En Java, un miembro protected también es visible para todas las clases del mismo paquete (una carpeta de código). Nuestros programas están en un archivo, así que están en un paquete. Por eso maria.salary en main compila, aunque el campo sea protected. En un proyecto grande con muchos paquetes no es así.

Pruébalo

¿Qué falta?

Un becario es un empleado. ¿Qué va en el hueco?

class Intern  Employee {
    public Intern(String name, int salary) {
        super(name, salary);
    }
}

La idea

Una clase padre y un antepasado común

Una clase tiene exactamente una clase padre. No puede heredar a la vez de Employee y de Student. Por eso la herencia es simple.

La cadena puede ser larga. Intern hereda de Employee. ¿Y de qué hereda Employee, si no tiene extends? Toda clase sin padre hereda de Object. Es el padre común de todas las clases. Por eso incluso una clase vacía tiene métodos:

class Ticket {
}
Ticket a = new Ticket();
Ticket b = new Ticket();
System.out.println(a.equals(a));
System.out.println(a.equals(b));
Salida
true
false

equals() viene de Object. Por ahora solo te dice si dos variables apuntan al mismo objeto.

toString() también viene de ahí. Si no lo escribes, System.out.println(ticket) imprime algo como Ticket@1b6d3586: el nombre de la clase y un número que cambia en cada ejecución. En el módulo «Clases y objetos» escribiste tu propio toString() con @Override encima. Ahora sabes que así reemplazas la versión de Object. Qué significa exactamente @Override está en la lección siguiente.

Más precisamente, para curiosos
  • De una clase con final delante de class no se puede heredar. String es una de ellas.
  • Una variable de tipo Object puede apuntar a cualquier objeto: Object anything = maria;.

Ejercicio

Clases hijas

Resuelve las tareas en tu editor. Las clases padre son del módulo «Clases y objetos». Si no las tienes a mano, escríbelas otra vez: son cortas.

1. Coche familiar. SimpleCar tiene una velocidad y un método accelerate() que la sube en 10. Escribe FamilyCar, que hereda de SimpleCar, empieza con 4 asientos y tiene un método addSeat(). Crea un coche familiar, acelera y añade un asiento:

Salida
Velocidad: 10 km/h
Asientos: 5

2. Estudiante. Person tiene un nombre, una edad e introduce(). Escribe Student, que hereda de Person y añade una nota media grade (un número decimal). Usa el constructor de la clase padre.

Salida
Soy Elena, tengo 19 años.
Nota media: 5.5

3. Breakdance. DanceMove tiene un nombre, una duración en segundos y perform(). Escribe BreakDanceMove con una complejidad del 1 al 10 y un método showComplexity().

Salida
Ejecuto moonwalk durante 5 segundos
Ejecuto windmill durante 12 segundos
windmill: complejidad 8 de 10

4. Coche eléctrico. Car tiene una marca, combustible en litros y los kilómetros recorridos. refuel(liters) añade combustible, y drive(distance) gasta 1 litro cada 10 km y suma los kilómetros (si no hay combustible suficiente, lo dice y no se mueve). Escribe ElectricCar:

  • sin combustible, y con una batería que empieza al 100 %;
  • con una eficiencia: cuántos km recorre con el 1 % de batería (5 para el «Tesla»);
  • charge() carga la batería al 100 %;
  • driveElectric(distance) gasta batería en lugar de combustible y también suma los kilómetros.
Los kilómetros son private en Car. ¿Cómo los cambia la hija? Pista: la tarjeta sobre protected.
Salida
Škoda: 150 km, combustible 35 l
Tesla: 200 km, batería 60%
No alcanza la batería para 400 km
Tesla: 600 km, batería 20%

Más tareas

  • Trabajador. Worker hereda de Person y añade un puesto de trabajo y un método work():
Salida
Soy Pablo, tengo 34 años.
Trabajo como contable
  • Becario con mentor. Añade a Intern un campo con el nombre del mentor y pásalo en el constructor, junto con el nombre y el salario para la clase padre.

Test

Ponte a prueba

1. class Manager extends Employee. ¿Qué es cierto?
2. El campo name es private en Employee. ¿Puede Manager escribir name = "María"; en uno de sus métodos?
3. ¿Qué hace super(name, salary) en el constructor de Manager?
4. ¿Cuántas clases padre puede tener una clase?
5. ¿Quién puede ver un campo protected?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·