Hasta ahora sabemos lo básico de las clases y ya podríamos crear programas orientados a objetos. En esta entrega introduciré unos de los conceptos que dan mayor potencia a la orientación a objetos: las ideas de herencia y polimorfimo.
El clásico ejemplo de herencia es el siguiente: supongamos que tenemos una clase Animal. Esta sería una clase muy genérica. Podríamos generar otras clases más concretas como Mamífero, Ave, Pez y Reptil, todas subclases de la superclase Animal. A la clase Animal se le llama clase padre o superclase y el resto serían las clases hijas o subclases. Tambien podemos seguir creando clases como Gaviota, Paloma que heredan de Ave y Humano, Elefante y Perro que heredan de mamífero (podríamos continuar creando las clases Caniche, Labrador, Husky, etc como subclases de perro). Imaginemos que nos interesa el número de patas de los animales. En la clase Animal la variable estaría inicializada a 4 en el constructor, por ejemplo, de forma que cualquier subclase heredará dicha variable con el valor 4, siempre que no se indique lo contrario. La clase Pez sobreescribirá el valor de la variable “patas” para inidicar que no tiene ninguna y la clase humano y ave también para indicar que tienen 2. Sin embargo, la clase Perro no tendrá que modificarla y heredará el valor por defecto.
Igualmente pasa con los métodos: Si la clase Perro tiene el método
void ladra (){ System.out.println ("Guau"); }
Las subclases de Perro no necesitarán volver a implementar el método, ya que lo heredan de la clase padre:
class Animal{ int numPatas; Animal (){ numPatas=4; } int getNumPatas (){ return (numPatas); } } class Mamifero extends Animal{ Mamifero (){ numPatas=4; } } class Humano extends Mamifero{ Humano (){ numPatas=2; } } class Perro extends Mamifero{ Perro (){} void ladra (){ System.out.println ("Guau"); } } class Chiwawa extends Perro{ Chiwawa (){} } Chiwawa miPerro = new Chiwawa (); miPerro.ladra(); // Devuelve Guau int numPatas = miPerro.getNumPatas(); // Devuelve 4 Humano yo = new Humano (); yo.getNumPatas (); // Devuelve 2
Como probablemente habrás deducido, la herencia se indica mediante la palabra reservada extends. Class B extends A indica que la clase B hereda de A.
Aunque las clases hijas heredan todas las propiedades de las clases padre por defecto, también pueden sobreescribirlas como veremos a continuación.
De acuerdo a la definición de Wikipedia, el polimorfismo se refiere a la capacidad para que varias clases derivadas de una antecesora utilicen un mismo método de forma diferente. Esto quiere decir simplemente que si tenemos dos clases A y B:
class A{ void m (){ /*implementacion*/ } } class B extends A{ void m (){ /*otra implementacion*/ } }
Si hacemos:
A objeto = new B (); objeto.m ();
La clase de “objeto” será aparentemente A pero su clase real será B. La llamada al método m ejecutará el método de la clase real, es decir, el método m de B.
¿Y esto para qué es util? Para explicarlo, y a modo de anécdota, os pondré un caso real de un programa que implementé hace algunos años: el juego del comecocos o Pacman. Para el que no lo conozca en profundidad, en el juego hay un laberinto donde se mueve Pacman y en el que hay 4 fantasmas rojo, rosa, naranja y azul (Blinky, Pinky, Inky y Clyde respectivamente), cada uno con una estrategia diferente para capturar a Pacman: Blinky va directo a la casilla donde está Pacman, Pinky intenta ir 3 casillas por delante, Inky se situa de forma que Pacman esté entre él y Blinky y Clyde simplemente se hacerca a Pacman si esta lejos o se aleja si está demasiado cerca.
Este es un caso claro donde la herencia nos ayudará a implementar el juego. Tendremos una clase base “Fantasma” y 4 subclases Blinky, Pinky, Inky y Clyde que heredan de Fantasma. En casi todos los aspectos, todos los fantasmas se comportan igual por lo que la mayor parte de la implementación iría en la clase Fantasma. La única diferencia está en la dirección en la que se mueve cada uno, por lo que tendríamos un método mover () en la clase Fantasma que devolverá la dirección en la que nos moveremos y que será sobreescrito en las subclases de forma que cada uno tenga una estrategia diferente.
Para almacenarlos tendríamos un array de fantasmas:
Fantasma fantasmas [] = new Fantasma [4]; fantasma [0] = new Blinky (); fantasma [1] = new Pinky (); fantasma [2] = new Inky (); fantasma [3] = new Clyde (); // Y para mover for (int i=0; i<4; i++) fantasma [i].mover ();
Aquí hay 2 cosas que merecen ser mencionadas:
fantasmas es un array de tipo Fantasma. Sin embargo a cada posición le estamos asignando un objeto que no es de clase Fantasma, sino de una subclase. Esto puede hacerse ya que asignamos un objeto de una clase heredada. Así, el tipo aparente de la variable es Fantasma, pero el tipo real será Blinky, Pinky etc.
Al llamar al método mover: ¿Se llama al metodo de la clase Fantasma o al de la subclase? En Java, se llama siempre al de la clase real, es decir, el de la subclase en este caso.
Si aún no le veis la utilidad, imaginad que tenemos un juego en el que hay múltiples tipos de enemigos con sus correspondientes subtipos. Gracias al polimorfismo, si tenemos una clase padre, podemos almacenar todos los enemigos aparentemente como objetos de dicha clase padre, cuando en realidad ejecutan métodos propios de su clase, haciendo la programación mucho más genérica, ordenada y elegante.