Generics (узагальнені типи) – це одна з найважливіших особливостей мови програмування Java, яка була впроваджена з версії Java 5. Основна ідея generics полягає в тому, щоб забезпечити можливість створення узагальнених класів, інтерфейсів та методів, які можуть працювати з різними типами даних, зберігаючи при цьому контроль типів на етапі компіляції.
У цій статті від FoxmindEd ми розглянемо, що таке generics у Java, як вони працюють, які переваги вони надають, а також з’ясуємо деякі їхні обмеження.
Історія виникнення
Generics були впроваджені у Java з випуском версії 5, що вийшла у 2004 році. До цього моменту Java не мала механізмів для створення узагальнених класів та методів, що призводило до певних незручностей та потенційних помилок під час виконання програм. Основною проблемою було те, що при роботі з колекціями, такими як List, Set або Map, всі об’єкти зберігалися як тип Object. Це вимагало від розробників явного приведення типів при отриманні елементів із колекцій, що могло призвести до класичних помилок на етапі виконання, коли тип даних не відповідав очікуваному.
Розглянемо на прикладі як було до появи generics. Уявимо, що у нас є сумка. І в цій сумці ми можемо зберігати книги, листи, гаманець… Що завгодно, але щось одне. Як би ми створити цей клас? Вірно, із використанням Object.
@Data
@AllArgsConstructor
public class Bag {
private Object item;
}
В чому можуть бути проблеми використання такого підходу?
public class Main {
public static void main(String[] args) {
String letter = "Letter to Olga";
Book book = new Book("Harry Potter");
Bag bagLetter = new Bag(letter);
Bag bagBook = new Bag(book);
String item = (String) bagLetter.getItem();
}
}
Щоб отримати об’єкт нам треба робити кастування. А що, якщо в Bag не тип String? Якщо там Book чи Integer? Це зараз код мість 5 строчок і все в одному класі. Але в реальних проєктах об’єкти можуть сетится в одних класах, діставатися в інших. Чи хтось змінить логіку і заміть String в bagLetter покладе тип Book, а ви будете діставати String. Тож допустити помилку із кастуванням об’єктів доволі просто. Звісно, можна зробити перевірку:
if (bagLetter.getItem() instanceof String) {
String item = (String) bagLetter.getItem();
}
Проте це все ускладнює код і додає купу перевірок. І у випадку, якщо в Bag лежить не String то зробити нічого із тим не можна і наша логіка в блоці if просто буде ігноруватися. Навіть дізнатися, що всередині Bag (без дженериків) не вийде.
Введення generics дозволило вирішити цю проблему, забезпечивши перевірку типів на етапі компіляції. Завдяки цьому, розробники отримали можливість створювати більш гнучкий і безпечний код, в якому немає необхідності постійно виконувати приведення типів чи додаткові перевірки. Generics також сприяли поліпшенню читаємості та підтримуваності коду, зменшуючи кількість помилок, що виникають через некоректну роботу з типами даних.
курси Junior саме для вас.
Поняття generics та його використання
Generic типи дозволяють оголосити клас або метод таким чином, щоб він працював із параметризованими типами. Це означає, що під час використання generics ви можете вказати, з якими типами даних буде працювати клас чи метод, що забезпечує валідацію типів під час компіляції та уникнення проблем, пов’язаних з неправильним типом даних.
Дженерики найчастіше використовуються при роботі з колекціями, такими як List, Set, Map та іншими. Це дозволяє зберігати об’єкти певного типу в колекції та уникати необхідності приведення типів під час отримання даних. Наприклад:
public class Main {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Irina");
names.add("Oleg");
String firstName = names.get(0); // приведення типу не потрібне
}
}
<> — це спеціальні дужки, в яких вказується параметризований тип, з яким буде працювати клас чи метод. Тобто наш масив names приймає лише String. Із іншими типами він не буде працювати. Це забезпечує перевірку типів під час компіляції та допомагають уникнути помилок, пов’язаних з неправильним приведенням типів.
Давайте перепишем наш клас Bag із використанням дженериків:
@Data
@AllArgsConstructor
public class Bag<T> {
private T item;
}
Як бачите, після назви класу ми додали <T>. Це означає, що ми параметризували клас. І при його створенні ми зразу визначаємо, який саме тип наша сумка буде приймати як item, адже item теж має тип Т. Зауважу, що замість T може бути будь яка буква чи навіть слово. Проте найпоширенішими параметри типів є:
- T – Type (загальний тип).
- E – Element (елемент, часто використовується в колекціях).
- K – Key (ключ, зазвичай використовується в асоціативних масивах).
- V – Value (значення, використовується разом з K).
- N – Number (числовий тип).
Але ці літери є лише конвенціями і не є обов’язковими до використання, проте така стандартизація допомагає зробити код зрозумілішим для інших розробників, оскільки ці літери відображають стандартні ролі, які вони виконують у коді.
public class Main {
public static void main(String[] args) {
Bag<Book> bookBag = new Bag<>(new Book("Harry Potter"));
Bag<String> letterBag = new Bag<>("Letter to Olga");
Bag<Book> bookBag2 = new Bag<>("Letter to Olga"); // буде помилка помпіляції
Book item = bookBag.getItem(); // кастування робити не треба
}
}
В результаті, для одного й того ж класу можна використовувати різні типи даних, такі як Book чи String, не змінюючи сам клас. Тепер нам не треба робити кастування, додавати перевірки адже ми захищені від того, що в сумку замість Book покладуть String. Дженеріки забезпечують перевірку типів на етапі компіляції. Це означає, що помилки, пов’язані з неправильними типами даних, будуть виявлені ще до запуску програми, що значно знижує кількість runtime помилок.
А що буде, якщо не параметризувати Bag при його створенні?
Bag bookBag = new Bag(new Book("Harry Potter"));
В такому випадку T буде сприйматися як Object і ми будемо мати всі ті самі проблеми, про які була мова вище. Такий запис, коли generics використовується без зазначення конкретного типу, називається Raw Type. Raw Type — це варіант використання класу generics, де тип опускається, що знижує безпеку і гнучкість коду, адже компілятор не зможе контролювати правильність типів.
Висновок
Generics у Java є одним із найпотужніших інструментів для забезпечення гнучкості та типобезпеки коду. Вони дозволяють створювати класи, методи та інтерфейси, які можуть працювати з різними типами, зберігаючи при цьому контроль над правильністю типів під час компіляції.
Generics значно покращують якість та гнучкість програмного забезпечення, особливо при роботі з java та колекціями, де вони забезпечують типобезпеку без втрати продуктивності. Вивчивши основні принципи та підводні камені generics, можна писати більш стійкий і зрозумілий код, який легко підтримувати та розширювати. Якщо прагнеш більш поглиблених знань у розробці на Java, реєструйся на відповідний курс по програмуванню від FoxmindEd.
🤔 Залишилися запитання про generics у Java? - Сміливо задавайте нижче! 💬