Casting en Variables de Referencia Java

Buenos días, en esta entrada vamos a demostrar el uso del polimorfismo. Adicionalmente, determinar cuando un cast será necesario y reconocer errores de compilación vs errores en tiempo de ejecución relacionados con el cast a referencias de objetos.


Hemos visto como es posible y común usar tipos de variable de referencia genéricos para referirse a tipos de objetos mas específicos. Esto es el corazón del polimorfismo. Por ejemplo, esta línea de código debería ser su segunda naturaleza por ahora:

Animal animal = new Dog();

Pero ¿Qué pasa cuando queremos usar la referencia de Animal para invocar un método que solo la clase Dog tiene? Sabemos que está referenciando a Dog, y queremos que haga algo específico de Dog. En el siguiente código, tenemos un array de Animal, y donde encontremos un Dog en el array, queremos que haga algo especial de Dog. Vamos a aceptar por ahora que todo este código está bien, excepto que no estamos seguros de que línea de código invoca el método playDead().

public class CastTest2 {

	public static void main(String[] args) {
		Animal[] a = {new Animal(), new Dog(), new Animal()};
		for (Animal animal : a){
			animal.makeNoise();
			if(animal instanceof Dog){
				animal.playDead();
			}
		}

	}

}

class Animal{
	void makeNoise(){
		System.out.println("Ruido generico");
	}
}

class Dog extends Animal{
	void makeNoise(){
		System.out.println("Ladra");
	}
	void playDead(){
		System.out.println("Tumbarse hacia arriba");
	}
}

Si intentamos compilar esto, el compilador nos lanzará un error.

El compilador está diciendo, “Hey, la clase Animal no tiene un método llamado playDead()“. Vamos a modificar el código del if:

if(animal instanceof Dog){
				Dog d = (Dog) animal;
				d.playDead();
			}

El nuevo e improvisado bloque de código contiene un Cast, lo cual en este caso a veces es llamado downcast, porque estamos haciendo una llamada hacia abajo en el arbol de la herencia a una clase mas específica. Ahora, el compilador está feliz. Antes de intentar invocar el método playDead, podemos hacer un cast a la variable animal al tipo Dog. Lo que le estamos diciendo al compilador es, “Sabemos que está realmente referenciandose a un objeto Dog, por lo que está bien hacer una nueva referencia de Dog para referirse a ese objeto”. En este caso estamos seguros porque antes de intentar hacer el cast, hemos hecho un test con instanceof para asegurarnos.

Es importante saber que el compilador es forzado a confiar en nosotros cuando hacemos un downcast, incluso cuando metemos la pata:

class Animal{ }
class Dog extends Animal { }
class DogTest{
	public static void main(String[] args){
		Animal animal = new Animal();
		Dog d = (Dog) animal;	// Compilará pero fallará mas tarde
	}
}

Cuando intentemos ejecutar tendremos un error parecido a este:

java.lang.ClassCastException

¿Por qué no podemos confiar en el compilador para que nos ayude aquí? ¿No puede ver que animal es del tipo Animal? Todo lo que el compilador puede hacer es verificar que los 2 tipos están en el mismo arbol de herencia, por lo que depende de cualquier código que venga despues del downcast, es posible que animal sea del tipo Dog. El compilador debe permitir que las cosas que puedan ser posibles funcionen en tiempo de ejecución. Sin embargo, si el compilador sabe con certeza que el cast podría no funcionar, la compilación fallará. El siguiente bloque de código reemplaza al anterior y NO compilará:

Animal animal = new Animal();
		Dog d = (Dog) animal;
		String s = (String) animal; //animal no puede ser NUNCA un String

En este caso, obtendremos un error parecido al siguiente:

inconvertible types

En diferencia al downcast, el upcasting (Hacer cast en el arbol de herencia a un tipo mas general) funciona implícitamente porque cuando hacemos upcast estamos implicitamente restringiendo el número de métodos que podemos invocar, en oposición al downcasting, lo cual implica que mas tarde, podremos querer invocar un método mas específico. Por ejemplo:

class Animal{ }
class Dog extends Animal { }
class DogTest{
	public static void main(String[] args){
		Dog d = new Dog();
		Animal a1 = d;	// Upcast está bien sin cast explicito
		Animal a2 = (Animal) d;  // Upcast está bien sin cast explicito
	}
}

Ambos de los previos upcast compilarán y see jecutarán sin excepciones, porque Dog ES-UN Animal, lo cual significa que cualquier cosa que un Animal pueda hacer, un Dog puede hacerlo tambien. Un Dog puede hacer mas, por supuesto. Los métodos de Animal pueden ser sustituidos en la clase Dog, pero tenemos que tener claro que un Dog puede siempre hacer al menos todo lo que un Animal puede hacer. El compilador y la JVM lo sabe tambien, por lo que un upcast implícito es siempre legal para asignar a un objeto d eun subtipo a referenciar uno de sus clases supertipo (o interfaces). Si Dog implementa Pet, y Pet define el método beFriendly(), entonces Dog puede ser impícitamente llamado Pet, pero solo el método de Dog que podemos invocar es beFriendly(), lo cual Dog fué forzado a implementar porque Dog implementa la interface Pet.

Una cosa mas…si Dog implementa Pet, entonces si Beagle hereda de Dog, pero Beagle no declara que implementa a Pet, Beagle seguirá siendo un Pet. Beagle es Pet simplemente porque hereda de Dog, y Dog ya ha acogido a Pet como parte de sí mismo, y a todos sus hijos. La clase Beagle puede siempre sustituir cualquier método que herede de Dog, incluyendo métodos que Dog haya implementado para cumplir con el contrato de su interface.

Y solo una cosa mas…Si Beagle declara que implementa a Pet, solo para que otros vean que la API de la clase Beagle pueda ser fácilmente vista como que Beagle ES-UN Pet, sin tener que ver las superclases de Beagle, Beagle todavía no necesitaría implementar el método beFriendly() si la clase Dog ya lo ha acogido. En otras palabras, si Beagle ES-UN Dog, y Dog ES-UN Pet, entonces Beagle ES-UN Pet, y ya ha conocido que Pet tiene obligaciones para implementar el método beFriendly() ya que hereda el método beFriendly().


Esto ha sido por hoy lo referente a los Cast en Java, llamados Downcast o Upcast según se usen respecto al árbol de herencia, y como con ellos podemos invocar métodos mas genéricos o mas específicos según nuestras necesidados y aplicando el polimorfismo.

Sin más, esto es todo por hoy, y cualquier corrección o aporte será bienvenido.

Saludos!!!