JavaRush/Java блог/Java Developer/Что такое дженерики в Java
Автор
Владимир Портянко
Java-разработчик в Playtika

Что такое дженерики в Java

Статья из группы Java Developer
участников
Привет! Сегодня мы поговорим о дженериках. Надо сказать, что ты выучишь много нового! Дженерикам будет посвящена не только эта, но еще и несколько следующих лекций. Что такое дженерики в Java - 1 Поэтому, если эта тема тебе интересна — тебе повезло: сегодня ты узнаешь многое об особенностях дженериков. Ну а если нет — смирись и расслабься! :) Это очень важная тема, и знать ее нужно. Давай начнем с простого: «что» и «зачем». Что такое дженерики? Дженерики — это типы с параметром. При создании дженерика ты указываешь не только его тип, но и тип данных, с которыми он должен работать. Думаю, самый очевидный пример уже пришел тебе в голову — это ArrayList! Вот как мы обычно создаем его в программе:
import java.util.ArrayList;
import java.util.List;

public class Main {

   public static void main(String[] args) {

       List<String> myList1 = 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) {
   this.data[count] = o;
   count++;
}
Один из программистов прозевал этот комментарий и попытался по невнимательности положить в список числа вперемешку со строками, а потом посчитать их сумму:
public class Main {

   public static void main(String[] args) {

       MyListClass list = new MyListClass();
       list.add(100);
       list.add(200);
       list.add("Lolkek");
       list.add("Shalala");

       Integer sum1 = (Integer) list.getData()[0] + (Integer) list.getData()[1];
       System.out.println(sum1);

       Integer sum2 = (Integer) list.getData()[2] + (Integer) list.getData()[3];
       System.out.println(sum2);
   }
}
Вывод в консоль: 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) {

       List<Integer> myList1 = new ArrayList<>();

       myList1.add(100);
       myList1.add(100);
       myList1.add("Lolkek");//ошибка!
       myList1.add("Shalala");//ошибка!
   }
}
Программист сразу «очухается» и моментально исправится. Кстати, нам не обязательно было создавать свой собственный класс-List, чтобы увидеть ошибку такого рода. Достаточно просто убрать скобки с указанием типа (<Integer>) из обычного ArrayList!
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<String> myList1. У тебя мог возникнуть вопрос: а почему в языке вообще позволено использовать raw types? Причина проста. Создатели Java оставили в языке поддержку raw types чтобы не создавать проблем с совместимостью. К моменту выхода Java 5.0 (в этой версии впервые появились дженерики) было написано уже очень много кода с использованием raw types. Поэтому такая возможность сохраняется и сейчас. Мы уже не раз упоминали классическую книгу Джошуа Блоха «Effective Java» в лекциях. Как один из создателей языка, он не обошел в книге и тему использования raw types и generic types. Что такое дженерики в Java - 2Глава 23 этой книги носит весьма красноречивое название: «Не используйте raw types в новом коде» Это то, что нужно запомнить. При использовании классов-дженериков ни в коем случае не превращай generic type в raw type.

Типизированные методы

Java позволяет тебе типизировать отдельные методы, создавая так называемые generic methods. Чем такие методы удобны? Прежде всего тем, что позволяют работать с разными типами параметров. Если к разным типам можно безопасно применять одну и ту же логику, дженерик-метод будет отличным решением. Рассмотрим пример. Допустим, у нас есть какой-то список myList1. Мы хотим удалить из него все значения, и заполнить все освободившиеся места новым значением. Вот так будет выглядеть наш класс с дженерик-методом:
public class TestClass {

   public static <T> void fill(List<T> list, T val) {
       for (int i = 0; i < list.size(); i++)
           list.set(i, val);
   }

   public static void main(String[] args) {

       List<String> strings = new ArrayList<>();
       strings.add("Старая строка 1");
       strings.add("Старая строка 2");
       strings.add("Старая строка 3");

       fill(strings, "Новая строка");

       System.out.println(strings);

       List<Integer> numbers = new ArrayList<>();
       numbers.add(1);
       numbers.add(2);
       numbers.add(3);

       fill(numbers, 888);
       System.out.println(numbers);
   }
}
Обрати внимание на синтаксис, он выглядит немного необычно:
public static <T> void fill(List<T> list, T val)
Перед типом возвращаемого значения написано <T>, что указывает на дженерик метод. В данном случае метод принимает на вход 2 параметра: список объектов T и еще один отдельный объект Т. За счет использования <T> и достигается типизация метода: мы не можем передать туда список строк и число. Список строк и строку, список чисел и число, список наших объектов Cat и еще один объект Cat — только так. В методе main() наглядно демонстрируется, что метод fill() легко работает с разными типами данных. Сначала он принимает на вход список строк и строку, а потом — список чисел и число. Вывод в консоль: [Новая строка, Новая строка, Новая строка] [888, 888, 888] Представь, если бы логика метода fill() нужна была бы нам для 30 разных классов, и у нас не было бы дженерик-методов. Мы вынуждены были бы писать один и тот же метод 30 раз, просто для разных типов данных! Но благодаря generic-методам мы можем использовать наш код повторно! :)

Типизированные классы

Ты можешь не только пользоваться представленными в Java дженерик-классами, но и создавать собственные! Вот простой пример:
public class Box<T> {

   private T t;

   public void set(T t) {
       this.t = t;
   }

   public T get() {
       return t;
   }

   public static void main(String[] args) {

       Box<String> stringBox = new Box<>();

       stringBox.set("Старая строка");
       System.out.println(stringBox.get());
       stringBox.set("Новая строка");

       System.out.println(stringBox.get());

       stringBox.set(12345);//ошибка компиляции!
   }
}
Наш класс Box<T> («коробка») является типизированным. Назначив для него при создании тип данных (<T>), мы уже не сможем помещать в него объекты других типов. Это видно в примере. При создании мы указали, что наш объект будет работать со строками:
Box<String> stringBox = new Box<>();
И когда в последней строке кода мы пытаемся положить внутрь коробки число 12345, получаем ошибку компиляции! Вот так просто мы создали свой собственный дженерик-класс! :) На этом наша сегодняшняя лекция подходит к концу. Но мы не прощаемся с дженериками! В следующий лекциях поговорим о более продвинутых возможностях, поэтому не прощаемся! ) Успехов в обучении! :)
Комментарии (41)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
Александр
Уровень 34
3 марта, 18:46
Чудесная для понимания статья. Спасибо!
Олег
Уровень 111
Expert
6 апреля 2023, 21:38
"Назначив для него при создании тип данных (<T>), мы уже не сможем помещать в него объекты других типов." Т.е. в этот класс не сможем поместить объекты других типов? Да сколько угодно(немного расширил код другими объектами): public class Box<T> { private T t; public void set(T t) { this.t = t; } public T get() { return t; } public static void main(String[] args) { Box<String> stringBox = new Box<>(); stringBox.set("Старая строка"); System.out.println(stringBox.get()); stringBox.set("Новая строка"); System.out.println(stringBox.get()); Box<Integer> intBox3 = new Box<>(); intBox3.set(258); System.out.println(intBox3.get()); intBox3.set(357); System.out.println(intBox3.get()); Box<Double> dubBox4 = new Box<>(); dubBox4.set(258.159); System.out.println(dubBox4.get()); dubBox4.set(357.267); System.out.println(dubBox4.get()); } }
wokku
Уровень 51
17 июля 2023, 12:23
Там речь про то, что в объект, у которого дженерик String ты не сможешь засунуть, например, int. В твоем же коде ты создаешь разные объекты с разными дженериками.
Denis Gritsay
Уровень 38
20 ноября 2023, 16:20
ваш код как раз подтверждает автора статьи а не опровергает.
Бромгексин
Уровень 16
28 марта, 15:56
не int а Integer*
Andrey Karelin
Уровень 41
30 апреля 2022, 15:03
Вот сколько читаю про дженерики, нигде не проговаривается очевидная вещь, которая помогает понять их суть, а именно то, что использование типа T, K, V и т.п. обозначает лишь условный тип класса. И главное тут не название, а то какие типы методов/переменных объекта будут возвращаться/работать, по отношению к определенному изначально типу. Например, указанный в примере метод public static <T> void fill(List<T> list, T val) говорит нам о том, что в метод мы можем передать list и val только одинакового между собой типа, и такого же типа, которым при создании был определен класс.
Kurama
Уровень 50
29 января 2023, 19:13
Ну, на 40+ уровне - это слишком очевидно... Такое замечание было бы уместно, после того, как я впервые прочитал про дженерики, хотя со временем они стали мне такими же понятными, как массивы
Глеб
Уровень 29
Expert
21 апреля 2022, 16:04
Полезная статья
Q1R27
Уровень 17
28 марта 2022, 18:31
Lex Bekker
Уровень 12
8 марта 2022, 17:12
Жора Нет
Уровень 39
17 апреля 2022, 16:34
Ссылки не работают
Anonymous #2721543
Уровень 20
10 мая 2022, 16:18
Работают, если в адресной строке .ru заменить на .com.ua
Сергей Зотов
Уровень 11
15 сентября 2022, 16:36
уже работают
Nikita Backend Developer
31 мая 2023, 08:07
Спасибо! В лекции к сожалению прям не хватает ссылок на следующие материалы 👍👍👍
LuneFox Java Developer в BIFIT Expert
26 января 2022, 10:45
Добавлю ещё, кстати, что писать именно T не обязательно. Как я понимаю, это конвенция от слова Type. Допускается написание любой лабуды в качестве маркера для дженерика, например:
public static <KEK> KEK fill(List<KEK> list, KEK value) {
    for (int i = 0; i < list.size(); i++) {
        list.set(i, value);
    }
    return list.get(0);
}
При этом повторный код с другим названием этого типа ловится IDE-шкой. Именно поэтому перед объявлением возвращаемого значения не написать <T>, то программа будет думать, что это не дженерик, а настоящий класс с названием T, и будет ругаться, что не знает такого класса или попросит, чтобы ты создал class T{ }. Конечно, то, что я написал -- наверняка очевидные вещи, но иногда полезно пощупать код руками и попытаться "сломать систему", чтобы в голове отложилась чёткая картинка, как делать можно, а как нельзя, и почему.
hamster🐹 ClipMaker в TikTok
12 ноября 2021, 07:53
Отличная статья! Спасибо)
MICRO_MVP_10011
Уровень 37
5 ноября 2021, 14:38
Спасибо!
Игорь Full Stack Developer в IgorApplications
27 июля 2021, 19:27
Дженирики существуют только на этапе компиляции, в байт коде они уничтожаются (стирание). К примеру нельзя вызвать метод у дженирика, только через приведение типов, но это тот же Object получается, нельзя унаследоваться.
LuneFox Java Developer в BIFIT Expert
26 января 2022, 10:29
Логично, вряд ли джава-машина «прозевает» свой собственный комментарий и положит в список то, что не нужно. Все ошибки будут уже отброшены на этапе компиляции. Дженерики, как я понял, нужны чисто для удобства и защиты от ошибок программистов.
Игорь Full Stack Developer в IgorApplications
5 февраля 2022, 08:59
Да, они нужны для этого, кроме List'а я в своей практике использовал Class<? extends Figure> и тд. Но в других языках это более мощные вещи, к примеру в c++ от него можно даже унаследовать.