Conversión de tipo de objeto en Java

1. Información general

El sistema de tipos de Java se compone de dos tipos de tipos: primitivos y referencias.

Cubrimos conversiones primitivas en este artículo, y nos centraremos en la conversión de referencias aquí, para obtener una buena comprensión de cómo Java maneja los tipos.

2. Primitivo frente a referencia

Aunque las conversiones primitivas y la conversión de variables de referencia pueden parecer similares, son conceptos bastante diferentes.

En ambos casos, estamos "convirtiendo" un tipo en otro. Pero, de forma simplificada, una variable primitiva contiene su valor, y la conversión de una variable primitiva significa cambios irreversibles en su valor:

double myDouble = 1.1; int myInt = (int) myDouble; assertNotEquals(myDouble, myInt);

Después de la conversión en el ejemplo anterior, la variable myInt es 1 y no podemos restaurar el valor anterior 1.1 a partir de ella.

Las variables de referencia son diferentes ; la variable de referencia solo se refiere a un objeto, pero no contiene el objeto en sí.

Y la conversión de una variable de referencia no toca el objeto al que se refiere, solo etiqueta este objeto de otra manera, expandiendo o reduciendo las oportunidades para trabajar con él. Upcasting reduce la lista de métodos y propiedades disponibles para este objeto, y downcasting puede extenderla.

Una referencia es como un control remoto a un objeto. El control remoto tiene más o menos botones dependiendo de su tipo, y el objeto en sí se almacena en un montón. Cuando hacemos casting, cambiamos el tipo de control remoto pero no cambiamos el objeto en sí.

3. Upcasting

La conversión de una subclase a una superclase se denomina conversión ascendente . Normalmente, el compilador realiza implícitamente la conversión.

El upcasting está estrechamente relacionado con la herencia, otro concepto central en Java. Es común usar variables de referencia para referirse a un tipo más específico. Y cada vez que hacemos esto, se produce una conversión ascendente implícita.

Para demostrar el upcasting, definamos una clase Animal :

public class Animal { public void eat() { // ... } }

Ahora extendamos Animal :

public class Cat extends Animal { public void eat() { // ... } public void meow() { // ... } }

Ahora podemos crear un objeto de clase Cat y asignarlo a la variable de referencia de tipo Cat :

Cat cat = new Cat();

Y también podemos asignarlo a la variable de referencia de tipo Animal :

Animal animal = cat;

En la asignación anterior, tiene lugar una conversión ascendente implícita. Podríamos hacerlo explícitamente:

animal = (Animal) cat;

Pero no es necesario hacer una conversión explícita del árbol de herencia. El compilador sabe que el gato es un animal y no muestra ningún error.

Tenga en cuenta que esa referencia puede hacer referencia a cualquier subtipo del tipo declarado.

Con el upcasting, hemos restringido la cantidad de métodos disponibles para la instancia de Cat , pero no hemos cambiado la instancia en sí. Ahora no podemos hacer nada que sea específico de Cat, no podemos invocar meow () en la variable animal .

Aunque el objeto Cat sigue siendo un objeto Cat , llamar a meow () provocaría el error del compilador:

// animal.meow(); The method meow() is undefined for the type Animal

Para invocar miau () necesitamos abatir animal , y lo haremos más tarde.

Pero ahora describiremos qué nos da el upcasting. Gracias al upcasting, podemos aprovechar el polimorfismo.

3.1. Polimorfismo

Definamos otra subclase de Animal , una clase de Perro :

public class Dog extends Animal { public void eat() { // ... } }

Ahora podemos definir el método feed () que trata a todos los gatos y perros como animales :

public class AnimalFeeder { public void feed(List animals) { animals.forEach(animal -> { animal.eat(); }); } }

No queremos que AnimalFeeder se preocupe por qué animal está en la lista: un gato o un perro . En el método feed () todos son animales .

El upcasting implícito ocurre cuando agregamos objetos de un tipo específico a la lista de animales :

List animals = new ArrayList(); animals.add(new Cat()); animals.add(new Dog()); new AnimalFeeder().feed(animals);

Agregamos gatos y perros y se suben al tipo Animal implícitamente. Cada gato es un animal y cada perro es un animal . Son polimórficos.

Por cierto, todos los objetos Java son polimórficos porque cada objeto es al menos un Objeto . Podemos asignar una instancia de Animal a la variable de referencia del tipo de objeto y el compilador no se quejará:

Object object = new Animal();

Es por eso que todos los objetos Java que creamos ya tienen métodos específicos de Object , por ejemplo, toString () .

La conversión a una interfaz también es común.

Podemos crear la interfaz Mew y hacer que Cat la implemente:

public interface Mew { public void meow(); } public class Cat extends Animal implements Mew { public void eat() { // ... } public void meow() { // ... } }

Ahora cualquier objeto Cat también se puede convertir en Mew :

Mew mew = new Cat();

Cat is a Mew , upcasting es legal e implícitamente.

Por lo tanto, el gato es un maullido , un animal , un objeto y un gato . Se puede asignar a variables de referencia de los cuatro tipos en nuestro ejemplo.

3.2. Primordial

En el ejemplo anterior, se anula el método eat () . Esto significa que aunque se llama a eat () en la variable del tipo Animal , el trabajo se realiza mediante métodos invocados en objetos reales: gatos y perros:

public void feed(List animals) { animals.forEach(animal -> { animal.eat(); }); }

If we add some logging to our classes, we'll see that Cat’s and Dog’s methods are called:

web - 2018-02-15 22:48:49,354 [main] INFO com.baeldung.casting.Cat - cat is eating web - 2018-02-15 22:48:49,363 [main] INFO com.baeldung.casting.Dog - dog is eating 

To sum up:

  • A reference variable can refer to an object if the object is of the same type as a variable or if it is a subtype
  • Upcasting happens implicitly
  • All Java objects are polymorphic and can be treated as objects of supertype due to upcasting

4. Downcasting

What if we want to use the variable of type Animal to invoke a method available only to Cat class? Here comes the downcasting. It’s the casting from a superclass to a subclass.

Let’s take an example:

Animal animal = new Cat();

We know that animal variable refers to the instance of Cat. And we want to invoke Cat’s meow() method on the animal. But the compiler complains that meow() method doesn’t exist for the type Animal.

To call meow() we should downcast animal to Cat:

((Cat) animal).meow();

The inner parentheses and the type they contain are sometimes called the cast operator. Note that external parentheses are also needed to compile the code.

Let’s rewrite the previous AnimalFeeder example with meow() method:

public class AnimalFeeder { public void feed(List animals) { animals.forEach(animal -> { animal.eat(); if (animal instanceof Cat) { ((Cat) animal).meow(); } }); } }

Now we gain access to all methods available to Cat class. Look at the log to make sure that meow() is actually called:

web - 2018-02-16 18:13:45,445 [main] INFO com.baeldung.casting.Cat - cat is eating web - 2018-02-16 18:13:45,454 [main] INFO com.baeldung.casting.Cat - meow web - 2018-02-16 18:13:45,455 [main] INFO com.baeldung.casting.Dog - dog is eating

Note that in the above example we're trying to downcast only those objects which are really instances of Cat. To do this, we use the operator instanceof.

4.1. instanceof Operator

We often use instanceof operator before downcasting to check if the object belongs to the specific type:

if (animal instanceof Cat) { ((Cat) animal).meow(); }

4.2. ClassCastException

If we hadn't checked the type with the instanceof operator, the compiler wouldn't have complained. But at runtime, there would be an exception.

To demonstrate this let’s remove the instanceof operator from the above code:

public void uncheckedFeed(List animals) { animals.forEach(animal -> { animal.eat(); ((Cat) animal).meow(); }); }

This code compiles without issues. But if we try to run it we’ll see an exception:

java.lang.ClassCastException: com.baeldung.casting.Dog cannot be cast to com.baeldung.casting.Cat

This means that we are trying to convert an object which is an instance of Dog into a Cat instance.

ClassCastException's always thrown at runtime if the type we downcast to doesn't match the type of the real object.

Note, that if we try to downcast to an unrelated type, the compiler won't allow this:

Animal animal; String s = (String) animal;

The compiler says “Cannot cast from Animal to String”.

For the code to compile, both types should be in the same inheritance tree.

Let's sum up:

  • Downcasting is necessary to gain access to members specific to subclass
  • Downcasting is done using cast operator
  • To downcast an object safely, we need instanceof operator
  • If the real object doesn't match the type we downcast to, then ClassCastException will be thrown at runtime

5. cast() Method

There's another way to cast objects using the methods of Class:

public void whenDowncastToCatWithCastMethod_thenMeowIsCalled() { Animal animal = new Cat(); if (Cat.class.isInstance(animal)) { Cat cat = Cat.class.cast(animal); cat.meow(); } }

In the above example, cast() and isInstance() methods are used instead of cast and instanceof operators correspondingly.

It's common to use cast() and isInstance() methods with generic types.

Let's create AnimalFeederGeneric class with feed() method which “feeds” only one type of animals – cats or dogs, depending on the value of the type parameter:

public class AnimalFeederGeneric { private Class type; public AnimalFeederGeneric(Class type) { this.type = type; } public List feed(List animals) { List list = new ArrayList(); animals.forEach(animal -> { if (type.isInstance(animal)) { T objAsType = type.cast(animal); list.add(objAsType); } }); return list; } }

The feed() method checks each animal and returns only those which are instances of T.

Note, that the Class instance should also be passed to the generic class as we can't get it from the type parameter T. In our example, we pass it in the constructor.

Let's make T equal to Cat and make sure that the method returns only cats:

@Test public void whenParameterCat_thenOnlyCatsFed() { List animals = new ArrayList(); animals.add(new Cat()); animals.add(new Dog()); AnimalFeederGeneric catFeeder = new AnimalFeederGeneric(Cat.class); List fedAnimals = catFeeder.feed(animals); assertTrue(fedAnimals.size() == 1); assertTrue(fedAnimals.get(0) instanceof Cat); }

6. Conclusion

En este tutorial básico, hemos explorado qué es upcasting, downcasting, cómo usarlos y cómo estos conceptos pueden ayudarlo a aprovechar el polimorfismo.

Como siempre, el código de este artículo está disponible en GitHub.