Java приведение типов при наследовании
В прошлой главе говорилось о преобразованиях объектов простых типов. Однако с объектами классов все происходит немного по-другому. Допустим, у нас есть следующая иерархия классов:
public class Program < public static void main(String[] args) < Person tom = new Person("Tom"); tom.display(); Person sam = new Employee("Sam", "Oracle"); sam.display(); Person bob = new Client("Bob", "DeutscheBank", 3000); bob.display(); >> // класс человека class Person < private String name; public String getName() < return name; >public Person(String name) < this.name=name; >public void display() < System.out.printf("Person %s \n", name); >> // служащий некоторой компании class Employee extends Person < private String company; public Employee(String name, String company) < super(name); this.company = company; >public String getCompany() < return company; >public void display() < System.out.printf("Employee %s works in %s \n", super.getName(), company); >> // класс клиента банка class Client extends Person < private int sum; // Переменная для хранения суммы на счете private String bank; public Client(String name, String bank, int sum) < super(name); this.bank=bank; this.sum=sum; >public void display() < System.out.printf("Client %s has account in %s \n", super.getName(), bank); >public String getBank() < return bank; >public int getSum() < return sum; >>
В этой иерархии классов можно проследить следующую цепь наследования: Object (все классы неявно наследуются от типа Object) -> Person -> Employee|Client.
Суперклассы обычно размещаются выше подклассов, поэтому на вершине наследования находится класс Object, а в самом низу Employee и Client.
Объект подкласса также представляет объект суперкласса. Поэтому в программе мы можем написать следующим образом:
Object tom = new Person("Tom"); Object sam = new Employee("Sam", "Oracle"); Object kate = new Client("Kate", "DeutscheBank", 2000); Person bob = new Client("Bob", "DeutscheBank", 3000); Person alice = new Employee("Alice", "Google");
Это так называемое восходящее преобразование (от подкласса внизу к суперклассу вверху иерархии) или upcasting. Такое преобразование осуществляется автоматически.
Обратное не всегда верно. Например, объект Person не всегда является объектом Employee или Client. Поэтому нисходящее преобразование или downcasting от суперкласса к подклассу автоматически не выполняется. В этом случае нам надо использовать операцию преобразования типов.
Object sam = new Employee("Sam", "Oracle"); // нисходящее преобразование от Object к типу Employee Employee emp = (Employee)sam; emp.display(); System.out.println(emp.getCompany());
В данном случае переменная sam приводится к типу Employee. И затем через объект emp мы можем обратиться к функционалу объекта Employee.
Мы можем преобразовать объект Employee по всей прямой линии наследования от Object к Employee.
Примеры нисходящих перобразований:
Object kate = new Client("Kate", "DeutscheBank", 2000); ((Person)kate).display(); Object sam = new Employee("Sam", "Oracle"); ((Employee)sam).display();
Но рассмотрим еще одну ситуацию:
Object kate = new Client("Kate", "DeutscheBank", 2000); Employee emp = (Employee) kate; emp.display(); // или так ((Employee)kate).display();
В данном случае переменная типа Object хранит ссылку на объект Client. Мы можем без ошибок привести этот объект к типам Person или Client. Но при попытке преобразования к типу Employee мы получим ошибку во время выполнения. Так как kate не представляет объект типа Employee.
Оператор instanceof
В примере выше мы явно видим, что переменная kate — это ссылка на объект Client, а не Employee. Однако нередко данные приходят извне, и мы можем точно не знать, какой именно объект эти данные представляют. Соответственно возникает большая вероятность столкнуться с ошибкой. И перед тем, как провести преобразование типов, мы можем проверить, а можем ли мы выполнить приведение с помощью оператора instanceof :
Object kate = new Client(«Kate», «DeutscheBank», 2000); if(kate instanceof Employee) < Employee employeeKate = (Employee) kate; employeeKate.display(); >else
Выражение kate instanceof Employee проверяет, является ли переменная kate объектом типа Employee. Но так как в данном случае явно не является, то такая проверка вернет значение false , и преобразование не сработает.
А выражение kate instanceof Client возвратило бы true :
Object kate = new Client(«Kate», «DeutscheBank», 2000); if(kate instanceof Client) < Client clientKate = (Client) kate; clientKate.display(); >else
Следует отметить, что начиная с версии Java 16 мы можем упростить преобразование типов следующим образом:
Object kate = new Client(«Kate», «DeutscheBank», 2000); if(kate instanceof Client clientKate) < clientKate.display(); >else
kate instanceof Client clientKate
Проверяет, представляет ли переменная kate класс Client , и если представляет (то есть оператор instanceof возвращает true ), то создает переменную clientKate типа Client . И в дальнейшем мы можем использовать эту переменную clientKate и производить с ней различные операции.
Приведение типов. Расширение и сужение
— Привет, Амиго! Тема сегодняшней лекции – расширение и сужение типов. С расширением и сужением примитивных типов ты познакомился уже давно. На 10 уровне. Сегодня мы расскажем, как это работает для ссылочных типов, т.е. для объектов классов.
Тут все довольно просто на самом деле. Представь себе цепочку наследования класса: класс, его родитель, родитель родителя и т.д. до самого класса Object. Т.к. класс содержит все методы класса, от которого он был унаследован , то объект этого класса можно сохранить в переменную любого из его типов родителей.
class Animal < public void doAnimalActions()<>> class Cat extends Animal < public void doCatActions()<>> class Tiger extends Cat < public void doTigerActions()<>>
public static void main(String[] args) < Tiger tiger = new Tiger(); Cat cat = new Tiger(); Animal animal = new Tiger(); Object obj = new Tiger(); >
Теперь рассмотрим, что же такое расширение и сужение типов.
Если в результате присваивания мы двигаемся по цепочке наследования вверх (к типу Object), то это — расширение типа (оно же — восходящее преобразование или upcasting), а если вниз, к типу объекта, то это — сужение типа (оно же — нисходящее преобразование или downcasting).
Движение вверх по цепочке наследования называется расширением, поскольку оно приводит к более общему типу. Но при этом теряется возможность вызвать методы, которые были добавлены в класс при наследовании.
public static void main(String[] args) < Object obj = new Tiger(); Animal animal = (Animal) obj; Cat cat = (Cat) obj; Tiger tiger = (Tiger) animal; Tiger tiger2 = (Tiger) cat; >
При этом Java-машина выполняет проверку, а действительно ли данный объект унаследован от Типа, к которому мы хотим его преобразовать.
Java 8 наследование
В предыдущих статьях я уже несколько раз упоминал наследование. Настало время написать подробную статью про эту вещь.
В Java класс может наследоваться от другого класса, получая его методы и поля, который в свою очередь может наследоваться от ещё одного класса и т. д. В Java нет множественного наследования классов. Один класс может наследоваться напрямую только от одного другого класса.
Класс, который наследуется от другого класса, называется подклассом (subclass), дочерним классом (child class), потомком или расширенным классом (extended class).
Класс, от которого наследуется дочерний класс, называется родительским классом (parent class), предком, суперклассом (superclass) или базовым классом (base class).
В самой вершине иерархии наследования находится класс Object , от которого наследуются все классы, для которых не указан явно суперкласс. Таким образом все классы (кроме самого Object ) напрямую или через какое-либо количество уровней наследования наследуются от класса Object .
Идея наследования классов состоит в том, что когда вы хотите создать новый класс, например Goblin , и уже существует какой-нибудь класс, который уже реализует часть функциональности, необходимой нашему классу, например Monster , то вы можете указать этот класс в качестве родительского класса, унаследовав таким образом все его члены (поля, вложенные классы и методы экземпляров). Конструкторы не наследуются и не являются членами классов, но можно вызвать конструктор базового класса из конструктора дочернего класса.
Дочерний класс наследует все public и protected члены своего родителя независимо от пакета, в котором расположен родительский класс. Если дочерний и родительский класс находятся в одном пакете, то дочерний класс наследует также package-private члены своего родителя.
- Унаследованные поля можно использовать напрямую, как все другие поля.
- Можно объявить в дочернем классе поле с таким же именем, как и поле в родительском классе, тогда это поле скроет (hide) поле родительского класса (НЕ рекомендуется так делать).
- В дочернем классе можно объявлять поля, которых нет в родительском классе.
- Унаследованные методы можно использовать напрямую.
- Можно объявить метод экземпляров в дочернем классе с точно такой же сигнатурой, что и метод экземпляров в родительском классе, тогда этот метод переопределит (override) метод суперкласса.
- Можно объявить в дочернем классе статический метод с точно такой же сигнатурой, что и статический метод в родительском классе, тогда этот метод скроет (hide) метод родительского класса.
- В дочернем классе можно объявлять новые методы, которых нет в родительском классе.
- В дочернем классе можно объявить конструктор, который будет явно (с помощью ключевого слова super ) или неявно вызывать конструктор базового класса.
Дочерний класс не наследует private члены родительского класса, однако если в родительском классе есть protected , public или package-private (для случая нахождения дочернего и родительского класса в одном пакете) методы для доступа к private полям, то они могут использоваться дочерним классом.
Приведение типов
Посмотрите на создание экземпляра объекта Goblin :