Как поменять класс с помощью generics java
Перейти к содержимому

Как поменять класс с помощью generics java

  • автор:

Generics, Inheritance, and Subtypes

As you already know, it is possible to assign an object of one type to an object of another type provided that the types are compatible. For example, you can assign an Integer to an Object, since Object is one of Integer‘s supertypes:

In object-oriented terminology, this is called an «is a» relationship. Since an Integer is a kind of Object, the assignment is allowed. But Integer is also a kind of Number, so the following code is valid as well:

The same is also true with generics. You can perform a generic type invocation, passing Number as its type argument, and any subsequent invocation of add will be allowed if the argument is compatible with Number:

Now consider the following method:

What type of argument does it accept? By looking at its signature, you can see that it accepts a single argument whose type is Box<Number>. But what does that mean? Are you allowed to pass in Box<Integer> or Box<Double>, as you might expect? The answer is «no», because Box<Integer> and Box<Double> are not subtypes of Box<Number>.

This is a common misunderstanding when it comes to programming with generics, but it is an important concept to learn.

Box<Integer> is not a subtype of Box<Number> even though Integer is a subtype of Number.

Note: Given two concrete types A and B (for example, Number and Integer), MyClass<A> has no relationship to MyClass<B>, regardless of whether or not A and B are related. The common parent of MyClass<A> and MyClass<B> is Object.

For information on how to create a subtype-like relationship between two generic classes when the type parameters are related, see Wildcards and Subtyping.

Generic Classes and Subtyping

You can subtype a generic class or interface by extending or implementing it. The relationship between the type parameters of one class or interface and the type parameters of another are determined by the extends and implements clauses.

Using the Collections classes as an example, ArrayList<E> implements List<E>, and List<E> extends Collection<E>. So ArrayList<String> is a subtype of List<String>, which is a subtype of Collection<String>. So long as you do not vary the type argument, the subtyping relationship is preserved between the types.

A sample Collections hierarchy

Now imagine we want to define our own list interface, PayloadList, that associates an optional value of generic type P with each element. Its declaration might look like:

The following parameterizations of PayloadList are subtypes of List<String>:

Пришел, увидел, обобщил: погружаемся в Java Generics

Java Generics — это одно из самых значительных изменений за всю историю языка Java. «Дженерики», доступные с Java 5, сделали использование Java Collection Framework проще, удобнее и безопаснее. Ошибки, связанные с некорректным использованием типов, теперь обнаруживаются на этапе компиляции. Да и сам язык Java стал еще безопаснее. Несмотря на кажущуюся простоту обобщенных типов, многие разработчики сталкиваются с трудностями при их использовании. В этом посте я расскажу об особенностях работы с Java Generics, чтобы этих трудностей у вас было поменьше. Пригодится, если вы не гуру в дженериках, и поможет избежать много трудностей при погружении в тему.

Работа с коллекциями

Предположим, банку нужно подсчитать сумму сбережений на счетах клиентов. До появления «дженериков» метод вычисления суммы выглядел так:

Мы итерировались, пробегались по списку аккаунтов и проверяли, действительно ли элемент из этого списка является экземпляром класса Account — то есть счетом пользователя. Выполняли приведение типа нашего объекта класса Account и метод getAmount , который возвращал сумму на этом счете. Дальше все это суммировали и возвращали итоговую сумму. Требовалось выполнить два действия:

Если не сделать проверку ( instanceof ) на принадлежность к классу Account , то на втором этапе возможен ClassCastException – то есть аварийное завершение программы. Поэтому такая проверка была обязательной.

С появлением Generics необходимость в проверке и приведении типа отпала:

Теперь метод принимает в качестве аргументов только список объектов класса Account . Это ограничение указано в самом методе, в его сигнатуре, программист просто не может передать никакой другой список — только список клиентских счетов.

Нам не нужно выполнять проверку типа элементов из этого списка: она подразумевается описанием типа у параметра метода (можно прочитать как список объектов класса Account ). И компилятор выдаст ошибку, если что-то пойдет не так — то есть если кто-то попробует передать в этот метод список объектов, отличных от класса Account .

Во второй строчке проверки необходимость тоже отпадала. Если потребуется, приведение типов ( casting ) будет сделано на этапе компиляции.

Принцип подстановки

Принцип подстановки Барбары Лисков – специфичное определение подтипа в объектно-ориентированном программировании. Идея Лисков о «подтипе» дает определение понятия замещения: если S является подтипом T , тогда объекты типа T в программе могут быть замещены объектами типа S без каких-либо изменений желательных свойств этой программы.

Тип Подтип
Number Integer
List<E> ArrayList<E>
Collection<E> List<E>
Iterable<E> Collection<E>

Примеры отношения тип/подтип

Вот пример использования принципа подстановки в Java:

Integer является подтипом Number , следовательно, переменной n типа Number можно присвоить значение, которое возвращает метод Integer.valueOf(42) .

Ковариантность, контравариантность и инвариантность

Сначала немного теории. Ковариантность — это сохранение иерархии наследования исходных типов в производных типах в том же порядке. Например, если Кошка — это подтип Животные, то Множество<Кошки> — это подтип Множество<Животные>. Следовательно, с учетом принципа подстановки можно выполнить такое присваивание:

Множество<Животные> = Множество<Кошки>

Контравариантность — это обращение иерархии исходных типов на противоположную в производных типах. Например, если Кошка — это подтип Животные , то Множество<Животные> — это подтип Множество<Кошки>. Следовательно, с учетом принципа подстановки можно выполнить такое присваивание:

Множество<Кошки> = Множество<Животные>

Инвариантность — отсутствие наследования между производными типами. Если Кошка — это подтип Животные, то Множество<Кошки> не является подтипом Множество<Животные> и Множество<Животные> не является подтипом Множество<Кошки>.

Массивы в Java ковариантны. Тип S[] является подтипом T[] , если S — подтип T . Пример присваивания:

Мы присвоили ссылку на массив строк переменной arr , тип которой – «массив объектов» . Если бы массивы не были ковариантными, нам бы это сделать не удалось. Java позволяет это сделать, программа скомпилируется и выполнится без ошибок.

Но если мы попытаемся изменить содержимое массива через переменную arr и запишем туда число 42, то получим ArrayStoreException на этапе выполнения программы, поскольку 42 является не строкой, а числом. В этом недостаток ковариантности массивов Java: мы не можем выполнить проверки на этапе компиляции, и что-то может сломаться уже в рантайме.

«Дженерики» инвариантны. Приведем пример:

Если взять список целых чисел, то он не будет являться ни подтипом типа Number , ни каким-либо другим подтипом. Он является только подтипом самого себя. То есть List <Integer> — это List<Integer> и ничего больше. Компилятор позаботится о том, чтобы переменная ints , объявленная как список объектов класса Integer, содержала только объекты класса Integer и ничего кроме них. На этапе компиляции производится проверка, и у нас в рантайме уже ничего не упадет.

Wildcards

Всегда ли Generics инварианты? Нет. Приведу примеры:

Это ковариантность. List<Integer> — подтип List<? extends Number>

Это контравариантность. List<Number> является подтипом List<? super Integer> .

Запись вида «? extends . » или «? super . » — называется wildcard или символом подстановки, с верхней границей ( extends ) или с нижней границей ( super ). List<? extends Number> может содержать объекты, класс которых является Number или наследуется от Number . List<? super Number> может содержать объекты, класс которых Number или у которых Number является наследником (супертип от Number ).

Запись вида T2 <= T1 означает, что набор типов описываемых T2 является подмножеством набора типов описываемых T1

т.е.
Number <=? extends Object
? extends Number <=? extends Object
и
? super Object <=? super Number

Пара задачек для проверки знаний:

1. Почему в примере ниже compile-time error? Какое значение можно добавить в список nums ?

2. Почему нельзя получить элемент из списка ниже?

Нельзя прочитать элемент из контейнера с wildcard ? super , кроме объекта класса Object

The Get and Put Principle или PECS (Producer Extends Consumer Super)

Особенность wildcard с верхней и нижней границей дает дополнительные фичи, связанные с безопасным использованием типов. Из одного типа переменных можно только читать, в другой — только вписывать (исключением является возможность записать null для extends и прочитать Object для super ). Чтобы было легче запомнить, когда какой wildcard использовать, существует принцип PECS — Producer Extends Consumer Super.

  • Если мы объявили wildcard с extends, то это producer. Он только «продюсирует», предоставляет элемент из контейнера, а сам ничего не принимает.
  • Если же мы объявили wildcard с super — то это consumer. Он только принимает, а предоставить ничего не может.

Метод осуществляет копирование элементов из исходного списка src в список dest . src — объявлен с wildcard ? extends и является продюсером, а dest — объявлен с wildcard ? super и является потребителем. Учитывая ковариантность и контравариантность wildcard, можно скопировать элементы из списка ints в список nums :

Если же мы по ошибке перепутаем параметры метода copy и попытаемся выполнить копирование из списка nums в список ints , то компилятор не позволит нам это сделать:

<?> и Raw типы

Ниже приведен wildcard с неограниченным символом подстановки. Мы просто ставим <?> , без ключевых слов super или extends :

На самом деле такой «неограниченный» wildcard все-таки ограничен, сверху. Collection<?> — это тоже символ подстановки, как и » ? extends Object «. Запись вида Collection<?> равносильна Collection<? extends Object> , а значит — коллекция может содержать объекты любого класса, так как все классы в Java наследуются от Object – поэтому подстановка называется неограниченной.

Если мы опустим указание типа, например, как здесь:

то, говорят, что ArrayList — это Raw тип параметризованного ArrayList<T>. Используя Raw типы, мы возвращаемся в эру до дженериков и сознательно отказываемся от всех фич, присущих параметризованным типам.

Если мы попытаемся вызвать параметризованный метода у Raw типа, то компилятор выдаст нам предупреждение «Unchecked call». Если мы попытаемся выполнить присваивание ссылки на параметризованный тип Raw типу, то компилятор выдаст предупреждение «Unchecked assignment». Игнорирование этих предупреждений, как мы увидим позже, может привести к ошибкам во время выполнения нашего приложения.

Wildcard Capture

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

Ошибка компиляции возникла, потому что в методе reverse в качестве аргумента принимается список с неограниченным символом подстановки <?> .
<?> означает то же что и <? extends Object> . Следовательно, согласно принципу PECS, list – это producer . А producer только продюсирует элементы. А мы в цикле for вызываем метод set() , т.е. пытаемся записать в list . И поэтому упираемся в защиту Java, что не позволяет установить какое-то значение по индексу.

Что делать? Нам поможет паттерн Wildcard Capture . Здесь мы создаем обобщенный метод rev . Он объявлен с переменной типа T . Этот метод принимает список типов T , и мы можем сделать сет.

Теперь у нас все скомпилируется. Здесь произошел захват символа подстановки (wildcard capture). При вызове метода reverse(List<?> list) в качестве аргумента передается список каких-то объектов (например, строк или целых чисел). Если мы можем захватить тип этих объектов и присвоить его переменной типа X , то можем заключить, что T является X .

Более подробно о Wildcard Capture можно прочитать здесь и здесь.

Вывод

Если необходимо читать из контейнера, то используйте wildcard с верхней границей » ? extends «. Если необходимо писать в контейнер, то используйте wildcard с нижней границей » ? super «. Не используйте wildcard, если нужно производить и запись, и чтение.

Не используйте Raw типы! Если аргумент типа не определен, то используйте wildcard <?> .

Переменные типа

Когда мы записываем при объявлении класса или метода идентификатор в угловых скобках, например <T> или <E> , то создаем переменную типа. Переменная типа — это неквалифицированный идентификатор, который можно использовать в качестве типа в теле класса или метода. Переменная типа может быть ограничена сверху.

В этом примере выражение T extends Comparable<T> определяет T (переменную типа), ограниченную сверху типом Comparable<T> . В отличие от wildcard, переменные типа могут быть ограничены только сверху (только extends ). Нельзя записать super . Кроме того, в этом примере T зависит от самого себя, это называется recursive bound — рекурсивная граница.

Вот еще пример из класса Enum:

Здесь класс Enum параметризован типом E, который является подтипом от Enum<E> .

Multiple bounds (множественные ограничения)

Multiple Bounds – множественные ограничения. Записывается через символ » & «, то есть мы говорим, что тип, представленный переменной типа T , должен быть ограничен сверху классом Object и интерфейсом Comparable .

Запись Object & Comparable<? super T&gt образует тип пересечения Multiple Bounds . Первое ограничение — в данном случае Object — используется для erasure , процесса затирания типов. Его выполняет компилятор на этапе компиляции.

Вывод

Переменная типа может быть ограничена только сверху одним или несколькими типами. В случае множественного ограничения левая граница (первое ограничение) используется в процессе затирания (Type Erasure).

Type Erasure

Type Erasure представляет собой отображение типов (возможно, включая параметризованные типы и переменные типа) на типы, которые никогда не являются параметризованными типами или переменными типами. Мы записываем затирание типа T как |T| .

  • Затиранием параметризованного типа G<T1. Tn> является |G|
  • Затиранием вложенного типа T.C является |T|.C
  • Затиранием типа массива T[] является |T|[]
  • Затиранием переменной типа является затирание ее левой границы
  • Затиранием любого иного типа является сам этот тип
  • добавляет приведение типов для обеспечения type safety, если это необходимо
  • генерирует Bridge методы для сохранения полиморфизма
T (Тип) |T| (Затирание типа)
List< Integer>, List< String>, List< List< String>> List
List< Integer>[] List[]
List List
int int
Integer Integer
<T extends Comparable<T>> Comparable
<T extends Object & Comparable<? super T>> Object
LinkedCollection<E>.Node LinkedCollection.Node

Эта таблица показывает, во что превращаются разные типы в процессе затирания, Type Erasure.

На скриншоте ниже два примера программы:

Разница между ними в том, что слева происходит compile-time error, а справа все компилируется без ошибок. Почему?

В Java два разных метода не могут иметь одну и ту же сигнатуру. В процессе Type Erasure компилятор добавит bridge-метод public int compareTo(Object o) . Но в классе уже содержится метод с такой сигнатурой, что и вызовет ошибку во время компиляции.

Скомпилируем класс Name, удалив метод compareTo(Object o) , и посмотрим на получившийся байткод с помощью javap:

Видим, что класс содержит метод int compareTo(java.lang.Object) , хотя мы его удалили из исходного кода. Это и есть bridge метод, который добавил компилятор.

Reifiable типы

  • Примитивные типы (int, long, boolean)
  • Непараметризованные (необобщенные) типы (String, Integer)
  • Параметризованные типы, параметры которых представлены в виде unbounded wildcard (неограниченных символов подстановки) (List<?>, Collection<?>)
  • Raw (несформированные) типы (List, ArrayList)
  • Массивы, компоненты которых — Reifiable типы (int[], Number[], List<?>[], List[)

Почему информация об одних типах доступна, а о других нет? Дело в том, что из-за процесса затирания типов компилятором информация о некоторых типах может быть потеряна. Если она потерялась, то такой тип будет уже не reifiable. То есть она во время выполнения недоступна. Если доступна – соответственно, reifiable.

Решение не делать все обобщенные типы доступными во время выполнения — это одно из наиболее важных и противоречивых проектных решений в системе типов Java. Так сделали, в первую очередь, для совместимости с существующим кодом. За миграционную совместимость пришлось платить — полная доступность системы обобщенных типов во время выполнения невозможна.

  • Переменная типа (T)
  • Параметризованный тип с указанным типом параметра (List<Number>ArrayList<String>, List<List<String>>)
  • Параметризованный тип с указанной верхней или нижней границей (List<? extends Number>, Comparable<? super String>). Но здесь стоит оговориться: List<? extends Object>не reifiable, а List<?> — reifiable

И еще одна задачка. Почему в примере ниже нельзя создать параметризованный Exception?

Каждое catch выражение в try-catch проверяет тип полученного исключения во время выполнения программы (что равносильно instanceof), соответственно, тип должен быть Reifiable. Поэтому Throwable и его подтипы не могут быть параметризованы.

Unchecked Warnings

Компиляция нашего приложения может выдать так называемый Unchecked Warning — предупреждение о том, что компилятор не смог корректно определить уровень безопасности использования наших типов. Это не ошибка, а предупреждение, так что его можно пропустить. Но желательно все-так исправить, чтобы избежать проблем в будущем.

Heap Pollution

Как мы упомянули ранее, присваивание ссылки на Raw тип переменной параметризованного типа, приводит к предупреждению «Unchecked assignment». Если мы проигнорируем его, то возможна ситуация под названием » Heap Pollution » (загрязнение кучи). Вот пример:

В строке (1) компилятор предупреждает об «Unchecked assignment».

Нужно привести и другой пример «загрязнения кучи» — когда у нас используются параметризованные объекты. Кусок кода ниже наглядно показывает, что недопустимо использовать параметризованные типы в качестве аргументов метода с использованием Varargs . В данном случае параметр метода m – это List<String>… , т.е. фактически, массив элементов типа List<String> . Учитывая правило отображения типов при затирании, тип stringLists превращается в массив raw списков ( List[] ), т.е. можно выполнить присваивание Object[] array = stringLists; и после записать в array объект, отличный от списка строк (1), что вызовет ClassCastException в строке (2).

Рассмотрим еще один пример:

Java разрешает выполнить присваивание в строке (1). Это необходимо для обеспечения обратной совместимости. Но если мы попытаемся выполнить метод add в строке (2), то получим предупреждение Unchecked call — компилятор предупреждает нас о возможной ошибке. В самом деле, мы же пытаемся в список строк добавить целое число.

Reflection

Хотя при компиляции параметризованные типы подвергаются процедуре стирания (type erasure), кое-какую информацию мы можем получить с помощью Reflection.

  • Все reifiable доступны через механизм Reflection
  • Информация о типе полей класса, параметров методов и возвращаемых ими значений доступна через Reflection.

С появлением Generics класс java.lang.Class стал параметризованным. Рассмотрим вот этот код:

Переменная ints имеет тип List<Integer> и она содержит ссылку на объект типа ArrayList< Integer> . Тогда ints.getClass() вернёт объект типа Class<ArrayLis> , так как List<Integer> затирается в List . Объект типа Class<ArrayList> можно присвоить переменной k типа Class<? extends List> , согласно ковариантности символов подстановки? extends . А ArrayList.class возвращает объект типа Class<ArrayList> .

Вывод

Если информация о типе доступна во время выполнения программы, то такой тип называется Reifiable. К Reifiable типам относятся: примитивные типы, непараметризованные типы, параметризованные типы с неограниченным символом подстановки, Raw типы и массивы, элементы которых являются reifiable.

Игнорирование Unchecked Warnings может привести к «загрязнению кучи» и ошибкам во время выполнения программы.

Reflection не позволяет получить информацию о типе объекта, если он не Reifiable. Но Reflection позволяет получить информацию о типе возвращаемого методом значения, о типе аргументов метода и о типе полей класса.

Type Inference

Термин можно перевести как «Вывод типа». Это возможность компилятора определять (выводить) тип из контекста. Вот пример кода:

С появлением даймонд-оператора в Java 7 мы можем не указывать тип у ArrayList :

Компилятор выведет тип ArrayList из контекста – List<Integer> . Этот процесс и называется type inference .

  • Приведение (reduction)
  • Объединение (incorporation)
  • Разрешение (resolution)

Предположим у нас есть вот такой класс, который описывает связный список:

Результат обобщенного метода List.nil() может быть выведен из правой части:

Механизм выбора типа компилятором показывает, что аргумент типа для вызова List.nil() действительно String — это работает в JDK 7, все хорошо.

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

В JDK 7 мы получили бы compile-time error. А в JDK 8 скомпилируется. Это и есть первая часть JEP-101, его первая цель — вывод типа в позиции аргумента. Единственная возможность осуществить это в версиях до JDK 8 — использовать явный аргумент типа при вызове обобщенного метода:

Вторая часть JEP-101 говорит о том, что неплохо бы выводить тип в цепочке вызовов обобщенных методов, например:

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

После выхода JEP 101 на StackOverflow появилось множество вопросов по теме. Программисты спрашивают, почему код, который выполнялся на 7-й версии, на 8-й выполняется иначе – или вообще не компилируется? Вот пример такого кода:

Посмотрим на байт-код после компиляции на JDK1.8:

Инструкция под номером 0 выполняет вызов метода g:()Ljava/lang/Object; Метод возвращает java.lang.Object . Далее, инструкция 3 производит приведение типа («кастинг») объекта, полученного на предыдущем шаге к типу массива java.lang.String , и инструкция 6 выполняет метод m:([Ljava/lang/String;) , что и напечатает в консоли «two».

А теперь байт-код после компиляции на JDK1.7 – то есть на Java 7:

Мы видим, что здесь нет инструкции checkcast , которую добавила Java 8, так что вызовется метод m:(Ljava/lang/Object;) , а в консоли напечатается «one». Checkcast – результат нового выведения типа, который был усовершенствован в Java 8.

Чтобы избежать таких проблем, Oracle выпустил руководство по переходу с JDK1.7 на JDK 1.8 в котором описаны проблемы, которые могут возникнуть при переходе на новую версию Java, и то, как эти проблемы можно решить.

Например если вы хотите, чтобы в коде выше после компиляции на Java 8 все работало так же, как и на Java 7, сделайте приведение типа вручную:

Заключение

На этом мой рассказ о Java Generics подходит к концу. Вот другие источники, которые помогут вам в освоении темы:

Java Language
Дженерики

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

Синтаксис

  • class ArrayList <E> <> // общий класс с параметром типа E
  • класс HashMap <K, V> <> // общий класс с двумя параметрами типа K и V
  • <E> void print (элемент E) <> // общий метод с параметром типа E
  • ArrayList <String> имена; // объявление общего класса
  • ArrayList <?> Объекты; // объявление общего класса с неизвестным параметром типа
  • new ArrayList <String> () // создание экземпляра универсального класса
  • new ArrayList <> () // экземпляр с типом вывода "diamond" (Java 7 или новее)

замечания

Генерики реализуются в Java через стирание типа, что означает, что во время выполнения информация типа, указанная в создании экземпляра универсального класса, недоступна. Например, оператор List<String> names = new ArrayList<>(); создает объект списка, из которого тип элемента String не может быть восстановлен во время выполнения. Однако, если список хранится в поле типа List<String> или передается параметру method / constructor этого же типа или возвращается из метода этого типа возврата, то полная информация о типе может быть восстановлена ​​во время выполнения через API Java Reflection.

Это также означает, что при выдаче родовому типу (например: (List<String>) list ), литой является непроверенный бросок . Поскольку параметр <String> стирается, JVM не может проверить правильность приведения из List<?> List<String> ; JVM видит только показ List для List во время выполнения.

Создание общего класса

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

В этом примере используется общий класс Param для принятия одного параметра типа T , ограниченного угловыми скобками ( <> ):

Чтобы создать экземпляр этого класса, укажите аргумент типа вместо T Например, Integer :

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

В Java SE 7 и более поздних версиях аргумент типа может быть заменен пустым набором аргументов типа ( <> ), называемым алмазом :

В отличие от других идентификаторов, параметры типа не имеют ограничений именования. Однако их имена обычно являются первой буквой их цели в верхнем регистре. (Это верно даже во всех официальных JavaDocs.)
Примеры включают T для «type» , E для «element» и K / V для «key» / «value» .

Расширение общего класса

AbstractParam — абстрактный класс, объявленный с параметром типа T При расширении этого класса этот параметр типа может быть заменен аргументом типа, записанным внутри <> , или параметр типа может оставаться неизменным. В первом и втором примерах ниже String и Integer заменяют параметр типа. В третьем примере параметр типа остается неизменным. В четвертом примере вообще не используются обобщения, поэтому он аналогичен классу Object . Компилятор будет предупреждать о том, что AbstractParam является сырым типом, но он скомпилирует класс ObjectParam . Пятый пример имеет 2 типа параметров (см. «Параметры нескольких типов» ниже), выбирая второй параметр как параметр типа, переданный суперклассу.

Ниже приведено использование:

Обратите внимание, что в классе Email метод T getValue() действует так, как будто он имеет подпись String getValue() , а метод void setValue(T) действует так, как если бы он был объявлен void setValue(String) .

Также возможно создать экземпляр с анонимным внутренним классом с пустыми фигурными фигурными скобками ( <> ):

Множественные параметры типа

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

Использование может быть выполнено следующим образом:

Объявление общего метода

Методы также могут иметь общие параметры типа.

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

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

Это необходимо в этом примере, потому что компилятор не может «смотреть вперед», чтобы увидеть, что Object нужен для T после вызова stream() и в противном случае он makeList бы String на makeList аргументов makeList . Обратите внимание, что язык Java не поддерживает исключение класса или объекта, на который вызывается метод ( this в приведенном выше примере), когда аргументы типа явно указаны.

Бриллиант

Java 7 представила Diamond 1, чтобы удалить некоторую котельную плиту вокруг создания экземпляра универсального класса. С Java 7+ вы можете написать:

Там, где вам приходилось писать в предыдущих версиях, это:

Одно ограничение — для анонимных классов , где вы все равно должны указывать параметр типа в экземпляре:

Хотя использование алмаза с анонимными внутренними классами не поддерживается в Java 7 и 8, оно будет включено как новая функция в Java 9 .

1 — Некоторые люди называют <> использование «алмазного оператора ». Это неверно. Алмаз не ведет себя как оператор и не описан или не указан нигде в JLS или (официальном) Java-учебном пособии как оператор. Действительно, <> не является даже явным символом Java. Скорее это < токен, за которым следует токен > , и это законный (хотя и плохой стиль), чтобы иметь пробелы или комментарии между ними. JLS и Tutorials последовательно ссылаются на <> как на «алмаз», и поэтому это правильный термин для него.

Требование нескольких верхних границ («расширяет A & B»)

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

Пример: мы хотим отсортировать список чисел, но Number не реализует Comparable .

В этом примере T должен расширять Number и реализовывать Comparable<T> который должен соответствовать всем «нормальным» реализациям встроенных чисел, таких как Integer или BigDecimal но не подходит для более экзотических, таких как Striped64 .

Поскольку множественное наследование не разрешено, вы можете использовать не более одного класса в качестве привязки и должны быть первыми перечислены. Например, <T extends Comparable<T> & Number> не допускается, потому что Comparable является интерфейсом, а не классом.

Создание ограниченного общего класса

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

Без ограниченных дженериков мы не можем создать класс-контейнер, который является общим и знает, что каждый элемент является животным:

С общим ограничением в определении класса это теперь возможно.

Это также ограничивает действительные экземпляры родового типа:

Решаем между `T`,`? супер T` и `? расширяет T`

Синтаксис Java генерирует ограниченные подстановочные знаки, представляя неизвестный тип ? является:

? extends T представляет собой верхний ограниченный шаблон. Неизвестный тип представляет тип, который должен быть подтипом T или сам тип T.

? super T представляет собой нижний ограниченный шаблон. Неизвестный тип представляет тип, который должен быть супертипом T или самим типом T.

Как правило, вы должны использовать

  • ? extends T если вам нужен только «чтение» доступа («ввод»)
  • ? super T если вам нужно «написать» доступ («выход»)
  • T если вам нужны оба («изменить»)

Использование extends или super обычно лучше, потому что это делает ваш код более гибким (как в: разрешении использования подтипов и супертипов), как вы увидите ниже.

Теперь компилятор сможет обнаружить определенное плохое использование:

Выбрав правильный T ? super T или ? extends T необходимо для использования с подтипами. Затем компилятор может обеспечить безопасность типа; вам не нужно бросать (который не является безопасным по типу и может вызвать ошибки программирования), если вы используете их правильно.

Если это непросто понять, пожалуйста, помните правило PECS :

P roducer использует « E xtends», а C onsumer использует « S uper».

(У производителя есть только доступ на запись, и у Потребителя есть только доступ для чтения)

Преимущества универсального класса и интерфейса

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

Более строгие проверки во время компиляции

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

Устранение отливок

Следующий фрагмент кода без дженериков требует кастинга:

При повторном написании для использования дженериков код не требует кастинга:

Включение программистов для реализации общих алгоритмов

Используя generics, программисты могут реализовывать общие алгоритмы, которые работают с коллекциями разных типов, могут быть настроены и безопасны в типе и их легче читать.

Связывание общего параметра с более чем 1 типом

Общие параметры также могут быть привязаны к нескольким типам с использованием синтаксиса T extends Type1 & Type2 & .

Предположим, вы хотите создать класс, универсальный тип которого должен реализовывать как Flushable и Closeable , вы можете написать

Теперь ExampleClass принимает только общие параметры, типы, которые реализуют как Flushable и Closeable .

Методы класса могут выбирать вывод общих аргументов типа как Closeable и Flushable .

Замечания:

Вы не можете привязать общий параметр к любому типу, используя предложение OR ( | ). Поддерживается только предложение AND ( & ). Общий тип может расширять только один класс и многие интерфейсы. Класс должен быть помещен в начало списка.

Создание экземпляра родового типа

Из-за стирания типа следующее не будет работать:

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

обходные

Передача класса T при вызове genericMethod :

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

Передача ссылки на конструктор T :

Обращаясь к объявленному типовому типу в пределах его собственного объявления

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

Предположим, что у нас есть DataSeries<T> (здесь), который определяет общий ряд данных, содержащий значения типа T Очень сложно работать с этим типом непосредственно, когда мы хотим выполнить много операций, например, с двойными значениями, поэтому мы определяем DoubleSeries extends DataSeries<Double> . Предположим, что исходный DataSeries<T> имеет метод add(values) который добавляет другую серию одинаковой длины и возвращает новую. Как мы применяем тип values и тип возврата как DoubleSeries а не DataSeries<Double> в нашем производном классе?

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

Здесь T представляет собой тип данных, который имеет ряд, например Double и DS . Унаследовал тип (или типы) теперь могут быть легко реализованы путем замены упомянутого выше параметра, соответствующего производного типа, таким образом, получая конкретное Double основанное определение формы:

В настоящий момент даже IDE будет реализовывать вышеупомянутый интерфейс с правильными типами на месте, что после того, как бит наполнения содержимого может выглядеть так:

Как вы можете видеть, метод add объявлен как DoubleSeries add(DoubleSeries values) и компилятор счастлив.

При необходимости шаблон может быть дополнительно вложен.

Использование экземпляра с Generics

Использование дженериков для определения типа в instanceof

Рассмотрим следующий общий класс Example объявленный с формальным параметром <T> :

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

Следующий класс представляет, что два разных класса Example , Example<String> и Example<Number> , выглядят так, что после того, как дженерики разделились стиранием типа :

Поскольку типы исчезли, JVM не знает, какой тип T

Исключение из предыдущего правила

Вы всегда можете использовать неограниченный шаблон (?) Для указания типа в instanceof следующим образом:

Это может быть полезно для оценки того, является ли экземпляр obj List или нет:

На самом деле неограниченный подстановочный знак считается воспроизводимым типом.

Использование общего экземпляра с instanceof

Другая сторона монеты заключается в том, что использование экземпляра t из T с instanceof является законным, как показано в следующем примере:

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

Так как даже если стирание типа происходит в любом случае, теперь JVM может различать разные типы в памяти, даже если они используют один и тот же ссылочный тип ( Object ), как показано в следующем фрагменте:

Различные способы реализации универсального интерфейса (или расширения общего класса)

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

Ниже перечислены возможные способы его реализации.

Внеродная реализация класса с определенным типом

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

Этот класс относится только к String , и это означает, что использование MyGenericInterface с различными параметрами (например, Integer , Object и т. Д.) Не будет компилироваться, как показано в следующем фрагменте:

Общая реализация класса

Объявите другой общий интерфейс с формальным параметром типа <T> который реализует MyGenericInterface следующим образом:

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

Внедрение класса Raw

Объявите не-общий класс, который реализует MyGenericInteface как необработанный тип (вообще не используя общий):

Этот способ не рекомендуется, так как он не на 100% безопасен во время выполнения, потому что он смешивает сырой тип (подкласса) с дженериками (интерфейса), и это также запутывает. Современные компиляторы Java поднимут предупреждение с такой реализацией, однако код — по соображениям совместимости с более старым JVM (1.4 или более ранним) — скомпилируется.

Все перечисленные выше способы также разрешены при использовании универсального класса в качестве супертипа вместо общего интерфейса.

Использование Generics для автоматического создания

С помощью дженериков можно вернуть все, что ожидает абонент:

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

Это менее безопасно, если вы используете общие типы:

Здесь приведение будет выполняться, когда возвращаемый тип представляет собой какой-либо List (т. ClassCastException Возвращаемый List<String> не приведет к ClassCastException , в конечном итоге вы получите его, когда ClassCastException элементы из списка).

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

наряду с этим методом put() :

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

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

Многие несвязанные общие параметры, такие как те, которые используются в статическом методе, не могут быть восстановлены во время выполнения (см. Другие темы на Erasure ). Однако существует общая стратегия, используемая для доступа к типу, удовлетворяющему параметру generic для класса во время выполнения. Это позволяет использовать общий код, который зависит от доступа к типу, без необходимости передавать информацию типа типа через каждый вызов.

Фон

Общая параметризация в классе может быть проверена путем создания анонимного внутреннего класса. Этот класс будет захватывать информацию о типе. В общем, этот механизм упоминается как токены супертекста , которые подробно описаны в блоге Neal Gafter .

Сменить тип объектов в generic списке

В условии я определил с каким именно типом я работаю, а вот как теперь в list сменить T на необходимый мне тип? То есть чтобы у меня был например list с элементами типа Category. Нужно мне это для того чтобы обращаться к полям объекта Category.

Egor Podoliak's user avatar

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

Но вообще, по-моему мнению, исходная задача сортировка решается другим способом.

В дополнении к данному ответу, опишу способ, который я имел ввиду выше.

Этот способ заключается в реализации интерфейса Comparable объектами, наборы которых будут сортироваться.

Предположим, есть некий объект – животное, которое имеет параметр – вес. Необходимо отсортировать список животных по возрастанию их веса. Для выполнения этой задачи создадим класс Animal , реализующий интерфейс Comparable :

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *