sábado, 21 de enero de 2012

Polimorfismo y herencia

votar
   El polimorfismo es una de las claves de la programación orientada a objetos, y aunque no es muy complicado, hay que tener bien claro el concepto para no liarse.

   En realidad, ya hemos visto el polimorfismo en los métodos en la entrada en que tratamos de overloading y overriding, pero ahora vamos a verlo relacionado con la herencia.

   Será más sencillo con un ejemplo. Supongamos que en un juego tenemos la clase Monstruo:

abstract class Monstruo{
      public void hacerCosasMonstruosas(){
         //código
      }
      public abstract andar(){ }
      public abstract atacar(){ }
}

  Pero nosotros queremos tener distintos monstruos en el juego, así que extendemos la clase Monstruo con los siguientes "hijos": Vampiro, Momia, Zombie, Hombre-Lobo.

   En Java, las clases hijas tienen los mismos miembros (métodos y datos) que la clase madre que no sean declarados privados. Eso es la herencia.

   Lo bueno es que estas clases hijas estarán más especializadas que la clase que extienden. Vampiro, Momia, Zombie y Hombre-Lobo, cada uno implementará su propia versión de los métodos abstractos andar() y atacar(); y en cuanto al método hacerCosasMonstruosas(), lo podrán usar tal cual si les conviene, o bien podran modificarlo si es necesario(overriding). Además, podrán tener sus propios campos si hace falta.

   Veamos ahora la forma correcta de usar la herencia. Creemos por ejemplo una instancia de Vampiro:

   Vampiro v = new Vampiro();

   Supongamos ahora que Monstruo no fuese abstracta y creemos una instancia m de dicha clase. Entonces podemos hacer esto:
  
   m = v;

   Y de esta forma ahora m y v hacen referencia al mismo objeto. 
   También podemos hacer esto otro:

   Monstruo m = new Vampiro();

   Pero nunca podemos hacer lo siguiente:

   Vampiro v = new Monstruo()//nunca jamás!!

   La razón es clara: todo vampiro es un monstruo, pero no todo monstruo es un vampiro; así que, podemos decir que un objeto Monstruo m es un vampiro sin problema, pero no podemos decir que un objeto Vampiro v es un monstruo, porque ¿y si ese monstruo resulta ser un zombie?. Aún así, si nos vemos en la necesidad de asignar v a m, podemos hacer un casting:

   v = (Vampiro)m;

   De esta forma, el programa compila y la conversión se comprueba durante la ejecución. Si el casting no es correcto, se produce una ClassCastExceptionPero aún hay más.

   Imaginemos ahora que en lugar de la clase Monstruo tenemos la inerfaz Monstruoso. En este caso, m puede referenciar (puede que no exista el verbo referenciar, pero es muy útil) a cualquier objeto de cualquier clase que implemente dicha interfaz. Un ejemplo típico de esto lo vemos en:

   List miLista = new ArrayList();

   Volvamos ahora al método hacerCosasMonstruosas(). Vemos que en la clase Monstruo tiene un código, pero queremos que en la clase Vampiro sea más especializado, y le añadimos cosas propias de un vampiro, como chupar sangre. Si ahora escribimos lo siguiente: 

   Monstruo m = new Vampiro();
   m.hacerCosasMonstruosas{
      //código
    }

 ¿Qué método será el utilizado, hacerCosasMonstruosas() de la clase Monstruo, o el nuevo y modificado de la clase Vampiro?.

   La respuesta es que para el compilador, un Vampiro es un Monstruo, que tiene el método hacerCosasMonstruosas(), pero en tiempo de ejecución la JVM sabe lo que realmente es m (un vampiro) así que utilizará la versión del método de la clase Vampiro.

   Pero hay que ser cuidadosos con esto, porque supongamos que a la clase Vampiro le añadimos un método que no existe en la clase Monstruo, como por ejemplo volar(). ¿Qué ocurrirá si hacemos esto?
   
   Monstruo m = new Vampiro();
   m.volar(){
      //código
   }

   Lo que pasará es que esto no funcionará de ninguna manera. Da igual que tú sepas que m es un vampiro, a todos los efectos, m para el compilador es de la clase Monstruo, y la clase Monstruo no tiene el método volar().

   Y aquí está la clave del polimorfismo, porque vemos que m es en realidad varias cosas: es un vampiro, es un monstruo y, por supuesto, es un objeto.

   Si utilizamos m como referencia de la clase Vampiro

   Vampiro m = new Vampiro();

podemos usar todos los métodos de dicha clase. Pero si escribimos

   Monstruo m = new Vampiro();

m podrá usar la versión de los métodos de la clase Vampiro que tenga la clase Monstruo, pero no un método que no exista en dicha clase, aunque sí en la clase Vampiro.

   Por supuesto, podemos usar un casting, como ya vimos:

   Monstruo m = new Vampiro();
   Vampiro v = (Vampiro)m;
   v.volar();

   Espero que esta entrada haya ayudado a comprender el polimorfismo.




   

2 comentarios:

  1. Buenas noches, una preguntilla:

    Monstruo m = new Vampiro();

    m podrá usar la versión de los métodos de la clase Vampiro que tenga la clase Monstruo, pero no un método que no exista en dicha clase, aunque sí en la clase Vampiro.

    m podrá usar la versión de los métodos de Vampiro que tenga Monstruo. Esto no se podría resumir con herencia simple?

    Por que si siempre la JVM va a utilizar los metodos de Vampiro, qué caso tiene crear una referencia Monstruo?


    Saludos.

    ResponderEliminar
    Respuestas
    1. Hola, Anónimo.
      Primero mis disculpas si no he explicado el tema con suficiente claridad.
      Verás, Vampiro es hijo de Monstruo, por lo que puede heredar todos los métodos del mismo y modificarlos si lo necesita, además de añadir métodos nuevos. Pero Monstruo, como padre, no puede heredar métodos de sus hijos.
      ¿Porqué entonces utilizar Monstruo m = new Vampiro() en lugar de Vampiro v = new Vampiro? Pues eso depende de las necesidades que tenga cada programador. Imagina por ejemplo que tienes que crear un nuevo monstruo pero aún no estás seguro del tipo que necesitas: puedes hacer Monstruo m = new Vampiro() y luego en caso necesario transformar m en un new Zombie().
      Ten en cuenta que aquí yo sólo explico lo que ocurre si haces una cosa u otra, no lo que tu tengas que hacer.
      Personalmente creo que la utilización de subclases debe dejarse para necesidades muy concretas, y que siempre que se pueda es preferible la utilización de interfaces. Si estás interesado en el tema, te recomiendo el libro de Joshua Bloch "Effective Java", donde lo trata en un capítulo. No sé si hay versión en castellano.
      Muchas gracias por leerme (¡un viernes de noche!) y un saludo.;-)

      Eliminar