Дженерики в Java

161
05 января 2020, 11:30

Возможно ли в теории на этапе компиляции обнаружить некорректное преобразование объекта одного класса в объект другого?

Пример кода:

Integer i = new Integer(1);
Object o = (Object)i;
String b = (String)o;

Невозможно ли на этапе компиляции проверить корректность приведения? Если да, то для чего используются дженерики? Их используют для того, чтобы помочь компилятору в определении класса объекта и тем самым ускорить процесс компиляции?

Answer 1

Вся строгая типизация и львиная доля компиляции по сути направлена на то, чтобы не допустить путаницу с типами данных. Проблема в том, что у вас нет понимания, как это работает. Object o = (Object)i - если вы делаете так, то это называется кастование. В этом случае вся ответсвенность за приведение типов лежит на том, кто написал этот код, поскольку вы явно указали, к какому типу данных нужно привести эту переменную. Тут компилятор бессильный и вы получите класкаст, если при выполнении тип в переменной не будет соответствовать типу, указанному при кастовании. Другой вопрос, что приведению у классу Object возможно всегда, поскольку это родительский класс для всех классов джавы (только не путайте это с примитивами, примитивы к Object не кастуются). Вы должны понимать, что приведение к родительскому классу возможно всегда, потому что между родительским классом и классом-наследником отношение "is a", т.е. класс-наследник по сути всегда принадлежит к классу-родителю. Например, супер класс, который описывает всех Animal, имеет множество наследников (Cat, Dog и т.д.). И кот , и собака являются животными, поэтому привести переменную типа Cat или Dog к переменной типа Animal возможно всегда и кастование здесь не требуется (в вашем случае, ксати тоже. Integer и сам приведется к Object, дополнительно указывать это в скобках , т.е. явно кастовать не нужно). А теперь рассмотрим обратный процесс - у нас есть переменная типа Animal. Теперь автоматическое приведение к дочерним классам Cat или Dog невозможно, потому что в переменной класса Animal может оказаться любой из указанных объектов (Cat или Dog ). Класс Animal позволяет нам реализовать некоторую общую логику для всех животных, соответственно , мы избегаем дублирование кода (это становится актуальнее с увеличением количества наследников этого супер-класса), следовательно, если мы уберем суперкласс, то нам придется всю общую логику писать в каждом классе (в дальнейшем внесение одного изменения влечет изменения во всех классах со всеми вытекающими). Однако теперь, когда мы привели всех животных к переменной типа суперкласса мы не можем знать, какое именно животное там находится, но все теряет смысл, если мы не сможеми работать с этой структурой данных. Рассмотрим пример.

Создадим наш абстактный класс Animal и классы - наследники Cat и Dog.

abstract class Animal {
    public abstract Animal move();    
}
class Cat extends Animal{
    @Override
    public Animal move() {
        //some logic
        return this;
    }
}
class Dog extends Animal{
    @Override
    public Animal move() {
        //some logic
        return this;
    }
}
В абстрактном класс Animal объявлен только один метод - move. Этот метод теперь должен быть переопределен у всех классов-наследников (я описал только один метод для простоты). 

А теперь мне хотелось бы создать класс-сервис(класс с поведением но без состояния) , который может работать со всеми животными.

class AnimalService<T extends Animal>{
    public Animal moveAnimal (T animal){
        T t = (T)animal.move();
        return t;
    }
    public void moveAnimal (List<T> animals){
        for (Animal animal : animals) animal.move();
    }
}

Для того,чтобы иметь такую возможность (работать со всеми наследниками класса Animal мне необходимо использовать дженерик, где я явно укажу, что данный класс будет работать только с наследниками класса Animal. Теперь я не провожу кастования, вместе с тем, компилятор следит за тем, чтобы в класс-сервис передавались исключительно наследники Animal. Таким образом вы никогда не получите исключение типа класскаст, поскольку всегда сможете отследить корреткность типов данных на этапе компиляции. Аналогичным способом использована коллекция List в сигнатуре сервисного метода. Все дело в том, что если мы не объявим лист с дженериком, а укажем просто лист, то это будет лист , типизированный классом Object, в этом случае мы не сможем вызвать у каждого из экземпляров класса метод move, ведь у класса Object нет такого метода. Разумеется, вы можете кастовать все к Animal и вызывать метод move, но это уже небезопасно, потому что фактически в данный метод можно передать лист с любыми объектами (не только наследниками Animal ) и компилятор уже никак не сможет это отследить.

Кроме того, обратите внимание на строчку кода T t = (T)animal.move(); в сервисном классе. По умолчанию метод move возвращает переменную типа Animal, как это декларировано в методе супер-класса. Однако, бывают ситуации, когда в сервисе необходимо получить переменную реального класса (это может быть полезно, например , при работе с базами данных). В этой строке кода сделано кастование к переменной типа Т, которая объявлена в дженерике. В классе сервисе мы заранее не знаем, с каким именно животным будет работать данный класс, поэтому условно это обозначено как Т, но фактически мы можем всегда сделать безопасное кастование вниз (от супер-класса к дочернему классу) к переменной Т без каких-либо дополнительных проверок.

Дженерики - очень гибкий и мощный механизм, но думаю, что это немного разъяснит ситуацию))

READ ALSO
Реализовать SIMD на java

Реализовать SIMD на java

У меня есть задача реализовать сортировку пузырьком архитектурой Флинна а именно SimdВ Simd сказано, что это должны быть векторные операции,...

182
Как сделать проверку на то, что исключение действительно выбросится?

Как сделать проверку на то, что исключение действительно выбросится?

на примере функции water(), в которой есть проверка на пустую строку и null

189
мой пример сеттера (инкапсуляция)

мой пример сеттера (инкапсуляция)

Не пойму почему выводит:

159