Глубокое понимание дженериков Java: достаточно ли хорошо вы понимаете дженерики?

Java

Дженерики

Обобщения предоставляют способ сообщить тип коллекции компилятору.Как только компилятор узнает тип элементов коллекции, компилятор может проверить его тип и установить ограничения типа.

Перед дженериками:

/**
 * 迭代 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

Без лишних слов, давайте сразу к ответу.

image.png

代码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метод поддерживает произвольные типы элементовCollectionprintCollection2метод может получить только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Команда для просмотра информации о файле класса:

class文件信息1

class文件信息2

Это видно из информации о файле класса: компилятор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

Через 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

Coder小黑