JavaRush /Java блог /Random UA /Що таке дженерики в Java
Professor Hans Noodles
41 рівень

Що таке дженерики в Java

Стаття з групи Random UA
Вітання! Сьогодні ми поговоримо про дженериків. Потрібно сказати, що ти вивчиш багато нового! Дженерикам буде присвячена не лише ця, а ще й кілька наступних лекцій. Що таке дженерики в Java - 1 Тому, якщо ця тема тобі цікава — тобі пощастило: сьогодні ти дізнаєшся багато про особливості дженериків. Ну а якщо ні – змирись і розслабся! :) Це дуже важлива тема і знати її потрібно. Давай почнемо з простого: «що» та «навіщо». Що таке дженерики? Дженерики – це типи з параметром. Під час створення дженерика ти вказуєш як його тип, а й тип даних, із якими має працювати. Думаю, найочевидніший приклад уже прийшов тобі на думку — це 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("Test String 1");
       myList1.add("Test String 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, і нам доведеться писати такий самий клас для всіх існуючих у світі типів даних! Ми вирішуємо покластися на наших програмістів і просто залишимо в коді коментар, щоб вони не додавали туди нічого зайвого:
//use it ONLY with Integer data type
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("Lolkek");
       list.add("Shalala");

       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 . Далеко не неуважність програміста. Найгірше те, що неправильний код потрапив у важливе місце нашої програми та успішно скомпілювався . Тепер ми побачимо помилку не на етапі написання коду, а лише на етапі тестування (і це у кращому випадку!). Виправлення помилок на пізніших етапах розробки коштує набагато більше — і грошей, і часу. Саме в цьому полягає перевага дженериків: клас-дженерик дозволить невдаху програмісту виявити помилку відразу ж. Код просто не скомпілюється!
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("Lolkek");//помилка!
       myList1.add("Shalala");//помилка!
   }
}
Програміст одразу «очухається» і миттєво виправиться. До речі, нам не обов'язково було створювати свій власний клас 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("Lolkek");
      list.add("Shalala");

       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 . Java, можна припуститися такої помилки і створити небезпечну колекцію. Однак, якщо вставити цей код в IDEa, ми побачимо попередження: “ Нам підказують , що при додаванні елемента до колекції без дженериків щось може піти не так. Але що означає фраза "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. Що таке дженерики в Java - 2Розділ 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)
Перед типом значення, що повертається написано <T>, що вказує на дженерик метод. В даному випадку метод приймає на вхід 2 параметри: список об'єктів T і ще один окремий об'єкт Т. За рахунок використання <T> і досягається типізація методу: ми не можемо передати туди список рядків та число. Список рядків та рядок, список чисел та число, список наших об'єктів 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, отримуємо помилку компіляції! Ось так просто ми створабо свій власний дженерік-клас! :) На цьому наша сьогоднішня лекція добігає кінця. Але ми не прощаємось із дженериками! У наступних лекціях поговоримо про більш просунуті можливості, тому не прощаємося! ) Успіхів у навчанні! :)
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ