Genrics* были добавлены в Java 5 и сейчас являются неотъемлемой частью Java Core. Если вы знакомы с Java Collections версии 5 или выше, то я уверен, что вы использовали generics в своих программах.
*Дженерики/Родовые типы — далее в статье я буду использовать название Generics
или дженерики как наиболее используемые, хотя правильный перевод все-таки Родовые типы
Использовать дженерики с классами коллекций не только легко, но и дает гораздо больше возможностей, чем просто задание типа коллекции. Чтобы процесс изучения дженериков был более простым и понятным, мы преимущественно будем использовать код и комментарии к нему.
Кратко о Generics
Начиная с Java 5 весь Collection Framework был переписан с нуля уже с использованием Generics. Давайте посмотрим как дженерики помогают безопасно использовать классы коллекций.
Пример без дженериков
1 2 3 4 5 6 7 |
List list = new ArrayList(); list.add("abc"); list.add(new Integer(5)); //OK for(Object obj : list){ String str = (String) obj; //здесь приведение типов бросит ClassCastException } |
Код выше компилируется нормально, но бросает ClassCastException
, потому что мы пытаемся привести объект в списке к типу String
в то время как один из элементов списка является Integer
. Начиная с Java 5 код выше будет переписан так как показано ниже:
Пример с дженериками:
1 2 3 4 5 6 7 |
List<String> list1 = new ArrayList<>(); list1.add("abc"); list1.add(new Integer(5)); //здесь будет ошибка во время компиляции программы for(String str : list1){ //здесь не нужно использовать приведение типов, следовательно не следует беспокоиться о ClassCastException } |
Обратите внимание, что на момент создания списка, мы указали, что типом элементов в списке будет строка (String
). Так что, если мы пытаемся добавить любой другой тип объекта в список, программа выдаст ошибку компиляции. Также обратите внимание, что во время обхода списка в цикле for
мы не используем приведение типов, а это значит, что нам не нужно беспокоиться о ClassCastException
.
Использование Generics в интерфейсах и классах
Мы можем определить свои классы и интерфейсы с дженериками. Для этого нужно использовать угловые скобки <>
, чтобы указать тип параметра класса или интерфейса.
Чтобы понять это, давайте создадим простой класс.
Без generics:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
package ua.com.prologistic; public class OldSchool { private Object t; public Object get() { return t; } public void set(Object t) { this.t = t; } public static void main(String args[]){ OldSchool type = new OldSchool (); type.set("Str"); String str = (String) type.get(); //приведение типов может стать причиной ClassCastException } } |
Обратите внимание, что при использовании этого класса, мы должны приводить типы объектов, что может привести к ClassCastException
. Теперь мы будем использовать дженерики — перепишем тот же класс но уже с generics.
С generics:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package ua.com.prologistic; public class GenericsType<T> { private T t; public T get(){ return this.t; } public void set(T t1){ this.t=t1; } public static void main(String args[]){ GenericsType<String> type = new GenericsType<>(); type.set("Str"); //здесь все нормально GenericsType type1 = new GenericsType(); type1.set("Str"); //и здесь все нормально type1.set(10); //здесь также все отлично, так как работает автоупаковка } } |
Обратите внимание на main()
метод класса GenericsType. Нам не нужно беспокоиться о приведении типов, поэтому нам не страшен ClassCastException
. Если мы не пишем тип в момент создания экземпляра класса, то компилятор предупредит вас следующим сообщением "GenericsType is a raw type. References to generic type GenericsType<T> should be parameterized"
. Давайте расшифруем это сообщение. Когда мы не пишем тип в момент создания экземпляра класса, то тип автоматически становится Object и, следовательно, это позволяет использовать как объекты String
, так и другие объекты Integer
. Это аннулирует преимущества дженериков в Java и заставит использовать приведение типов, то есть это будет класс OldSchool (код смотри выше).
(Хотя мы можем использовать аннотацию @SuppressWarnings("rawtypes")
, чтобы подавить предупреждение компилятора, но это уже тема другой статьи)
Также следует отметить, что мы использовали автоупаковку в Java (Java Autoboxing).
Generics в интерфейсах
Интерфейс Comparable
является отличным примером использования Generics в интерфейсах.
1 2 3 |
public interface Comparable<T> { public int compareTo(T o); } |
Мы можем брать этот код для примера и использовать дженерики в наших интерфейсах и классах. Мы также можем использовать несколько параметров, например, интерфейс Map
. Вот пример: new HashMap<String, List<String>>()
— круто, правда? 🙂
Generics: Соглашение об именовании
Соглашение об именовании помогает нам понимать код и унифицирует его, поэтому ему нужно следовать. В дженериках есть правила именования. Обычно это прописные буквы, что легко отличает их от Java переменных. Наиболее часто используются следующие имена параметров типа:
- E — элемент (широко используется в Java Collections Framework, например,
ArrayList
,Set
и т.д.) - К — ключ (используется в
Map
) - Т — Тип
- В — значение (используется в
Map
) - S, U, V и т.д. — 2й, 3й, 4й… тип
Дженерики в методах и конструкторах
Иногда нам не нужно, чтобы весь класс параметризировался. В этом случае мы можем использовать тип дженериков только в методах.
Давайте рассмотрим это сразу на примере класса с дженериками в методах.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package ua.com.prologistic; public class GenericsMethods { //Дженерики в методах public static <T> boolean isEqual(GenericsType<T> g1, GenericsType<T> g2){ return g1.get().equals(g2.get()); } public static void main(String args[]){ GenericsType<String> g1 = new GenericsType<>(); g1.set("Prologistic"); GenericsType<String> g2 = new GenericsType<>(); g2.set("Str"); //вот один пример использования boolean isEqual = GenericsMethods.<String>isEqual(g1, g2); // а вот второй - упрощенный isEqual = GenericsMethods.isEqual(g1, g2); } } |
Дженерики и наследование
Мы знаем, что наследование Java позволяет назначить переменную A другой переменной В, если А является подклассом В. Таким образом, мы могли бы предположить, что любой тип дженерика А может быть назначен дженерику типа В, но это не так. Давайте посмотрим пример простой программы.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package ua.com.prologistic; public class GenericsInheritance { public static void main(String[] args) { String str = "abc"; Object obj = new Object(); obj = str; // в данном случае все будет работать, так как String является наследником Object MyClass<String> myClass1 = new MyClass<String>(); MyClass<Object> myClass2 = new MyClass<Object>(); myClass2 = myClass1; // не скомпилируется, так как MyClass<String> не является MyClass<Object> obj = myClass1; // MyClass<T> наследник Object } public static class MyClass<T>{} } |
Следите за обновлениями на Javadevblog.com