Лиття типу об’єкта в 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

Щоб викликати мяу (), нам потрібно знизити тварину , і ми зробимо це пізніше.

Але зараз ми опишемо, що дає нам оновлення. Завдяки модернізації ми можемо скористатися поліморфізмом.

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);

Ми додаємо котів і собак, і вони неявно переходять до типу тварин . Кожна кішка - тварина, а кожна собака - тварина . Вони поліморфні.

До речі, усі об'єкти Java є поліморфними, оскільки кожен об'єкт є як мінімум об'єктом . Ми можемо призначити екземпляр Animal до посилальної змінної типу Object, і компілятор не скаржиться:

Object object = new Animal();

Ось чому всі об’єкти Java, які ми створюємо, вже мають специфічні для об’єкта методи, наприклад, toString () .

Поширення до інтерфейсу також є загальним явищем.

Ми можемо створити інтерфейс Mew і змусити Cat реалізувати його:

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

Тепер будь-який об'єкт Cat також можна оновити до Mew :

Mew mew = new Cat();

Кіт - це Mew , оновлення дозволено і здійснюється неявно.

Отже, Кіт - це Mew , тварина , об’єкт і кішка . У нашому прикладі його можна призначити посилальним змінним усіх чотирьох типів.

3.2. Перевизначення

У наведеному вище прикладі метод eat () замінено. Це означає, що, хоча eat () називається змінною типу Animal , робота виконується методами, що викликаються на реальних об'єктах - котах і собаках:

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.