Перейти к основному содержимому

Приведение типов объектов в Java

· 9 мин. чтения

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 .