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

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

1. обзор

Система типов Java состоит из двух типов типов: примитивы и ссылки.

Мы рассмотрели примитивные преобразования вthis article, и здесь мы сосредоточимся на приведении ссылок, чтобы получить хорошее представление о том, как Java обрабатывает типы.

Дальнейшее чтение:

Основы Java Generics

Краткое введение в основы Java Generics.

Read more

Java экземпляр оператора

Узнайте об операторе instanceof в Java

Read more

2. Примитив против Ссылка

Хотя примитивные преобразования и приведение ссылочных переменных могут выглядеть одинаково, они довольноdifferent concepts.

В обоих случаях мы «превращаем» один тип в другой. Но в упрощенном виде примитивная переменная содержит свое значение, а преобразование примитивной переменной означает необратимые изменения ее значения:

double myDouble = 1.1;
int myInt = (int) myDouble;

assertNotEquals(myDouble, myInt);

После преобразования в приведенном выше примере переменнаяmyInt равна1, и мы не можем восстановить из нее предыдущее значение1.1.

Reference variables are different; ссылочная переменная относится только к объекту, но не содержит самого объекта.

Приведение ссылочной переменной не затрагивает объект, к которому она относится, а только помечает этот объект другим способом, расширяя или сужая возможности для работы с ним. Upcasting narrows the list of methods and properties available to this object, and downcasting can extend it.с

Ссылка похожа на дистанционное управление объектом. Пульт дистанционного управления имеет больше или меньше кнопок в зависимости от его типа, а сам объект хранится в куче. Когда мы выполняем кастинг, мы меняем тип пульта дистанционного управления, но не меняем сам объект.

3. Апкастинг

Casting from a subclass to a superclass is called upcasting. Как правило, преобразование выполняется неявным образом.

Обновление тесно связано с наследованием - еще одна ключевая концепция в 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(), нам нужно снизитьanimal, и мы сделаем это позже.

Но теперь мы опишем, что дает нам апокалипсис. Благодаря апскейтингу мы можем воспользоваться полиморфизмом.

3.1. Полиморфизм

Давайте определим другой подклассAnimal, классDog:

public class Dog extends Animal {

    public void eat() {
         // ...
    }
}

Теперь мы можем определить методfeed(), который обрабатывает всех кошек и собак какanimals:

public class AnimalFeeder {

    public void feed(List animals) {
        animals.forEach(animal -> {
            animal.eat();
        });
    }
}

Мы не хотим, чтобыAnimalFeeder заботился о том, какойanimal находится в списке -Cat илиDog. В методеfeed() все ониanimals.

Неявное преобразование происходит, когда мы добавляем объекты определенного типа в списокanimals:

List animals = new ArrayList<>();
animals.add(new Cat());
animals.add(new Dog());
new AnimalFeeder().feed(animals);

Мы добавляем кошек и собак, и они неявно повышаются до типаAnimal. КаждыйCat - этоAnimal, а каждыйDog - этоAnimal. Они полиморфны.

Кстати, все объекты Java полиморфны, потому что каждый объект имеет как минимумObject. Мы можем присвоить экземплярAnimal ссылочной переменной типаObject, и компилятор не пожалуется:

Object object = new Animal();

Вот почему все объекты Java, которые мы создаем, уже имеют специальные методыObject, например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();

Cat - этоMew, преобразование разрешено и выполняется неявно.

Таким образом,Cat - этоMew,Animal,Object иCat. В нашем примере это может быть присвоено ссылочным переменным всех четырех типов.

3.2. Переопределение

В приведенном выше примере методeat() переопределен. Это означает, что хотяeat() вызывается для переменной типаAnimal, работа выполняется методами, вызываемыми для реальных объектов - кошек и собак:

public void feed(List animals) {
    animals.forEach(animal -> {
        animal.eat();
    });
}

Если мы добавим журналирование в наши классы, мы увидим, что вызываются методыCat иDog:

web - 2018-02-15 22:48:49,354 [main] INFO com.example.casting.Cat - cat is eating
web - 2018-02-15 22:48:49,363 [main] INFO com.example.casting.Dog - dog is eating

Подводить итоги:

  • Ссылочная переменная может ссылаться на объект, если объект того же типа, что и переменная, или если это подтип

  • Upcasting происходит неявно

  • Все объекты Java являются полиморфными и могут рассматриваться как объекты супертипа из-за апскейтинга

4. понижающее приведение

Что, если мы хотим использовать переменную типаAnimal для вызова метода, доступного только для классаCat? Вот идет уныние. It’s the casting from a superclass to a subclass.

Давайте возьмем пример:

Animal animal = new Cat();

Мы знаем, что переменнаяanimal относится к экземпляруCat. И мы хотим вызвать методmeow()Cat дляanimal. Но компилятор жалуется, что методmeow() не существует для типаAnimal.

Чтобы вызватьmeow(), мы должны понизитьanimal доCat:

((Cat) animal).meow();

Внутренние скобки и тип, который они содержат, иногда называют оператором приведения. Обратите внимание, что внешние скобки также необходимы для компиляции кода.

Давайте перепишем предыдущий примерAnimalFeeder с помощью методаmeow():

public class AnimalFeeder {

    public void feed(List 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.example.casting.Cat - cat is eating
web - 2018-02-16 18:13:45,454 [main] INFO com.example.casting.Cat - meow
web - 2018-02-16 18:13:45,455 [main] INFO com.example.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 animals) {
    animals.forEach(animal -> {
        animal.eat();
        ((Cat) animal).meow();
    });
}

Этот код компилируется без проблем. Но если мы попробуем запустить его, мы увидим исключение:

java.lang.ClassCastException: com.example.casting.Dog нельзя преобразовать вcom.example.casting.Cat

Это означает, что мы пытаемся преобразовать объект, являющийся экземпляромDog, в экземплярCat.

ClassCastException's всегда выдается во время выполнения, если тип, который мы понижаем, не соответствует типу реального объекта.

Обратите внимание: если мы попытаемся выполнить приведение к несвязанному типу, компилятор не допустит этого:

Animal animal;
String s = (String) animal;

Компилятор говорит: «Невозможно привести от Animal к String».

Для компиляции кода оба типа должны находиться в одном и том же дереве наследования.

Подведем итоги:

  • Понижение рейтинга необходимо для получения доступа к членам, относящимся к подклассу

  • Даункастинг выполняется с использованием оператора приведения

  • Чтобы безопасно отключить объект, нам нужен оператор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() иisInstance() используются вместо операторов cast иinstanceof соответственно.

Обычно методыcast() иisInstance() используются с универсальными типами.

Давайте создадим классAnimalFeederGeneric<T> с методомfeed(), который «кормит» только один тип животных - кошек или собак, в зависимости от значения параметра type:

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() {
    List animals = 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. Заключение

В этом основополагающем руководстве мы изучили, что такое повышение и понижение, как их использовать и как эти концепции могут помочь вам воспользоваться преимуществами полиморфизма.

Как всегда, доступен код для этой статьиover on GitHub.