Вступ
Починаючи з JSE 5.0 до арсеналу мови Java були додані дженерики.Що таке дженерики в Java?
Дженерики (узагальнення) - це особливі засоби мови Java для реалізації узагальненого програмування: особливого підходу до опису даних та алгоритмів, що дозволяє працювати з різними типами даних без зміни їх опису. На сайті Oracle дженерикам присвячений окремий tutorial: " Lesson: Generics ".
import java.util.*;
public class HelloWorld{
public static void main(String []args){
List list = new ArrayList();
list.add("Hello");
String text = list.get(0) + ", world!";
System.out.print(text);
}
}
Цей код виконається добре. Але якщо до нас прийшли і сказали, що фраза "Hello, world!" побита і можна повернути лише Hello? Видалимо з коду конкатенацію з рядком ", world!"
. Здавалося б, що може бути невинніше? Але насправді ми отримаємо помилку ПРИ КОМПІЛЯЦІЇ : error: incompatible types: Object cannot be converted to String
Справа в тому, що в нашому випадку List зберігає список об'єктів типу Object. Так як String - спадкоємець для Object (бо всі класи неявно успадковуються Java від Object), то вимагає явного приведення, чого ми не зробабо. А при конкатенації об'єкта буде викликаний статичний метод String.valueOf(obj), який у результаті викличе метод toString для Object. Тобто List містить Object. Виходить, там, де нам потрібен конкретний тип, а не Object, нам доведеться самим робити приведення типів:
import java.util.*;
public class HelloWorld{
public static void main(String []args){
List list = new ArrayList();
list.add("Hello!");
list.add(123);
for (Object str : list) {
System.out.println((String)str);
}
}
}
Проте, у разі, т.к. List приймає список об'єктів, він зберігає як String, а й Integer. Але найгірше, у цьому випадку компілятор не побачить нічого поганого. І тут ми отримаємо помилку вже ПІД ЧАС ВИКОНАННЯ (ще кажуть, що помилка отримана "в Runtime"). Помилка буде: java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
Погодьтеся, не найприємніше. І все це тому, що компілятор - не штучний інтелект і він не може вгадати все, що передбачає програміст. Щоб розповісти компілятору докладніше про свої наміри, які типи ми збираємося використовувати, Java SE 5 ввели дженерики . Виправимо наш варіант, підказавши компілятору, що ми хочемо:
import java.util.*;
public class HelloWorld {
public static void main(String []args){
List<String> list = new ArrayList<>();
list.add("Hello!");
list.add(123);
for (Object str : list) {
System.out.println(str);
}
}
}
Як ми бачимо, нам більше не потрібне приведення до String. Крім того, у нас з'явабося кутові дужки (angle brackets), які обрамляють дженерики. Тепер компілятор не дасть скомпілювати клас, доки ми не видалимо додавання 123 до списку, т.к. це Integer. Він нам так і скаже. Багато хто називає дженерики "синтаксичним цукром". І вони мають рацію, тому що дженерики дійсно при компіляції стануть тими самими кастами. Подивимося на байткод скомпілованих класів: з кастом вручну та з використанням дженериків:
Raw Types або сирі типи
Говорячи про дженериків ми завжди маємо дві категорії: типізовані типи (Generic Types) та "сирі" типи (Raw Types). Сирі типи - це типи без вказівки "уточнення" у фігурних дужках (angle brackets):<>
Також Diamond синтаксис пов'язаний з поняттям " Type Inference ", або виведення типів. Адже компілятор, бачачи праворуч дивиться на ліву частину, де розташоване оголошення типу змінної, в яку присвоюється значення. І з цієї частини розуміє, яким типом типізується значення праворуч. Насправді, якщо в лівій частині вказано дженерик, а справа не вказано, компілятор зможе вивести тип:
import java.util.*;
public class HelloWorld{
public static void main(String []args) {
List<String> list = new ArrayList();
list.add("Hello World");
String data = list.get(0);
System.out.println(data);
}
}
Однак це буде змішування нового стилю з дженериками і старого стилю без них. І це вкрай небажано. При компіляції коду ми отримаємо повідомлення: Note: HelloWorld.java uses unchecked or unsafe operations
. Насправді здається незрозумілим, навіщо взагалі треба тут додавати. Але приклад:
import java.util.*;
public class HelloWorld{
public static void main(String []args) {
List<String> list = Arrays.asList("Hello", "World");
List<Integer> data = new ArrayList(list);
Integer intNumber = data.get(0);
System.out.println(data);
}
}
Як ми пам'ятаємо, ArrayList має і другий конструктор, який приймає на вхід колекцію. І ось тут і криється підступність. Без diamond синтаксису компілятор не розуміє, що його обманюють, а ось із diamond – розуміє. Тому, правило #1 : завжди використовувати синтексис алмаз, якщо ми використовуємо типізовані типи. А якщо ні, то ми ризикуємо пропустити, де у нас використовується raw type. Щоб уникнути попереджень у лозі про те, що "uses unchecked or unsafe operations" можна над методом або класом вказати особливу анотацію: @SuppressWarnings("unchecked")
Suppress перекладається як придушувати, тобто дослівно - придушити попередження. Але подумайте, чому ви вирішабо її вказати? Згадайте правило номер один і, можливо, вам потрібно додати типізацію.
Типізовані методи (Generic Methods)
Дженеріки дозволяють типизувати методи. Даній можливості в tutorial від Oracle присвячений окремий розділ: " Generic Methods ". З цього tutorial важливо запам'ятати про синтаксис:- включає список типизованих параметрів усередині кутових дужок;
- список типизованих параметрів йде до методу, що повертається.
import java.util.*;
public class HelloWorld{
public static class Util {
public static <T> T getValue(Object obj, Class<T> clazz) {
return (T) obj;
}
public static <T> T getValue(Object obj) {
return (T) obj;
}
}
public static void main(String []args) {
List list = Arrays.asList("Author", "Book");
for (Object element : list) {
String data = Util.getValue(element, String.class);
System.out.println(data);
System.out.println(Util.<String>getValue(element));
}
}
}
Якщо подивитися на клас Util, бачимо в ньому два типизовані методи. Завдяки можливості виведення типів ми можемо надати визначення типу безпосередньо компілятор, а можемо самі це вказати. Обидва варіанти представлені у прикладі. До речі, синтаксис дуже логічний, якщо подумати. При типізації методу ми вказуємо дженерик до методу, тому що якщо ми будемо використовувати дженерик після методу, Java не зможе зрозуміти, який тип використовувати. Тому спочатку оголошуємо, що використовуватимемо дженерик T, а потім уже говоримо, що цей дженерик ми збираємось повертати. Звичайно, Util.<Integer>getValue(element, String.class)
впаде з помилкою incompatible types: Class<String> cannot be converted to Class<Integer>
. При використанні типізованих методів варто завжди пам'ятати про стирання типів. Подивимося на приклад:
import java.util.*;
public class HelloWorld {
public static class Util {
public static <T> T getValue(Object obj) {
return (T) obj;
}
}
public static void main(String []args) {
List list = Arrays.asList(2, 3);
for (Object element : list) {
System.out.println(Util.<Integer>getValue(element) + 1);
}
}
}
Він чудово працюватиме. Але тільки доти, поки компілятор буде розуміти, що у методу, що викликається, тип Integer. Замінимо виведення на консоль на наступний рядок: System.out.println(Util.getValue(element) + 1);
І ми отримаємо помилку: bad operand types for binary operator '+', перший тип: Object, second type: int Тобто відбулося стирання типів. Компілятор бачить, що тип ніхто не вказав, тип вказується як Object та виконання коду падає з помилкою.
Типізовані класи (Generic Types)
Типізувати можна як методи, а й самі класи. У Oracle в їх гайді цьому присвячено розділ " Generic Types ". Розглянемо приклад:public static class SomeType<T> {
public <E> void test(Collection<E> collection) {
for (E element : collection) {
System.out.println(element);
}
}
public void test(List<Integer> collection) {
for (Integer element : collection) {
System.out.println(element);
}
}
}
Тут все просто. Якщо ми використовуємо клас, дженерик вказується після імені класу. Давайте тепер у методі main створимо екземпляр цього класу:
public static void main(String []args) {
SomeType<String> st = new SomeType<>();
List<String> list = Arrays.asList("test");
st.test(list);
}
Він добре відпрацює. Компілятор бачить, що є List із чисел і Collection типу String. Але якщо ми зітремо дженерики і зробимо так:
SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
Ми отримаємо помилку: java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
Знову стирання типів. Оскільки у класу більше немає дженерика, компілятор вирішує: якщо ми передали List, метод із List<Integer> більш підходящий. І ми падаємо з помилкою. Тому, правило #2: Якщо клас типизований, завжди вказувати тип у дженериці .
Обмеження
До типів, що вказуються в дженериках ми можемо застосувати обмеження. Наприклад, ми хочемо, щоб контейнер приймав на вхід лише Number. Ця можливість описана в Oracle Tutorial у розділі Bounded Type Parameters . Подивимося на приклад:import java.util.*;
public class HelloWorld{
public static class NumberContainer<T extends Number> {
private T number;
public NumberContainer(T number) { this.number = number; }
public void print() {
System.out.println(number);
}
}
public static void main(String []args) {
NumberContainer number1 = new NumberContainer(2L);
NumberContainer number2 = new NumberContainer(1);
NumberContainer number3 = new NumberContainer("f");
}
}
Як бачимо, ми обмежабо тип дженерика як клас/інтерфейс Number та спадкоємці. Цікаво, що можна вказати не лише клас, а й інтерфейси. Наприклад: public static class NumberContainer<T extends Number & Comparable> {
Ще дженерики мають поняття Wildcard https://docs.oracle.com/javase/tutorial/java/generics/wildcards.html Вони в свою чергу діляться на три типи:
- Upper Bounded Wildcards - < ? extends Number >
- Unbounded Wildcards - < ? >
- Lower Bounded Wildcards - < ? super Integer >
public static class TestClass {
public static void print(List<? extends String> list) {
list.add("Hello World!");
System.out.println(list.get(0));
}
}
public static void main(String []args) {
List<String> list = new ArrayList<>();
TestClass.print(list);
}
Але якщо замінити extends на super, все стане гаразд. Так як ми наповнюємо список list значенням перед виведенням, він для нас є споживачем, тобто consumer'ом. Отже, використовуємо super.
успадкування
Є ще одна незвичайна особливість дженериків – це їхнє успадкування. Спадкування дженериків описано в tutorial від Oracle у розділі " Generics, Inheritance, and Subtypes ". Головне це запам'ятати та усвідомити таке. Ми не можемо зробити так:List<CharSequence> list1 = new ArrayList<String>();
Тому що успадкування працює з дженериками по-іншому:
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
Тут також все просто. List<String> не є спадкоємцем List<Object>, хоча String є спадкоємцем Object.
Final
Ось ми й освіжабо у пам'яті дженерики. Якщо їх рідко використовувати у всій їх мощі, якісь деталі випадають із пам'яті. Сподіваюся, цей невеликий огляд допоможе освіжити у пам'яті. А для більшого результату рекомендую ознайомитися з наступними матеріалами:- Юрій Ткач: Сирі типи - Generics #1 - Advanced Java
- Спадкування та розширювачі узагальнень - Generics #2 - Advanced Java
- Рекурсивне розширення типу - Generics #3 - Advanced Java
- Олександр Маторін - Неочевидні Дженерики
- Введення у Java. Generics. Wildcards | Технострім
- O'Reilly: Java Generics and Collections
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ