Дженерики
Обобщения предоставляют способ сообщить тип коллекции компилятору.Как только компилятор узнает тип элементов коллекции, компилятор может проверить его тип и установить ограничения типа.
Перед дженериками:
/**
* 迭代 Collection ,注意 Collection 里面只能是 String 类型
*/
public static void forEachStringCollection(Collection collection) {
Iterator iterator = collection.iterator();
while (iterator.hasNext()) {
String next = (String) iterator.next();
System.out.println("next string : " + next);
}
}
Вот программа после использования дженериков:
public static void forEachCollection(Collection<String> collection) {
Iterator<String> iterator = collection.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
System.out.println("next string : " + next);
}
}
До появления дженериков мы могли информировать вызывающих о методах только с помощью более интуитивно понятных имен методов и комментариев к документам.forEachStringCollection
Методы могут принимать только элементы типаString
коллекция. Однако это только «условие», если пользователь передает элемент, это неString
Набор типов, код не будет сообщать об ошибках во время компиляции, только во время выполнения он выдастClassCastException
исключение, которое не является дружественным для вызывающей стороны.
С помощью дженериков комментарий документа метода может быть передан в сигнатуру метода:forEachCollection(Collection<String> collection)
, вызывающий метод видит сигнатуру метода и знает, чтоCollection<String>
, компилятор также может проверять нарушения ограничений типа во время компиляции. Надо отметить, что проверку компилятором тоже очень легко обойти, как обойти? Пожалуйста, смотрите ниже~
Голос за кадром: Код — лучший комментарий.
Обобщения и преобразования типов
Подумайте, допустим ли следующий код:
List<String> strList = new ArrayList<>();
List<Object> objList = new ArrayList<>();
objList.add("公众号:Coder小黑"); // 代码1
objList = strList; // 代码2
Без лишних слов, давайте сразу к ответу.
代码1
Явно законно.Object
типString
Родительский класс Type.
Так代码2
Почему это не законно?
В Java присвоение типов объектов на самом деле является присвоением ссылочных адресов, т. е. предполагается, что代码2
Задание выполнено успешно,objList
иstrList
Переменная ссылается на тот же адрес. Так в чем проблема?
Если в это время кobjList
добавил не-String
элемент типа, который эквивалентенstrList
добавил не-String
тип элемента. Ясно, что здесь сломаноList<String> strList
. Итак, компилятор Java будет думать, что代码2
является незаконным, и это безопасная практика.
Голос за кадром: Это может не совпадать с интуицией большинства людей. Это потому, что мы не рассматривали проблему всесторонне. Причина здесь важнее результата.
Общие подстановочные знаки
Мы уже знаем, что выше代码2
является незаконным. Затем рассмотрим следующие два метода:
public static void printCollection1(Collection c) {}
public static void printCollection2(Collection<Object> c) {}
В чем разница между этими двумя методами?
printCollection1
метод поддерживает произвольные типы элементовCollection
,иprintCollection2
метод может получить толькоObject
ТипCollection
. Несмотря на то чтоString
даObject
подкласс , ноCollection<String>
нетCollection<Object>
подкласс , и代码2
Есть сходства в том же смысле.
Взгляните на этот метод еще раз:
public static void printCollection3(Collection<?> c) {}
printCollection3
和上面的两个方法又有什么区别呢? как понятьprintCollection3
В дороге?
Шерстяная ткань?
?
представляет любой тип, указывая, чтоprintCollection3
Метод принимает коллекцию любого типа.
Ну, тогда еще вопрос, пожалуйста, смотрите следующий код:
List<?> c = Lists.newArrayList(new Object());
Object o = c.get(0);
c.add("12"); // 编译错误
Почему ошибка компиляции?
Мы можем назначить любой тип коллекцииList<?> c
Переменная. но,add
Тип параметра метода?
, который представляет неизвестный тип, поэтому вызовadd
метод, это безопасная практика делать ошибки программирования.
иget
Метод возвращает элементы в коллекции.Хотя тип элементов в коллекции неизвестен, независимо от того, какой это тип, онObject
тип, поэтому используйтеObject
тип для получения безопасен.
Ограниченный подстановочный знак
public static class Person extends Object {}
public static class Teacher extends Person {}
// 只知道这个泛型的类型是Person的子类,具体是哪一个不知道
public static void method1(List<? extends Person> c) {}
// 只知道这个泛型的类型是Teacher的父类,具体是哪一个不知道
public static void method2(List<? super Teacher> c) {}
Рассмотрим результат выполнения следующего кода:
public static void test3() {
List<Teacher> teachers = Lists.newArrayList(new Teacher(), new Teacher());
// method1 处理的是 Person 的 子类,Teacher 是 Person 的子类
method1(teachers);
}
// 只知道这个泛型的类型是Person的子类,具体是哪一个不知道
public static void method1(List<? extends Person> c) {
// Person 的子类,转Person, 安全
Person person = c.get(0);
c.add(new Person()); //代码3,编译错误
}
代码3
Почему возникает ошибка компиляции?
method1
Знайте только, что тип этого дженерикаPerson
Подкласс , какой именно, неизвестно. если代码3
Компиляция прошла успешно, тогда в приведенном выше коде нужноList<Teacher> teachers
добавилPerson
элемент. В это время последующая операцияList<Teacher> teachers
, велика вероятность выкинутьClassCastException
аномальный.
Давайте посмотрим на следующий код:
public static void test4() {
List<Person> teachers = Lists.newArrayList(new Teacher(), new Person());
// method1 处理的是 Person 的 子类,Teacher 是 Person 的子类
method2(teachers);
}
// 只知道这个泛型的类型是Teacher的父类,具体是哪一个不知道
public static void method2(List<? super Teacher> c) {
// 具体是哪一个不知道, 只能用Object接收
Object object = c.get(0); // 代码4
c.add(new Teacher()); // 代码5,不报错
}
method2
Общий типTeacher
родительский класс, в то время какTeacher
Существует много родительских классов , поэтому代码4
использовать толькоObject
получить. Дочерний класс наследует родительский класс, поэтому добавьте его в коллекциюTeacher
Объекты являются безопасными операциями.
Передовой опыт: принципы PECS
PECS:producer extends, consumer super
.
- производитель, производящий данные, используя
<? extends T>
- потребитель, потребляющий данные, использующий
<? super T>
Как понять это? Перейдем непосредственно к коду:
/**
* producer - extends, consumer- super
*/
public static void addAll(Collection<? extends Object> producer,
Collection<? super Object> consumer) {
consumer.addAll(producer);
}
Некоторые студенты могут сказать, что мне делать, если я не могу вспомнить этот принцип?
Неважно, я иногда не помню. К счастью, в JDK есть метод:java.util.Collections#copy
, который прекрасно иллюстрирует принцип PECS. Каждый раз, когда вы хотите использовать его, но не можете вспомнить, просто взгляните на этот метод, и вы все поймете~
// java.util.Collections#copy
public static <T> void copy(List<? super T> dest, List<? extends T> src){}
Голос за кадром: Знаний очень много и они очень сложные, надо построить индекс в мозгу, когда мы сталкиваемся с проблемами, мы можем быстро найти решения через индекс.
Более безопасные общие проверки
Некоторые из приведенных выше проверок являются проверками во время компиляции, и компилятор легко обмануть:
public static void test5() {
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
List copy = list;
copy.add("a");
List<Integer> list2 = copy;
}
test5
Метод обманывает компилятор и работает успешно.
Когда он сообщит об ошибке? Когда программа переходит к чтениюlist2
бросается, когда элемент вClassCastException
аномальный.
Java предоставляет намjava.util.Collections#checkedList
метод при вызовеadd
проверяется соответствие типов.
public static void test6() {
List<Integer> list = Collections.checkedList(Arrays.asList(1, 2, 3, 4, 5), Integer.class);
List copy = list;
// Exception in thread "main" java.lang.ClassCastException: Attempt to insert class java.lang.String element into collection with element type class java.lang.Integer
copy.add("a");
}
Голос за кадром: Это отказоустойчивая идея.При добавлении, если тип не соответствует, будет немедленно сообщено об ошибке, вместо того, чтобы продолжать запускать программу, которая может иметь проблемы.
Введите стирание
Мы знаем, что компилятор сотрет дженерики, так как же мы понимаем стирание дженериков? объединен вObject
?
Общее стирание следует следующим правилам:
- Если общий параметр не ограничен, компилятор заменяет его на
Object
. - Если универсальный параметр ограничен, компилятор заменяет его ограниченным типом.
public class TypeErasureDemo {
public <T> void forEach(Collection<T> collection) {}
public <E extends String> void iter(Collection<E> collection) {}
}
использоватьjavap
Команда для просмотра информации о файле класса:
Это видно из информации о файле класса: компиляторforEach
Общий метод заменен наObject
,будетiter
Общий метод заменяется наString
.
Обобщения и перегрузка методов
Поняв общие правила стирания, давайте посмотрим, какие проблемы возникнут, когда дженерики столкнутся с перегрузкой методов?
Прочтите следующий код:
// 第一组
public static void printArray(Object[] objs) {}
public static <T> void printArray(T[] objs) {}
// 第二组
public static void printArray(Object[] objs) {}
public static <T extends Person> void printArray(T[] objs) {}
Приведенные выше два набора методов представляют собой перегрузку?
-
Группа 1: дженерики стираются, то есть во время выполнения,
T[]
На самом деле этоObject[]
, поэтому первая группа не является перегрузкой. -
Вторая группа:
<T extends Person>
Указывает, что способ полученияPerson
Подкласс , который представляет собой перегрузку.
Разрешение дженериков с помощью ResolvableType
Spring Framework предоставляетorg.springframework.core.ResolvableType
для элегантного разбора дженериков.
Простой пример использования выглядит следующим образом:
public class ResolveTypeDemo {
private static final List<String> strList = Lists.newArrayList("a");
public <T extends CharSequence> void exchange(T obj) {}
public static void resolveFieldType() throws Exception {
Field field = ReflectionUtils.findField(ResolveTypeDemo.class, "strList");
ResolvableType resolvableType = ResolvableType.forField(field);
// class java.lang.String
System.out.println(resolvableType.getGeneric(0).resolve());
}
public static void resolveMethodParameterType() throws Exception {
Parameter[] parameters = ReflectionUtils.findMethod(ResolveTypeDemo.class, "exchange", CharSequence.class).getParameters();
ResolvableType resolvableType = ResolvableType.forMethodParameter(MethodParameter.forParameter(parameters[0]));
// interface java.lang.CharSequence
System.out.println(resolvableType.resolve());
}
public static void resolveInstanceType() throws Exception {
PayloadApplicationEvent<String> instance = new PayloadApplicationEvent<>(new Object(), "hi");
ResolvableType resolvableTypeForInstance = ResolvableType.forInstance(instance);
// class java.lang.String
System.out.println(resolvableTypeForInstance.as(PayloadApplicationEvent.class).getGeneric().resolve());
}
}
Дженерики и десериализация JSON
Недавно я видел такой код, использующий Джексона для преобразования JSON в Map.
public class JsonToMapDemo {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
public static <K, V> Map<K, V> toMap(String json) throws JsonProcessingException {
return (Map) OBJECT_MAPPER.readValue(json, new TypeReference<Map<K, V>>() {
});
}
public static void main(String[] args) throws JsonProcessingException {
// {"1":{"id":1}}
String json = "{\"1\":{\"id\":1}}";
Map<Integer, User> userIdMap = OBJECT_MAPPER.readValue(json, new TypeReference<Map<Integer, User>>() {
});
}
@Data
public static class User implements Serializable {
private static final long serialVersionUID = 8817514749356118922L;
private int id;
}
}
Запустите основной метод, хотя код завершается нормально. Но этот код на самом деле проблематичен, в чем проблема? Давайте вместе посмотрим на следующий код:
public static void main(String[] args) {
// {"1":{"id":1}}
String json = "{\"1\":{\"id\":1}}";
Map<Integer, User> userIdMap = toMap(json);
userIdMap.forEach((integer, user) -> {
// 出处代码会报错
// Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
System.out.println(user.getId());
});
}
Зачем сообщатьClassCastException
Шерстяная ткань? Давайте отладим, чтобы узнать.
Через Debug вы можете найти:Map<Integer, User> userIdMap
Ключ объекта на самом делеString
тип и значениеLinkedHashMap
. Это легко понять, приведенный выше код написан без знания того, что такое K и V. Правильное написание выглядит следующим образом:
public static void main(String[] args) throws JsonProcessingException {
// {"1":{"id":1}}
String json = "{\"1\":{\"id\":1}}";
Map<Integer, User> userIdMap = OBJECT_MAPPER.readValue(json, new TypeReference<Map<Integer, User>>() {
});
userIdMap.forEach((integer, user) -> {
System.out.println(user.getId());
});
}
Добро пожаловать в публичный аккаунт WeChat: Coder Xiaohe