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.




   

sábado, 14 de enero de 2012

Clases anidadas

votar
   En Java, cuando una clase se complica demasiado, o cuando necesita métodos de otra clase, a veces es conveniente utilizar clases dentro de otras clases, o dentro de interfaces, antes que escribirlas varias páginas más allá en el código. Estas clases se llaman clases anidadas y pueden ser de dos tipos:
  • Estáticas
  • Internas
   A su vez, las clases internas pueden ser:
  • Miembro
  • Locales
  • Anónimas
   Vamos a ver con más detalle estas clases, comenzando por las estáticas.
   Las clases anidadas estáticas son muy sencillas de declarar:

   class Externa{
      static class AnidEstatic{
          //código
      }
   }

   Una clase anidada estática puede acceder directamente  a los métodos y atributos estáticos de la en la que está inmersa, incluido los privados, pero no a los no estáticos (es lo mismo que ocurre con los métodos estáticos). Para acceder a estos necesita hacerlo a través de una instancia de la clase:

   class Externa{
      int i;
      static class AnidEstatic{
         Externa e = new Externa( );
         {
            e.i = 8; //acceso a un miembro variable a través de una instancia de la clase externa
          }
      }
   }

   El nombre completo de una clase estática anidada incluye el de la clase externa, así que para crear una instancia de esta clase desde fuera de la clase contenedora, tendríamos que crear escribir lo siguiente:

   Externa.AnidEstatic ae = new Externa.AnidEstatic( );

   Pasemos ahora a las clases internas, comenzando por las del tipo miembro. Una clase interna miembro se llama así porque es un miembro más de la clase que la contiene y tiene por lo tanto acceso a cualquier otro miembro, incluso los privados. La clase externa debe declarar una instancia de la clase interna antes de poder invocar sus métodos o utilizar sus atributos.
   Para declarar una clase interna miembro, hacemos exactamente lo mismo que con una clase anidada estática, solo que sin el static delante:

   class Externa{
      class InMi{
         //código
      }
   }

   Al compilar una clase interna miembro, se crean dos archivos distintos:

      Externa.class
      Externa$InMi.class

   Al contrario que la clase interna miembro, una clase interna local no es un miembro de la clase externa. Es declarada dentro de un bloque, normalmente un método, y no es accesible fuera del mismo, pero se puede crear un objeto de ella y utilizarlo como referencia.
   
   void método( ){
      class InLo{
         //código
      }
      InLo il = new InLo( );
      nuevoMétodo(il);
   }

   Una clase interna local no puede utilizar las variables locales del método en el que está alojada, a no ser que estén marcadas como final, es decir, que no puedan cambiar, ya que la vida de una instancia de la clase interna puede ser mayor que la del método en la que dicha clase fue creada y las variables locales solo pueden vivir mientras viva su método.

   Veamos finalmente las clases internas anónimas, así llamadas porque son declaradas sin ningún nombre de clase.
   Las clases anónimas pueden ser declaradas dentro de un método e incluso dentro del argumento de un método. Esto puede hacerse porque se declaran e instancian en una única expresión.

   class Ejemplo{
      public void ej( ){
         System.out.println("clase Ejemplo");
      }
   }
   class Externa{
      Ejemplo ej = new Ejemplo( ){ //esto no es una referencia a una instancia de Ejemplo, sino una referencia 
                                                    //a una subclase de Ejemplo (anónima)
         public void ej( ){
            System.out.println("clase Ejemplo anónima");
         }
      }; //¡acaba con un punto y coma!
   }

   Las clases anónimas son tratadas por el compilador como si fuesen clases locales internas, por lo tanto, sólo pueden acceder a las variables del método en el que están inmersas si éstas están marcadas como final.