Заметки о generics
Обобщённое программирование — это такой подход к описанию данных и алгоритмов, который позволяет их использовать с различными типами данных без изменения их описания. В Java, начиная с версии J2SE 5.0, добавлены средства обобщённого программирования, синтаксически основанные на C++. Ниже будут рассматриваться generics (дженерики) или <<контейнеры типа T>> — подмножество обобщённого программирования.
Существенное преимущество при использовании дженериков состоит в том, что некоторые ошибки, связанные с приведением типов могут быть отловлены на момент компиляции. Ошибка компиляции «лучше» ошибки времени выполнения, т.к. чисто теоретически скомпилированный код с ошибкой может попасть туда, куда ему лучше бы и не попадать. Это очевидное достоинство дженериков.
После имени класса в угловых скобках "<" и ">" указано имя типа "Т", которое может использоваться внутри класса. Фактически Т – это тип, который должен быть определён позже (при создании объекта класса).
При создании дженерик-классов мы не ограничены одним лишь типом (Т) – их может быть несколько.
Алмазный синтаксис (Diamond syntax)
Немного лениво каждый раз заполнять типы и при этом можно ошибиться. Чтобы упростить жизнь программистам в Java 7 был введён алмазный синтаксис (diamond syntax), в котором можно опустить параметры типа. Т.е. можно предоставить компилятору определение типов при создании объекта. Вид упрощённого объявления:
Pair
Универсальные методы (Generic methods)
По аналогии с универсальными классами (дженерик-классами), можно создавать универсальные методы (дженерик-методы), то есть методы, которые принимают общие типы параметров. Универсальные методы не надо путать с методами в дженерик-классе. Универсальные методы удобны, когда одна и та же функциональность должна применяться к различным типам. (Например, есть многочисленные общие методы в классе java.util.Collections.)
import java.util.ArrayList;
import java.util.List;
class Utilities {
public static <T> void fill(List<T> list, T val) {
for (int i = 0; i < list.size(); i++)
list.set(i, val);
}
}
class Test {
public static void main(String[] args) {
List<Integer> intList = new ArrayList<Integer>();
intList.add(1);
intList.add(2);
}
}
Wildcards (Маски)
Сейчас будут рассмотрены Wildcard Parameters (wildcards). Этот термин в разных источниках переводится по-разному: метасимвольные аргументы, подстановочные символы, групповые символы, шаблоны, маски и т.д.
Вот такая строка кода не скомпилируется:
List
Но есть возможность похожей реализации:
List<?> intList = new ArrayList
Под маской мы будем понимать вот эту штуку – " <?> ".
Пример кода использующего маску и пригодного к компиляции:
class Test {
static void printList(List<?> list) {
for (Object l : list)
System.out.println("{" + l + "}");
}
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(10);
list.add(100);
printList(list);
List<String> strList = new ArrayList<>();
strList.add("10");
strList.add("100");
printList(strList);
}
}
Однако вот это не скомпилируется:
List<?> intList = new ArrayList<Integer>();
intList.add(new Integer(10));
/* intList.add(new Float(10.0f)); даже с закомментированной последней строкой не скомпилируется */
Почему не компилируется? При использовании маски мы сообщаем компилятору, чтобы он игнорировал информацию о типе, т.е. <?> - неизвестный тип. При каждой попытке передачи аргументов дженерик-типа компилятор Java пытается определить тип переданного аргумента. Однако теперь мы используем метод add () для вставки элемента в список. При использовании маски мы не знаем, какого типа аргумент может быть передан. Тут вновь видна возможность ошибки, т.к. если бы добавление было возможно, то мы могли бы попытаться вставить в наш список, предназначенный для чисел, строковое значение. Во избежание этой проблемы, компилятор не позволяет вызывать методы, которые могут добавить невалидный тип - например, добавить значение типа Float, с которым мы потом попробуем работать как с Integer (или String - по маске не определишь точно). Тем не менее есть возможность получить доступ к информации, хранящейся в объекте, с использованием маски, как это было показано выше.
Маске может быть задано ограничение:
List<? extends Number> numList = new ArrayList<Integer>();
numList = new ArrayList<Double>();
То, что было описано выше называется ограниченными масками (Bounded wildcards). Применение таких конструкций может быть весьма красивым и полезным. Допустим нам необходимо посчитать сумму чисел различного типа, которые хранятся в одном списке:
public static Double sum(List<? extends Number> numList) {
Double result = 0.0;
for (Number num : numList) {
result += num.doubleValue();
}
return result;
}
В завершение этой темы добавлю, что аналогично ключевому слову extends в подобного рода выражениях может использоваться ключевое слово super - "<? super Integer> ". Выражение <? super X> означает, что вы можете использовать любой базовый тип (класс или интерфейс) типа Х, а также и сам тип Х. Пара строк, которые нормально скомпилируются:
List<? super Integer> intList = new ArrayList<Integer>();
System.out.println("The intList is: " + intList);