Возможно ли в теории на этапе компиляции обнаружить некорректное преобразование объекта одного класса в объект другого?
Пример кода:
Integer i = new Integer(1);
Object o = (Object)i;
String b = (String)o;
Невозможно ли на этапе компиляции проверить корректность приведения? Если да, то для чего используются дженерики? Их используют для того, чтобы помочь компилятору в определении класса объекта и тем самым ускорить процесс компиляции?
Вся строгая типизация и львиная доля компиляции по сути направлена на то, чтобы не допустить путаницу с типами данных. Проблема в том, что у вас нет понимания, как это работает. 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, как это декларировано в методе супер-класса. Однако, бывают ситуации, когда в сервисе необходимо получить переменную реального класса (это может быть полезно, например , при работе с базами данных). В этой строке кода сделано кастование к переменной типа Т, которая объявлена в дженерике. В классе сервисе мы заранее не знаем, с каким именно животным будет работать данный класс, поэтому условно это обозначено как Т, но фактически мы можем всегда сделать безопасное кастование вниз (от супер-класса к дочернему классу) к переменной Т без каких-либо дополнительных проверок.
Дженерики - очень гибкий и мощный механизм, но думаю, что это немного разъяснит ситуацию))
Продвижение своими сайтами как стратегия роста и независимости