JavaRush /Java курси /Java Syntax Zero /Параметризовані типи в Java — generics

Параметризовані типи в Java — generics

Java Syntax Zero
Рівень 13 , Лекція 7
Відкрита

1. Усі класи успадковано від Object

Усі класи в Java неявно (приховано) успадковано від класу Object.

Що таке успадкування та як воно працює в Java, ми розберемо у квесті Java Core. А наразі ми розглянемо один простий факт, який із цього випливає:

Змінній типу Object можна присвоїти об'єкт будь-якого класу. Приклад:

Код Примітка
Object o = new Scanner(System.in);
У змінній o збережено посилання на об'єкт типу Scanner
Object o = new String();
У змінній o збережено посилання на об'єкт типу String
Object o = new Integer(15);
У змінній o збережено посилання на об'єкт типу Integer
Object o = "Привіт";
У змінній o збережено посилання на об'єкт типу String

На цьому добрі новини закінчуються. Компілятор не стежить за тим, якого саме типу об'єкт було збережено в змінній типу Object, тому викликати методи, що були у збереженого об'єкта, але відсутні у змінної типу Object, не можна.

Якщо треба викликати методи такого об'єкта, то спочатку посилання на нього потрібно зберегти в змінній правильного типу, а лише потім викликати методи для цієї змінної:

Код Примітка
Object o = new Scanner(System.in);
int x = o.nextInt();
Програма не скомпілюється. Клас Object не має методу nextInt().
Object o = new Scanner(System.in);

Scanner console = (Scanner) o;

int x = console.nextInt();
Так працюватиме.

Тут ми зберігаємо посилання на об'єкт типу Scanner у змінній типу Scanner за допомогою оператора перетворення типу.

Просто так змінну типу Object не можна присвоїти змінній типу Scanner, навіть якщо змінна типу Object зберігає посилання на об'єкт типу Scanner. Натомість це можна зробити, якщо використати вже відомий вам оператор перетворення типу. У загальному випадку це має такий вигляд:

Тип ім'я1 = (Тип) ім'я2;

де ім'я1 — це ім'я змінної типу Тип, а ім'я2 — це ім'я змінної типу Object, яка зберігає посилання на об'єкт типу Тип.

Перетворення типу

Якщо типи змінної та об'єкта не збігаються, виникне помилка ClassCastException. Приклад:

Код Примітка
Object o = new Integer(5);
String s = (String) o;
Під час виконання виникне помилка:
тут буде кинуто виняток ClassCastException

У Java є спосіб обійти цю помилку: існує можливість перевірити, який тип насправді має змінна:

ім'я instanceof Тип

Оператор instanceof перевіряє, чи є змінна ім'я об'єктом типу Тип.

Приклад — пошук рядка серед елементів масиву даних:

Код Примітка
Object[] objects = {10, "Привіт", 3.14};

for (int i = 0; i < objects.length; i++)
{
   if (objects[i] instanceof String)
   {
      String s = (String) objects[i];
      System.out.println(s);
   }
}
Autoboxing перетворить ці значення на Integer, String і Double.

Цикл по масиву об'єктів

Якщо об'єкт має тип String,

зберігаємо його в змінній типу String,
виводимо змінну на екран.


2. Причина виникнення шаблонів (колекції)

Повертаємося до колекцій.

Коли Java-розробники тільки створювали клас ArrayList, вони хотіли зробити його універсальним, щоб у ньому можна було зберігати об'єкти будь-якого типу. Тому для зберігання елементів вони скористалися масивом типу Object.

Сильна сторона такого підходу в тому, що в колекцію можна додати об'єкт будь-якого типу.

Ну а слабких сторін одразу декілька.

Недолік 1.

Завжди доводилося писати оператор перетворення типу, коли діставали елементи з колекції:

Код Примітка
ArrayList numbers = new ArrayList();


for (int i = 0; i < 10; i++)
   numbers.add(i * 10);


int sum = 0;
for (int i = 0; i < 10; i++)
{
   sum = sum + (Integer) numbers.get(i);
}
Створюємо об'єкт-колекцію для зберігання посилань на об'єкти типу Object

Заповнюємо колекцію числами 10, 20, … 100;



Знаходимо суму елементів колекції


Потрібно використовувати перетворення типу

Недолік 2.

Не було гарантії, що в колекції зберігаються елементи певного типу

Код Примітка
ArrayList numbers = new ArrayList();


for (int i = 0; i < 10; i++)
   numbers.add(i * 2.5);


int sum = 0;
for (int i = 0; i < 10; i++)
{
   sum = sum + (Integer) numbers.get(i);
}
Створюємо об'єкт-колекцію для зберігання посилань на об'єкти типу Object

Заповнюємо колекцію числами типу Double:
0.0, 2.5, 5.0, ...


Знаходимо суму елементів колекції


Буде помилка: тип Double не можна перетворити на тип Integer

Дані в колекцію можуть вноситися де завгодно:

  • в іншому методі
  • в іншій програмі
  • завантажуватися з файлу
  • отримуватися з мережі

Недолік 3.

Дані колекції можна випадково змінити через незнання.

Ви можете передати колекцію, заповнену своїми даними, в якийсь метод, а цей метод, написаний зовсім іншим програмістом, додасть до вашої колекції свої дані.

З назви колекції незрозуміло, які саме типи даних можна в ній зберігати. Та навіть якщо й дати змінній зрозумілу назву, посилання на неї можна передати в десяток методів, і там уже точно про початкове ім'я змінної нічого не буде відомо.


3. Узагальнення

Узагальнення в Java

Усі ці проблеми усуває така чудова річ у Java, як узагальнення (generics).

Під узагальненнями в Java мають на увазі можливість додавати до типів типи-параметри. Таким чином утворюються складені типи. Такий складений тип у загальному випадку має ось який вигляд:

ОсновнийТип<ТипПараметр>

Усе разом — це саме тип. І він може використовуватися там, де зазвичай можна використовувати типи.

Код Опис
ArrayList<Integer> list;
Створення змінних
list = new ArrayList<Integer> ();
Створення об'єктів
ArrayList<Integer>[] array;
Створення масивів

У такій колекції можна зберегти тільки змінні типу Integer:

Код Опис
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(new Integer(1));
list.add(2);
list.add("Привіт");
Колекція типу ArrayList з елементами типу Integer
Так можна
І так можна: працюватиме
autoboxing

А так не можна: помилка компіляції

Як створювати свої класи з типами-параметрами, ви дізнаєтеся у квесті Java Collections. А наразі ми розглянемо, як цим користуватися та як воно працює.


4. Як працюють generics

Насправді generics працюють надзвичайно примітивно.

Компілятор просто замінює тип із параметром на нього ж, тільки без параметра. А при взаємодії з його методами додає операцію перетворення типу на тип-параметр:

Код Що зробить компілятор
ArrayList<Integer> list = new ArrayList<Integer>();
ArrayList list = new ArrayList();
list.add(1);
list.add( (Integer) 1 );
int x = list.get(0);
int x = (Integer) list.get(0);
list.set(0, 10);
list.set(0, (Integer) 10);

Припустімо, ми мали код методу, що підсумовує числа в колекції цілих чисел:

Код Що зробить компілятор
public int sum(ArrayList<Integer> numbers)
{
   int result = 0;

   for (int i = 0; i < numbers.size(); i++)
      result = result + numbers.get(i);

   return result;
}
public int sum(ArrayList numbers)
{
   int result = 0;

   for (int i = 0; i < numbers.size(); i++)
      result = result + (Integer) numbers.get(i);

   return result;
}

Тобто по суті узагальнення — це так само різновид синтаксичного цукру, як і autoboxing, тільки більший. Під час autoboxing компілятор за нас додає методи для перетворення типу int на Integer і навпаки, а для generics додає оператори перетворення типу.

Після того як компілятор скомпілював ваш код з узагальненнями, у ньому всі класи з параметрами буде перетворено на прості класи та оператори перетворення типу. Інформація про те, які типи-параметри спочатку мали змінні складних типів, загубилася. Цей ефект інакше називають стиранням типів.

Іноді програмістам, які пишуть свої класи з типами-параметрами, дуже не вистачає інформації про типи, які туди передаються як параметри. Як із цим борються та що з цього виходить, ви дізнаєтеся у квесті Java Collections.



5. Кілька фактів про узагальнення

Ще кілька цікавих фактів про узагальнення.

У класів може бути не один тип-параметр, а декілька. Отакий вигляд це має:

ОсновнийТип<ТипПараметр1, ТипПараметр2, ТипПараметр3>

Власне кажучи, у цьому немає нічого дивного. Там, де компілятор може додати оператор перетворення на один тип, він може додати й декілька таких.

Приклади:

Код Примітка
HashMap<Integer, String> map = new HashMap<Integer, String>();
map.put(7, "Привіт");
map.put(-15, "Привіт");
Перший параметр методу put має тип Integer, другий — тип String

Крім того, складні типи теж можна використовувати як параметри. Отакий вигляд це має:

ОсновнийТип<ТипПараметр<ТипПараметрПараметра>>

Припустімо, нам треба створити список, який зберігатиме списки рядків. У цьому разі ми отримаємо приблизно такий код:

// список привітань
ArrayList<String> listHello = new ArrayList<String>();
listHello.add("Привіт");
listHello.add("Hi");

// список прощань
ArrayList<String> listBye = new ArrayList<String>();
listBye.add("Бувай");
listBye.add("Good Bye");

// список списків
ArrayList<ArrayList<String>> lists = new ArrayList<ArrayList<String>>();
lists.add(listHello);
lists.add(listBye);
Коментарі (13)
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ
Christopher Ward 595 Рівень 16
6 серпня 2024
Шось я завтикав шо в ArrayList елементи індексуються elements.get(i) а не як в массивах elements[i]
19 листопада 2023
"Припустімо, нам треба створити список, який зберігатиме списки рядків." - схоже на двовимірний масив.
Гаркін Рівень 14
20 травня 2024
Ну так там ще раніше було ArrayList<Integer>[] array; Тобто у колекції (що типу є більш просунутим масивом), зберігаються масиви. Відмінність від двомірних масивів – кількість збереження масивів необмежена. Ось тільки як звертатись до таких даних не написали. Щось типу: int x = list.get(0.array[i]); ? Ps. 57 рівень? Круто! З 19.11.23 по 20.05.24 підняти 44 рівня! Є на що орієнтуватись. Хоча я не зможу. Тут рівень на тиждень, буде дуже-дуже гарно. А скоріше рівень на 2 тиждні.
Yaroslav Tkachyk Рівень 23 Expert
4 січня 2023
Виклюкаю пояснювальну бригаду: 1) Object[] objects = {10, "Привіт", 3.14}; - усі комірки масиву мають однаковий тип даних, який вже неможливо змінити після створення масиву. - у наведеному виразі - який тип мають комірки? Object? 2) ArrayList numbers = new ArrayList(); - при створенні колекції ArrayList вже необов'язково вказувати тип елементів, які в ній будуть зберігатись? ArrayList<Object> numbers = new ArrayList<>(); Чи це також синтаксичний цукор?
5 січня 2023
не потрібно знову створювати ArrayList
Yaroslav Tkachyk Рівень 23 Expert
6 січня 2023
ем..... це до чого? Я просто запитую чому в прикладах записано код саме в такому варіанті?
les_yeux_blancs Рівень 50
25 квітня 2023
Трохи запізно, але поясню) 1. Усе наслідується від Object,а тобто усе є Object. Таким чином, тут використовується поліморфізм, що дозволяє зберігати різні, як здається, типи, як один, даючи можливість викликати на них лише методи класу Object 2. Нетипізований дженерік дозволяє запхнути туди будь-якого ніслідника Object (тобто будь-що), але IDEA свариться на це)
kalkulator¹ Рівень 51
11 листопада 2022
доволі легка і зрозуміла штука (лекція написана чудово🙂)
Roma Chernesh Рівень 16
30 січня 2023
Згоден! Чи тема дуже легка, чи подача дуже гарна:)
Anonymous #696530 Рівень 19
22 вересня 2022
Голова кипить, проте, здається, цей ArrayList вельми зручна штука як нормально розібратися...
Taras Woytowitch Рівень 16
11 січня 2022
В недоліку 1: for (int i = 0; i < 10; i++) numbers.add(i * 10); Хіба тут не буде заповнення колекції числами (0,10,...,90) ?
Харченко Иван Рівень 15
10 лютого 2022
То, мабуть, великий і могутній ГалаБаг!
kalkulator¹ Рівень 51
11 листопада 2022
чувак хто ти?