Приведение типов объектов в Java

1. Обзор

Система типов Java состоит из двух типов типов: примитивов и ссылок.

В этой статье мы рассмотрели примитивные преобразования и сосредоточимся на приведении ссылок здесь, чтобы получить хорошее представление о том, как Java обрабатывает типы.

2. Примитив против ссылки

Хотя примитивные преобразования и приведение ссылочных переменных могут выглядеть одинаково, это совершенно разные концепции.

В обоих случаях мы «превращаем» один тип в другой. Но в упрощенном виде примитивная переменная содержит свое значение, а преобразование примитивной переменной означает необратимые изменения ее значения:

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

После преобразования в приведенном выше примере переменная myInt равна 1 , и мы не можем восстановить из нее предыдущее значение 1.1 .

Справочные переменные разные ; ссылочная переменная относится только к объекту, но не содержит самого объекта.

И приведение ссылочной переменной не затрагивает объект, на который она ссылается, а только помечает этот объект другим способом, расширяя или сужая возможности для работы с ним. Повышение сужает список методов и свойств, доступных для этого объекта, а понижающее может его расширить.

Ссылка похожа на пульт дистанционного управления объектом. На пульте дистанционного управления больше или меньше кнопок в зависимости от его типа, а сам объект хранится в куче. Когда мы выполняем приведение, мы меняем тип пульта дистанционного управления, но не меняем сам объект.

3. Повышение качества

Преобразование подкласса в суперкласс называется преобразованием вверх . Как правило, преобразование неявно выполняется компилятором.

Апкастинг тесно связан с наследованием - еще одной ключевой концепцией Java. Обычно ссылочные переменные используются для ссылки на более конкретный тип. И каждый раз, когда мы это делаем, происходит неявное апкастинг.

Чтобы продемонстрировать апкастинг, давайте определим класс Animal :

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

Теперь расширим Animal :

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

Теперь мы можем создать объект класса Cat и назначить его ссылочной переменной типа Cat :

Cat cat = new Cat();

И мы также можем присвоить его ссылочной переменной типа Animal :

Animal animal = cat;

В приведенном выше назначении имеет место неявное восходящее преобразование. Мы могли бы сделать это явно:

animal = (Animal) cat;

Но нет необходимости делать явное приведение дерева наследования. Компилятор знает, что кошка - это животное, и не отображает никаких ошибок.

Обратите внимание, что эта ссылка может относиться к любому подтипу объявленного типа.

Используя восходящее преобразование, мы ограничили количество методов, доступных для экземпляра Cat, но не изменили сам экземпляр. Теперь мы не можем ничего, специфичный сделать Cat - мы не может ссылаться на мяуканье () на животных переменной.

Хотя объект Cat остается объектом Cat , вызов meow () приведет к ошибке компилятора:

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

Чтобы вызвать meow (), нам нужно опустить животное , и мы сделаем это позже.

Но сейчас опишем, что дает нам апкастинг. Благодаря апкастингу мы можем воспользоваться преимуществами полиморфизма.

3.1. Полиморфизм

Давайте определим еще один подкласс Animal , класс Dog :

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

Теперь мы можем определить метод feed (), который обращается со всеми кошками и собаками как с животными :

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

Мы не хотим, чтобы AnimalFeeder заботился о том, какое животное в списке - кошка или собака . В методе feed () все они животные .

Неявное апкастинг происходит, когда мы добавляем в список животных объекты определенного типа :

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

Мы добавляем кошек и собак, и они неявно повышаются до типа Animal . Каждая кошка - это животное, а каждая собака - это животное . Они полиморфны.

Кстати, все объекты Java являются полиморфными , поскольку каждый объект представляет собой объект , по крайней мере. Мы можем присвоить экземпляр Animal ссылочной переменной типа Object, и компилятор не будет жаловаться:

Object object = new Animal();

Вот почему все объекты Java, которые мы создаем, уже имеют методы, специфичные для объекта , например toString () .

Также распространено преобразование в интерфейс.

We can create Mew interface and make Cat implement it:

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

Now any Cat object can also be upcast to Mew:

Mew mew = new Cat();

Cat is a Mew, upcasting is legal and done implicitly.

Thus, Cat is a Mew, Animal, Object, and Cat. It can be assigned to reference variables of all four types in our example.

3.2. Overriding

In the example above, the eat() method is overridden. This means that although eat() is called on the variable of the Animal type, the work is done by methods invoked on real objects – cats and dogs:

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

В этом основополагающем руководстве мы изучили, что такое повышение и понижение, как их использовать и как эти концепции могут помочь вам воспользоваться преимуществами полиморфизма.

Как всегда, код для этой статьи доступен на GitHub.