:notebook: Эта статья была заархивирована в: "blog"
:keyboard: Пример кода в этой статье был заархивирован в: "javacore"
Зачем нужны дженерики
JDK5 представил общий механизм.
Зачем нужны дженерики? Прежде чем ответить на этот вопрос, давайте рассмотрим пример.
public class NoGenericsDemo {
public static void main(String[] args) {
List list = new ArrayList<>();
list.add("abc");
list.add(18);
list.add(new double[] {1.0, 2.0});
Object obj1 = list.get(0);
Object obj2 = list.get(1);
Object obj3 = list.get(2);
System.out.println("obj1 = [" + obj1 + "]");
System.out.println("obj2 = [" + obj2 + "]");
System.out.println("obj3 = [" + obj3 + "]");
int num1 = (int)list.get(0);
int num2 = (int)list.get(1);
int num3 = (int)list.get(2);
System.out.println("num1 = [" + num1 + "]");
System.out.println("num2 = [" + num2 + "]");
System.out.println("num3 = [" + num3 + "]");
}
}
// Output:
// obj1 = [abc]
// obj2 = [18]
// obj3 = [[D@47089e5f]
// Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
// at io.github.dunwu.javacore.generics.NoGenericsDemo.main(NoGenericsDemo.java:23)
Пример описания:
В приведенном выше примере
List
Контейнер не указывает тип данных хранилища, в этом случае вы можетеList
Добавьте любой тип данных, компилятор не будет выполнять проверку типов, а молча преобразует все данные вObject
.Допустим, мы изначально хотим
List
В нем хранятся целочисленные данные, допустим, кто-то случайно сохранил другой тип данных. Когда вы пытаетесь получить целочисленные данные из контейнера из-заList
в видеObject
тип для хранения, вы должны использовать принуждение типа. Во время выполнения он будет найденList
Проблема несогласованного хранения данных в программе влечет за собой большой риск для работы программы (невидимая поломка - самая смертельная).
Появление дженериков решает проблему безопасности типов.
Дженерики имеют следующие преимущества:
- Строгая проверка типов во время компиляции
Обобщения требуют, чтобы фактический тип данных был указан во время объявления.Компилятор Java выполнит строгую проверку типов универсального кода во время компиляции и выдаст предупреждение, когда код нарушит безопасность типов. Раннее обнаружение, раннее управление и скрытые опасности убиваются еще в колыбели.Затраты на поиск и исправление ошибок во время компиляции намного меньше, чем во время выполнения.
- избегает преобразования типов
Дженерики не используются:
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
Используйте дженерики:
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0); // no cast
- Общее программирование может реализовать общие алгоритмы
Используя дженерики, программисты могут реализовывать алгоритмы общего назначения, которые работают с коллекциями разных типов, которые можно настраивать, которые безопасны для типов и легко читаются.
универсальный тип
泛型类型
параметризованный класс или интерфейс.
общий класс
Синтаксис универсального класса:
class name<T1, T2, ..., Tn> { /* ... */ }
Объявления универсального класса аналогичны объявлениям неуниверсального класса, за исключением того, что раздел объявления параметра типа добавляется после имени класса. угловыми скобками (<>
) часть параметра типа с разделителями следует за именем класса. Он определяет параметры типа (также называемые переменными типа) T1, T2, ... и Tn.
Как правило, имя класса в универсальном типепрототип, в то время как<>
Указанный параметр называетсятип параметра.
- Класс без применения дженериков
До появления дженериков, если класс хотел хранить данные любого типа, он мог использовать толькоObject
Сделайте преобразование типов. Пример выглядит следующим образом:
public class Info {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
- Общий класс с параметром одного типа
public class Info<T> {
private T value;
public Info() { }
public Info(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
@Override
public String toString() {
return "Info{" + "value=" + value + '}';
}
}
public class GenericsClassDemo01 {
public static void main(String[] args) {
Info<Integer> info = new Info<>();
info.setValue(10);
System.out.println(info.getValue());
Info<String> info2 = new Info<>();
info2.setValue("xyz");
System.out.println(info2.getValue());
}
}
// Output:
// 10
// xyz
В приведенном выше примере при инициализации универсального класса используйте<>
Если указан внутренний конкретный тип, во время компиляции будет выполняться строгая проверка типа на основе этого типа.
На самом деле не использовать<>
Указание внутреннего конкретного типа также поддерживается синтаксически (не рекомендуется) следующим образом:
public static void main(String[] args) {
Info info = new Info();
info.setValue(10);
System.out.println(info.getValue());
info.setValue("abc");
System.out.println(info.getValue());
}
Пример описания:
Приведенный выше пример не будет генерировать ошибок компиляции и будет работать нормально. Но такой вызов теряет преимущества универсальных типов.
- Общий класс с несколькими параметрами типа
public class MyMap<K,V> {
private K key;
private V value;
public MyMap(K key, V value) {
this.key = key;
this.value = value;
}
@Override
public String toString() {
return "MyMap{" + "key=" + key + ", value=" + value + '}';
}
}
public class GenericsClassDemo02 {
public static void main(String[] args) {
MyMap<Integer, String> map = new MyMap<>(1, "one");
System.out.println(map);
}
}
// Output:
// MyMap{key=1, value=one}
- Типовая вложенность универсальных классов
public class GenericsClassDemo03 {
public static void main(String[] args) {
Info<String> info = new Info("Hello");
MyMap<Integer, Info<String>> map = new MyMap<>(1, info);
System.out.println(map);
}
}
// Output:
// MyMap{key=1, value=Info{value=Hello}}
общий интерфейс
Интерфейсы также могут объявлять дженерики.
Общая форма синтаксиса интерфейса:
public interface Content<T> {
T text();
}
Существует два способа реализации универсального интерфейса:
- Подклассы, реализующие интерфейс, явно объявляют общий тип
public class GenericsInterfaceDemo01 implements Content<Integer> {
private int text;
public GenericsInterfaceDemo01(int text) {
this.text = text;
}
@Override
public Integer text() { return text; }
public static void main(String[] args) {
GenericsInterfaceDemo01 demo = new GenericsInterfaceDemo01(10);
System.out.print(demo.text());
}
}
// Output:
// 10
- Подкласс, реализующий интерфейс, явно не объявляет универсальный тип.
public class GenericsInterfaceDemo02<T> implements Content<T> {
private T text;
public GenericsInterfaceDemo02(T text) {
this.text = text;
}
@Override
public T text() { return text; }
public static void main(String[] args) {
GenericsInterfaceDemo02<String> gen = new GenericsInterfaceDemo02<>("ABC");
System.out.print(gen.text());
}
}
// Output:
// ABC
общий метод
Универсальный метод — это метод, который вводит свой собственный параметр типа. Общие методы могут быть обычными методами, статическими методами и конструкторами.
Синтаксис универсального метода следующий:
public <T> T func(T obj) {}
Наличие у него универсального метода не имеет ничего общего с тем, является ли класс, в котором он находится, универсальным или нет.
Синтаксис универсального метода состоит из списка параметров типа в угловых скобках, которые появляются перед возвращаемым типом метода. Для статических универсальных методов часть параметра типа должна стоять перед возвращаемым типом метода. Параметры типа могут использоваться для объявления возвращаемого типа и могут использоваться в качестве заполнителей для фактических параметров типа, которые получает универсальный метод.
При использовании универсальных методов обычно нет необходимости указывать параметр типа, потому что компилятор сам узнает конкретный тип за нас. Это называется выводом аргумента типа. Вывод типа допустим только для операций присваивания, а не иначе. Если результат вызова универсального метода передается в качестве параметра другому методу, компилятор не будет выполнять вывод. Компилятор будет думать, что после вызова универсального метода его возвращаемое значение присваивается переменной типа Object.
public class GenericsMethodDemo01 {
public static <T> void printClass(T obj) {
System.out.println(obj.getClass().toString());
}
public static void main(String[] args) {
printClass("abc");
printClass(10);
}
}
// Output:
// class java.lang.String
// class java.lang.Integer
Списки переменных параметров также можно использовать в универсальных методах.
public class GenericVarargsMethodDemo {
public static <T> List<T> makeList(T... args) {
List<T> result = new ArrayList<T>();
Collections.addAll(result, args);
return result;
}
public static void main(String[] args) {
List<String> ls = makeList("A");
System.out.println(ls);
ls = makeList("A", "B", "C");
System.out.println(ls);
}
}
// Output:
// [A]
// [A, B, C]
стирание типа
В языке Java появились дженерики для обеспечения более строгой проверки типов во время компиляции и для поддержки универсальных программ. В отличие от механизма шаблонов C++,Дженерики Java реализованы с использованием стирания типов, при использовании дженериков любая конкретная информация о типе стирается..
Итак, что делает стирание типа? Он делает следующее:
- Заменяет все параметры типа в универсальном на Object, и если указана граница типа, вместо нее используется граница типа. Поэтому сгенерированный байт-код содержит только обычные классы, интерфейсы и методы.
- Стереть вхождения объявлений типа, т.е. удалить
<>
Содержание. НапримерT get()
Объявление метода становитсяObject get()
;List<String>
это становитсяList
. При необходимости вставьте преобразования типов для обеспечения безопасности типов. - Создавайте промежуточные методы для сохранения полиморфизма в расширенных универсальных типах. Стирание типов гарантирует, что для параметризованных типов не будут создаваться новые классы, поэтому дженерики не несут накладных расходов во время выполнения.
Давайте посмотрим пример:
public class GenericsErasureTypeDemo {
public static void main(String[] args) {
List<Object> list1 = new ArrayList<Object>();
List<String> list2 = new ArrayList<String>();
System.out.println(list1.getClass());
System.out.println(list2.getClass());
}
}
// Output:
// class java.util.ArrayList
// class java.util.ArrayList
Пример описания:
В приведенном выше примере, несмотря на то, что указаны параметры разных типов, информация о классах list1 и list2 одинакова.
Это потому что:При использовании дженериков вся информация о конкретном типе стирается.. это означает:
ArrayList<Object>
иArrayList<String>
Во время выполнения JVM обрабатывает их как один и тот же тип.
Реализация дженериков Java не очень элегантна, но это потому, что дженерики были введены в JDK 5. Чтобы быть совместимым со старым кодом, в дизайне должны быть сделаны определенные компромиссы.
Дженерики и наследование
Универсальные шаблоны нельзя использовать в операциях, которые явно ссылаются на типы времени выполнения, таких как приведения типов, операции instanceof и новые выражения. потому что вся информация о типе параметра теряется. Когда вы пишете общий код, вы всегда должны напоминать себе, что у вас просто есть информация о типе параметров.
Именно потому, что дженерики реализованы на основе стирания типов, поэтомуОбщие типы не могут быть преобразованы.
Повышение приведения относится к инициализации родительского класса экземпляром подкласса, что является важным проявлением полиморфизма в объектной ориентации.
Integer
наследоватьObject
;ArrayList
наследоватьList
;ноList<Interger>
не унаследованоList<Object>
.
Это связано с тем, что общие классы не имеют собственных уникальныхClass
объект класса. Например: не существуетList<Object>.class
илиList<Interger>.class
, компилятор Java будет рассматривать оба какList.class
.
List<Integer> list = new ArrayList<>();
List<Object> list2 = list; // Erorr
граница типа
Иногда может потребоваться ограничить типы, которые можно использовать в качестве параметров типа в параметризованном типе.类型边界
Вы можете установить ограничения на параметры универсального типа. Например, метод, работающий с числами, может принимать толькоNumber
или экземпляр его подклассов.
Чтобы объявить параметр ограниченного типа, укажите имя параметра типа, за которым следуетextends
ключевое слово, за которым следует его ограничивающий класс или интерфейс.
Синтаксическая форма границы типа выглядит следующим образом:
<T extends XXX>
Пример:
public class GenericsExtendsDemo01 {
static <T extends Comparable<T>> T max(T x, T y, T z) {
T max = x; // 假设x是初始最大值
if (y.compareTo(max) > 0) {
max = y; //y 更大
}
if (z.compareTo(max) > 0) {
max = z; // 现在 z 更大
}
return max; // 返回最大对象
}
public static void main(String[] args) {
System.out.println(max(3, 4, 5));
System.out.println(max(6.6, 8.8, 7.7));
System.out.println(max("pear", "apple", "orange"));
}
}
// Output:
// 5
// 8.8
// pear
Пример описания:
В приведенном выше примере объявляется универсальный метод с параметрами типа.
T extends Comparable<T>
Указывает, что тип, передаваемый в метод, должен реализовывать интерфейс Comparable.
Можно установить несколько границ типов, и синтаксис выглядит следующим образом:
<T extends B1 & B2 & B3>
Примечание. Первый параметр типа после ключевого слова extends может быть классом или интерфейсом, а другие параметры типа могут быть только интерфейсами.
Пример:
public class GenericsExtendsDemo02 {
static class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }
static class D1 <T extends A & B & C> { /* ... */ }
static class D2 <T extends B & A & C> { /* ... */ } // 编译报错
static class E extends A implements B, C { /* ... */ }
public static void main(String[] args) {
D1<E> demo1 = new D1<>();
System.out.println(demo1.getClass().toString());
D1<String> demo2 = new D1<>(); // 编译报错
}
}
введите подстановочный знак
类型通配符
обычно используют?
Вместо параметров конкретного типа. НапримерList<?>
логически даList<String>
,List<Integer>
ждать всехList<具体类型实参>
родительский класс.
подстановочный знак верхней границы
можно использовать**上界通配符
** чтобы сузить диапазон типов параметра типа.
Его грамматическая форма:<? extends Number>
public class GenericsUpperBoundedWildcardDemo {
public static double sumOfList(List<? extends Number> list) {
double s = 0.0;
for (Number n : list) {
s += n.doubleValue();
}
return s;
}
public static void main(String[] args) {
List<Integer> li = Arrays.asList(1, 2, 3);
System.out.println("sum = " + sumOfList(li));
}
}
// Output:
// sum = 6.0
Подстановочный знак Нижнего мира
**下界通配符
** Ограничьте неизвестные типы определенными типами или типами суперкласса этого типа.
Уведомление:Подстановочный знак верхней границы и подстановочный знак нижней границы не могут использоваться одновременно.
Его грамматическая форма:<? super Number>
public class GenericsLowerBoundedWildcardDemo {
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 5; i++) {
list.add(i);
}
}
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
addNumbers(list);
System.out.println(Arrays.deepToString(list.toArray()));
}
}
// Output:
// [1, 2, 3, 4, 5]
неограниченный подстановочный знак
Существует два сценария применения неограниченных подстановочных знаков:
- Методы, которые могут быть реализованы с использованием функций, предоставляемых в классе Object.
- Используйте методы в универсальных классах, которые не зависят от параметров типа.
Синтаксическая форма:<?>
public class GenericsUnboundedWildcardDemo {
public static void printList(List<?> list) {
for (Object elem : list) {
System.out.print(elem + " ");
}
System.out.println();
}
public static void main(String[] args) {
List<Integer> li = Arrays.asList(1, 2, 3);
List<String> ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);
}
}
// Output:
// 1 2 3
// one two three
Подстановочные знаки и преобразование
Ранее мы упоминали:Дженерики нельзя повышать. Тем не менее, мы можем повысить уровень, используя подстановочные знаки..
public class GenericsWildcardDemo {
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
List<Number> numList = intList; // Error
List<? extends Integer> intList2 = new ArrayList<>();
List<? extends Number> numList2 = intList2; // OK
}
}
Дальнейшее чтение:Документация по универсальным средствам Oracle
общие ограничения
Pair<int, char> p = new Pair<>(8, 'a'); // 编译错误
public static <E> void append(List<E> list) {
E elem = new E(); // 编译错误
list.add(elem);
}
public class MobileDevice<T> {
private static T os; // error
// ...
}
public static <E> void rtti(List<E> list) {
if (list instanceof ArrayList<Integer>) { // 编译错误
// ...
}
}
List<Integer> li = new ArrayList<>();
List<Number> ln = (List<Number>) li; // 编译错误
List<Integer>[] arrayOfLists = new List<Integer>[2]; // 编译错误
// Extends Throwable indirectly
class MathException<T> extends Exception { /* ... */ } // 编译错误
// Extends Throwable directly
class QueueFullException<T> extends Throwable { /* ... */ // 编译错误
public static <T extends Exception, J> void execute(List<J> jobs) {
try {
for (J job : jobs)
// ...
} catch (T e) { // compile-time error
// ...
}
}
public class Example {
public void print(Set<String> strSet) { }
public void print(Set<Integer> intSet) { } // 编译错误
}
Рекомендации по использованию дженериков
общее наименование
Некоторые общие соглашения об именах для дженериков:
- E - Element
- K - Key
- N - Number
- T - Type
- V - Value
- S,U,V etc. - 2nd, 3rd, 4th types
Рекомендации по использованию дженериков
- Устранение предупреждений о проверке типов
- Список предпочтительнее массива
- Расставьте приоритеты в использовании дженериков для повышения универсальности кода.
- Предпочитайте универсальные методы для универсальных методов
- Повышение гибкости API с ограниченными подстановочными знаками
- Отдавайте предпочтение гетерогенным контейнерам с типобезопасностью