Что такое сырой тип и почему мы не должны его использовать?

Вопросы:

  • Что такое типы raw в Java, и почему я часто слышу, что они не должны использоваться в новом коде?
  • Какая альтернатива, если мы не сможем использовать сырые типы и как это лучше?

Ответ 1

Что такое необработанный тип?

Спецификация языка Java определяет необработанный тип следующим образом:

JLS 4.8 Необработанные типы

Необработанный тип определяется как один из:

  • Тип ссылки, который формируется путем принятия имени объявления типового типа без списка аргументов сопутствующего типа.

  • Тип массива, тип элемента которого является необработанным.

  • Тип члена static не static необработанного типа R, который не наследуется от суперкласса или суперинтерфейса R.

Вот пример, иллюстрирующий:

public class MyType<E> {
    class Inner { }
    static class Nested { }

    public static void main(String[] args) {
        MyType mt;          // warning: MyType is a raw type
        MyType.Inner inn;   // warning: MyType.Inner is a raw type

        MyType.Nested nest; // no warning: not parameterized type
        MyType<Object> mt1; // no warning: type parameter given
        MyType<?> mt2;      // no warning: type parameter given (wildcard OK!)
    }
}

Здесь MyType<E> является параметризованным типом (JLS 4.5). Общепринято, чтобы разговорно ссылаться на этот тип как на просто MyType для краткости, но технически имя MyType<E>.

mt имеет исходный тип (и генерирует предупреждение о компиляции) первой точкой маркера в приведенном выше определении; inn также имеет необработанный тип третьей точкой маркера.

MyType.Nested не является параметризованным типом, хотя это тип члена параметризованного типа MyType<E>, потому что он static.

mt1, а mt2 объявляются с фактическими параметрами типа, поэтому они не являются сырыми типами.


Что такого особенного в сырых типах?

По сути, сырые типы ведут себя так же, как и до появления дженериков. То есть, во время компиляции полностью законно.

List names = new ArrayList(); // warning: raw type!
names.add("John");
names.add("Mary");
names.add(Boolean.FALSE); // not a compilation error!

Приведенный выше код работает очень хорошо, но предположим, что у вас также есть следующее:

for (Object o : names) {
    String name = (String) o;
    System.out.println(name);
} // throws ClassCastException!
  //    java.lang.Boolean cannot be cast to java.lang.String

Теперь мы сталкиваемся с проблемами во время выполнения, потому что names содержит то, что не является instanceof String.

Предположительно, если вы хотите, чтобы names содержал только String, вы могли бы по-прежнему использовать необработанный тип и вручную проверять каждый add самостоятельно, а затем вручную отбрасывать на String каждый элемент из names. Еще лучше, хотя НЕ использовать необработанный тип и позволить компилятору выполнять всю работу за вас, используя возможности генераторов Java.

List<String> names = new ArrayList<String>();
names.add("John");
names.add("Mary");
names.add(Boolean.FALSE); // compilation error!

Конечно, если вы хотите names разрешить Boolean, тогда вы можете объявить его как List<Object> names, и приведенный выше код будет скомпилирован.

См. также


Как исходный тип отличается от использования <Object> как параметры типа?

Ниже приведена цитата из Effective Java 2nd Edition, Item 23: Не используйте raw-типы в новом коде:

Какая разница между исходным типом List и параметризованным типом List<Object>? Понятно, что первый отказался от проверки общего типа, в то время как последний явно сказал компилятору, что он способен хранить объекты любого типа. Пока вы можете передать List<String> параметру типа List, вы не можете передать его параметру типа List<Object>. Существуют правила подтипирования для генериков, а List<String> - подтип исходного типа List, но не параметризованного типа List<Object>. Как следствие, вы теряете безопасность типов, если используете тип raw, например List, но не для использования параметризованного типа типа List<Object>.

Чтобы проиллюстрировать эту точку, рассмотрим следующий метод, который принимает List<Object> и добавляет a new Object().

void appendNewObject(List<Object> list) {
   list.add(new Object());
}

Дженерики в Java инвариантны. A List<String> не является List<Object>, поэтому следующее генерирует предупреждение компилятора:

List<String> names = new ArrayList<String>();
appendNewObject(names); // compilation error!

Если вы указали appendNewObject на использование параметра raw type List as, то это скомпилируется, и поэтому вы потеряете безопасность типа, которую вы получаете от дженериков.

См. также


Как исходный тип отличается от использования <?> как параметра типа?

List<Object>, List<String> и т.д. все List<?>, поэтому может возникнуть соблазн просто сказать, что они просто List. Однако существует большая разница: поскольку a List<E> определяет только add(E), вы не можете добавить только любой произвольный объект в List<?>. С другой стороны, поскольку исходный тип List не имеет безопасности типа, вы можете add сделать что-либо с List.

Рассмотрим следующий вариант предыдущего фрагмента:

static void appendNewObject(List<?> list) {
    list.add(new Object()); // compilation error!
}
//...

List<String> names = new ArrayList<String>();
appendNewObject(names); // this part is fine!

Компилятор отлично справился с защитой от потенциального нарушения инвариантности типа List<?>! Если вы объявили параметр как необработанный тип List list, тогда код будет компилироваться, и вы нарушите инвариант типа List<String> names.


Необработанный тип - это стирание этого типа

Вернуться к JLS 4.8:

В качестве типа стирания можно использовать тип параметризованного типа или стирание типа массива, тип элемента которого является параметризованным типом. Такой тип называется необработанным.

[...]

Суперклассы (соответственно суперинтерфейсы) необработанного типа - это стирания суперклассов (суперинтерфейсов) любой из параметризации родового типа.

Тип конструктора, метод экземпляра или не static необработанного типа C, который не унаследован от его суперклассов или суперинтерфейсов, является необработанным типом, который соответствует стиранию его типа в родовом объявление, соответствующее C.

В более простых выражениях, когда используется тип raw, конструкторы, методы экземпляра и не static также удаляются.

Возьмем следующий пример:

class MyType<E> {
    List<String> getNames() {
        return Arrays.asList("John", "Mary");
    }

    public static void main(String[] args) {
        MyType rawType = new MyType();
        // unchecked warning!
        // required: List<String> found: List
        List<String> names = rawType.getNames();
        // compilation error!
        // incompatible types: Object cannot be converted to String
        for (String str : rawType.getNames())
            System.out.print(str);
    }
}

Когда мы используем raw MyType, getNames также стирается, так что он возвращает raw List!

JLS 4.6 продолжает объяснять следующее:

Стирание стилей также сопоставляет подпись конструктора или метода сигнатуре, которая не имеет параметризованных типов или переменных типа. Стирание конструктора или сигнатуры метода s является сигнатурой, состоящей из то же имя, что и s, и стирания всех формальных типов параметров, указанных в s.

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

Стирание сигнатуры общего метода не имеет параметров типа.

В следующем отчете об ошибке содержатся некоторые мысли от Маурицио Чимадамора, разработчика-компилятора, и Алекс Бакли, одного из авторов JLS, о том, почему должно происходить такое поведение: https://bugs.openjdk.java.net/browse/JDK-6400189. (Короче говоря, это упрощает спецификацию.)


Если это небезопасно, почему разрешено использовать необработанный тип?

Здесь другая цитата из JLS 4.8:

Использование типов raw допускается только как уступка совместимости устаревшего кода. Использование необработанных типов в коде, написанном после введения родословности в язык программирования Java, настоятельно не рекомендуется. Возможно, что будущие версии языка программирования Java будут запрещать использование необработанных типов.

В эффективном Java 2nd Edition также есть следующее:

Учитывая, что вы не должны использовать необработанные типы, почему дизайнеры языка допускают их? Чтобы обеспечить совместимость.

Платформа Java вот-вот вступит в свое второе десятилетие, когда были представлены дженерики, и существовало огромное количество Java-кода, который не использовал дженерики. Было сочтено критически важным, что весь этот код остается законным и совместим с новым кодом, который использует дженерики. Должно быть законным передавать экземпляры параметризованных типов методам, которые были предназначены для использования с обычными типами, и наоборот. Это требование, известное как совместимость с миграцией, приняло решение поддерживать типы raw.

Таким образом, необработанные типы НИКОГДА не должны использоваться в новом коде. Вы всегда должны использовать параметризованные типы.


Нет ли исключений?

К сожалению, поскольку Java-дженерики не переопределены, есть два исключения, в которых необработанные типы должны использоваться в новом коде:

  • Литералы классов, например. List.class, а не List<String>.class Оператор
  • instanceof, например. o instanceof Set, не o instanceof Set<String>

См. также

Ответ 2

Что такое типы raw в Java и почему я часто слышу, что они не должны использоваться в новом коде?

Необработанные типы - это древняя история языка Java. В начале было Collections, и они держали Objects не более и не меньше. Каждая операция на Collections требовала от Object отбрасывания до требуемого типа.

List aList = new ArrayList();
String s = "Hello World!";
aList.add(s);
String c = (String)aList.get(0);

В то время как это работало большую часть времени, произошли ошибки

List aNumberList = new ArrayList();
String one = "1";//Number one
aNumberList.add(one);
Integer iOne = (Integer)aNumberList.get(0);//Insert ClassCastException here

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

List<String> aNumberList = new ArrayList<String>();
aNumberList.add("one");
Integer iOne = aNumberList.get(0);//Compile time error
String sOne = aNumberList.get(0);//works fine

Для сравнения:

// Old style collections now known as raw types
List aList = new ArrayList(); //Could contain anything
// New style collections with Generics
List<String> aList = new ArrayList<String>(); //Contains only Strings

Более сложный интерфейс Compareable:

//raw, not type save can compare with Other classes
class MyCompareAble implements CompareAble
{
   int id;
   public int compareTo(Object other)
   {return this.id - ((MyCompareAble)other).id;}
}
//Generic
class MyCompareAble implements CompareAble<MyCompareAble>
{
   int id;
   public int compareTo(MyCompareAble other)
   {return this.id - other.id;}
}

Обратите внимание, что невозможно реализовать интерфейс CompareAble с compareTo(MyCompareAble) с необработанными типами. Почему вы не должны их использовать:

  • Любые Object, хранящиеся в Collection, должны быть представлены перед использованием.
  • Использование обобщений позволяет проверять время компиляции
  • Использование исходных типов - это то же самое, что хранить каждое значение как Object

Что делает компилятор: Дженерики обратно совместимы, они используют одни и те же классы Java, как это делают сырые типы. Магия происходит в основном во время компиляции.

List<String> someStrings = new ArrayList<String>();
someStrings.add("one");
String one = someStrings.get(0);

Будет скомпилирован как:

List someStrings = new ArrayList();
someStrings.add("one"); 
String one = (String)someStrings.get(0);

Это тот же код, который вы могли бы написать, если бы вы использовали исходные типы напрямую. Думаю, я не уверен, что происходит с интерфейсом CompareAble, я предполагаю, что он создает две функции compareTo, один из которых принимает MyCompareAble, а другой принимает Object и передает его в первую очередь после его литья.

Каковы альтернативы сырым типам: Используйте generics

Ответ 3

Необработанный тип - это имя общего класса или интерфейса без каких-либо аргументов типа. Например, учитывая общий класс Box:

public class Box<T> {
    public void set(T t) { /* ... */ }
    // ...
}

Чтобы создать параметризованный тип Box<T>, вы указываете фактический аргумент типа для параметра формального типа T:

Box<Integer> intBox = new Box<>();

Если аргумент фактического типа опущен, вы создаете необработанный тип Box<T>:

Box rawBox = new Box();

Следовательно, Box является необработанным типом родового типа Box<T>. Однако не общий тип или тип интерфейса не является сырым типом.

Необработанные типы отображаются в устаревшем коде, поскольку многие классы API (например, классы Collections) не были типичными до JDK 5.0. При использовании сырых типов вы по существу получаете поведение до генериков - a Box дает вам Object s. Для обратной совместимости допускается присвоение параметризованного типа его необработанному типу:

Box<String> stringBox = new Box<>();
Box rawBox = stringBox;               // OK

Но если вы назначаете необработанный тип параметризованному типу, вы получите предупреждение:

Box rawBox = new Box();           // rawBox is a raw type of Box<T>
Box<Integer> intBox = rawBox;     // warning: unchecked conversion

Вы также получаете предупреждение, если используете тип raw для вызова общих методов, определенных в соответствующем родовом типе:

Box<String> stringBox = new Box<>();
Box rawBox = stringBox;
rawBox.set(8);  // warning: unchecked invocation to set(T)

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

В разделе "Тип стирания" содержится дополнительная информация о том, как компилятор Java использует необработанные типы.

Непроверенные сообщения об ошибках

Как уже упоминалось ранее, при смешивании устаревшего кода с общим кодом вы можете встретить предупреждающие сообщения, похожие на следующие:

Примечание. Example.java использует непроверенные или небезопасные операции.

Примечание. Перекомпиляция с -Xlint: непроверенная для деталей.

Это может произойти при использовании более старого API, который работает с необработанными типами, как показано в следующем примере:

public class WarningDemo {
    public static void main(String[] args){
        Box<Integer> bi;
        bi = createBox();
    }

    static Box createBox(){
        return new Box();
    }
}

Термин "unchecked" означает, что компилятор не имеет достаточной информации о типе для выполнения всех проверок типов, необходимых для обеспечения безопасности типа. Предупреждение "unchecked" по умолчанию отключено, хотя компилятор дает подсказку. Чтобы просмотреть все "непроверенные" предупреждения, перекомпилируйте с помощью -Xlint: unchecked.

Повторная компиляция предыдущего примера с -Xlint: unchecked показывает следующую дополнительную информацию:

WarningDemo.java:4: warning: [unchecked] unchecked conversion
found   : Box
required: Box<java.lang.Integer>
        bi = createBox();
                      ^
1 warning

Чтобы полностью отключить непроверенные предупреждения, используйте флаг -Xlint: -unchecked. Аннотация @SuppressWarnings("unchecked") подавляет непроверенные предупреждения. Если вы не знакомы с синтаксисом @SuppressWarnings, см. Аннотации.

Оригинальный источник: Учебники по Java

Ответ 4

 private static List<String> list = new ArrayList<String>();

Вы должны указать параметр типа.

Предупреждение сообщает, что типы, которые определены для поддержки generics, должны быть параметризованы, а не использовать их необработанную форму.

List определяется для поддержки дженериков: public class List<E>. Это позволяет выполнять много типов безопасных операций, которые проверяются во время компиляции.

Ответ 5

"Явный" тип в Java - это класс, который не является общим и имеет дело с "сырыми" объектами, а не с типом типа типичных параметров типа.

Например, до того, как были созданы дженерики Java, вы должны использовать класс коллекции следующим образом:

LinkedList list = new LinkedList();
list.add(new MyObject());
MyObject myObject = (MyObject)list.get(0);

Когда вы добавляете свой объект в список, ему все равно, какой тип объекта он есть, и когда вы его получите из списка, вы должны явно указать его на тот тип, который вы ожидаете.

Используя generics, вы удаляете "неизвестный" коэффициент, потому что вы должны явно указать, какой тип объектов может идти в списке:

LinkedList<MyObject> list = new LinkedList<MyObject>();
list.add(new MyObject());
MyObject myObject = list.get(0);

Обратите внимание, что с помощью дженериков вам не нужно бросать объект, исходящий из вызова get, сбор предварительно задан для работы с MyObject. Этот факт является основным движущим фактором для генериков. Он меняет источник ошибок времени выполнения во что-то, что можно проверить во время компиляции.

Ответ 6

Компилятор хочет, чтобы вы это записывали:

private static List<String> list = new ArrayList<String>();

потому что в противном случае вы могли бы добавить любой тип, который вам нравится, в list, делая экземпляр как new ArrayList<String>() бессмысленным. Генераторы Java - это функция времени компиляции, поэтому объект, созданный с помощью new ArrayList<String>(), с радостью примет элементы Integer или JFrame, если они назначены ссылке "raw type" list - сам объект ничего не знает о какие типы он должен содержать, только компилятор делает.

Ответ 7

Что такое необработанный тип и почему я часто слышу, что их нельзя использовать в новом коде?

"Необработанный тип" - это использование универсального класса без указания аргумента (ов) типа для его параметризованного типа (ов), например. используя List вместо List<String>. Когда дженерики были введены в Java, несколько классов были обновлены для использования дженериков. Использование этого класса в качестве "необработанного типа" (без указания аргумента типа) позволило сохранить прежний код.

"Необработанные типы" используются для обратной совместимости. Их использование в новом коде не рекомендуется, потому что использование универсального класса с аргументом типа позволяет более сильную типизацию, что, в свою очередь, может улучшить понятность кода и привести к появлению потенциальных проблем раньше.

Какая альтернатива, если мы не сможем использовать необработанные типы и как это лучше?

Предпочтительной альтернативой является использование общих классов по назначению - с подходящим аргументом типа (например, List<String>). Это позволяет программисту более конкретно указывать типы, придавая будущим сопровождающим больше смысла предполагаемое использование переменной или структуры данных и позволяет компилятору обеспечивать лучшую безопасность типов. Эти преимущества вместе могут улучшить качество кода и помочь предотвратить появление некоторых ошибок кодирования.

Например, для метода, в котором программист хочет, чтобы переменная List, называемая "имена", содержала только строки:

List<String> names = new ArrayList<String>();
names.add("John");          // OK
names.add(new Integer(1));  // compile error

Ответ 8

Здесь я рассматриваю несколько случаев, через которые вы можете очистить концепцию

1. ArrayList<String> arr = new ArrayList<String>();
2. ArrayList<String> arr = new ArrayList();
3. ArrayList arr = new ArrayList<String>();

Случай 1

ArrayList<String> arr это ссылочная переменная ArrayList с типом String, которая ссылается на ArralyList Object of Type String. Это означает, что он может содержать только объект типа String.

Это строгое значение String не Raw Type, поэтому оно никогда не будет вызывать предупреждение.

    arr.add("hello");// alone statement will compile successfully and no warning.

    arr.add(23);  //prone to compile time error.
     //error: no suitable method found for add(int)

Случай 2

В этом случае ArrayList<String> arr является строгим типом, но ваш объект new ArrayList(); является сырым типом.

    arr.add("hello"); //alone this compile but raise the warning.
    arr.add(23);  //again prone to compile time error.
    //error: no suitable method found for add(int)

здесь arr является строгим типом. Таким образом, он будет увеличивать время компиляции при добавлении integer.

Предупреждение: - A Raw Тип Объект ссылается на тип Strict Референтная переменная ArrayList.

Случай 3

В этом случае ArrayList arr является сырым типом, но ваш Object new ArrayList<String>(); является строгим типом.

    arr.add("hello");  
    arr.add(23);  //compiles fine but raise the warning.

Он добавит в него любой тип объекта, потому что arr - это тип Raw.

Предупреждение: - A Strict Объект Type ссылается на переменную Raw, на которую ссылается переменная.

Ответ 9

Необработанный тип - это отсутствие параметра типа при использовании родового типа.

Необработанный тип не должен использоваться, поскольку он может вызвать ошибки времени выполнения, например, вставить double в то, что должно было быть Set int s.

Set set = new HashSet();
set.add(3.45); //ok

При извлечении материала из Set вы не знаете, что выйдет. Предположим, что вы ожидаете, что это все int s, вы отбрасываете его на Integer; исключение во время выполнения, когда идет double 3.45.

С параметром типа, добавленным в ваш Set, вы сразу же получите ошибку компиляции. Эта превентивная ошибка позволяет устранить проблему, прежде чем что-то взорвется во время работы (таким образом, экономя время и усилия).

Set<Integer> set = new HashSet<Integer>();
set.add(3.45); //NOT ok.

Ответ 10

Вот еще один случай, когда необработанные типы будут кусать вас:

public class StrangeClass<T> {
  @SuppressWarnings("unchecked")
  public <X> X getSomethingElse() {
    return (X)"Testing something else!";
  }

  public static void main(String[] args) {
    final StrangeClass<String> withGeneric    = new StrangeClass<>();
    final StrangeClass         withoutGeneric = new StrangeClass();
    final String               value1,
                               value2;

    // Compiles
    value1 = withGeneric.getSomethingElse();

    // Produces compile error:
    // incompatible types: java.lang.Object cannot be converted to java.lang.String
    value2 = withoutGeneric.getSomethingElse();
  }
}

Как уже упоминалось в принятом ответе, вы теряете всю поддержку обобщений в коде необработанного типа. Каждый параметр типа преобразуется в его стирание (которое в приведенном выше примере является просто Object).

Ответ 11

Говорят, что ваш list является list объектов, не идентифицированных. Это значит, что Java не знает, какие объекты находятся внутри списка. Затем, когда вы хотите итерировать список, вы должны использовать каждый элемент, чтобы иметь доступ к свойствам этого элемента (в данном случае String).

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

private static List<String> list = new ArrayList<String>();

Ответ 12

учебная страница.

Необработанный тип - это имя общего класса или интерфейса без каких-либо аргументов типа. Например, учитывая общий класс Box:

public class Box<T> {
    public void set(T t) { /* ... */ }
    // ...
}

Чтобы создать параметризованный тип поля, вы указываете фактический аргумент типа для параметра формального типа T:

Box<Integer> intBox = new Box<>();

Если аргумент фактического типа опущен, вы создаете необработанный тип Box:

Box rawBox = new Box();

Ответ 13

Я нашел эту страницу после выполнения некоторых выборочных упражнений и с тем же самым загадкой.

============== Я перешел от этого кода, предоставив образец ================

public static void main(String[] args) throws IOException {

    Map wordMap = new HashMap();
    if (args.length > 0) {
        for (int i = 0; i < args.length; i++) {
            countWord(wordMap, args[i]);
        }
    } else {
        getWordFrequency(System.in, wordMap);
    }
    for (Iterator i = wordMap.entrySet().iterator(); i.hasNext();) {
        Map.Entry entry = (Map.Entry) i.next();
        System.out.println(entry.getKey() + " :\t" + entry.getValue());
    }

====================== Этот код ======================= ==

public static void main(String[] args) throws IOException {
    // replace with TreeMap to get them sorted by name
    Map<String, Integer> wordMap = new HashMap<String, Integer>();
    if (args.length > 0) {
        for (int i = 0; i < args.length; i++) {
            countWord(wordMap, args[i]);
        }
    } else {
        getWordFrequency(System.in, wordMap);
    }
    for (Iterator<Entry<String, Integer>> i = wordMap.entrySet().iterator(); i.hasNext();) {
        Entry<String, Integer> entry =   i.next();
        System.out.println(entry.getKey() + " :\t" + entry.getValue());
    }

}

=============================================== ================================

Это может быть безопаснее, но потребовалось 4 часа, чтобы одурачить философию...

Ответ 14

Сырые типы прекрасны, когда они выражают то, что вы хотите выразить.

Например, функция десериализации может вернуть List, но не знает тип элемента списка. Таким образом, List - это соответствующий тип возврата.

Ответ 15

Избегайте необработанных типов

Необработанные типы относятся к использованию универсального типа без указания параметра типа.

Например,

Список является необработанным типом, а List<String> является параметризованным типом.

Когда дженерики были представлены в JDK 1.5, необработанные типы были сохранены только для обеспечения обратной совместимости со старыми версиями Java. Хотя использование необработанных типов все еще возможно,

Их следует избегать:

  • Они обычно требуют приведения
  • Они не являются безопасными по типу, и некоторые важные виды ошибок будут появляться только во время выполнения
  • Они менее выразительны и не документируют себя так же, как параметризованные типы. Пример

    import java.util.*;
    
    public final class AvoidRawTypes {
    
    void withRawType() {
    
        //Raw List doesn't self-document, 
        //doesn't state explicitly what it can contain
    
        List stars = Arrays.asList("Arcturus", "Vega", "Altair");
    
        Iterator iter = stars.iterator();
    
        while (iter.hasNext()) {
    
            String star = (String) iter.next(); //cast needed
    
            log(star);
        }
    
    }
    
    void withParameterizedType() {
    
        List < String > stars = Arrays.asList("Spica", "Regulus", "Antares");
    
        for (String star: stars) {
    
            log(star);
        }
    
    }
    
    private void log(Object message) {
    
        System.out.println(Objects.toString(message));
    
    }
    
    }
    

Для справки: https://docs.oracle.com/javase/tutorial/java/generics/rawTypes.html