Generics у Java дозволяють створювати гнучкі та типобезпечні класи, методи та інтерфейси. Вони допомагають уникнути помилок із неправильним приведенням типів та покращують читабельність і підтримуваність коду. Однак, з generics пов’язано чимало нюансів, які можуть бути неочевидними для новачків, особливо коли справа стосується наслідування та використання generics із інтерфейсами.
Наслідування в generics дещо відрізняється від звичайної ієрархії класів. Generics не тільки підвищують безпеку типів, але й додають певні обмеження, пов’язані з використанням параметрів типів у класах і методах. Важливо розуміти, як працює наслідування з generics, щоб уникати поширених помилок у коді.
До речі, якщо вас цікавлять якісні курси програмування та більш глибоке розуміння теми generics, компанія FoxmindEd пропонує відмінні навчальні та просунуті програми. Вони спеціалізуються на сучасних ІТ-технологіях і допоможуть вам розвинути ваші професійні навички.
У Java generics можна використовувати не лише для створення окремих класів, але й для наслідування. Якщо батьківський клас параметризований певним типом, дочірні класи можуть або конкретизувати цей тип, або залишити його параметризованим. Наслідування з generics надає гнучкість при роботі з різними типами даних.
Однією з важливих особливостей є те, що ми можемо наслідувати непараметризований клас параметризованим, а також конкретизувати або продовжити параметризацію в дочірніх класах. Це дає змогу створювати класи з різними рівнями абстракції та використовувати generics у різноманітних сценаріях.
Уявімо, що у нас є клас Container, який може зберігати деякий елемент типу T. Ми можемо створити різні класи коробок (Box), що будуть наслідувати Container, але по-різному працюватимуть з узагальненими типами (generics). Кожен з цих класів буде наслідувати Container та по-своєму обробляти generics, що дозволить реалізувати різні підходи до зберігання та роботи з даними.
@Data
class Container<T> {
private T item;
}
Якщо клас Box наслідує Container без вказівки типу (Box extends Container), це викличе попередження про використання сирого типу Container. Java інтерпретує це як використання “сирих” типів, що може призвести до проблем із безпекою типів.
public class Box extends Container{
public Box(Object item) {
super(item);
}
}
public class Main {
public static void main(String[] args) {
Box box = new Box("Item"); // Box наслідує сирий тип Container
Object item = box.getItem(); // Повернеться Object, а не конкретний тип
Box<String> box = new Box<>("Item"); // Буде помилка компіляції, адже ми не вказали, що наш Box параметризований
}
}
У цьому випадку клас Box1 наслідує Container, але також підтримує узагальнений тип T. Це найбільш безпечний і гнучкий спосіб наслідування, адже при створенні Box ми зможемо вказати тип, що там буде зберігатися.
class Box1<T> extends Container<T>{
public Box(T item) {
super(item);
}
}
public class Main {
public static void main(String[] args) {
Box1<String> box = new Box1<>("Item");
String item = box.getItem(); // Повернеться тип String
}
}
Box2 наслідує Container, фіксуючи тип T як String. Це означає, що всі екземпляри Box2 будуть зберігати лише рядки (String):
public class Box2 extends Container<String>{
public Box2(String item) {
super(item);
}
}
public class Main {
public static void main(String[] args) {
Box2 box = new Box2("Item");
String item = box.getItem(); // Повернеться тип String
Box2 box2 = new Box2(5); // буде помилка компіляції, адже Box2 зберігає лише тип String
}
}
У даному класі Box3 підтримує кілька узагальнених типів (T, V, K), але наслідує Container з типом T. Це означає, що T буде використовуватися для поля item, тоді як V та K можуть використовуватися для інших цілей
@Getter
@Setter
class Box3<T, V, K> extends Container<T> {
private V extraValue;
private K anotherValue;
public Box3(T item, V extraValue, K anotherValue) {
super(item);
this.extraValue = extraValue;
this.anotherValue = anotherValue;
}
}
public class Main {
public static void main(String[] args) {
Box3<String, Integer, Double> box = new Box3<>("Hello", 42, 3.14);
String item = box.getItem(); // Отримаємо String
Integer extra = box.getExtraValue(); // Отримаємо Integer
Double another = box.getAnotherValue(); // Отримаємо Double
}
}
Наслідування з обмеженнями
Ми розглянули стандартне наслідування з використанням generics, де класи-нащадки можуть працювати з будь-яким типом даних, що передається як параметр. Це забезпечує гнучкість та універсальність у використанні, але іноді виникає потреба обмежити типи, з якими може працювати клас.
курси Junior саме для вас.
Саме для таких випадків generics дозволяють застосовувати наслідування з обмеженнями, використовуючи ключове слово extends. Це особливо корисно, коли потрібно працювати лише з певною групою типів, наприклад, усіма класами, які наслідуються від конкретного суперкласу або реалізують певний інтерфейс. Такий підхід називається обмеженням параметра типу (bounded type parameter) і дозволяє створювати більш безпечний і структурований код, обмежуючи використання generics певними рамками.
Коли ми використовуємо ключове слово extends, це означає, що параметр типу може бути або самим вказаним типом, або його підтипом. Розглянемо простий приклад:
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
class Container<T extends Animal> {
private T item;
public Container(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
У цьому прикладі клас Container параметризований типом T, який обмежений класом Animal. Це означає, що T може бути або Animal, або будь-яким його підтипом, таким як Dog або Cat. Отже, ми можемо створювати екземпляри контейнера тільки для тварин:
Container<Dog> dogContainer = new Container<>(new Dog());
Container<Cat> catContainer = new Container<>(new Cat());
Container<Animal> animalContainer = new Container<>(new Animal());
Однак, якщо ми спробуємо передати тип, який не є підкласом Animal, це призведе до помилки компіляції:
Container<Book> book Container = new Container<>(new Book()); // буде помилка компіляції
Наслідування з декількома обмеженнями
У Java також можна встановити кілька обмежень на параметр типу. Це робиться за допомогою ключового слова &, що дозволяє обмежувати типи класами та інтерфейсами одночасно. Наприклад, ми можемо обмежити параметр типу таким чином, щоб він був підтипом певного класу і одночасно реалізовував один або кілька інтерфейсів:
interface Flyable {
void fly();
}
Це простий інтерфейс із методом fly(). Класи, які реалізують цей інтерфейс, повинні надавати свою власну реалізацію цього методу. Інтерфейс позначає, що об’єкт може літати.
class Parrot extends Animal implements Flyable {
@Override
public void fly() {
System.out.println("Parrot is flying");
}
}
class Lion extends Animal {
}
class Sparrow extends Animal implements Flyable {
@Override
public void fly() {
System.out.println("Sparrow is flying");
}
}
Всі класи тваринок наслідують клас Animal, проте не всі імплементують Flyable та надають власну реалізацію метода fly.
class FlyingContainer<T extends Animal & Flyable> {
private T item;
public FlyingContainer(T item) {
this.item = item;
}
public void letFly() {
item.fly();
}
}
У цьому прикладі клас FlyingContainer обмежує тип T так, що він має бути підтипом Animal і реалізовувати інтерфейс Flyable. Це означає, що можна використовувати тільки ті класи, які наслідуються від Animal і реалізовують інтерфейс Flyable (наприклад, Parrot або Sparrow):
FlyingContainer<Parrot> parrotContainer = new FlyingContainer<>(new Parrot());
parrotContainer.letFly(); // Виведе "Parrot is flying"
FlyingContainer<Sparrow> sparrowContainer = new FlyingContainer<>(new Sparrow());
sparrowContainer.letFly(); // Виведе "Sparrow is flying"
FlyingContainer<Lion> lionContainer = new FlyingContainer<>(new Lion()); // буде помилка компіляції
Передати в контейнер тип Lion ми вже не можем, адже хоча наш Lion і тварина, проте він не імплементує інтерфейс Flyable.
Цей приклад показує, як generics можна використовувати для обмеження типів за допомогою T extends Animal & Flyable, забезпечуючи, що тільки ті класи, які є підтипами певного класу (в даному випадку Animal) і одночасно реалізують певний інтерфейс (Flyable), можуть бути передані в generics-клас. Це дозволяє гарантувати типобезпечність і уникати помилок під час компіляції. FoxmindEd пропонує комплексний курс JAVA, де ви можете дізнатися більше про використання generics, а також багато других аспектів програмування на Java.
🤔 Залишилися запитання щодо наслідування в Generics? - Сміливо задавайте нижче! 💬