Programación Orientada a Objetos: Herencia
Este Post es la segunda parte de la serie “Programación Orientada a Objetos”. Esta serie tiene como objetivo establecer los fundamentos de la POO, utilizando Java como ejemplo. Los temas de esta serie son los siguientes:
- Definición de POO
- Clases y Objetos
- Herencia
- Abstracción
- Encapsulamiento
- Polimorfismo
- Composición
- Principios utilizados en la POO
Todo el código de la serie está disponible en GitHub.
El código correspondiente a esta segunda parte lo pueden encontrar aquí.
Todos los ejemplos están hechos en Netbeans.
Introducción
La POO nos provee algunos conceptos y herramientas para hacer un diseño de nuestro código de una mejor manera, para que tenga un mejor rendimiento y además que sea más sencillo actualizarlo. Si bien los ejemplos que presentaremos en esta y otras secciones serán muy sencillos, cumplen con la intención de mostrar los conceptos necesarios para entender la POO. En aplicaciones más grandes se utilizan los mismos conceptos, con la diferencia de que involucran más código y objetos.
Vamos a crear un programa que pueda mostrar el comportamiento de varios sistemas de transporte: Auto, Avión, Nave Espacial. Crearemos una clase para cada uno de esos sistemas de transporte.
La clase Auto tendrá color, marca, cantidad de combustible y capacidad de pasajeros como datos y tendrá como operaciones arrancar, apagar, avanzar, cargar combustible y cambiar de velocidades. Cada una de las operaciones solamente imprimirá un mensaje de texto en la consola. Nuestra clase Auto quedará de la siguiente manera:
public class Auto {
private float cantidadCombustible;
private String marca;
private String color;
private int capacidad;
public Auto(String marca, String color, int capacidad) {
this.marca = marca;
this.color = color;
this.capacidad = capacidad;
}
public void arrancar() {
System.out.println("Arrancando el auto.");
}
public void apagar() {
System.out.println("Apagando el auto.");
}
public void avanzar() {
System.out.println("Avanzando el auto.");
}
public void ponerCombustible(float cantidad) {
cantidadCombustible += cantidad;
}
public void cambiarVelocidades() {
System.out.println("Cambiando de velocidad.");
}
public float getCombustible() {
return cantidadCombustible;
}
public String getMarca() {
return marca;
}
public String getColor() {
return color;
}
public int getCapacidad() {
return capacidad;
}
}
Ahora crearemos una clase Avión, con los datos cantidad de combustible, capacidad, marca y modelo. Tendrá como operaciones arrancar, apagar, despegar, aterrizar, avanzar y poner combustible.
public class Avion {
private float cantidadCombustible;
private int capacidad;
private String marca;
private String modelo;
public Avion(int capacidad, String marca, String modelo) {
this.capacidad = capacidad;
this.marca = marca;
this.modelo = modelo;
}
public void arrancar() {
System.out.println("Arrancando el avión.");
}
public void apagar() {
System.out.println("Apagando el avión.");
}
public void despegar() {
System.out.println("Despegando.");
}
public void avanzar() {
System.out.println("Avanzando el avión.");
}
public void aterrizar() {
System.out.println("Aterrizando.");
}
public void ponerCombustible(float cantidad) {
cantidadCombustible += cantidad;
}
public float getCombustible() {
return cantidadCombustible;
}
public String getMarca() {
return marca;
}
public String getModelo() {
return modelo;
}
public int getCapacidad() {
return capacidad;
}
}
Por último crearemos una clase NaveEspacial con los datos cantidad de combustible, capacidad, nombre y país. Tendrá como operaciones arrancar, apagar, despegar, aterrizar, avanzar, poner combustible y colocar satélite en órbita.
public class NaveEspacial {
private float cantidadCombustible;
private int capacidad;
private String nombre;
private String pais;
public NaveEspacial(int capacidad, String nombre, String pais) {
this.capacidad = capacidad;
this.nombre = nombre;
this.pais = pais;
}
public void arrancar() {
System.out.println("Arrancando la nave espacial.");
}
public void apagar() {
System.out.println("Apagando la nave espacial.");
}
public void despegar() {
System.out.println("Despegando la nave espacial.");
}
public void avanzar() {
System.out.println("Avanzando la nave espacial.");
}
public void aterrizar() {
System.out.println("Aterrizando la nave espacial.");
}
public void colocarSatelite() {
System.out.println("Colocando satélite en órbita.");
}
public void ponerCombustible(float cantidad) {
cantidadCombustible += cantidad;
}
public float getCombustible() {
return cantidadCombustible;
}
public String getNombre() {
return nombre;
}
public String getPais() {
return pais;
}
public int getCapacidad() {
return capacidad;
}
}
Podemos hacer uso de nuestras clases de la siguiente manera, en nuestro método main:
Auto auto = new Auto("Audi", "Rojo", 2);
Avion avion = new Avion(700, "Boeing", "787");
NaveEspacial naveEspacial = new NaveEspacial(8, "Orion", "EUA");
auto.ponerCombustible(30);
avion.ponerCombustible(250);
naveEspacial.ponerCombustible(25000);
auto.arrancar();
auto.avanzar();
auto.cambiarVelocidades();
auto.apagar();
avion.arrancar();
avion.despegar();
avion.avanzar();
avion.aterrizar();
naveEspacial.arrancar();
naveEspacial.despegar();
naveEspacial.colocarSatelite();
naveEspacial.aterrizar();
Parece que todo funciona adecuadamente. Pero si miramos detalladamente nuestras tres clases, podemos ver que hay métodos y comportamientos que se repiten en las tres clases. ¿Habrá manera de evitar eso?
Definición
Los lenguajes de programación que son orientados a objetos, nos ofrecen el concepto de Herencia. La herencia es crear objetos basados en otros objetos. Al hacer esto, los objetos pueden utilizar métodos y variables que se encuentren en el padre. Con esto podemos reutilizar código y centralizarlo para evitar repetición de código y que a la hora de modificarlo sea más sencillo. La clase que hereda de otra clase se llama Clase Hijo o Subclase, mientras que la clase de la que se hereda se conoce como Clase Padre o Super Clase.
Herencia en acción
Para heredar de una clase en Java, se utiliza la palabra extends. Utilizando esto, podemos mejorar nuestro código de Sistemas de Transporte. Podemos crear una clase padre llamada Vehiculo donde se realicen las operaciones generales de un vehículo.
public class Vehiculo {
private float cantidadCombustible;
private String marca;
private int capacidad;
public Vehiculo(String marca, int capacidad) {
this.marca = marca;
this.capacidad = capacidad;
}
public void arrancar() {
System.out.println("Arrancando.");
}
public void apagar() {
System.out.println("Apagando.");
}
public void avanzar() {
System.out.println("Avanzando.");
}
public void ponerCombustible(float cantidad) {
cantidadCombustible += cantidad;
}
public float getCombustible() {
return cantidadCombustible;
}
public String getMarca() {
return marca;
}
public int getCapacidad() {
return capacidad;
}
}
Ahora podemos hacer que nuestros vehículos hereden de la clase Vehículo, y eliminar los métodos que se han implementado en la clase Vehiculo.
super
Para poder utilizar los constructores de la clase padre, se cuenta con el método super(). El constructor de la clase Auto quedaría de la siguiente manera:
public Auto(String marca, String color, int capacidad) {
super(marca, capacidad);
this.color = color;
}
Después de heredar de la clase Vehiculo, modificar el constructor y eliminar los métodos que están implementados en la clase padre, nuestra clase Auto quedaría así:
public class Auto extends Vehiculo {
private String color;
public Auto(String marca, String color, int capacidad) {
super(marca, capacidad);
this.color = color;
}
public void cambiarVelocidades() {
System.out.println("Cambiando de velocidad.");
}
public String getColor() {
return color;
}
}
De manera similar las clases Avion y NaveEspacial:
public class Avion extends Vehiculo {
private String modelo;
public Avion(int capacidad, String marca, String modelo) {
super(marca, capacidad);
this.modelo = modelo;
}
public void despegar() {
System.out.println("Despegando.");
}
public void aterrizar() {
System.out.println("Aterrizando.");
}
public String getModelo() {
return modelo;
}
}
public class NaveEspacial extends Vehiculo {
private String nombre;
private String pais;
public NaveEspacial(int capacidad, String nombre, String pais) {
super("", capacidad);
this.nombre = nombre;
this.pais = pais;
}
public void despegar() {
System.out.println("Despegando la nave espacial.");
}
public void aterrizar() {
System.out.println("Aterrizando la nave espacial.");
}
public void colocarSatelite() {
System.out.println("Colocando satélite en órbita.");
}
public String getNombre() {
return nombre;
}
public String getPais() {
return pais;
}
}
Podemos ver como la lógica común de los tres sistemas de transporte están contenidos en la clase Vehículo. Si se necesita modificar algo de esos métodos solamente se tendría que modificar en un solo lugar, y además el mismo código es utilizado por las tres clases. También podemos ver que se pueden agregar métodos específicos para cada clase, extendiendo la funcionalidad de la clase padre.
Sobreescribir métodos: @Override
Si ejecutamos nuestro código ahora, se mostrará en consola el mensaje para cada método. Pero ahora no sabemos cuál salida es de cuál vehículo!
Hay varias maneras de modificar esto, pero en esta ocasión utilizaremos la sobreescritura de métodos. Podemos volver a definir un método de la clase padre en la clase hija. A la hora de utilizar ese método de nuestro objeto, se ejecutará el código de la clase hija. Para indicar que un método se está sobreescribiendo, se agrega la anotación @Override antes del método.
En nuestra clase Auto, vamos a sobreescribir los métodos arrancar(), apagar() y avanzar().
@Override
public void arrancar() {
System.out.println("Arrancando el auto.");
}
@Override
public void apagar() {
System.out.println("Apagando el auto.");
}
@Override
public void avanzar() {
System.out.println("Avanzando el auto.");
}
Para la clase Avión, vamos a utilizar una manera diferente de hacer lo mismo. Vamos a sobreescribir el método, agregarle nueva funcionalidad y después ejecutar el código del método de la clase padre. Para esto podemos utilizar la palabra super nuevamente, seguida del método que necesitemos: super.arrancar();
@Override
public void arrancar() {
System.out.println("AVION:");
super.arrancar();
}
@Override
public void apagar() {
System.out.println("AVION:");
super.apagar();
}
@Override
public void avanzar() {
System.out.println("AVION:");
super.avanzar();
}
Herencia: sus ventajas y sus problemas
Como hemos visto, la herencia nos permite reutilizar y centralizar código, haciendo nuestro código más reusable y limpio. Pero trae también algunos problemas.
En Java, solamente se permite la herencia simple - heredar de una sola clase. En sistemas más grandes esto representa un problema. De igual manera, la clase hija depende estrechamente de la implementación que tenga la clase padre y si en algún momento esta implementación llega a cambiar, la clase hija podría tener un comportamiento distinto al deseado. Hay maneras de evitar estos problemas, y lo veremos en los siguientes módulos.
Deja un comentario