Что такое "сырые типы" (raw types) в Java

Если в среде разработки набрать что-то вроде:
List list = new ArrayList();
то среда выдаст предупреждение, наподобие: "Raw use of parametrized class 'List'", что переводится как "Сырое использование параметризованного класса 'List'". Обычно такой код выделяется средой разработки и может возникнуть вопрос о том, к чему может привести использование так называемых "сырых типов" (raw types).

Понятие "сырого типа"

На самом деле, под сырыми типами подразумеваются не типы данных (классы), а переменные параметризованных (обобщённых) классов, при объявлении которых не указан конкретный тип параметра. Говорят, что они параметризованы сырым, то есть неопределённым, типом.
Сначала приведём пример параметризованного класса:
public class Model<K> { K id;}
Мы объявили класс Model, параметризованный K. У него есть поле id, типа K. На данном этапе мы не знаем, какого именно типа будет поле id у объектов этого класса. Действительно, если мы будем хранить в объектах этого класса данные из базы, а в поле id значение первичного ключа, то для разных таблиц тип данных первичного ключа может оказаться разным: где-то это будет Long, а в какой-то таблице -- String. Поэтому у нас будут варианты создания переменных типа Model:
Model<Long> longId = new Model<>();Model<String> strId = new Model<>();
Соответственно у переменной longId поле id будет типа Long, а у strId -- типа String. Именно то, что нам и нужно. То есть объявляя переменную так: Model<Long> longId, мы говорим, что в нашем классе Model переменная типа K равна Long. И во всех местах класса, где встречается K, она будет как бы заменена на Long.
Но, что если мы не укажем тип параметра, объявляя переменную:
Model undefId = new Model<>();
Какого типа будет K в таком случае? Вполне можно считать, что id будет типа Object и следующий код скомпилируется без проблем и отработает без ошибок:
Model undefId = new Model<>();undefId.id = new Object();
Тем не менее, переменная, объявленная таким образом: Model undefId считается тем самым "сырым использованием параметризованного класса" или "сырым типом" и имеет некоторые отличия от переменной объявленной так: Model<Object> objId.

Основное отличие переменных объявленных с параметром неопределённого типа

Самым частым примером использованием параметризованных классов в Java является использование коллекций. Если переменная, например, списка параметризована, то компилятор будет проверять, что в неё попадают только данные соответствующего типа:
List<String> fruits = new ArrayList<>();fruits.add("apple");fruits.add("pear");fruits.add(1);//Ошибка компиляции
Последняя строка примера вообще не скомпилируется, потому что компилятор проверяет, чтобы в метод add передавались только данные типа String, так как мы сами параметризовали список этим типом, объявив его List<String> fruits.
Стоит убрать параметризацию и объявить fruits с "сырым" типом, как проверка прекратится:
List fruits = new ArrayList<>();fruits.add("apple");fruits.add("pear");fruits.add(1);//Ошибки не будет
Чтение из такой коллекции всегда сопровождается приведением типа:
String apple = (String) fruits.get(0);String one = (String) fruits.get(2);
Последняя строка выбросит java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String.
Такой код плох тем, что он пройдёт компиляцию, а ошибка обнаружится только во время выполнения. Таким образом, не указывая параметр явно, мы теряем преимущества проверки типов на этапе компиляции.

Отличие переменных с параметром неопределённого типа от переменных параметризованных типом <Object>

Рассмотрим пример:
public static void main(String[] args) { List<String> strings = new ArrayList<>(); rawAdder(strings); System.out.println(strings.get(0) instanceof String); //false!}
public static void rawAdder(List list) { list.add(1);}
Посмотрите внимательно на этот код. Последняя инструкция метода main сделает казалось бы невозможное: она из списка параметризованного типом <String> извлечёт первый элемент и покажет нам, что он не String. Такое возможно из-за крайне неудачной реализации метода rawAdder, а именно из-за того, что его параметр List list не параметризован должным образом.
Если метод rawAdder собирается добавлять в получаемый список случайные объекты, то он и должен принимать список объектов: List<Object> list, тогда наша программы просто бы не скомпилировалась и мы бы узнали о проблеме ещё на этапе компиляции, а не во время выполнения. Ошибка была бы в строке rawAdder(strings);, потому что следующее присваивание недопустимо:
List<String> strs = new ArrayList<>();List<Objects> objs = new ArrayList<>();objs = strs;
Такой код не откомпилируется с ошибкой: List<String> cannot be converted to List<Objects>. А значит и невозможно передать List<String> strings в метод rawAdder(List<Object> list).
Таким образом, переменные, параметризованные типом <Object> проходят все проверки компилятора на соответствие типов и исключают ситуации, когда, как в нашем первом примере, в списке явно параметризованном типом <String> всё-таки оказывается элемент типа Integer.

Отличие переменных с параметром неопределённого типа от переменных параметризованных заменителем <?>

В данном примере:
public static void main(String[] args) { List<String> strings = new ArrayList<>(); strings.add("string"); wildAdder(strings);}
public static void wildAdder(List<?> list) { Object o = list.get(0); list.add("another string");//Ошибка компиляции}
метод wildAdder(List<?> list) отличается от метода rawAdder(List list) тем, что параметр List параметризован заменителем <?>. Хотя заменитель также ассоциируется с типом Object и строка Object o = list.get(0); выполнится без проблем, но следующая строка: list.add("another string"); не будет откомпилирована.
Дело в том, что для <?> продолжает действовать проверка типов. <?> неопределённый или любой тип. Это значит, что нам неизвестно, какой тип у параметра метода add переменной list. А значит компилятор просто запрещает использовать этот метод, поэтому не откомпилирует list.add("another string").
В то же время компилятору <?> указывает на то, что нам неизвестно, какой тип данных возвращает метод get переменной list. А значит компилятор позволит присвоить возвращённое значение только переменной типа Object, которой можно присваивать практически любые значения.
Таким образом, мы можем в этом месте программы передать в метод wildAdder(List<?> list) переменную типа List<String>, а другом месте -- переменную типа List<Integer>. И при этом мы не будем ловить множество ошибок во время выполнения программы, как если бы использовали "сырой тип" List list, без какой-либо параметризации вообще.

Непроизвольное получение непараметризованных переменных

В Java, если вы создаёте переменную параметризованного класса, не указывая типы параметров, то есть с "сырыми" типами, то даже заявленные типы параметров конструктора, не статических методов и не статических полей будут затёрты.
Рассмотрим пример:
class Model<T> { List<Integer> getNums() { return Arrays.asList(1, 2); }}
Класс Model параметризован <T>. А метод getNames() возвращает список, который тоже параметризован, но не обобщением, а конкретным типом Integer. Так вот. Если создать непараметризованную переменную типа Model, но не указать, какого типа у нас K, то метод getNums() будет возвращать просто List, без параметра. Integer затрётся и компилятор не будет для этого списка проверять типы:
public static void main(String[] args) { Model rawType = new Model(); List<Boolean> names = rawType.getNums(); System.out.println(names);}
Мало того, что строка List<Boolean> names = rawType.getNums(); откомпилируется, она ещё и выполнится, выведя на консоль: [1, 2]. А вот следующий код не откомпилируется:
public static void main(String[] args) { Model rawType = new Model(); for (Integer num : rawType.getNums()) { System.out.print(num); }}
Компилятор выдаст ошибку: error: incompatible types: Object cannot be converted to Integer.
Таким образом, совершенно очевидно, что использование "сырых" типов, то есть создание переменных параметризованных классов без указания конкретных типов -- очень плохая практика, чреватая крайне неожиданными побочными эффектами.

Почему синтаксис языка допускает использование "сырых" типов

Обобщения появились лишь в 5-й версии Java, вышедшей в 2004 году. До этого момента не было даже понятия "сырого" типа. Поэтому в целях обратной совместимости, синтаксис языка допускает использование такого стиля, но он крайне не рекомендуем.

Когда использование "сырых" типов нормально

  1. При использовании в программе литералов класса: List.class, указать List<Integer>.class просто не получится.
  2. При использовании оператора instanceof: obj instance of List, а не obj instanceof List<Integer>. Хотя вариант obj instanceof List<?> тоже возможен, но никаких преимуществ он не даст. В рантайме невозможно выявить чем параметризована переменная, из-за "стирания типов" (type erasure).