Рубрики
Без рубрики

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

Обзор приведения типов в Java, покрытый простыми и понятными примерами.

Автор оригинала: baeldung.

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(List animals) {
        animals.forEach(animal -> {
            animal.eat();
        });
    }
}

Мы не хотим, чтобы AnimalFeeder заботился о том, какое животное находится в списке – кошка или Собака . В методе feed() все они являются животными .

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

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

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

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