Дженерики или: Как я научился беспокоиться и стирать ненависть
Ваша JVM пытается определить, что вы имели в виду, когда вы дали ей определенную серию байт-кодов / литералов через скомпилированный исходный код. «Обычно» все красиво объявляется с использованием конкретных типов классов, таких как String
или конкретных примитивных типов, таких как int
. Например, значение 6
поставляется с некоторыми метаданными, которые сообщают JVM, что это литерал int
, а не какой-то другой тип. Ряд байтов сам по себе не имеет смысла; вам нужен способ сообщить JVM, что именно кодируют двоичные числа. Одно и то же двоичное значение может представлять бесконечное число идей или состояний, поэтому важно, чтобы метаданные типа были включены, чтобы помочь разобраться во всем этом. Без типов JVM не сможет определить char
со значением 'a'
и байт со значением 0x61
друг от друга. Когда вы задаете универсальный тип, такой как типы, используемые реализациями интерфейса List
(например, используемый вами ArrayList
), вы говорите компилятору: «Это не конкретный конкретный тип, это может быть что угодно» (хотя используя extends
вы можете немного ограничить вещи). Обычно это может не быть проблемой, но из-за проблем обратной совместимости, с которыми я не буду сталкиваться, динамические типы стираются в процессе, называемом стиранием типов (очень подробный взгляд на этот ответ ). объяснение рассуждений). Короткий и приятный ответ, который решает вашу проблему, состоит в том, чтобы просто учесть дополнительные сложности родовых типов и активно проверять и предотвращать приведение, которые не допускаются. Таким образом, безопасность типов может быть достигнута, главным образом, путем проверки присваиваемости общих типов вручную, вместо того чтобы полагаться на автоматическую безопасность типов, как при использовании конкретных типов.
Пример метода
Это относительно безопасная для вашего типа версия вашего кода (вы получите сообщение об ошибке во время выполнения в худшем случае неправильного использования) с нулевой проверкой и общим ограничением типов (ObjectInputStream
и ObjectOutputStream
используют методы из Serializable
интерфейс для реализации их чтения и записи Object
экземпляров в потоки).
public static <OUTPUT extends Serializable, INPUT extends Serializable>
OUTPUT getObjectFromFile(Class<INPUT> type, CharSequence fileName)
throws IOException, ClassNotFoundException {
final String filenameString = Objects.requireNonNull(fileName).toString();
final Class<INPUT> inputType = Objects.requireNonNull(type);
try (ObjectInput ois = new ObjectInputStream(new FileInputStream(filenameString))) {
final Object rawObject = ois.readObject();
@SuppressWarnings("unchecked")
final OUTPUT output = (OUTPUT) (rawObject == null ? null : inputType.cast(rawObject));
return output;
}
}
public static <DESIRED extends Serializable, STORED extends Serializable>
boolean addArrayListFromFile(Collection<DESIRED> out, Class<STORED> type, CharSequence fileName)
throws IOException, ClassNotFoundException {
@SuppressWarnings("unchecked")
final ArrayList<DESIRED> inList = getObjectFromFile(type, fileName);
return inList != null && out.addAll(inList);
}
Подробнее
Эти методы читают сериализованные объекты из файла со спецификацией безопасности типа и местоположения файла, заданной через параметры метода. Возвращаемое значение метода верхнего уровня на самом деле является boolean
, указывающим на успех или неудачу (true
- это успех), но фактический вывод данных записывается в экземпляр Collection
, предоставленный вызываемым объектом. В отличие от неуниверсального метода, который может явно указывать приведение типов безопасно с использованием знакомого синтаксиса a = (a) b;
, универсальные методы с универсальными типами (такими как DESIRED
, STORED
, INPUT
и OUTPUT
) не могут быть точно уверены какой тип они будут обрабатывать. Они теряют эту информацию о типах через процесс, называемый стиранием типов.
Через разработчиков стирания типов и среду выполнения JVM теряются метаданные о конкретных типах для типов, которые генерируются. В результате среда выполнения и разработчик зависят от типа ввода, назначаемого для требуемого типа вывода. Только компилятор может даже иметь доказуемость для приведений, и он не может проверять универсальные типы в общем смысле, как это может делать конкретные типы. Часто бывает так, что до времени выполнения проверка безопасности типов невозможна. List<?>
во время выполнения мог бы фактически быть скомпилирован и запрограммирован как List<String>
, но мы не можем определить разницу между двумя списками до выполнения, потому что компилятор не может поддерживать или определять информацию, которую нам нужно сделать поэтому.
В дополнение к стиранию типа, вызывающему потерю метаданных типа, компилятор может также не полностью знать график типов программы во время выполнения. Потенциал для таких вещей, как связывание и введение новых типов, которые наш текущий компилятор не распознает, означает, что мы никогда не сможем избежать небезопасных приведений к универсальным типам с полностью алгоритмическими подходами. Фактически, нет никакого способа удовлетворить эту проблему безопасности типов для обобщенных типов в обобщенном виде, поскольку это составляет проблему булевой выполнимости. Однако, есть где-то еще, мы можем получить конкретную информацию о том, какие типы вызывал вызывающий метод и предоставлял ... разработчику! Разработчики должны указать явное намерение для универсальных типов, чтобы обеспечить безопасность типов. Сделав так, чтобы метод, вызывающий метод (в конечном счете, разработчик), отвечал за то, чтобы сообщить среде выполнения, что ожидать при передаче экземпляра Class<T>
в код / метод, мы можем попытаться проверить универсальные типы таким образом, чтобы избежать ClassCastException
.
Если у нас есть List<T>
, он просто становится List
, поэтому мы больше не можем идентифицировать его как отличающийся от любого другого List
. Однако это не так для Class<T>
, который мы можем использовать для передачи метаданных типа вокруг компилятора и во время выполнения (поскольку объекты Class
хранят информацию о типе в графе объектов вместо того, чтобы использовать исключительно систему типов Java ). Однако в конечном итоге программист несет ответственность за предоставление правильного Class<T>
для каждого вызова метода. Выполняя проверку типов во время выполнения на достоверность, способность компилятора обнаруживать ошибки безопасности типов во время компиляции в основном приносится в жертву для выражений, использующих универсальные типы. Вы больше не будете получать предупреждения, такие как «int
нельзя преобразовать как String
» при компиляции, вместо этого вы просто получите исключение приложения или ошибку проверки безопасности приведения, которая должна быть надлежащим образом обработана во время выполнения.
Если вы хотите поэкспериментировать с вещами и посмотреть, что произойдет, когда вы используете этот метод, я создал gist на GitHub, содержащий небольшой маленький файл класса с основным методом, который вы можете запустить как маленькая тестовая программа.