Что такое дженерики в Java
Привет! Сегодня мы поговорим о дженериках. Надо сказать, что ты выучишь много нового! Дженерикам будет посвящена не только эта, но еще и несколько следующих лекций. Поэтому, если эта тема тебе интересна — тебе повезло: сегодня ты узнаешь многое об особенностях дженериков. Ну а если нет — смирись и расслабься! 🙂 Это очень важная тема, и знать ее нужно. Давай начнем с простого: «что» и «зачем». Что такое дженерики? Дженерики — это типы с параметром. При создании дженерика ты указываешь не только его тип, но и тип данных, с которыми он должен работать. Думаю, самый очевидный пример уже пришел тебе в голову — это ArrayList! Вот как мы обычно создаем его в программе:
import java.util.ArrayList; import java.util.List; public class Main < public static void main(String[] args) < ListmyList1 = 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)
Один из программистов прозевал этот комментарий и попытался по невнимательности положить в список числа вперемешку со строками, а потом посчитать их сумму:
Вывод в консоль: 300 Exception in thread «main» java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer at Main.main(Main.java:14) Что худшее в этой ситуации? Далеко не невнимательность программиста. Худшее то, что неправильный код попал в важное место нашей программы и успешно скомпилировался. Теперь мы увидим ошибку не на этапе написания кода, а только на этапе тестирования (и это в лучшем случае!). Исправление ошибок на более поздних этапах разработки стоит намного больше — и денег, и времени. Именно в этом заключается преимущество дженериков: класс-дженерик позволит незадачливому программисту обнаружить ошибку сразу же. Код просто не скомпилируется!
import java.util.ArrayList; import java.util.List; public class Main < public static void main(String[] args) < ListmyList1 = new ArrayList<>(); myList1.add(100); myList1.add(100); myList1.add("Lolkek");//ошибка! myList1.add("Shalala");//ошибка! > >
Программист сразу «очухается» и моментально исправится. Кстати, нам не обязательно было создавать свой собственный класс- List , чтобы увидеть ошибку такого рода. Достаточно просто убрать скобки с указанием типа (
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.Integer at Main.main(Main.java:16) То есть даже используя «родные» средства Java, можно допустить такую ошибку и создать небезопасную коллекцию. Однако, если вставить этот код в IDEa, мы увидим предупреждение: “Unchecked call to add(E) as a member of raw type of java.util.List” Нам подсказывают, что при добавлении элемента в коллекцию без дженериков что-то может пойти не так. Но что значит фраза «raw type»? Дословный перевод будет вполне точным — «сырой тип» или «грязный тип». Raw type — это класс-дженерик, из которого удалили его тип. Иными словами, List myList1 — это Raw type . Противоположностью raw type является generic type — класс-дженерик (также известный как parameterized type ), созданный правильно, с указанием типа. Например, List myList1 . У тебя мог возникнуть вопрос: а почему в языке вообще позволено использовать raw types ? Причина проста. Создатели Java оставили в языке поддержку raw types чтобы не создавать проблем с совместимостью. К моменту выхода Java 5.0 (в этой версии впервые появились дженерики) было написано уже очень много кода с использованием raw types . Поэтому такая возможность сохраняется и сейчас. Мы уже не раз упоминали классическую книгу Джошуа Блоха «Effective Java» в лекциях. Как один из создателей языка, он не обошел в книге и тему использования raw types и generic types . Глава 23 этой книги носит весьма красноречивое название: «Не используйте raw types в новом коде» Это то, что нужно запомнить. При использовании классов-дженериков ни в коем случае не превращай generic type в raw type .
Типизированные методы
Java позволяет тебе типизировать отдельные методы, создавая так называемые generic methods. Чем такие методы удобны? Прежде всего тем, что позволяют работать с разными типами параметров. Если к разным типам можно безопасно применять одну и ту же логику, дженерик-метод будет отличным решением. Рассмотрим пример. Допустим, у нас есть какой-то список myList1 . Мы хотим удалить из него все значения, и заполнить все освободившиеся места новым значением. Вот так будет выглядеть наш класс с дженерик-методом:
public class TestClass < public static void fill(List list, T val) < for (int i = 0; i < list.size(); i++) list.set(i, val); >public static void main(String[] args) < Liststrings = new ArrayList<>(); strings.add("Старая строка 1"); strings.add("Старая строка 2"); strings.add("Старая строка 3"); fill(strings, "Новая строка"); System.out.println(strings); List numbers = new ArrayList<>(); numbers.add(1); numbers.add(2); numbers.add(3); fill(numbers, 888); System.out.println(numbers); > >
public static void fill(List list, T val)
Перед типом возвращаемого значения написано
Типизированные классы
Ты можешь не только пользоваться представленными в Java дженерик-классами, но и создавать собственные! Вот простой пример:
public class Box < private T t; public void set(T t) < this.t = t; >public T get() < return t; >public static void main(String[] args) < BoxstringBox = new Box<>(); stringBox.set("Старая строка"); System.out.println(stringBox.get()); stringBox.set("Новая строка"); System.out.println(stringBox.get()); stringBox.set(12345);//ошибка компиляции! > >
Наш класс Box
И когда в последней строке кода мы пытаемся положить внутрь коробки число 12345, получаем ошибку компиляции! Вот так просто мы создали свой собственный дженерик-класс! 🙂 На этом наша сегодняшняя лекция подходит к концу. Но мы не прощаемся с дженериками! В следующий лекциях поговорим о более продвинутых возможностях, поэтому не прощаемся! ) Успехов в обучении! 🙂
Дженерики в Java для самых маленьких: синтаксис, границы и дикие карты
Разбираемся, зачем нужны дженерики и как добавить их в свой код.
Оля Ежак для Skillbox Media
Фулстек-разработчик. Любимый стек: Java + Angular, но в хорошей компании готова писать хоть на языке Ада.
У нас в парадной подъезде рядом с почтовыми ящиками стоит коробка. Предполагалось, что туда будут выбрасывать бумажный спам, который какие-то вредители упорно кладут в эти самые ящики. Но в коробке вместе с бумажками лежат пустые бутылки и банки, подозрительного вида пакеты, а в нынешних реалиях — ещё и использованные медицинские маски. Почему люди так делают? Потому что так можно.
Теперь представьте, что содержимое коробки вы отвозите на переработку, а перед этим каждый раз приходится отделять бумагу от прочего мусора. Не хотели бы вы заполучить такую коробку, которая не даст положить в себя что-то, кроме бумаги? Если ваш ответ «да» — вам понравятся дженерики (generics).
Содержание
Знакомимся с дженериками
До появления дженериков программисты могли неявно предполагать, что какой-то класс, интерфейс или метод работает с элементами определённого типа.
Посмотрите на этот фрагмент кода:
Паша быстро нашёл истинных виновников и попросил их исправить заполнение списка. Но на будущее решил подстраховаться от подобных ситуаций и переписал метод с использованием дженериков. Вот так:
Обратите внимание, что во второй версии Пашиного метода item не приводится насильно к типу String. Мы просто получаем в цикле очередной элемент списка, и компилятор соглашается, что это, очевидно, будет строка. Код стал менее громоздким, читать его стало проще.
Объявляем дженерик-классы и создаём их экземпляры
Давайте запрограммируем ту самую коробку, о которой шла речь в начале статьи: создадим класс Box, который умеет работать только с элементами определённого типа. Пусть для простоты в этой коробке пока будет только один элемент:
Собираем понятия, связанные с дженериками
Мы не успели разобраться с более сложными вещами — например, с заменами аргументов типов в классах-наследниках, с переопределением дженерик-методов, не узнали об особенностях коллекций с wildcards.
Обо всём этом и не только — в следующих статьях. А пока соберём небольшой словарик из терминов, которые связаны с использованием дженериков, — они пригодятся при чтении специальной литературы:
Термин | Расшифровка |
---|---|
Дженерик-типы (generic types) | Дженерик-класс или дженерик-интерфейс с одним или несколькими параметрами в заголовке |
Параметризованный тип (parameterized types) | Вызов дженерик-типа. Для дженерик-типа List параметризованным типом будет, например, List |
Параметр типа (type parameter) | Используются при объявлении дженерик-типов. Для Box T — это параметр типа |
Аргумент типа (type argument) | Тип объекта, который может использоваться вместо параметра типа. Например, для Box Paper — это аргумент типа |
Wildcard | Обозначается символом «?» — неизвестный тип |
Ограниченный wildcard (bounded wildcard) | Wildcard, который ограничен сверху — или снизу — |
Сырой тип (raw type) | Имя дженерик-типа без аргументов типа. Для List сырой тип — это List |
Ещё больше о дженериках, коллекциях и других элементах языка Java узнайте на нашем курсе «Профессия Java-разработчик». Научим программировать на самом востребованном языке и поможем устроиться на работу.
Переменные ссылочного типа хранят адрес ячейки в памяти, в которой лежит значение этой переменной.
В этом их ключевое отличие от примитивных типов, когда в переменной хранится само значение.
Все ссылочные типы в Java наследуются от типа Object.