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 также способствовали улучшению читаемости и поддерживаемости кода, уменьшая количество ошибок, возникающих из-за некорректной работы с типами данных.
Понятие 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? - смело задавайте ниже! 💬