1. Обзор
Система типов Java состоит из двух типов: примитивов и ссылок.
В этой статье мы рассмотрели примитивные преобразования , и здесь мы сосредоточимся на приведении ссылок, чтобы получить хорошее представление о том, как Java обрабатывает типы.
Дальнейшее чтение:
Основы дженериков Java
Оператор Java instanceof
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() { // ... } }
Теперь давайте расширим Животное :
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 является Животным и не отображает никаких ошибок.
Обратите внимание, что ссылка может ссылаться на любой подтип объявленного типа.
Используя upcasting, мы ограничили количество методов, доступных для экземпляра Cat , но не изменили сам экземпляр. Теперь мы не можем сделать ничего специфичного для Cat – мы не можем вызвать meow() в переменной animal .
Хотя Cat объект остается Cat объектом, вызов meow() вызовет ошибку компилятора:
// animal.meow(); The method meow() is undefined for the type Animal
Чтобы вызвать meow() нам нужно опустить animal , и мы сделаем это позже.
Но теперь мы опишем, что дает нам это предсказание. Благодаря апкастингу мы можем воспользоваться преимуществами полиморфизма.
3.1. Полиморфизм
Давайте определим еще один подкласс класса Animal , a Dog :
public class Dog extends Animal { public void eat() { // ... } }
Теперь мы можем определить метод feed () , который обрабатывает всех кошек и собак, как животных :
public class AnimalFeeder { public void feed(Listanimals) { animals.forEach(animal -> { animal.eat(); }); } }
Мы не хотим, чтобы AnimalFeeder заботился о том, какое животное находится в списке – кошка или Собака . В методе feed() все они являются животными .
Неявная передача происходит, когда мы добавляем объекты определенного типа в список animals :
Listanimals = new ArrayList<>(); animals.add(new Cat()); animals.add(new Dog()); new AnimalFeeder().feed(animals);
Мы добавляем кошек и собак, и они неявно переводятся в тип Animal . Каждая Кошка является Животным и каждая Собака является Животным . Они полиморфны.
Кстати, все объекты Java полиморфны, потому что каждый объект, по крайней мере, является Объектом . Мы можем назначить экземпляр Animal ссылочной переменной типа Object , и компилятор не будет жаловаться:
Object object = new Animal();
Вот почему все объекты Java, которые мы создаем, уже имеют Object конкретные методы, например, toString() .
Также распространена передача на интерфейс.
Мы можем создать Новый интерфейс и сделать 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();
Cat – это Мяу , апкастинг является законным и выполняется неявно.
Это/| Кошка является Новым , Животным , Объектом и Кошкой . В нашем примере он может быть назначен ссылочным переменным всех четырех типов.
3.2. Переопределение
В приведенном выше примере метод eat() переопределен. Это означает, что, хотя eat() вызывается для переменной типа Animal , работа выполняется методами, вызываемыми на реальных объектах – кошках и собаках:
public void feed(Listanimals) { animals.forEach(animal -> { animal.eat(); }); }
Если мы добавим некоторые записи в наши классы, мы увидим, что вызываются методы Cat ‘и Dog :
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
Подводить итоги:
- Ссылочная переменная может ссылаться на объект, если объект имеет тот же тип, что и переменная, или если он является подтипом
- Апкастинг происходит неявно
- Все объекты Java являются полиморфными и могут рассматриваться как объекты супертипа из-за восходящей трансляции
4. Даункастинг
Что делать, если мы хотим использовать переменную типа Animal для вызова метода, доступного только классу Cat ? А вот и даункастинг. Это приведение из суперкласса в подкласс.
Давайте возьмем пример:
Animal animal = new Cat();
Мы знаем, что переменная animal относится к экземпляру Cat . И мы хотим вызвать метод Cat ‘s meow() на животном . Но компилятор жалуется, что метод meow() не существует для типа Animal .
Чтобы вызвать мяу() мы должны опустить животное к Кошке :
((Cat) animal).meow();
Внутренние скобки и тип, который они содержат, иногда называются оператором приведения. Обратите внимание, что внешние скобки также необходимы для компиляции кода.
Давайте перепишем предыдущий пример Animal Feeder с помощью метода meow() :
public class AnimalFeeder { public void feed(Listanimals) { animals.forEach(animal -> { animal.eat(); if (animal instanceof Cat) { ((Cat) animal).meow(); } }); } }
Теперь мы получаем доступ ко всем методам, доступным классу Cat . Посмотрите на журнал, чтобы убедиться, что meow() действительно вызван:
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
Обратите внимание, что в приведенном выше примере мы пытаемся опустить только те объекты, которые на самом деле являются экземплярами Cat . Для этого мы используем оператор instanceof .
4.1. Оператор instanceof
Мы часто используем оператор instanceof перед понижением, чтобы проверить, принадлежит ли объект определенному типу:
if (animal instanceof Cat) { ((Cat) animal).meow(); }
4.2. Исключение ClassCastException
Если бы мы не проверили тип с помощью оператора instanceof , компилятор не жаловался бы. Но во время выполнения будет исключение.
Чтобы продемонстрировать это, давайте удалим оператор instanceof из приведенного выше кода:
public void uncheckedFeed(Listanimals) { animals.forEach(animal -> { animal.eat(); ((Cat) animal).meow(); }); }
Этот код компилируется без проблем. Но если мы попытаемся запустить его, мы увидим исключение:
ява,ланг.ClassCastException: com.baeldung.casting.Собака не может быть брошена в com.baeldung.casting.Cat
Это означает, что мы пытаемся преобразовать объект, который является экземпляром Dog , в экземпляр Cat .
ClassCastException’ s всегда выбрасывается во время выполнения, если тип, к которому мы относимся, не соответствует типу реального объекта.
Обратите внимание, что если мы попытаемся перейти к несвязанному типу, компилятор этого не допустит:
Animal animal; String s = (String) animal;
Компилятор говорит: “Не может привести от животного к строке”.
Для компиляции кода оба типа должны находиться в одном дереве наследования.
Давайте подведем итоги:
- Понижение необходимо для получения доступа к членам, специфичным для подкласса
- Даункастинг выполняется с помощью оператора cast
- Чтобы безопасно опустить объект, нам нужен оператор instanceof
- Если реальный объект не соответствует типу, к которому мы относимся, во время выполнения будет выдано исключение ClassCastException
5. метод cast()
Есть еще один способ приведения объектов с помощью методов Class :
public void whenDowncastToCatWithCastMethod_thenMeowIsCalled() { Animal animal = new Cat(); if (Cat.class.isInstance(animal)) { Cat cat = Cat.class.cast(animal); cat.meow(); } }
В приведенном выше примере вместо операторов cast и instanceof используются методы cast ( ) и isInstance () соответственно.
Обычно используются методы cast() и isInstance() с универсальными типами.
Давайте создадим AnimalFeederGeneric класс с feed() методом, который “кормит” только один тип животных – кошек или собак, в зависимости от значения параметра типа:
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; } }
Метод feed() проверяет каждое животное и возвращает только те, которые являются экземплярами T .
Обратите внимание, что экземпляр Class также должен быть передан в универсальный класс, поскольку мы не можем получить его из параметра типа T . В нашем примере мы передаем его в конструктор.
Давайте сделаем T равным Cat и убедимся, что метод возвращает только кошек:
@Test public void whenParameterCat_thenOnlyCatsFed() { Listanimals = 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. Заключение
В этом основополагающем учебнике мы изучили, что такое восходящий и нисходящий переход, как их использовать и как эти концепции могут помочь вам воспользоваться преимуществами полиморфизма.
Как всегда, код для этой статьи доступен на GitHub .