Приведение типов объектов в Java
Система типов Java состоит из двух видов типов: примитивов и ссылок.
В этой статье мы рассмотрели примитивные преобразования , а здесь сосредоточимся на приведении ссылок, чтобы получить хорошее представление о том, как Java обрабатывает типы.
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() // . > >
Теперь давайте расширим Animal :
public class Cat extends Animal public void eat() // . > public void meow() // . > >
Теперь мы можем создать объект класса Cat и присвоить его ссылочной переменной типа Cat :
И мы также можем присвоить его ссылочной переменной типа Animal :
В приведенном выше присваивании имеет место неявное повышение приведения.
Мы могли бы сделать это явно:
Но нет необходимости делать явное приведение дерева наследования. Компилятор знает, что cat — это Animal , и не выводит никаких ошибок.
Обратите внимание, что ссылка может относиться к любому подтипу объявленного типа.
Используя восходящее преобразование, мы ограничили количество методов, доступных для экземпляра Cat , но не изменили сам экземпляр. Теперь мы не можем делать ничего специфичного для Cat — мы не можем вызывать meow() для переменной animal .
Хотя объект Cat остается объектом Cat , вызов meow() вызовет ошибку компилятора:
// animal.meow(); The method meow() is undefined for the type Animal
Чтобы вызвать meow() , нам нужно привести животное в нисходящее состояние , и мы сделаем это позже.
Но сейчас мы опишем, что дает нам приведение вверх. Благодаря восходящему преобразованию мы можем воспользоваться преимуществами полиморфизма.
3.1. Полиморфизм
Давайте определим еще один подкласс Animal , класс Dog :
public class Dog extends Animal public void eat() // . > >
Теперь мы можем определить метод feed() , который обращается со всеми кошками и собаками как с животными :
public class AnimalFeeder public void feed(ListAnimal> animals) animals.forEach(animal -> animal.eat(); >); > >
Мы не хотим, чтобы AnimalFeeder заботился о том, какое животное находится в списке — кошка или собака . В методе feed() все они являются животными .
Неявное преобразование происходит, когда мы добавляем объекты определенного типа в список животных :
ListAnimal> animals = new ArrayList>(); animals.add(new Cat()); animals.add(new Dog()); new AnimalFeeder().feed(animals);
Мы добавляем кошек и собак, и они неявно преобразуются в тип Animal . Каждая кошка — животное , и каждая собака — животное . Они полиморфны.
Кстати, все объекты Java полиморфны, потому что каждый объект является как минимум Object . Мы можем присвоить экземпляр 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 :
Кошка Мяу ; _ upcasting является законным и выполняется неявно.
Следовательно, Cat — это Mew , Animal , Object и Cat . В нашем примере его можно присвоить ссылочным переменным всех четырех типов.
3.2. Переопределение
В приведенном выше примере метод eat() переопределен. Это означает, что хотя eat() вызывается для переменной типа Animal , работа выполняется методами, вызываемыми для реальных объектов — кошек и собак:
public void feed(ListAnimal> animals) animals.forEach(animal -> animal.eat(); >); >
Если мы добавим логирование в наши классы, то увидим, что вызываются методы Cat и Dog :
web - 2018-02-15 22:48:49,354 [main] INFO com.foreach.casting.Cat - cat is eating web - 2018-02-15 22:48:49,363 [main] INFO com.foreach.casting.Dog - dog is eating
Подводить итоги:
- Ссылочная переменная может ссылаться на объект, если объект того же типа, что и переменная, или если он является подтипом.
- Обновление происходит неявно.
- Все объекты Java являются полиморфными и могут рассматриваться как объекты супертипа благодаря восходящему преобразованию.
4. Понижение
Что, если мы хотим использовать переменную типа Animal для вызова метода, доступного только для класса Cat ? А вот и уныние. Это приведение от суперкласса к подклассу.
Давайте посмотрим на пример:
Мы знаем, что переменная animal относится к экземпляру Cat . И мы хотим вызвать метод Cat meow () для животного . Но компилятор жалуется, что для типа Animal не существует метода meow() . «
Чтобы вызвать meow() , мы должны преобразовать animal в Cat :
Внутренние скобки и тип, который они содержат, иногда называют оператором приведения. Обратите внимание, что внешние круглые скобки также необходимы для компиляции кода.
Давайте перепишем предыдущий пример AnimalFeeder с методом meow () :
public class AnimalFeeder public void feed(ListAnimal> animals) 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.foreach.casting.Cat - cat is eating web - 2018-02-16 18:13:45,454 [main] INFO com.foreach.casting.Cat - meow web - 2018-02-16 18:13:45,455 [main] INFO com.foreach.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(ListAnimal> animals) animals.forEach(animal -> animal.eat(); ((Cat) animal).meow(); >); >
Этот код компилируется без проблем. Но если мы попытаемся запустить его, мы увидим исключение:
java.lang.ClassCastException: com.foreach.casting.Dog не может быть приведен к com.foreach.casting.Cat
Это означает, что мы пытаемся преобразовать объект, являющийся экземпляром Dog , в экземпляр Cat .
ClassCastException всегда выбрасывается во время выполнения, если тип, к которому мы приводим, не соответствует типу реального объекта.
Обратите внимание, что если мы попытаемся выполнить понижающее приведение к несвязанному типу, компилятор этого не допустит:
Animal animal; String s = (String) animal;
Компилятор говорит: «Невозможно привести тип Animal к String».
Чтобы код компилировался, оба типа должны находиться в одном дереве наследования.
- Понижающее приведение необходимо для получения доступа к членам, специфичным для подкласса.
- Даункастинг выполняется с помощью оператора приведения.
- Чтобы безопасно понизить объект, нам нужен оператор instanceof .
- Если реальный объект не соответствует типу, к которому мы приводим его, во время выполнения будет выброшено исключение ClassCastException .
5. Метод приведения()
Есть еще один способ приведения объектов с помощью методов класса :
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() , который «кормит» только один вид животных, кошек или собак, в зависимости от значения параметра type:
public class AnimalFeederGenericT> private ClassT> type; public AnimalFeederGeneric(ClassT> type) this.type = type; > public ListT> feed(ListAnimal> animals) ListT> list = new ArrayListT>(); animals.forEach(animal -> if (type.isInstance(animal)) T objAsType = type.cast(animal); list.add(objAsType); > >); return list; > >
Метод feed() проверяет каждое животное и возвращает только те, которые являются экземплярами T .
Обратите внимание, что экземпляр класса также должен быть передан универсальному классу, поскольку мы не можем получить его из параметра типа T. В нашем примере мы передаем его в конструктор.
Сделаем T равным Cat и убедимся, что метод возвращает только котов:
@Test public void whenParameterCat_thenOnlyCatsFed() ListAnimal> animals = new ArrayList>(); animals.add(new Cat()); animals.add(new Dog()); AnimalFeederGenericCat> catFeeder = new AnimalFeederGenericCat>(Cat.class); ListCat> fedAnimals = catFeeder.feed(animals); assertTrue(fedAnimals.size() == 1); assertTrue(fedAnimals.get(0) instanceof Cat); >
6. Заключение
В этом базовом руководстве мы рассмотрели повышение и понижение приведения, способы их использования и то, как эти концепции могут помочь вам воспользоваться преимуществами полиморфизма.
Как всегда, код для этой статьи доступен на GitHub .