1. Обзор
Система типов 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
:
Cat cat = new Cat();
И мы также можем присвоить его ссылочной переменной типа Animal
:
Animal animal = cat;
В приведенном выше присваивании имеет место неявное повышение приведения.
Мы могли бы сделать это явно:
animal = (Animal) cat;
Но нет необходимости делать явное приведение дерева наследования. Компилятор знает, что 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(List<Animal> animals) {
animals.forEach(animal -> {
animal.eat();
});
}
}
Мы не хотим, чтобы AnimalFeeder
заботился о том, какое животное
находится в списке — кошка
или собака
. В методе feed()
все они являются животными
.
Неявное преобразование происходит, когда мы добавляем объекты определенного типа в список животных
:
List<Animal> 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
:
Mew mew = new Cat();
Кошка
Мяу
; _ upcasting является законным и выполняется неявно.
Следовательно, Cat
— это Mew
, Animal
, Object
и Cat
. В нашем примере его можно присвоить ссылочным переменным всех четырех типов.
3.2. Переопределение
В приведенном выше примере метод eat()
переопределен. Это означает, что хотя eat()
вызывается для переменной типа Animal
, работа выполняется методами, вызываемыми для реальных объектов — кошек и собак:
public void feed(List<Animal> 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 animal = new Cat();
Мы знаем, что переменная animal
относится к экземпляру Cat
. И мы хотим вызвать метод Cat
meow () для
животного
. Но компилятор жалуется, что для типа Animal
не существует метода meow()
. ``
Чтобы вызвать meow()
, мы должны преобразовать animal
в Cat
:
((Cat) animal).meow();
Внутренние скобки и тип, который они содержат, иногда называют оператором приведения. Обратите внимание, что внешние круглые скобки также необходимы для компиляции кода.
Давайте перепишем предыдущий пример AnimalFeeder
с методом meow ()
:
public class AnimalFeeder {
public void feed(List<Animal> 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(List<Animal> 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<T>
с методом feed()
, который «кормит» только один вид животных, кошек или собак, в зависимости от значения параметра type:
public class AnimalFeederGeneric<T> {
private Class<T> type;
public AnimalFeederGeneric(Class<T> type) {
this.type = type;
}
public List<T> feed(List<Animal> animals) {
List<T> list = new ArrayList<T>();
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() {
List<Animal> animals = new ArrayList<>();
animals.add(new Cat());
animals.add(new Dog());
AnimalFeederGeneric<Cat> catFeeder
= new AnimalFeederGeneric<Cat>(Cat.class);
List<Cat> fedAnimals = catFeeder.feed(animals);
assertTrue(fedAnimals.size() == 1);
assertTrue(fedAnimals.get(0) instanceof Cat);
}
6. Заключение
В этом базовом руководстве мы рассмотрели повышение и понижение приведения, способы их использования и то, как эти концепции могут помочь вам воспользоваться преимуществами полиморфизма.
Как всегда, код для этой статьи доступен на GitHub .