Conversion de type d'objet en Java

1. Vue d'ensemble

Le système de types Java est composé de deux types de types: les primitives et les références.

Nous avons couvert les conversions primitives dans cet article, et nous nous concentrerons sur le cast de références ici, pour bien comprendre comment Java gère les types.

2. Primitive vs référence

Bien que les conversions primitives et la conversion de variables de référence puissent sembler similaires, ce sont des concepts assez différents.

Dans les deux cas, nous «transformons» un type en un autre. Mais, de manière simplifiée, une variable primitive contient sa valeur, et la conversion d'une variable primitive signifie des changements irréversibles de sa valeur:

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

Après la conversion dans l'exemple ci-dessus, la variable myInt vaut 1 et nous ne pouvons pas restaurer la valeur précédente 1.1 à partir de celle-ci.

Les variables de référence sont différentes ; la variable de référence ne fait référence qu'à un objet mais ne contient pas l'objet lui-même.

Et la diffusion d'une variable de référence ne touche pas l'objet auquel elle fait référence, mais étiquette uniquement cet objet d'une autre manière, élargissant ou réduisant les possibilités de travailler avec lui. La conversion ascendante réduit la liste des méthodes et des propriétés disponibles pour cet objet et la conversion descendante peut l'étendre.

Une référence est comme une télécommande à un objet. La télécommande a plus ou moins de boutons selon son type et l'objet lui-même est stocké dans un tas. Lorsque nous faisons du casting, nous changeons le type de télécommande mais ne changeons pas l'objet lui-même.

3. Upcasting

La diffusion d'une sous-classe vers une superclasse s'appelle la diffusion ascendante . En règle générale, la conversion ascendante est implicitement effectuée par le compilateur.

L'upcasting est étroitement lié à l'héritage - un autre concept de base en Java. Il est courant d'utiliser des variables de référence pour faire référence à un type plus spécifique. Et chaque fois que nous faisons cela, un upcasting implicite a lieu.

Pour démontrer l'upcasting, définissons une classe Animal :

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

Maintenant, étendons Animal :

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

Nous pouvons maintenant créer un objet de classe Cat et l'assigner à la variable de référence de type Cat :

Cat cat = new Cat();

Et nous pouvons également l'affecter à la variable de référence de type Animal :

Animal animal = cat;

Dans l'affectation ci-dessus, une remontée implicite a lieu. Nous pourrions le faire explicitement:

animal = (Animal) cat;

Mais il n'est pas nécessaire de faire un cast explicite de l'arbre d'héritage. Le compilateur sait que chat est un animal et n'affiche aucune erreur.

Notez que cette référence peut faire référence à n'importe quel sous-type du type déclaré.

En utilisant la conversion ascendante, nous avons limité le nombre de méthodes disponibles pour l' instance Cat mais n'avons pas changé l'instance elle-même. Maintenant, nous ne pouvons rien faire de spécifique à Cat - nous ne pouvons pas invoquer meow () sur la variable animale .

Bien que l' objet Cat reste un objet Cat , l'appel de meow () provoquerait l'erreur du compilateur:

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

Pour invoquer meow (), nous devons abattre l' animal , et nous le ferons plus tard.

Mais maintenant, nous allons décrire ce qui nous donne le upcasting. Grâce à l'upcasting, nous pouvons profiter du polymorphisme.

3.1. Polymorphisme

Définissons une autre sous-classe d' Animal , une classe Dog :

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

Nous pouvons maintenant définir la méthode feed () qui traite tous les chats et chiens comme des animaux :

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

Nous ne voulons pas qu'AnimalFeeder se soucie de quel animal figure sur la liste - un chat ou un chien . Dans la méthode feed () , ce sont tous des animaux .

L'upcasting implicite se produit lorsque nous ajoutons des objets d'un type spécifique à la liste des animaux :

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

Nous ajoutons des chats et des chiens et ils sont implicitement convertis au type Animal . Chaque chat est un animal et chaque chien est un animal . Ils sont polymorphes.

À propos, tous les objets Java sont polymorphes car chaque objet est au moins un objet . Nous pouvons affecter une instance d' Animal à la variable de référence de type Object et le compilateur ne se plaindra pas:

Object object = new Animal();

C'est pourquoi tous les objets Java que nous créons ont déjà des méthodes spécifiques à Object , par exemple toString () .

La diffusion ascendante vers une interface est également courante.

Nous pouvons créer l' interface Mew et la faire implémenter par Cat :

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

Désormais, tout objet Cat peut également être transmis à Mew :

Mew mew = new Cat();

Cat est un Mew , l'upcasting est légal et fait implicitement.

Ainsi, le chat est un Mew , un animal , un objet et un chat . Il peut être affecté à des variables de référence des quatre types dans notre exemple.

3.2. Primordial

Dans l'exemple ci-dessus, la méthode eat () est remplacée. Cela signifie que bien que eat () soit appelé sur la variable de type Animal , le travail se fait par des méthodes invoquées sur des objets réels - chats et chiens:

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

Dans ce didacticiel de base, nous avons exploré ce qu'est le upcasting, downcasting, comment les utiliser et comment ces concepts peuvent vous aider à tirer parti du polymorphisme.

Comme toujours, le code de cet article est disponible à l'adresse over sur GitHub.