Углубитесь в Java Generics

Java

Чтобы помнить, что тип элемента контейнера коллекции является основной причиной введения jdk1.5 дженериков (универсальных типов).

Обобщения, кажется, параметризуют фактический тип, чтобы вы могли передать фактический тип при его использовании или вывести тип, который он представляет (например, ArrayList). Но по сути, jvm не знает типа ArrayList, это просто синтаксический сахар java, то есть выражается только на уровне исходного кода, и это просто ArrayList при загрузке jvm после компиляции.

1. Зачем вводить дженерики

Давайте посмотрим на пример:

List list = new ArrayList();
list.add(100);
list.add("100");

// 第一个元素就是int类型,OK
System.out.println((int)list.get(0) + 1);
// 第二个元素实际为String,因此会引发ClassCastException
System.out.println((int)list.get(1) + 1);

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

С точки зрения практического использования мы предпочитаем контейнер для хранения элементов одного типа или одного класса (включая подклассы). Проверка обобщений во время компиляции может помочь нам избежать случайного добавления элементов других типов. Дженерики в java — это своего рода синтаксический сахар, способ его использованиястирание типатак что дженерики Java являютсяPseudo Generics.Это также для совместимости со старыми версиями.

2. Общий класс, общий интерфейс

Мы можем объявить параметры типа для интерфейсов и классов и сделать их универсальными:

public interface Collection<E> extends Iterable<E> {
  boolean add(E e);
  ...
}

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    public V put(K key, V value) {
    	//...
    }
}    

Классы с дженериками должны передавать фактический тип при создании подклассов или без дженериков:

class Base<T> {}

// 错误
class Sub extends Base<T> {}

// Ok
class Sub extends Base<String> {}
// Ok
class Sub extends Base {}
// Ok
class Sub<T> extends Base<T> {}

Укажите границы для дженериков через расширения:

class Base<T extends Comparable & Serializable & Cloneable> {}
class Base<T extends ArrayList & Comparable & Serializable & Cloneable> {}

TОн определен для достижения указанного класса или интерфейса. Вы можете указать несколько интерфейсов, но только определенный класс и должен быть первой категорией. Во время компиляции тип T будет замененextendsПосле первого класса или типа интерфейса.

  • интерфейс перехвата базового класса

    abstract class Animal implements Comparable<Animal> {}
    
    class Dog extends Animal implements Comparable<Dog> {
    		/** 无论CompareTo参数是Dog还是Animal,都不行 */
        @Override
        public int compareTo(Dog o) {
            return 0;
        }
    }
    

    Dog реализует Comparable, а универсальный параметр — Dog, но, к сожалению, его базовый класс Animal также реализует интерфейс Comparable и передает другой универсальный параметр Animal, что приводит к конфликту типов параметров compareTo.Это явление называется перехватом интерфейса базового класса.

3. Общие методы

Другим сценарием использования дженериков являются универсальные методы.Если вы не определяете универсальные параметры в интерфейсе или классе, но хотите использовать дженерики в своем методе, вы можете определить универсальный метод следующим образом:

public static <T> Set<T> synchronizedSet(Set<T> s) {
    return new SynchronizedSet<>(s);
}

// 明确传人泛型参数类型
Collections.<String>synchronizedSet(new HashSet<>());
// 隐式使用,由编译器推导实际类型
Collections.synchronizedSet(new HashSet<String>());

4. Введите подстановочные знаки

Предположим, что есть список совпадений (

public static Map<Number, Long> count(List<Number> list) {
    return list.stream()
            .filter(n -> n.intValue() < 100)
            .collect(Collectors.groupingBy(l -> l, Collectors.counting()));
}

Ожидается, что вы можете принимать любые числа:

List<Integer> numsA = Arrays.asList(1, 2, 3, 100, 200, 300);
// 错误
Map<Number, Long> countA = count(numsA);
       
List<Double> numsB = Arrays.asList(1D, 2D, 3.55D, 100D, 200D, 330D);
// 错误
Map<Number, Long> countB = count(numsB);

Приведенный выше код сообщит об ошибке, List, List не является подтипом List. Измените параметр метода наcount(List<Object> list)Нет, они не являются подтипами List, даже если список объектов передается во время выполнения. Потому что если это так, то передача подкласса List, но попытка привести его элементы к другому подклассу вызовет проблемы.

Хотя эта проверка во время компиляции повышает безопасность программы, она снижает гибкость кодирования.Если нужно подсчитать несколько типов, мы должны написать метод подсчета для каждого типа, и метод подсчета не может быть повторен. в классе можно написать countInt, countDouble... Такой код.

4.1 Подстановочные знаки

Чтобы решить вышеуказанную проблему, мы можем использовать подстановочные знаки:

// list的元素可以是任意类型
public static Map<Number, Long> count(List<?> list) {
    return list.stream()
        .map(n -> (Number)n)
        .filter(n -> n.intValue() < 100)
        .collect(Collectors.groupingBy(l -> l, Collectors.counting()));
}

?Это подстановочный знак представляет любой тип. Так что вы можете получить любой тип списка, и значительно улучшить гибкость, код также очень прост, но отсутствие безопасности, но также уменьшается, представьте, что кто-то пройдетList<String> s = Arrays.asList("1", "2", "3", "4", "5");Что случится?

4.2 Верхняя граница подстановочного знака

Продолжая вышеупомянутый вопрос, наше реальное требование состоит в том, чтобы передать не какой-либо тип, а подкласс любого числа. В этом случае могут быть сделаны дополнительные ограничения на подстановочные знаки:

public static Map<Number, Long> count(List<? extends Number> list) {
    return list.stream()
        .filter(n -> n.intValue() < 100)
        .collect(Collectors.groupingBy(l -> l, Collectors.counting()));
}

<? extends Number>Элемент списка, указывающий преемника, должен быть Number и его подклассами, то есть?Верхняя граница для представленного типа равнаNumber, верхние границы подстановочных знаков также можно использовать в определениях универсального класса или интерфейса.

В методе еще не сосчитатьlist.add(1);Добавьте в него число или элемент его подкласса, ПОЧЕМУ?

4.3 Нижние границы подстановочных знаков
List<? super Number> list = new ArrayList<>();
list.add(Integer.valueOf(1));//ok
list.add(Long.valueOf(2L));//ok
// 因为只指定下届,所以元素类型为Object
Object object = list.get(0);

указывает, что типом элемента List является Number и его базовый класс, то есть?Нижняя граница — это число, а нижние границы с подстановочными знаками также можно использовать в определениях универсального класса или интерфейса. Почему к верхней границе подстановочного знака можно добавить подкласс Number?

На самом деле это нетрудно понять, потому чтоList<? super Number>Видно, что элементы в списке должны быть Number или его базовым классом, Integer, Long и т. д. являются подклассами Number, а также должны быть подклассами родительского класса Number. A является подклассом B, B является подклассом C, и A должен быть подклассом C. Так что согласно LSP это возможно.

4.4 Контравариантность и ковариантность

Инвертировать:Тип A является ковариантным, если его можно заменить своим подклассом B.

Ковариант:Когда тип A может быть заменен его базовым классом B, тогда A поддерживает контравариантность.

Из предыдущего мы знаем, что ниList<Number> list = new ArrayList<Integer>();, тоже нельзяList<Integer> list = new ArrayList<Number>();Потому что дженерики Java разработаны, чтобы быть информативными (кроме массивов).

Но мы можем добиться инвертора и координации с помощью подстановочного знака:

// 协变
List<? extends Number> list = new ArrayList<Integer>();
// 逆变
List<? super Integer> list = new ArrayList<Number>();

другой пример:

class Animal {}
class Pet extends Animal {}
class Cat extends Pet {}

static class Person<T extends Animal> {
    T pet;
}

// 协变
Person<? extends Pet> lily = new Person<Cat>();
// error
lily.pet = new Cat();
// 逆变
Person<? super Pet> alien = new Person<Animal>();
// ok
alien.pet = new Cat();
  • Когда общие параметры совпадают, ковариация поддерживается в универсальном классе, напримерArrayList<String> -> List<String> -> Collection<String>
  • Когда для универсальных параметров используются подстановочные знаки, ковариация поддерживается в самом универсальном классе, а ковариация поддерживается в универсальном типе параметра, напримерCollection<? extends Number>, подтип может бытьList<? extends Number>,Set<? extends Number>, или может бытьCollection<Integer>,Collection<Long>, Вы можете узнать, пройдяHashSet<Long>даCollection<? extends Number>подтип .
  • Содержит несколько параметров универсального типа, приведенные выше правила применяются к каждому параметру типа отдельно.HashMap<String, Long>даMap<? extends CharSequence, ? extends Number>подтип .
4.5 PECS

Когда следует использовать верхнюю границу подстановочного знака и когда следует использовать нижнюю границу подстановочного знака? «Эффективная Java» предлагает PECS (производитель-расширения, потребитель-супер), то есть использует расширения, когда объект генерирует общие данные, и супер используется, когда объект получает (потребляет) общие данные.

/** 
 * Collections #copy方法
 * src产生了copy需要的泛型数据,用extens
 * dest消费了copy产生的泛型数据,用super
 */
public static <T> void copy(List<? super T> dest, List<? extends T> src)
4.6 Подстановочные знаки и общие методы

Реализуйте предыдущий метод подсчета с помощью универсального метода:

/** 与之前通配符实现相同功能,同时在方法中可以添加新元素 */
public static <T extends Number> Map<T, Long> count(List<T> list) {
    return list.stream()
        .filter(n -> n.intValue() < 100)
        .collect(Collectors.groupingBy(l -> l, Collectors.counting()));
}

Еще 🌰, предположим, есть метод класса инструмента, добавляющий непустое число в список наследников:

public static void safeAdd(List<? extends Number> list, Number num) {
    if (num == null) {
        return;
    }

  	//error,虽然使用通配符限定了泛型的范围,但具体类型仍是不确定的
    list.add(num);
}

//将其替换为:
public static <T extends Number> void safeAdd(List<T> list, T num) {
    if (num == null) {
        return;
    }

  	//ok,不过num是什么类型,它都和list元素是同一类型
    list.add(num);
}

Суммировать:

  • Когда методу не нужно менять контейнер, используйте подстановочные знаки, в противном случае используйте универсальные методы.
  • Если метод имеет другие параметры и возвращаемое значение зависит от универсального параметра, используйте универсальный метод.

5. Введите стирание

Упомянутые выше универсальные параметры — это все стандартные определения java на уровне синтаксиса, которые ориентированы на компилятор.При работе в jvm нет универсального типа, тип стирается, а все универсальные типы заменяются объектным или подстановочным знаком.Верхняя граница type, если это тип контейнера, такой как List, он становится List.

ArrayList<Integer> listA = new ArrayList<>();
ArrayList<String> listB = new ArrayList<>();

// listA和listB运行时的类型都是java.util.ArrayList.class, 返回true
System.out.println(listA.getClass() == listB.getClass());

Из-за типов стирания нельзя использовать в общих статических переменных, статическом методе, статический блок инициализатора не может быть использованobj instanceof java.util.ArrayList<String>Оцените общий класс, универсальный тип, определенный в интерфейсе.

6. Получите общую информацию путем размышлений

Причина существования общего стирания заключается в том, что среда выполнения не может получить общую информацию о классе. Но для поля класса общая информация о методе класса сохраняется в константном пуле файла класса (именно атрибут Signature) при компиляции компилятора, поэтому можно получить общую информацию о поле и методе. через отражение.

Тип предоставляется в java.lang.reflect (Type является родительским интерфейсом всех типов в java, а класс реализует Type) и несколькими подинтерфейсами для получения соответствующей общей информации.В качестве примера возьмем List:

TypeVariable:обозначает переменную типа, E

ParameterizedType:Представитель типа параметров, таких как список, строка параметров

WildcardType:Подстановочные знаки, такие как List>, List?, ? extends Number

GenericArrayType:Общий массив, такой как List[], его базовый тип — ParameterizedType List

Конкретный API можно увидеть в javadoc, простая демонстрация:

public class GenericCls<T> {

    private T data;

    private List<String> list;

    private List<Integer>[] array;

    public <T> List<String> strings(List<T> data) {
        return Arrays.asList(data.toString());
    }

    public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException {
        Class<GenericCls> cls = GenericCls.class;

        System.out.println("============== class - GenericCls ==============\n");
        TypeVariable<Class<GenericCls>> classTypeVariable = cls.getTypeParameters()[0];
        System.out.println(classTypeVariable.getName());

        Field field = cls.getDeclaredField("list");
        Type genericType = field.getGenericType();
        ParameterizedType pType = (ParameterizedType) genericType;
        System.out.println("============== filed - list ==============\n");
        System.out.println("type: " + genericType.getTypeName());
        System.out.println("rawType: " + pType.getRawType());
        System.out.println("actualType: " + pType.getActualTypeArguments()[0]);

        Method method = cls.getDeclaredMethod("strings", List.class);
        Type genericParameterType = method.getGenericParameterTypes()[0];
        ParameterizedType pMethodType = (ParameterizedType) genericParameterType;
        System.out.println("============== method - strings parameter ==============\n");
        System.out.println("type: " + genericParameterType.getTypeName());
        System.out.println("rawType: " + pMethodType.getRawType());
        System.out.println("actualType: " + pMethodType.getActualTypeArguments()[0]);

        Field array = cls.getDeclaredField("array");
        GenericArrayType arrayType = (GenericArrayType) array.getGenericType();
        System.out.println("============== filed - array ==============\n");
        System.out.println("array type: " + arrayType.getTypeName());
        ParameterizedType arrayParamType = (ParameterizedType) arrayType.getGenericComponentType();
        System.out.println("type: " + arrayParamType.getTypeName());
        System.out.println("rawType: " + arrayParamType.getRawType());
        System.out.println("actualType: " + arrayParamType.getActualTypeArguments()[0]);

    }
}

Что касается рефлексии и дженериков, я подробно расскажу о них в другой статье.

7. Дженерики и массивы

Массивы Java Covariant:Pet[] pets = new Cat[10];Тем не менее, он не может создать общий массив, вы можете создать массив без универсальных средств, а затем заставить его или объявить цитату из общих массивов.

Person<Pet>[] people = new Person<Pet>[10];//error
Person<Pet>[] people = new Person[10];//ok
Person<Pet>[] people = (Person<Pet>[])new Person[10];//ok
public static void consume(Person<? extends Pet>[] people){}//ok

Вопрос: Почему классы исключений не могут использовать дженерики?


Следующее уведомление: Подробное объяснение файлов класса (байт-кода)

Добро пожаловать в мой личный блог WeChat