Rzutowanie typów obiektów w Javie

1. Przegląd

System typów Java składa się z dwóch rodzajów typów: prymitywów i referencji.

W tym artykule omówiliśmy konwersje prymitywne i skupimy się tutaj na rzutowaniu odwołań, aby dobrze zrozumieć, jak Java obsługuje typy.

2. Prymityw a odniesienie

Chociaż pierwotne konwersje i rzutowanie zmiennych referencyjnych mogą wyglądać podobnie, są to zupełnie różne koncepcje.

W obu przypadkach „zmieniamy” jeden typ w inny. Ale w uproszczeniu zmienna pierwotna zawiera swoją wartość, a konwersja zmiennej pierwotnej oznacza nieodwracalne zmiany jej wartości:

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

Po konwersji w powyższym przykładzie zmienna myInt ma wartość 1 i nie możemy przywrócić z niej poprzedniej wartości 1.1 .

Zmienne referencyjne są różne ; zmienna referencyjna odnosi się tylko do obiektu, ale nie zawiera samego obiektu.

Rzutowanie zmiennej referencyjnej nie dotyka obiektu, do którego się odnosi, a jedynie nadaje etykietę temu obiektowi w inny sposób, zwiększając lub zawężając możliwości pracy z nim. Upcasting zawęża listę metod i właściwości dostępnych dla tego obiektu, a downcasting może ją rozszerzyć.

Odniesienie jest jak pilot do obiektu. Pilot posiada więcej lub mniej przycisków w zależności od jego typu, a sam obiekt jest przechowywany w stercie. Kiedy wykonujemy rzutowanie, zmieniamy typ pilota, ale nie zmieniamy samego obiektu.

3. Nadawanie

Przesyłanie z podklasy do nadklasy nazywa się przesyłaniem w górę . Zazwyczaj upcasting jest niejawnie wykonywany przez kompilator.

Upcasting jest ściśle powiązany z dziedziczeniem - kolejną podstawową koncepcją w Javie. Często używa się zmiennych referencyjnych w celu odniesienia się do bardziej konkretnego typu. I za każdym razem, gdy to robimy, ma miejsce niejawne upcasting.

Aby zademonstrować upcasting, zdefiniujmy klasę Animal :

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

Teraz rozszerzmy Animal :

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

Teraz możemy stworzyć obiekt klasy Cat i przypisać go do zmiennej referencyjnej typu Cat :

Cat cat = new Cat();

Możemy również przypisać ją do zmiennej referencyjnej typu Animal :

Animal animal = cat;

W powyższym zadaniu ma miejsce niejawne upcasting. Moglibyśmy to zrobić wprost:

animal = (Animal) cat;

Ale nie ma potrzeby jawnego tworzenia drzewa dziedziczenia. Kompilator wie, że cat to Animal i nie wyświetla żadnych błędów.

Zauważ, że to odwołanie może odnosić się do dowolnego podtypu zadeklarowanego typu.

Korzystając z upcastingu, ograniczyliśmy liczbę metod dostępnych dla instancji Cat, ale nie zmieniliśmy samej instancji. Teraz nie możemy zrobić niczego, co jest specyficzne dla Cat - nie możemy wywołać meow () na zmiennej zwierzęcej .

Chociaż obiekt Cat pozostaje obiektem Cat , wywołanie meow () spowodowałoby błąd kompilatora:

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

Aby wywołać meow () , musimy przygnębić zwierzę , a zrobimy to później.

Ale teraz opiszemy, co nas podnieca. Dzięki upcastingowi możemy wykorzystać polimorfizm.

3.1. Wielopostaciowość

Zdefiniujmy inną podklasę klasy Animal , klasę Dog :

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

Teraz możemy zdefiniować metodę feed () , która traktuje wszystkie koty i psy jak zwierzęta :

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

Nie chcemy, aby AnimalFeeder przejmował się tym, które zwierzę znajduje się na liście - Kot czy Pies . W metodzie feed () wszystkie są zwierzętami .

Niejawne upcasting pojawia się, gdy do listy zwierząt dodajemy obiekty określonego typu :

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

Dodajemy koty i psy i są one domyślnie upokorzone do typu zwierzęcia . Każdy kot to zwierzę, a każdy pies to zwierzę . Są polimorficzne.

Nawiasem mówiąc, wszystkie obiekty Java są polimorficzne, ponieważ każdy przedmiot jest obiekt , co najmniej. Możemy przypisać instancję Animal do zmiennej referencyjnej typu Object, a kompilator nie będzie narzekał:

Object object = new Animal();

Dlatego wszystkie obiekty Java, które tworzymy, mają już metody specyficzne dla obiektu , na przykład toString () .

Powszechne jest również przesyłanie do interfejsu.

Możemy stworzyć interfejs Mew i sprawić, by Cat zaimplementował go:

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

Teraz każdy obiekt Cat można również przesłać do Mew :

Mew mew = new Cat();

Cat is a Mew , upcasting is legal and in implicite.

Zatem kot jest miauczeniem , zwierzęciem , przedmiotem i kotem . W naszym przykładzie można go przypisać do zmiennych referencyjnych wszystkich czterech typów.

3.2. Nadrzędny

W powyższym przykładzie metoda eat () jest nadpisywana. Oznacza to, że chociaż funkcja eat () jest wywoływana na zmiennej typu Animal , praca wykonywana jest metodami przywołanymi na rzeczywistych obiektach - kotach i psach:

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

Jeśli dodamy trochę rejestrowania do naszych klas, zobaczymy, że metody Cat i Dog nazywają się:

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 

Podsumowując:

  • Zmienna odniesienia może odwoływać się do obiektu, jeśli obiekt jest tego samego typu co zmienna lub jeśli jest podtypem
  • Nadawanie odbywa się niejawnie
  • Wszystkie obiekty Java są polimorficzne i mogą być traktowane jako obiekty nadtypu ze względu na upcasting

4. Downcasting

A co jeśli chcemy użyć zmiennej typu Animal do wywołania metody dostępnej tylko dla klasy Cat ? Nadchodzi przygnębienie. To casting z superklasy do podklasy.

Weźmy przykład:

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

W tym podstawowym samouczku zbadaliśmy, czym jest upcasting, downcasting, jak ich używać i jak te koncepcje mogą pomóc Ci wykorzystać polimorfizm.

Jak zawsze kod tego artykułu jest dostępny w serwisie GitHub.