Привет! В одной из прошлых лекций мы обсуждали приведение примитивных типов. Давай вкратце вспомним, о чем шла речь.
Мы представляли примитивные типы (в данном случае — числовые) в виде матрешек согласно объему памяти, которое они занимают.
Как ты помнишь, поместить меньшую матрешку в большую будет просто как в реальной жизни, так и в программировании на Java.
Есть расширяющее и сужающее приведение.
Мы видим, что класс
Рассмотрим другой пример, поинтереснее:
public class Main {
public static void main(String[] args) {
short smallNumber = 100;
int bigNumber = smallNumber;
System.out.println(bigNumber);
}
}
Это пример автоматического преобразования, или расширения.
Оно происходит само по себе, поэтому дополнительный код писать не нужно. В конце концов, мы не делаем ничего необычного: просто кладем матрешку поменьше в матрешку побольше.
Другое дело, если мы попытаемся сделать наоборот и положить большую матрешку в меньшую. В жизни такое сделать нельзя, а в программировании можно. Но есть один нюанс.
Если мы попытаемся положить значение int в переменную short, у нас это так просто не выйдет. Ведь в переменную short поместится всего 16 бит информации, а значение int занимает 32 бита!
В результате передаваемое значение исказится.
Компилятор выдаст нам ошибку («чувак, ты делаешь что-то подозрительное!»), но если мы явно укажем, к какому типу приводим наше значение, он все-таки выполнит такую операцию.
public class Main {
public static void main(String[] args) {
int bigNumber = 10000000;
bigNumber = (short) bigNumber;
System.out.println(bigNumber);
}
}
В примере выше мы так и поступили. Операция выполнена, но поскольку в переменную short поместилось только 16 бит из 32, итоговое значение было искажено, и в результате мы получили число -27008. Такая операция называется явным преобразованием, или сужением.Что такое приведение типов
Приведение типов (преобразование типов) — преобразование значения переменной одного типа в значение другого типа.Примеры расширения и сужения ссылочных типов
Сейчас мы поговорим о тех же операциях, но применимо не к примитивным типам, а к объектам и ссылочным переменным! Как же это работает в Java? На самом деле, довольно просто. Есть объекты, которые не связаны между собой. Было бы логично предположить, что их нельзя преобразовать друг в друга ни явно, ни автоматически:
public class Cat {
}
public class Dog {
}
public class Main {
public static void main(String[] args) {
Cat cat = new Dog();//ошибка!
}
}
Здесь мы, конечно, получим ошибку. Классы Cat и Dog между собой не связаны, и мы не написали «преобразователя» одних в других. Логично, что сделать это у нас не получится: компилятор понятия не имеет, как конвертировать эти объекты между собой.
Другое дело, если объекты будут между собой связаны! Как? Прежде всего, с помощью наследования.
Давай попробуем создать небольшую систему классов с наследованием. У нас будет общий класс, обозначающий животных:
public class Animal {
public void introduce() {
System.out.println("i'm Animal");
}
}
Животные, как известно, бывают домашними и дикими:
public class WildAnimal extends Animal {
public void introduce() {
System.out.println("i'm WildAnimal");
}
}
public class Pet extends Animal {
public void introduce() {
System.out.println("i'm Pet");
}
}
Для примера возьмем собачек — домашнего пса и койота:
public class Dog extends Pet {
public void introduce() {
System.out.println("i'm Dog");
}
}
public class Coyote extends WildAnimal {
public void introduce() {
System.out.println("i'm Coyote");
}
}
Классы у нас специально самые примитивные, чтобы легче было воспринимать их. Поля нам тут особо не нужны, а метода хватит и одного.
Попробуем выполнить вот такой код:
public class Main {
public static void main(String[] args) {
Animal animal = new Pet();
animal.introduce();
}
}
Как ты думаешь, что будет выведено на консоль? Сработает метод introduce класса Pet или класса Animal?
Попробуй обосновать свой ответ, прежде чем продолжать чтение.
А вот и результат!
i'm Pet
Почему ответ получился таким? Все просто. У нас есть переменная-родитель и объект-потомок. Написав:
Animal animal = new Pet();
мы произвели расширение ссылочного типа Pet и записали его объект в переменную Animal.
Как и в случае с примитивными, расширение ссылочных типов в Java производится автоматически. Дополнительный код для этого писать не нужно.
Теперь у нас к ссылке-родителю привязан объект-потомок, и в итоге мы видим, что вызов метода производится именно у класса-потомка.
Если ты все еще не до конца понимаешь, почему такой код работает, перепиши его простым языком:
Животное животное = new ДомашнееЖивотное();
В этом же нет никаких проблем, правильно?
Представь, что это реальная жизнь, а ссылка в данном случае — простая бумажная бирка с надписью «Животное». Если ты возьмешь такую бумажку и прицепишь на ошейник любому домашнему животному, все будет в порядке. Любое домашнее животное все равно животное!
Обратный процесс, то есть движение по дереву наследования вниз, к наследникам — это сужение:
public class Main {
public static void main(String[] args) {
WildAnimal wildAnimal = new Coyote();
Coyote coyote = (Coyote) wildAnimal;
coyote.introduce();
}
}
Как видишь, здесь мы явно указываем к какому классу хотим привести наш объект.
Ранее у нас была переменная WildAnimal, а теперь Coyote, которая идет по дереву наследования ниже. Логично, что без явного указания компилятор такую операцию не пропустит, но если в скобках указать тип, все заработает.Давайте посмотрим на примере, что такое приведение типов и с чем его едят.
У нас есть некоторая иерархия классов. Тут видно все классы иерархии, кто кого наследует и методы каждого класса.
Есть расширяющее и сужающее приведение.
Мы видим, что класс Cat является наследником класса Pet. Pet, в свою очередь, наследник класса Animal.
Когда мы напишем:
Animal animalCat = new Cat();
Animal animalDog = new YorkshireTerrier();
Это расширяющее приведение (или неявное). Мы расширили ссылки animalCat и animalDog. Они ссылаются на объекты Cat и Dog. При таком приведении мы не можем через ссылку animalCat/animalDog вызвать методы, которые есть в Cat/Dog, но которых нету в Animal.
Сужающее приведение(или явное) происходит в обратную сторону:
Animal animalCat = new Cat();
Animal animalDog = new YorkshireTerrier();
Cat cat =(Cat)animalCat;
YorkshireTerrier dog = (YorkshireTerrier) animalDog;
Мы явно указали к какому типу хотим привести данный объект.
BUT, BE CAREFUL!!!
Если Вы сделаете вот так:
Animal animalCat = new Cat();
YorkshireTerrier dog = (YorkshireTerrier) animalCat;
компилятор пропустит этот код. А вот RunTime выкинет вам:
Exception in thread "main" java.lang.ClassCastException: Animals.Cat cannot be cast to Animals.YorkshireTerrier
RunTime видит, что Cat и YorkshireTerrier два разных класса.
Рассмотрим другой пример, поинтереснее:
public class Main {
public static void main(String[] args) {
Pet pet = new Animal();//ошибка!
}
}
Компилятор выдает ошибку! В чем же причина?
В том, что ты пытаешься присвоить переменной-потомку объект-родителя.
Иными словами, ты хочешь сделать вот так:
ДомашнееЖивотное домашнееЖивотное = new Животное();
Но, может быть, если мы явно укажем тип, к которому пытаемся сделать приведение, у нас все получится?
С числами вроде получилось, давай попробуем! :)
public class Main {
public static void main(String[] args) {
Pet pet = (Pet) new Animal();
}
}
Exception in thread "main" java.lang.ClassCastException: Animal cannot be cast to Pet
Ошибка! Компилятор в этот раз ругаться не стал, однако в результате мы получили исключение.
Причина нам уже известна: мы пытаемся присвоить переменной-потомку объект-родителя. А почему, собственно, нельзя этого делать?
Потому что не все Животные являются ДомашнимиЖивотными.
Ты создал объект Animal и пытаешься присвоить его переменной Pet. Но, к примеру, койот тоже является Animal, но он не является Pet, домашним животным.
Иными словами, когда ты пишешь:
Pet pet = (Pet) new Animal();
На месте new Animal() может быть какое угодно животное, и совсем не обязательно домашнее!
Естественно, твоя переменная Pet pet подходит только для хранения домашних животных (и их потомков), и не для всех подряд.
Поэтому для таких случаев в Java было создано специальное исключение — ClassCastException, ошибка при приведении классов.Безопасное приведение типов с instanceof
Чтобы избежать ClassCastException, в Java есть специальный оператор instanceof, который проверяет, является ли объект экземпляром определенного класса. Что бы избежать ClassCastException при сужающем преобразовании, используйтеinstanceof.
Animal animalCat = new Cat();
if (animalCat instanceof YorkshireTerrier)
{
YorkshireTerrier dog = (YorkshireTerrier) animalCat;
}
Если animalCat является YorkshireTerrier, тогда произойдет присвоение, если нет — ничего не произойдет.Pattern Matching для instanceof
Начиная с Java 16, появилась более удобная возможность работы с instanceof — Pattern Matching. Это современный подход, который упрощает код:// Старый подход (до Java 16)
if (animal instanceof Cat) {
Cat cat = (Cat) animal; // явное приведение
cat.sleepOnKeyboard();
}
// Современный подход (Java 16+)
if (animal instanceof Cat cat) { // сразу создается переменная cat
cat.sleepOnKeyboard();
}Как видишь, в новом синтаксисе мы сразу получаем переменную нужного типа без явного приведения. Это делает код короче и безопаснее.
Давай проговорим еще раз, чтобы было понятнее.
Переменная(ссылка)-родитель может указывать на объект класса-потомка:
public class Main {
public static void main(String[] args) {
Pet pet = new Pet();
Animal animal = pet;
Pet pet2 = (Pet) animal;
pet2.introduce();
}
}
Например, здесь у нас проблем не возникнет. У нас есть объект Pet, на который указывает ссылка Pet. Потом на этот же объект стала указывать новая ссылка Animal. После чего мы делаем преобразование animal в Pet. Почему у нас это получилось, кстати? В прошлый раз мы получили исключение!
Потому что в этот раз наш изначальный объект — Pet pet!
Pet pet = new Pet();
А в прошлом примере это был объект Animal:
Pet pet = (Pet) new Animal();
Переменной-наследнику нельзя присвоить объект предка. Наоборот делать можно.А теперь зачем же это нужно, если мы теряем методы и можем получить такие ошибки
Рассмотрим код который я сделал по диаграмме выше "иерархия классов". КлассAnimal
public abstract class Animal
{
String name;
int age;
String nameOfClass = getClass().getSimpleName();
public void eat(){
System.out.println(nameOfClass + ": Omnomnom");
}
public void sleep(){
System.out.println(nameOfClass + ": Z-z-z-z");
}
}
Класс WildAnimal который наследуется от Animal
public abstract class WildAnimal extends Animal
{
public void steelChicken()
{
System.out.println(nameOfClass+": Muhaha,I stole a chicken!");
}
}
Класс Pet который наследуется от Animal
public abstract class Pet extends Animal
{
public void peeInTray(){
System.out.println(nameOfClass + ": Master, I peed");
}
}
Класс Fox который наследуется от WildAnimal
public class Fox extends WildAnimal
{
public void eatColobok(){
System.out.println(nameOfClass + ": I will eat you, Colobok");
}
}
Класс Wolf который наследуется от WildAnimal
public class Wolf extends WildAnimal
{
public void hawlAtTheMoon(){
System.out.println(nameOfClass + ": Ouuuuu!!!Ouuuu!!!");
}
}
Класс Cat который наследуется от Pet
public class Cat extends Pet
{
public void sleepOnKeyboard(){
System.out.println(nameOfClass + ": Master, stop working!!I wanna sleep on your keyboard");
}
}
Класс YorkshireTerrier который наследуется от Pet
public class YorkshireTerrier extends Pet
{
public void bark(){
System.out.println(nameOfClass + ": Meow!!! Meow!!!");
}
}
Представьте себе ситуацию. Нам нужно собрать всех животных в один список, накормить их и потом уложить спать. Это легко сделать, если мы создадим ArrayList животных (Animal). И потом у каждого животного вызовем соответствующие методы:
public class ZOO
{
public static void main(String[] args)
{
List<Animal> allAnimals = new ArrayList<>();
allAnimals.add(new Cat());
allAnimals.add(new Wolf());
allAnimals.add(new Fox());
allAnimals.add(new YorkshireTerrier());
for (Animal animal : allAnimals)
{
animal.eat();
animal.sleep();
}
}
}
Я не могу у animal вызвать метод bark() или sleepOnKeyboard(). Потому что в листе allAnimals хранятся кот, волк, йорик и лиса, но они приведены к Animal. И у них есть только те методы, которые есть в Animal.
Это очень хорошо, потому что, если бы мы могли вызвать все методы, то зачем нам нужен волк который спит на клавиатуре, или йорик который ворует куриц?
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ