Сегодня я узнал..

Обобщения — это средства универсального программирования, которые были добавлены к языку программирования Java в 2004 году в рамках официальной версии J2SE 5.0. Они были разработаны для расширения системы типов Java, чтобы позволить «типу или методу работать с объектами различных типов, обеспечивая при этом безопасность типов во время компиляции. — Википедия

Скобки со стрелками, которые вы видите в Java, определяют дженерик. Обобщения выглядят так: ‹T› или ‹Integer› или что-то еще, что вы хотите поместить в него. Вы часто будете видеть, что он используется с такими типами данных, как ArrayList или HashMap. Например, ArrayList<Integer> означает, что массив ограничен использованием только типов Integer или любых подклассов Integer.

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

Например, вот 3 метода, которые очень похожи и отличаются только входными массивами:

// Print all elements in an array of Integers
public static void printArray(Integer[] arr)
{
  for (Integer el : arr)
    System.out.printf("%s ", el);

  System.out.println();
}
// Print all elements in an array of Doubles
public static void printArray(Double[] arr)
{
  for (Double el : arr)
    System.out.printf("%s ", el);

  System.out.println();
}
// Print all elements in an array of Chars
public static void printArray(Character[] arr)
{
  for (Character el : arr)
    System.out.printf("%s ", el);

  System.out.println();
}

Используя дженерик, мы можем абстрагироваться и провести рефакторинг:

// Print all elements in an array, whether it be chars, doubles, or integers.
public static <T> void printArray(T[] arr)
{
  for (T el : arr)
    System.out.printf("%s ", el);

  System.out.println();
}

Обобщения устраняют необходимость приведения типов. Например:

import java.util.ArrayList;
  
public class GenericTypeCastTest
{
  public static void main(String[] args)
  {
    // Following requires typecasting
    ArrayList list = new ArrayList();
    list.add("hello");
    String s = (String) list.get(0);
    System.out.printf("%s%n", s);
    
    // Following uses generics and requires no typecasting
    ArrayList<String> list2 = new ArrayList<String>();
    list2.add("world");
    String s2 = list2.get(0);
    System.out.printf("%s%n", s2);
  }
}
OUTPUT:
hello
world

Кроме того, первый пример, требующий приведения типов, выдает предупреждающее сообщение:

1 warning found:
File: C:\PATH_TO_Java_File\GenericTypeCastTest.java  [line: 9]
Warning: unchecked call to add(E) as a member of the raw type java.util.ArrayList

Компилятор Java применяет строгую проверку типов при использовании дженериков. Это означает, что вместо отладки сложной ошибки времени выполнения мы получим ошибки времени компиляции.

Лично я считаю, что дженерики полезны для СУХОЙ обработки вашего кода, сводя к минимуму использование перегруженных (и повторяющихся) методов. Вы можете использовать дженерики не только для методов, но и для классов, интерфейсов и типов.

Я не буду рассматривать следующие понятия, но если вы хотите углубиться в них: общие подстановочные знаки, ограниченные и неограниченные подстановочные знаки, верхняя граница и нижняя граница, автобокс, необработанный тип и стирание.