Привет! Сегодня мы поговорим о дженериках. Если нужен короткий ответ: дженерик это тип с параметром, то есть класс или метод, которому при объявлении задают, с какими данными он работает. Запись List<String> означает список, куда компилятор пустит только строки. Появились дженерики в Java 5, и главный их смысл в том, чтобы ошибка с типом всплывала при компиляции, а не через полгода в работающей программе.

Кратко

  • Дженерик это класс или метод, которому при создании указывают тип данных. List<String> умеет работать только со строками, и это проверяет компилятор.
  • Польза не в удобстве записи, а в раннем обнаружении ошибок: неверный тип просто не скомпилируется, вместо того чтобы уронить программу потом.
  • Без дженериков список хранит Object, поэтому на выходе нужно приведение типов, и любая ошибка обернется ClassCastException уже при выполнении.
  • Raw type это дженерик, у которого убрали параметр типа: List list вместо List<String> list. Он остался в языке ради совместимости с кодом, написанным до Java 5, и в новом коде не нужен.
  • Типизировать можно и отдельный метод: <T> перед типом возвращаемого значения делает метод дженерик-методом, и одна и та же логика работает с разными типами.
  • Свой дженерик-класс пишется так же просто: public class Box<T>, а дальше T используется внутри как обычный тип.
Надо сказать, что ты выучишь много нового! Дженерикам будет посвящена не только эта, но еще и несколько следующих лекций. Три цветных блока, в каждом по четыре одинаковые чашки: в зеленом чайные, в красном кофейные, в фиолетовом стаканы навынос Поэтому, если эта тема тебе интересна, считай, что повезло: сегодня ты узнаешь многое об особенностях дженериков. Ну а если нет, смирись и расслабься! :) Это очень важная тема, и знать ее нужно. Давай начнем с простого: «что» и «зачем».

Что такое дженерики?

Дженерики это типы с параметром. При создании дженерика ты указываешь не только его тип, но и тип данных, с которыми он должен работать. Думаю, самый очевидный пример уже пришел тебе в голову, это 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 instanceof 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: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap') 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");//ошибка!
   }
}
Программист сразу «очухается» и моментально исправится.

Та же ошибка с обычным ArrayList

Кстати, нам не обязательно было создавать свой собственный класс-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: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap') 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»? Дословный перевод будет вполне точным: «сырой тип» или «грязный тип». Raw type это класс-дженерик, из которого удалили его тип. Иными словами, List myList1 это Raw type. Противоположность ему это generic type, то есть сам класс-дженерик, у которого есть параметр типа. А когда параметр подставлен и получилась конкретная запись вроде List<String> myList1, ее называют parameterized type, параметризованный тип. У тебя мог возникнуть вопрос: а почему в языке вообще позволено использовать raw types? Причина проста. Создатели Java оставили в языке поддержку raw types, чтобы не создавать проблем с совместимостью. К моменту выхода Java 5.0 (в этой версии впервые появились дженерики) было написано уже очень много кода с использованием raw types. Поэтому такая возможность сохраняется и сейчас. Как это устроено внутри, разбирается в отдельной лекции про стирание типов, а формальное описание есть в документации Oracle про raw types. Мы уже не раз упоминали классическую книгу Джошуа Блоха «Effective Java» в лекциях. Как один из архитекторов платформы Java и автор Java Collections Framework, он не обошел в книге и тему использования raw types и generic types. Обложка книги Джошуа Блоха Effective Java, третье изданиеПравило 26 этой книги носит весьма красноречивое название: «Не используйте raw types» Это то, что нужно запомнить. При использовании классов-дженериков ни в коем случае не превращай generic type в raw type. Если свести разницу в одну таблицу, получится так.
  List list (raw type) List<String> list (generic type)
Что примет add() любой объект, компилятор промолчит только String, все остальное не соберется
Что вернет get() Object, приводить тип придется вручную сразу String, приведение не нужно
Когда всплывет ошибка при выполнении, в виде ClassCastException при компиляции, прямо в редакторе
Что скажет IDE предупреждение Unchecked call to add(E) ничего, тут все типобезопасно
Зачем такая запись в языке совместимость с кодом, написанным до Java 5 обычный способ писать новый код

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

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, получаем ошибку компиляции! Вот так просто мы создали свой собственный дженерик-класс! :) На этом наша сегодняшняя лекция подходит к концу. Но мы не прощаемся с дженериками! В следующих лекциях поговорим о более продвинутых возможностях, поэтому не прощаемся! ) Успехов в обучении! :)

Вопросы и ответы

Что такое дженерики в Java простыми словами?

Это способ сказать классу или методу, с каким типом данных он работает. Вместо просто списка вы объявляете список строк или список чисел, и компилятор с этого момента следит, чтобы туда не попало ничего постороннего. Отсюда и определение: дженерик это тип с параметром.

Зачем нужны дженерики, если можно хранить все как Object?

Хранить можно, но ошибку вы поймаете не там, где ее сделали. Список из Object спокойно примет и число, и строку, а упадет он позже, при приведении типа, с ClassCastException. С дженериком тот же неверный элемент просто не даст программе собраться, и вы увидите ошибку сразу в редакторе. Чем позже найдена ошибка, тем дороже она обходится.

Что такое raw type и почему его не стоит использовать?

Raw type это дженерик, записанный без параметра типа: List list вместо List<String> list. Компилятор в этом случае теряет всю информацию о типе и перестает вас страховать, а IDE выдает предупреждение об unchecked-вызове. Такая запись оставлена в языке только ради старого кода, написанного до Java 5.

С какой версии Java появились дженерики?

С Java 5.0, она вышла в 2004 году. До нее коллекции хранили Object, и каждый элемент на выходе приходилось приводить к нужному типу руками. Именно поэтому raw types и остались в языке: к моменту выхода Java 5 такого кода было написано слишком много, чтобы просто сломать его новой версией.

Как написать свой дженерик-метод?

Перед типом возвращаемого значения ставится параметр типа в угловых скобках, а дальше он используется в сигнатуре как обычный тип: public static <T> void fill(List<T> list, T val). Такой метод работает с любым типом, но следит за согласованностью: список строк можно заполнить только строкой, список чисел только числом.

Почему нельзя написать List с примитивом внутри?

Параметром дженерика может быть только ссылочный тип, поэтому List<int> не скомпилируется. Вместо примитива берут класс-обертку: List<Integer>. Именно поэтому во всех примерах выше стоит Integer, а не int. Само число при добавлении в список превращается в объект автоматически.

Читайте также