1. Общ преглед
Системата тип Java се състои от два вида типове: примитиви и препратки.
В тази статия разгледахме примитивни преобразувания и ще се съсредоточим върху препратките тук, за да разберем добре как Java обработва типовете.
2. Примитивен срещу референтен
Въпреки че примитивните преобразувания и леенето на референтни променливи може да изглеждат сходни, те са доста различни концепции.
И в двата случая ние „превръщаме“ един тип в друг. Но по опростен начин примитивната променлива съдържа стойността си, а преобразуването на примитивна променлива означава необратими промени в нейната стойност:
double myDouble = 1.1; int myInt = (int) myDouble; assertNotEquals(myDouble, myInt);
След преобразуването в горния пример променливата myInt е 1 и не можем да възстановим предишната стойност 1.1 от нея.
Референтните променливи са различни ; референтната променлива се отнася само до обект, но не съдържа самия обект.
И хвърлянето на референтна променлива не докосва обекта, към който се отнася, а само обозначава този обект по друг начин, разширявайки или стеснявайки възможностите за работа с него. Upcasting стеснява списъка с методи и свойства, достъпни за този обект, и downcasting може да го разшири.
Препратката е като дистанционно управление на обект. Дистанционното управление има повече или по-малко бутони в зависимост от вида си, а самият обект се съхранява в купчина. Когато правим кастинг, ние променяме типа на дистанционното управление, но не променяме самия обект.
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;
Но няма нужда да се прави изрично изхвърляне на дървото за наследяване. Компилаторът знае, че котката е животно и не показва грешки.
Имайте предвид, че тази препратка може да се отнася до всеки подтип от декларирания тип.
Използвайки upcasting, ограничихме броя на методите, достъпни за екземпляра на Cat, но не сме променили самия екземпляр. Сега не можем да направим нищо, което е специфично за Cat - не можем да извикаме meow () върху променливата на животното .
Въпреки че обектът 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.