Первый взгляд на стирание типов Java

задняя часть
Первый взгляд на стирание типов Java

Этот блог в основном знакомит с определением стирания типов в Java и подробно описывает сценарии стирания типов в Java.

1. Что такое стирание шрифта

Чтобы дать вам быстрое представление о стирании шрифта, давайте сначала возьмем очень простой и классический пример.

// 指定泛型为String
List<String> list1 = new ArrayList<>();
// 指定泛型为Integer
List<Integer> list2 = new ArrayList<>();

// true
System.out.println(list1.getClass() == list2.getClass()); 

Результатом вышеуказанного судебного решения являетсяtrue. Представление двух списков, переданных с разными универсальными элементами, наконец, компилируется в ArrayList, который становится того же типа, а исходные универсальные параметры String и Integer стираются. Это классический пример стирания шрифта.

И если мы говорим о том, почему происходит стирание типов, мы должны понимать дженерики.

2. Дженерики

2.1. Определение дженериков

30 сентября 2004 года был выпущен проект JDK 1.5 под кодовым названием Tiger, и с тех пор дженерики с вами. JDK 1.5 значительно улучшил простоту использования синтаксиса Java. В дополнение к дженерикам в ту же версию добавлены автоупаковка, динамические аннотации, перечисления, параметры переменной длины, циклы foreach и многое другое.

В версиях до 1.5, чтобы сделать классы Java универсальными, тип параметра и тип возвращаемого значения обычно устанавливаются в Object, который является видимым.Если вам нужен другой тип, вам нужно привести его в соответствующем месте.Это очень хлопотно работать нормально, и ошибки могут возникнуть, если вы не будете осторожны.

Дженерики — это, по сути, параметризованные типы. То есть указать тип данных в качестве параметра. Каковы преимущества внедрения дженериков?

Обобщения могут приводить к ошибкам, которые были обнаружены только во время выполнения до JDK 1.5 во время компиляции. То есть дженерики обеспечивают механизм обнаружения безопасности типов во время компиляции. Например, переменная изначально имеет тип Integer, а в коде мы устанавливаем для нее значение String.Когда дженерики не используются, сообщение об ошибке будет выдано только тогда, когда код дойдет до этой точки.

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

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

3. Как использовать дженерики

3.1 Общие классы

3.1.1 Определение универсального класса

Простой универсальный класс можно определить следующим образом.

public class Generic<T> {
    T data;
    
    public Generic(T data) {
        setData(data);
    }
    
    public T getData() {
        return data;
    }
    
    public void setData(T data) {
        this.data = data;
    }
}

где T обозначает тип параметра, который обозначает любой тип. Конечно, это не обязательно должно быть написано как Т, это просто привычка каждого. С вышеуказанным универсальным классом мы можем использовать его следующим образом.

3.1.2 Использование универсальных классов

// 假设有这样一个具体的类
public class Hello {
    private Integer id;

    private String name;

    private Integer age;

    private String email;
}

// 使用泛型类
Hello hello = new Hello();
Generic<Hello> result = new Generic<>();
resule.setData(hello);

// 通过泛型类获取数据
Hello data = result.getData();

Конечно, если универсальный класс не передает указанный тип, тип, определенный методом или переменной-членом в универсальном классе, может быть любым типом.result.getClass(), ты получишьGeneric.

3.2 Общие методы

3.2.1 Определение универсальных методов

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

// 定义不带返回值的泛型方法
public <T> void genericMethod(T field) {
    System.out.println(field.getClass().toString());
}

// 定义带返回值的泛型方法
private <T> T genericWithReturnMethod(T field) {
    System.out.println(field.getClass().toString());
    return field;
}

3.2.2 Вызов универсальных методов

// 调用不带返回值泛型方法
genericMethod("This is string"); // class java.lang.String
genericMethod(56L); // class java.lang.Long

// 调用带返回值的泛型方法
String test = genericWithReturnMethod("TEST"); // TEST class java.lang.String

В методе с возвращаемым значением T — это тип возвращаемого значения текущей функции.

3.3 Общие интерфейсы

Общий интерфейс определяется следующим образом

public interface genericInterface<T> {
}

Используемый метод подобен методу универсальных классов, поэтому я не буду повторять их здесь.

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

Что такое общие подстановочные знаки? Официальное объяснение состоит в том, что

Type of unknown.

То есть неквалифицированный подстановочный знак, который может представлять любой тип. Есть также три варианта использования: >, и .

Поскольку уже существуют подстановочные знаки, такие как T, представляющие любой тип, зачем нам нужен такой неквалифицированный подстановочный знак? Это потому, что основная проблема, которую он решает, - это проблема, вызванная общим наследованием.

4.1 Проблемы наследования дженериков

Сначала рассмотрим пример

List<Integer> integerList = new ArrayList<>();
List<Number> numberList = integerList;

мы знаем,Integerунаследовано отNumberКатегория.

public final class Integer extends Number implements Comparable {

    ....

}

Так может ли приведенный выше код скомпилироваться? Конечно нет. Целочисленное наследование от Number не означает, что существует отношение наследования между List и List. Каковы сценарии применения подстановочных знаков?

4.2 Сценарии применения подстановочных знаков

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

В строго типизированном языке, таком как Java, типы параметров функции фиксированы. Так что, если вы хотите реализовать универсальную функцию, такую ​​​​как JavaScript, в Java? Вот почему нам нужны общие подстановочные знаки.

Предположим, у нас есть много классов животных, таких как Dog, Pig и Cat, нам нужна общая функция для подсчета общего количества ног всех животных в списке животных, если на Java, то как это сделать?

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

4.3. Неограниченные подстановочные знаки

Неограниченный подстановочный знак?. Увидев это, вы можете спросить, а разве это не то же самое, что Т? Почему?. Основное различие между ними заключается в том, что T в основном используется для объявления универсального класса или метода, а ? в основном используется для использования универсальных классов и универсальных методов. Вот простой пример.

// 定义打印任何类型列表的函数
public static void printList(List<?> list) {
    for (Object elem: list) {
        System.out.print(elem + " ");
    }
}

// 调用上述函数
List<Integer> intList = Arrays.asList(1, 2, 3);
List<String> stringList = Arrays.asList("one", "two", "three");
printList(li);// 1 2 3 
printList(ls);// one two three

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

Возвращаясь к T, выше мы также перечислили, как определять универсальные методы и как вызывать универсальные методы. Внутри универсального метода нужно заботиться о конкретном типе, а не только о числе и не о нулевом значении.

4.4. Подстановочный знак верхней границы extends T>

теперь, когда?Он может представлять любой тип, так что же делают расширения?

Предположим, есть такое требование, мы разрешаем вызывать наши функции только определенным типам (например, всем классам Animal и их производным классам), но в настоящее время используем?, все типы могут вызывать функции, которые не могут удовлетворить наши потребности.

private int countLength(List< ? extends Animal> list) {...}

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

List<Pig> pigs = new ArrayList<>();
List<Dog> dogs = new ArrayList<>();
List<Cat> cats = new ArrayList<>();

// 假装写入了数据
int sum = 0;
sum += countLength(pigs);
sum += countLength(dogs);
sum += countLength(cats);

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

Некоторые люди могут быть немного сбиты с толку, но я воспользуюсь приведенным выше примером, чтобы объяснить это просто человеческими словами: верхняя граница подстановочного знака — это поле, в которое можно поместить любое животное.

4.5. Подстановочный знак Нижнего мира

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

В подстановочном знаке верхней границы выше мы привели пример. Напишите функцию, которая может обрабатывать любой класс животных и классы, производные от классов животных. А теперь мы собираемся написать функцию, которая обрабатывает любую функцию, которая является Integer и является суперклассом Integer.

public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}

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

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

Пример, приведенный в начале статьи, является типичным примером. После компиляции будут предприняты меры по дегенерализации.В процессе компиляции связанная информация об универсальном типе будет удалена после обнаружения результата универсального типа. Как и в примере, упомянутом в начале статьи, мы используем общий класс Generic, определенный выше, чтобы дать простой пример.

Generic<String> generic = new Generic<>("Hello");
Field[] fs = generic.getClass().getDeclaredFields();
for (Field f : fs) {
    System.out.println("type: " + f.getType().getName()); // type: java.lang.Object
}

getDeclaredFieldsЭто метод отражения, который может получать различные поля, объявленные в текущем классе, включая общедоступные, защищенные и частные.

Вы можете видеть, что общая строка, которую мы передали, была стерта и заменена объектом. Так куда же делась общая информация String и Integer? Может быть, у вас в это время появится озарение, все ли дженерики станут Объектами после стирания? Не волнуйтесь, продолжайте читать.

Что происходит, когда мы используем подстановочные знаки верхней границы для дженериков? Мы изменим класс Generic на следующую форму.

public class Generic<T extends String> {
    T data;

    public Generic(T data) {
        setData(data);
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

Затем снова используйте отражение, чтобы увидеть тип после общего стирания. На этот раз консоль выведетtype: java.lang.String. Как вы можете видеть, если мы поместим верхнюю границу универсального класса, универсальное стирание будет заменено верхней границей типа. И если он не указан, то он будет заменен на Object равномерно. Соответственно, тип метода определен в универсальном классе.

6. Пишите в конце

Если вы обнаружите какие-либо проблемы в статье, пожалуйста, дайте мне знать, и я исправлю их вовремя.

Ссылаться на:

  1. Стирание типа языка Java
  2. Подстановочный знак Нижнего мира
  3. Разница между списком> и списком

Прошлые статьи:

Связанный:

  • Персональный сайт:Lunhao Hu
  • Официальная учетная запись WeChat: заметки о полном стеке SH (или прямой поиск WeChat LunhaoHu в интерфейсе добавления официальной учетной записи)