Общий код требует, чтобы тип был детерминированным, что накладывает большие ограничения на код, который может быть повторно использован разными типами.
Объявление типа в качестве суперкласса или интерфейса может обеспечить повторное использование кода в определенном диапазоне, но это только расширяет ограничение на суперкласс и его подклассы или классы, реализующие интерфейс.В некоторых случаях этот диапазон все еще не может быть удовлетворен. особенно java - это наследование с одним корнем. То, что нам нужно, это «нейтральное к типу» кодирование, а не конкретный класс или интерфейс.
Обобщения, представленные в Java 5, позволяют нам писать «нейтральный к типу» код. Обобщения реализуют параметризацию типов, объявляют параметры типа при определении класса, интерфейса или метода и определяют его конкретный тип при его использовании.
Обобщения — это функция времени компиляции.Во время компиляции компилятор проверит универсальный тип и добавит некоторый дополнительный код преобразования на «границе (ввод и возврат)» класса, чтобы обеспечить безопасность универсального типа во время выполнения. Мы используем его так, как если бы мы заменили наш объявленный параметр типа конкретным типом. Однако с точки зрения реализации информация о параметризованном типе теряется после компиляции, а конкретный тип, который мы указываем, стирается во время выполнения.
Java использует стирание типов вместо замены типов, как в C++.Поскольку не было универсального типа до Java 5, у стирания типов нет другого выбора, кроме как быть совместимым с кодом до Java 5.
Глоссарий:
-
Параметры типа: параметры типа, объявленные в угловых скобках при объявлении универсального класса, интерфейса или метода, например E of List.
-
Универсальный класс. Классы, интерфейсы и методы, объявляющие параметры типа, называются универсальными классами, универсальными интерфейсами и универсальными методами соответственно.
-
Параметризованный тип: при использовании универсального класса указание определенного типа называется параметризованным типом, например List.
-
Примитивные типы: универсальные классы параметризованных типов.
Class
, например исходный тип спискаList
,List[]
Примитивный тип такжеList
1. Определение и использование дженериков
1.1 Общие классы
Используйте угловые скобки для объявления параметров типа после имени класса.Объявленные параметры типа можно использовать при объявлении типа, как и обычные типы, а затем определить их конкретные типы, когда они используются, и тогда компилятор поможет нам справиться с некоторыми преобразованиями типов Детали.
public class Holder<T> {
T val;
public Holder(T val) {
this.val = val;
}
public T getVal() {
return val;
}
public void setVal(T val) {
this.val = val;
}
public static void main(String[] args) {
Holder<String> strHolder = new Holder<String>("abc");
String s = h.getVal();
}
}
указывается на момент использованияHolder
Параметр типаString
. можетgetVal()
Возвращаемое значение напрямую присваиваетсяString
переменные без явного приведения. в настоящее время используетsetVal
тоже надо пройтиString
класс или его подклассы, если аргумент неString
или его подклассы, во время компиляции будет сообщено об ошибке.
До Java7new
Необходимо указать тип при параметризации типа, но после Java7new
Операцию можно выполнить без явного указания типа, компилятор сам его выведет:
Holder<String> h = new Holder<>("abc");
Параметры нескольких типов разделяются запятыми:
public class Holder<A, B, C> {
public A v1;
public B v2;
public C v3;
public Holder(A v1, B v2, C v3) {
this.v1 = v1;
this.v2 = v2;
this.v3 = v3;
}
public static void main(String[] args) {
Holder<String, Integer, Float> h = new Holder<>("abc", 1, 2.5);
}
}
Внутренние классы могут использовать параметры типа внешнего класса:
class A<T> {
class B {
T a;
}
}
Анонимные внутренние классы также могут быть параметризованными типами.
interface A<T> {
T next();
}
new A<String>() {
@Override
public String next() {
return null;
}
};
Статические свойства, статические методы и статические внутренние классы не могут использовать универсальные параметры класса. Если вы хотите сделать статический метод универсальным, вы можете использовать универсальный метод.
public class Calculate<T> {
// 静态方法时无法使用T的,编译时就会报错
public static T add(T a, T b) {
T c = a + b;
}
}
1.2 Общий интерфейс
Интерфейсы также могут быть объявлены универсальными так же, как и универсальные классы.
public interface Generator<T> {
T next();
}
При реализации универсального класса вам необходимо указать конкретный тип для параметра типа:
public interface Bottle<T> {
void pourInto(T t);
T pourOut();
}
// 实现Bottle时指定类型参数为Juice
public class GlassBottle implements Bottle<Juice> {
public void pourInto(Juice juice) {
}
public Juice pourOut() {
return null;
}
}
1.3 Общие методы
Универсальный класс может быть объявлен только для метода, и класс не обязательно должен быть универсальным классом. Чтобы определить универсальный метод, просто поместите список общих параметров перед возвращаемым значением. Объявленные параметры типа используются как обычные классы, где тип определен в методе.
public class Test {
public static <T> void t(T x) {
System.out.println(x.getClass().getName());
}
public static <K,V> Map<K, V> newMap() {
return new HashMap<K, V>();
}
public static void main(String[] args) {
t(11); // java.lang.Integer
t("abc"); // java.lang.String
Map<String, Date> m = newMap();
m.put("now", new Date());
}
}
При использовании универсального метода вам не нужно явно указывать конкретный тип. Компилятор выведет конкретный тип в соответствии с входным параметром параметра типа метода или типом возвращаемого присваивания. Однако, если результат вызова напрямую передается в качестве параметра другому методу. В настоящее время компилятор не выполняет вывод типа. Если это примитивный тип, он будет автоматически упакован как тип-оболочка.
public static <T> String className(T v) {
return v.getClass().getSimpleName();
}
public static void main(String[] args) {
// 输出Integer,自动推断出是Integer
System.out.println(Test.className(11));
}
Вы также можете явно указать тип при вызове универсального метода, вставить угловые скобки между оператором точки и именем метода и поместить тип внутрь.
Test.<String, Date>newMap();
Списки параметров переменной длины также могут использовать общие параметры:
public static <T> List<T> toList(T... args) {
List<T> l = new ArrayList<T>(args.length);
for (T e : args) {
l.add(e);
}
return l;
}
Когда вызывается вариативный метод, будет создан массив для хранения вариативных параметров.Если тип параметра является универсальным, то будет создан универсальный массив, но разве Java не разрешает прямое использование дженериков для создания массивов? Здесь java идет на некоторые компромиссы, позволяя создавать общий массив для вариативных параметров.
Однако входные параметры списка переменных параметров могут быть разных типов, поэтому иногда компилятор не может определить конкретный тип общего параметра переменной, и может быть выбран только самый общий тип.
public class Test {
public static void main(String[] args) {
System.out.println(toArray(Integer.valueOf(11), Double.valueOf(13)).getClass());
}
public static <T> T[] toArray(T... args) {
return args;
}
}
вывод:
class [Ljava.lang.Number;
2. Наследовать универсальные классы/реализовать универсальные интерфейсы
2.1 Указать тип при наследовании
При наследовании универсального класса или реализации универсального интерфейса необходимо указать конкретный тип.После указания конкретного типа для подкласса его родительский класс или реализованный интерфейс является параметризованным типом.Class
изgetGenericSuperclass
Тип, возвращаемый при получении типа родительского класса,ParameterizedType
из.
public class Holder<T> {
private T val;
public Holder(T val) {
this.val = val;
}
public T getVal() {
return val;
}
public void setVal(T val) {
this.val = val;
}
}
class Apple {
public void show() {
System.out.println(getClass().getSimpleName());
}
}
public class AppleHolder extends Holder<Apple> {
public AppleHolder(Apple apple) {
super(apple);
}
public static void main(String[] args) {
AppleHolder appleHolder = new AppleHolder(new Apple());
Apple apple = appleHolder.getVal();
apple.show();
System.out.println(appleHolder.getClass().getGenericSuperclass() instanceof ParameterizedType);
}
}
вывод:
Apple
true
2.2 Не указывайте тип при наследовании
Если при наследовании класса или реализации интерфейса тип не указан, родительский класс или интерфейс является обычным классом или интерфейсом для подкласса, а его параметры типа стираются какObject
,пройти черезClass
изgetGenericSuperclass
Возвращаемый типClass
из.
public class CommonHolder extends Holder {
public CommonHolder(Object val) {
super(val);
}
public static void main(String[] args) {
System.out.println(CommonHolder.class.getGenericSuperclass() instanceof Class);
}
}
вывод:
true
2.3 Задается как параметры типа в подклассах
Вы также можете передать суперклассу параметр типа, объявленный в подклассе, и когда вы позже укажете тип для подкласса, суперкласс также получит тот же тип. Для подклассов его родительский класс по-прежнему является параметризованным типом, а возвращаемый тип от getGenericSuperclass до Class по-прежнему имеет тип ParameterizedType.
public class CommonHolder<T> extends Holder<T> {
public CommonHolder(T val) {
super(val);
}
public static void main(String[] args) {
System.out.println(CommonHolder.class.getGenericSuperclass() instanceof ParameterizedType);
}
}
вывод:
true
3. Общие границы
Из-за стирания типа мы не можем напрямую использовать определенные свойства или методы для параметров типа. Следующий вызов не скомпилируется:
import java.sql.DriverManager;
import java.util.*;
public class Test<T> {
public T val;
public void show() {
// 编译时失败
val.show();
}
public static class Apple {
public void show() {
}
}
public static void main(String[] args) throws ClassNotFoundException {
Test<Apple> t = new Test<>();
t.show();
}
}
В приведенном выше примере, даже если мы знаемval
Тип будет сопровождаться когдаShow
, но поскольку безопасность этого не может быть гарантирована после стирания типа, компилятор запрещает такое использование.
Однако черезextends
Отображается верхняя граница объявленного параметра типа, если он не объявлен, то верхняя границаObject
. После объявления верхней границы класса тип, указанный при использовании универсального класса, может быть только верхней границей или его подклассами.
public class Show {
public void show() {}
}
public class Test<T extends Show> {
public T val;
public void show() {
// 可以调用
val.show();
}
public static void main(String[] args) {
Test<Show> t = new Test<>();
t.show();
}
}
Если верхняя граница не объявлена, верхняя граница по умолчанию равнаObject
, все, что мы можем назвать, не объявляя верхнюю границуObject
Методы.
public class Test<T> {
public T val;
public void show() {
val.getClass();
val.toString();
val.hashCode();
}
}
4. Подстановочные знаки
// 继承关系:Drink -> Juice -> AppleJuice
public class Drink {}
public class Juice extends Drink {}
public class AppleJuice extends Juice {}
public class Bottle<T> {
private T drink;
public Bottle(T drink) {
drink = drink;
}
public T getDrink() {
return drink;
}
public void setDrink(T drink) {
drink = drink;
}
}
Для обычных классов объекты одного класса могут быть назначены друг другу, а объекты подкласса также могут быть назначены объектам родительского класса.
Juice juice = new Juice();
juice = new AppleJuice();
Но для универсальных классов, пока параметры указанного типа различны, даже если они являются одним и тем же универсальным классом, они также являются разными параметризованными типами и не могут присваивать значения напрямую друг другу:
// Error
Bottle<Juice> b1 = new Bottle<AppleJuice>(new AppleJuice());
Хотя после стирания типа они обаBottle
, но код обработки типов, вставленный компилятором на границе универсального класса, отличается во время компиляции и, очевидно, не может быть обработан с помощьюAppleJuice
Код для работы с другими типами, поэтому они являются разными типами с точки зрения компилятора, и во время компиляции будет сообщено об ошибке.
Чтобы решить проблему назначения между универсальными экземплярами, параметры типа которых имеют отношение наследования, в java предусмотрены подстановочные знаки.
4.1 Подстановочный знак верхней границы
может использоваться при определении общих переменныхextends
Ключ указывает верхнюю границу типа, так что объявленной переменной может быть присвоен общий параметризованный тип, параметром типа которого является класс верхней границы и его подклассы, при условии, конечно, что общие классы являются одними и теми же или родительскими и дочерними классами.
Bottle<? extends Juice> b = new Bottle<AppleJuice>(new AppleJuice());
b, который объявляет верхнюю границу Juice, может быть присвоено значение Bottle. Однако использование универсальных экземпляров после использования подстановочных знаков верхней границы также ограничено.
Хотя используется подстановочный знак extends, компилятор по-прежнему не знает, является ли конкретный тип b AppleJuice или подклассом OrangeJuice, поэтому компилятор не может гарантировать безопасность входных параметров методов, типы параметров которых имеют параметры типа, например:
Bottle<? extends Juice> bottle = new Bottle<AppleJuice>(new AppleJuice());
// error
bottle.setDrink(new OrangeJuice());
setDrink
определяется как:
void setDrink(T drink)
тогда очевидноbottle
Фактический тип переменнойBottle
,такsetDrink
будет скомпилирован в:
setDrink((AppleJuice) val)
Очевидно, что принудительное преобразование типов между одноуровневыми типами небезопасно, поэтому экземпляр, объявленный с подстановочным знаком верхней границы, не может вызывать методы с параметрами с параметрами типа. Но это нормально, когда входной параметр имеет значение null, потому что null не имеет определенного типа. Но безопасно возвращаться и безопасно назначать подкласс суперклассу, поэтому методы, возвращающие параметры тип-тип, не затрагиваются.
Bottle<? extends Juice> b = new Bottle<AppleJuice>(new AppleJuice());
Juice juice = b.getDrink();
4.2 Подстановочные знаки Нижнего мира
Используйте ключевое слово super, чтобы указать универсальную переменную с нижней границей.Переменной с указанной нижней границей может быть присвоен только тип родительского класса, параметром типа которого является указанная нижняя граница или нижняя граница.
Bottle<? super Juice> b = new Bottle<Drink>(new AppleJuice());
Аргументы преобразуются в фактический тип Drink во время компиляции:
setDrink((Drink) val)
Безопасно использовать супертип для управления подтипом, поэтому безопасно использовать метод с параметром типа в экземпляре объявления подстановочного знака нижней границы. Однако, поскольку родительский класс не может быть назначен дочернему классу, экземпляр, объявленный подстановочным знаком нижней границы, не может назначать возвращаемое значение метода, тип возвращаемого значения которого является типом параметра, другим переменным.
Bottle<? super Juice> b = new Bottle<Drink>(new AppleJuice());
// Error
Drink drink = b.getDrink();
4.3 Неограниченные подстановочные знаки
Тип параметра указывается в виде знака ?, указывающего, что можно использовать любой тип.
Bottle<?> b = new Bottle<>(new AppleJuice());
Drink drink = (Drink) b.getDrink();
// ERROR
b.setDrink(new AppleJuice());
Использование неограниченных подстановочных знаков ничем не отличается от примитивных необработанных типов, но смысл неограниченных подстановочных знаков заключается в том, что мы знаем, что используем здесь любой тип, а неограниченные подстановочные знаки будут выполнять проверку типов, поскольку неограниченные подстановочные знаки не знают точного типа, поэтому нет гарантия безопасности. Переменная с неограниченным подстановочным знаком не может вызывать метод, тип входного параметра которого является параметром типа.
5. Введите стирание
Тип, указанный при использовании дженериков, действует только во время компиляции.После компиляции все параметры типа будут стерты до его первой границы.Если граница не указана, он будет стерт доObject
.
Из-за стирания типа параметры типа больше не существуют во время выполнения, поэтому вы не можете явно использовать операции универсального типа во время выполнения, такие какinstanceof
,new
,T.class
И тому подобное, но при предварительном преобразовании типа:
public class Test<T> {
Class<?> type;
public Test(Class<?> type) {
this.type = type;
}
public T[] newArray(int size) {
return (T[]) Array.newInstance(type, size);
}
public static void main(String[] args) {
Test<String> t = new Test<>(String.class);
String[] strArr = t.newArray(10);
}
}
Хотя мы можем указать различные типы параметров, а затем, но после стирания этих точек к одному типу. какList
иList
изClass
такие жеList.Class
.
Поскольку конкретная информация о типе стирается во время компиляции, а для класса гарантируется правильное поведение типа во время выполнения, компилятор выполняет общую «границу» во время компиляции, то есть методы, которые имеют общие входные параметры и возвращаются в класс. Проверка типов и вставка кода принуждения, преобразование типов входных параметров при вызове метода и преобразование возвращаемого значения при возврате.
6. Предложения
6.1 Укажите информацию о типе
Из-за стирания типа мы не можем получить информацию о конкретном типе параметра во время выполнения.Если требуется информация о конкретном типе, можно отобразить тип передачи.Class
объект.
public class Test<T> {
private Class<T> kind;
public T val;
public Test(Class<T> kind) {
this.kind = kind;
}
public boolean isType(Object o) {
return kind.isInstance(o);
}
}
6.2 Можно использовать универсальные методы без использования универсальных классов
Если вы можете заменить универсальный класс универсальным методом, вам следует попытаться заменить универсальный класс класса универсальным методом.
6.3 Попробуйте использовать параметризованные дженерики
Если класс или интерфейс является универсальным, он должен попытаться использовать свой параметризованный тип, чтобы компилятор выполнял некоторую проверку типов для нас во время компиляции, чтобы избежать ошибок во время выполнения.
Если конкретного типа нет, также рекомендуется использовать подстановочные знаки, напримерList<?>
. Использование подстановочных знаков можно проверить во время компиляции и предотвратить вызов методов с параметрами типа.
Прямое использование универсальных типов-примитивов рискованно, типы-примитивы не проверяются во время компиляции, а параметры типа стираются доObject
,Object
Допускаются экземпляры любого типа.Если классу предоставлены экземпляры разных типов, существуют определенные риски безопасности при работе с этими экземплярами в классе, и эти риски могут проявляться только во время выполнения. Причина, по которой java поддерживает прямое использование универсальных примитивных типов, заключается только в совместимости с кодом до Java5.
6.4 Не назначайте параметризованные типы примитивным типам
В целях совместимости Java не запрещает преобразовывать переменные параметризованных типов в примитивные типы, и это приведет только к появлению предупреждения во время компиляции. Однако после присвоения параметризованного типа примитивному типу компилятор больше не будет выполнять проверку типов операций над экземплярами примитивного типа, что может вызвать ошибки времени выполнения.
class Calculator<T> {
public int intAdd(T v1, T v2) {
return ((Number) v1).intValue() + ((Number) v1).intValue();
}
}
public class Test {
public static void main(String[] args) {
Calculator<Integer> intCal= new Calculator<>();
Calculator cal = intCal;
cal.intAdd("a", "b");
}
}
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number
at com.test.java.Calculator.intAdd(Test.java:10)
at com.test.java.Test.main(Test.java:20)
Назначьте intCal параметризованного типа Calculator калу Calculator, и компилятор не будет выполнять проверку типов при работе кал позже, и эта ошибка будет выдана во время выполнения.
public static void main(String[] args) {
List<String> strList = new ArrayList<>();
List list = strList;
list.add(Integer.valueOf(11));
String s = strList.get(0);
}
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at com.test.java.Test.main(Test.java:27)
Приведенный выше код будет генерировать предупреждение только во время компиляции, но сообщит о фатальной ошибке во время выполнения. потому что будетstrList
назначенный List
Типlist
, замок не скомпилируется правильноlist
Операции над переменными проверяются на тип. И из-за стирания типа во время выполненияString
стер какObject
,такlist.add(Integer.valueOf(11))
Работает отлично. Но потому чтоstrList
даList
тип, для которого вставляет время компиляцииString
введите код преобразования, при преобразованииInteger
превратиться вString
время незаконно.
6.5 Старайтесь не использовать общие списки переменных параметров
Вариативный параметр универсального типа иногда не может определить конкретный тип, и только тип массива вариационного параметра может быть позиционирован как общий тип. Для массивов вариативных списков параметров мы не просто передаем значения, мы можем работать с ними, что создает риск для безопасности типов. Вы должны стараться избегать универсальных вариативных параметров или использовать параметризованные типы List вместо вариативных параметров.
У эффективного java есть классический пример: передача трех объектов и случайный выбор двух наиболее оценочных массивов для возврата:
public class Test {
public static void main(String[] args) {
String[] strArr = pickTwo("a", "b", "c");
}
public static <T> T[] toArray(T... args) {
return args;
}
public static <T> T[] pickTwo(T a, T b, T c) {
switch (ThreadLocalRandom.current().nextInt(3)) {
case 0: return toArray(a, b);
case 1: return toArray(a, c);
case 2: return toArray(b, c);
}
throw new AssertionError();
}
}
Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
at cn.ly.test.java.Test.main(Test.java:21)
Этот класс не сообщит об ошибке во время компиляции, но выдаст во время выполненияClassCastException
аномальный.pickTwo
Параметр является параметром типа, при передаче параметра этого типа в метод toArray компилятор не может определить параметр типа, а может только создатьObject[]
Массив для хранения переменных параметров. правильноpickTwo
Возврат компилируется для нас, чтобы вставитьString[]
преобразование типа, но фактический типObject[]
не конвертируется вString[]
из.
6.6 При приведении универсального типа его следует преобразовать в тип с подстановочными знаками.
При приведении, если конечный тип является универсальным типом, он должен быть преобразован в параметризованный тип с подстановочными знаками этого типа, а не в исходный тип. Таким образом, переменная проверяется компилятором после преобразования.
if (o instanceof List) {
List<?> l = (List<?>) o;
}