Propiedades de la Herencia en Java

Buenas tardes/noches, en esta entrada veremos las importancias de la Herencia en Java, al igual que las relaciones Es-un (Is-A) y Tiene un (Has-A) en Java, y ver como se desarrolla el código que implementa estas relaciones.


La herencia se encuentra en cualquier lugar en Java. Es seguro decir que es pácticamente imposible escribir el programa mas pequeño en Java sin usar la herencia. Veamos el siguiente código:

		class Test {
			public static void main(String[] args){
				Test t1 = new Test();
				Test t2 = new Test();
				if (!t1.equals(t2))
					System.out.println("No son iguales");
				if (t1 instanceof Object)
					System.out.println("T1 es un objeto");
			}
		}
		

Produce lo siguiente:

		No son iguales
		T1 es un objeto
		

Nos preguntamos algo, ¿De donde viene el método equals?. La variable de referencia t1 es del tipo Test, y no hay ningún método equals en la clase Test. El segundo test realizado con if pregunta si t1 es una instancia de la clase Object, y como esto es así, el test es completado.

Esto pasa porque todas las clases en Java son subclases de la clase Object. En otras palabras, todas las clases que usemos o escribamos heredarán de la clase Object. Siempre tendremos un método equals, un método clone, notify, wait, y otros, disponibles para su uso. Allí donde creemos una clase, automáticamente hereda todos los métodos de la clase Object.

Vamos a ver mas sobre el método equals por ejemplo. Los creadores de Java asumieron correctamente que sería muy común para los programadores de Java querer comparar instancias de sus clases para comparar su igualdad. Este método equals ha sido heredado billones de veces, y también ha sido sustituido billones de veces.

Podemos crear relaciones de herencia en java extendiendo una clase. es importante saber las 2 razones mas comunes e importantes para usar la herencia son:

  • Para promocionar la reutilización del código.
  • Para usar el polimorfismo.

Empezaremos con la reutilización. Un enfoque de diseño común es crear una versión bastante genérica con la intención de crear subclases mas especializadas que hereden de ella. Por ejemplo:

		class GameShape{
		    public void displayShape(){
		        System.out.println("Displaying shape");
		    }
		    //Mas código
		}
		
		class PlayerPiece extends GameShape{
		    public void movePiece(){
		        System.out.println("Moving game piece");
		    }
		    //Más código
		}
		public class TestShapes {
		    public static void main (String[] args){
		        PlayerPiece shape = new PlayerPiece();
		        shape.displayShape();
		        shape.movePiece();
		    }
		}
		

La salida será:

		Displaying shape
		Moving game piece
		

Vemos que la clase PlayingPiece hereda el método genérico display() de una clase menos especializada GameShape, y también añade su propio método, movePiece(). Esto significa que todas las subclases especializadas de GameShape tienen la garantía de que tienen las capacidades de la clase mas genérica o superclase.

El segundo uso de la herencia es permitir a las clases ser accedidas polimorficamente, una capacidad proveida en las interfaces también. Podemos decir que tenemos una clase GameLauncher que quiere hacer un bucle a través de una lista de diferentes tipos de objetos GameShape, e invocar el método display() en cada uno de ellos.

La idea importante sobre el polimorfismo es que podemos tratar cualquier subclase de GameShape como un GameShape. En otras palabras, podemos escribir código en nuestra clase GameLauncher que dice:

“No me importa que tipo de objeto eres mientras heredas de GameShape. Y hasta lo que me concierna, si heredas de la clase GameShape tendrás un método display(), por lo que sé que puedo llamarlo”

Imaginemos que ahora tenemos 2 subclases especializadas que heredan de una clase GameShape mas genérica, llamadas PlayerPiece y TilePiece:

		class GameShape{
		    public void displayShape(){
		        System.out.println("Displaying shape");
		    }
		    //Mas código
		}
		
		class PlayerPiece extends GameShape{
		    public void movePiece(){
		        System.out.println("Moving game piece");
		    }
		    //Más código
		}
		
		class TilePiece extends GameShape{
		    public void getAdjacent(){
		        System.out.println("Getting adjacent tiles");
		    }
		}
		

Ahora imaginemos que la clase test tiene un método que tiene un argumento declarado del tipo GameShape, que significa que puede coger cualquier tipo de GameShape. Esto quiere decir, que cualquier subclase de GameShape puede ser pasado a este método con un argumento del tipo GameShape. Este código:

		public class TestShapes {
		    public static void main (String[] args){
		        PlayerPiece player = new PlayerPiece();
		        TilePiece tile = new TilePiece();
		        
		        doShapes(player);
		        doShapes(tile);
		    }
		    
		    public static void doShapes(GameShape shape){
		        shape.displayShape();
		    }
		}
		

Su salida sería la siguiente:

		Displaying shape
		Displaying shape
		

El punto clave es que el método doShapes() está declarado con un argumento GameShape pero puede ser pasado cualquier subtipo o subclase de GameShape. El método puede entonces invocar cualquier método de GameShape, sin ninguna importancia para la clase actual en tiempo de ejecución del objeto pasado al método. El método doShapes() conoce solo que los objetos son del tipo GameShape por como el parámetro ha sido declarado. Usando la variable de referencia declarada como el tipo GameShape significa que solo los métodos de GameShape puede ser invocado en el.

  • Es-un (Is-A)

    En la Orientación a Objetos, el concepto de Es-Un o (Is-A) está basado en la herencia de clases o en la implementación de interfaces. Es una manera de decir “Esta cosa es un tipo de esta otra cosa”. Nosotros expresaremos esta relación en java con la palabra clave extends (para herencia de clases) e implements (para la herencia de interfaces).

    				public class Car {
    				    //código de coche
    				}
    				
    				public class Subaru extends Car{
    				    // El código importante específico de SUbaro va aqui
    				    // Subaru hereda los miembros accesibles de Car
    				    // incluyendo métodos y variables
    				}
    				

    Un Car es del tipo Vehiculo, por lo que la herencia podría empezar desde la clase Vehicle de la siguiente manera:

    				public class Vehicle{}
    				public class Car extends Vehicle{}
    				public class Subaru extends Car{}
    				

    En términos de la OO, podemos decirlo de la siguiente manera:

    • Vehicle es una superclase de Car.
    • Car es una subclase de Vehicle.
    • Car es una superclase de Subaru.
    • Subaru es una subclase de Vehicle.
    • Car hereda de Vehicle.
    • Subaru hereda de Vehicle y de Car.
    • Subaru es derivado de Car.
    • Car es derivado de Vehicle.
    • Subaru es derivado de Vehicle.
    • Subaru es un subtipo de Vehicle y Car.

    Por ejemplo, si decimos que la expresión (Foo instanceof Bar) es verdad, entonces la clase Foo ES-UN Bar, aunque Foo no herede directamente Bar, pero en vez de eso hereda de alguna tora clase que es subclase de Bar.

  • Tiene-Un (Has-A)

    Estas relaciones se basan en el uso, en vez de la herencia. En otras palabras, A Tiene-Un B si el código de la clase A tiene una referencia a una instancia de la clase B. Por ejemplo, podemos decir lo siguiente,

    Un Horse Es-Un Animal. Un Horse Tiene-Un Halter.

    El código podría parecerse a esto:

    				public class Animal{}
    				public class Horse extends Animal{
    					private Halter myHalter;
    				}
    				

    En el código anterior, la clase Horse tiene una variable de instancia del tipo halter, por lo que podemos decir “Horse Tiene-Un Halter”. En otras palabras, Horse tiene una referencia a Halter. El código puede usar las referencias de Halter para invocar los métodos que hay en Halter, y conseguir el comportamiento sin tener el código relacionado con Halter dentro de la clase Horse.

    Las relaciones Tiene-Un (Has-A) permiten el diseño de clases que persiguen buenas prácticas de la OO no teniendo clases monolíticas que hagan mil cosas diferentes. Las clases serían especialistas. Mientras mas especializada sea una clase, lo mas probable será que podamos reutilizar la clase en otras aplicaciones.

    Los usuarios de la clase Horse, piensan que la clase Horse tiene un comportamiento Halter. La clase Horse puede tener un método tie(LeadRope rope), por ejemplo. Los usuarios de la clase Horse nunca tendrían que saber cuando se invoca el método tie(), el objeto Horse delega la llamada a su clase Halter invocando myHalter.tie(rope). El escenario descrito podría parecerse al siguiente:

    				public class Horse {
    				    private Halter myHalter = new Halter();
    				    public void tie(LeadRope rope){
    				        myHalter.tie(rope);
    				    }
    				}
    				
    				public class Halter {
    				    public void tie(LeadRope aRope){
    				        //Se realiza el trabajo aquí
    				    }
    				}
    				

    En la OO, nosotros no queremos que los que utilicen la llamada no sepan cual de los objetos actuales están realizando el trabajo. Para que esto ocurra, la clase Horse oculta los detalles de implementación a los usuarios de Horse.

Hasta aquí este resumen breve sobre la Herencia, y los 2 de sus visiones mas importantes entre las clases, con lo cual comprenderemos un poco mejor como funciona todo esto de la Herencia, como nos la podemos encontrar, y como podemos crearla.

Sin mas, cualquier corrección o aporte es bienvenido.

Saludos!!!