Привіт! Сьогодні ми поговоримо про дженеріки.
Треба сказати, що ти вивчиш багато нового! Дженерікам буде присвячена не тільки ця, але ще й кілька наступних лекцій.
Тому, якщо ця тема тобі цікава — тобі пощастило: сьогодні ти дізнаєшся багато про особливості дженеріків. Ну а якщо ні — змирись і розслабся! :) Це дуже важлива тема, і знати її потрібно.
Давай почнемо з простого: «що» і «навіщо».
Що таке дженеріки?
Дженеріки — це типи з параметром.
При створенні дженеріка ти вказуєш не тільки його тип, але й тип даних, з якими він має працювати.
Думаю, найочевидніший приклад вже прийшов тобі в голову — це ArrayList! Ось як ми зазвичай створюємо його в програмі:
Розділ 23 цієї книги має дуже промовисту назву: «Не використовуйте raw types у новому коді»
Це те, що потрібно запам'ятати. При використанні класів-дженеріків ні в якому разі не перетворюй , що вказує на дженерик-метод. У цьому випадку метод приймає на вхід 2 параметри: список об'єктів T і ще один окремий об'єкт Т.
Завдяки використанню досягається типізація методу: ми не можемо передати туди список рядків і число. Список рядків і рядок, список чисел і число, список наших об'єктів
Тому, якщо ця тема тобі цікава — тобі пощастило: сьогодні ти дізнаєшся багато про особливості дженеріків. Ну а якщо ні — змирись і розслабся! :) Це дуже важлива тема, і знати її потрібно.
Давай почнемо з простого: «що» і «навіщо».
Що таке дженеріки?
Дженеріки — це типи з параметром.
При створенні дженеріка ти вказуєш не тільки його тип, але й тип даних, з якими він має працювати.
Думаю, найочевидніший приклад вже прийшов тобі в голову — це ArrayList! Ось як ми зазвичай створюємо його в програмі:
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> myList1 = new ArrayList<>();
myList1.add("Тестовий рядок 1");
myList1.add("Тестовий рядок 2");
}
}
Як неважко здогадатися, особливість списку полягає в тому, що в нього не можна буде «запихувати» все поспіль: він працює виключно з об'єктами String.
Тепер давай зробимо невеликий екскурс в історію Java і спробуємо відповісти на питання: «навіщо?». Для цього ми самі напишемо спрощену версію класу ArrayList.
Наш список вміє тільки додавати дані у внутрішній масив і отримувати ці дані:
public class MyListClass {
private Object[] data;
private int count;
public MyListClass() {
this.data = new Object[10];
this.count = 0;
}
public void add(Object o) {
this.data[count] = o;
count++;
}
public Object[] getData() {
return data;
}
}
Припустимо, ми хочемо, щоб наш список зберігав тільки числа Integer. Дженеріків у нас немає.
Ми не можемо явно вказати перевірку o instance of Integer у методі add(). Тоді весь наш клас буде придатний тільки для Integer, і нам доведеться писати такий самий клас для всіх існуючих у світі типів даних!
Ми вирішуємо покластися на наших програмістів, і просто залишимо в коді коментар, щоб вони не додавали туди нічого зайвого:
//використовувати ТІЛЬКИ з даними типу Integer
public void add(Object o) {
this.data[count] = o;
count++;
}
Один із програмістів проґавив цей коментар і спробував ненавмисно покласти у список числа впереміш зі строками, а потім порахувати їх суму:
public class Main {
public static void main(String[] args) {
MyListClass list = new MyListClass();
list.add(100);
list.add(200);
list.add("Лолкек");
list.add("Шалала");
Integer sum1 = (Integer) list.getData()[0] + (Integer) list.getData()[1];
System.out.println(sum1);
Integer sum2 = (Integer) list.getData()[2] + (Integer) list.getData()[3];
System.out.println(sum2);
}
}
Вивід у консоль:
300
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
at Main.main(Main.java:14)
Що найгірше в цій ситуації?
Зовсім не увага програміста. Найгірше те, що неправильний код потрапив у важливе місце нашої програми і успішно скомпілювався.
Тепер ми побачимо помилку не на етапі написання коду, а тільки на етапі тестування (і це в кращому випадку!).
Виправлення помилок на пізніших етапах розробки коштує набагато більше — і грошей, і часу.
Саме в цьому полягає перевага дженеріків: клас-дженерік дозволить невдалому програмісту виявити помилку відразу ж. Код просто не скомпілюється!
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> myList1 = new ArrayList<>();
myList1.add(100);
myList1.add(100);
myList1.add("Лолкек");//помилка!
myList1.add("Шалала");//помилка!
}
}
Програміст одразу «прокинеться» і моментально виправиться.
До речі, нам не обов'язково було створювати свій власний клас-List, щоб побачити помилку такого типу.
Достатньо просто прибрати дужки з вказанням типу (<Integer>) з звичайного ArrayList!
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List list = new ArrayList();
list.add(100);
list.add(200);
list.add("Лолкек");
list.add("Шалала");
System.out.println((Integer) list.get(0) + (Integer) list.get(1));
System.out.println((Integer) list.get(2) + (Integer) list.get(3));
}
}
Вивід у консоль:
300
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
at Main.main(Main.java:16)
Тобто навіть використовуючи «рідні» засоби Java, можна припуститися такої помилки і створити небезпечну колекцію.
Однак, якщо вставити цей код у IDEa, ми побачимо попередження: “Unchecked call to add(E) as a member of raw type of java.util.List”
Нам підказують, що при додаванні елемента в колекцію без дженеріків щось може піти не так.
Але що означає фраза «raw type»?
Дослівний переклад буде цілком точним — «сирий тип» або «брудний тип».
Raw type — це клас-дженерік, з якого видалили його тип.
Іншими словами, List myList1 — це Raw type. Протилежністю raw type є generic type — клас-дженерік (також відомий як parameterized type), створений правильно, з вказанням типу. Наприклад, List<String> myList1.
У тебе міг виникнути питання: а чому в мові взагалі дозволено використовувати raw types?
Причина проста. Створювачі Java залишили в мові підтримку raw types, щоб не створювати проблем із сумісністю. На момент виходу Java 5.0 (в цій версії вперше з'явилися дженеріки) було написано вже дуже багато коду з використанням raw types.
Тому така можливість зберігається і зараз.
Ми вже не раз згадували класичну книгу Джошуа Блоха «Effective Java» в лекціях.
Як один із творців мови, він не обійшов у книзі і тему використання raw types і generic types.
Розділ 23 цієї книги має дуже промовисту назву: «Не використовуйте raw types у новому коді»
Це те, що потрібно запам'ятати. При використанні класів-дженеріків ні в якому разі не перетворюй generic type у raw type.
Типізовані методи
Java дозволяє тобі типізувати окремі методи, створюючи так звані generic methods. Чим такі методи зручні? Перш за все тим, що дозволяють працювати з різними типами параметрів. Якщо до різних типів можна безпечно застосовувати одну і ту ж логіку, дженерик-метод буде відмінним рішенням. Розглянемо приклад. Припустимо, у нас є якийсь списокmyList1. Ми хочемо видалити з нього всі значення, і заповнити всі звільнені місця новим значенням.
Ось так виглядатиме наш клас із дженерик-методом:
public class TestClass {
public static <T> void fill(List<T> list, T val) {
for (int i = 0; i < list.size(); i++)
list.set(i, val);
}
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
strings.add("Старий рядок 1");
strings.add("Старий рядок 2");
strings.add("Старий рядок 3");
fill(strings, "Новий рядок");
System.out.println(strings);
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
fill(numbers, 888);
System.out.println(numbers);
}
}
Зверни увагу на синтаксис, він виглядає трохи незвично:
public static <T> void fill(List<T> list, T val)
Перед типом значення, що повертається, написано Cat і ще один об'єкт Cat — лише так.
У методі main() наочно демонструється, що метод fill() легко працює з різними типами даних.
Спочатку він приймає на вхід список рядків і рядок, а потім — список чисел і число.
Вивід у консоль:
[Новий рядок, Новий рядок, Новий рядок]
[888, 888, 888]
Уяви, якщо б логіка методу fill() була потрібна нам для 30 різних класів, і у нас не було б дженерик-методів.
Ми були б змушені писати один і той самий метод 30 разів, просто для різних типів даних! Але завдяки generic-методам ми можемо використовувати наш код повторно! :)
Типізовані класи
Ти можеш не лише користуватися представленими в Java дженерик-класами, але й створювати власні! Ось простий приклад:
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public static void main(String[] args) {
Box<String> stringBox = new Box<>();
stringBox.set("Старий рядок");
System.out.println(stringBox.get());
stringBox.set("Новий рядок");
System.out.println(stringBox.get());
stringBox.set(12345);//помилка компіляції!
}
}
Наш клас Box<T> («коробка») є типізованим. Призначивши для нього при створенні тип даних (<T>), ми вже не зможемо поміщати в нього об'єкти інших типів.
Це видно в прикладі. При створенні ми вказали, що наш об'єкт буде працювати з рядками:
Box<String> stringBox = new Box<>();
І коли в останньому рядку коду ми намагаємося покласти всередину коробки число 12345, отримуємо помилку компіляції!
Ось так просто ми створили свій власний дженерик-клас! :)
На цьому наша сьогоднішня лекція підходить до кінця. Але ми не прощаємося з дженеріками! У наступних лекціях поговоримо про більш просунуті можливості, тому не прощаємось! )
Успіхів у навчанні! :)
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ