Перейти к основному содержимому

Объяснение типа Erasure в Java

· 5 мин. чтения

1. Обзор

В этой быстрой статье мы обсудим основы важного механизма в дженериках Java, известного как стирание типов.

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

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

Например:

public static  <E> boolean containsElement(E [] elements, E element){
for (E e : elements){
if(e.equals(element)){
return true;
}
}
return false;
}

Компилятор заменяет несвязанный тип E фактическим типом Object :

public static  boolean containsElement(Object [] elements, Object element){
for (Object e : elements){
if(e.equals(element)){
return true;
}
}
return false;
}

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

3. Типы стирания шрифта

Стирание типа может происходить на уровне класса (или переменной) и метода.

3.1. Тип класса Стирание

На уровне класса компилятор отбрасывает параметры типа в классе и заменяет их первой привязкой или Object , если параметр типа не привязан.

Давайте реализуем стек , используя массив:

public class Stack<E> {
private E[] stackContent;

public Stack(int capacity) {
this.stackContent = (E[]) new Object[capacity];
}

public void push(E data) {
// ..
}

public E pop() {
// ..
}
}

При компиляции компилятор заменяет несвязанный параметр типа E на Object :

public class Stack {
private Object[] stackContent;

public Stack(int capacity) {
this.stackContent = (Object[]) new Object[capacity];
}

public void push(Object data) {
// ..
}

public Object pop() {
// ..
}
}

В случае, когда параметр типа E связан:

public class BoundStack<E extends Comparable<E>> {
private E[] stackContent;

public BoundStack(int capacity) {
this.stackContent = (E[]) new Object[capacity];
}

public void push(E data) {
// ..
}

public E pop() {
// ..
}
}

Компилятор заменит параметр связанного типа E первым связанным классом, в данном случае Comparable :

public class BoundStack {
private Comparable [] stackContent;

public BoundStack(int capacity) {
this.stackContent = (Comparable[]) new Object[capacity];
}

public void push(Comparable data) {
// ..
}

public Comparable pop() {
// ..
}
}

3.2. Тип метода Стирание

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

Давайте рассмотрим метод для отображения содержимого любого заданного массива:

public static <E> void printArray(E[] array) {
for (E element : array) {
System.out.printf("%s ", element);
}
}

При компиляции компилятор заменяет параметр типа E на Object :

public static void printArray(Object[] array) {
for (Object element : array) {
System.out.printf("%s ", element);
}
}

Для параметра типа связанного метода:

public static <E extends Comparable<E>> void printArray(E[] array) {
for (E element : array) {
System.out.printf("%s ", element);
}
}

У нас будет параметр типа E , стертый и замененный на Comparable:

public static void printArray(Comparable[] array) {
for (Comparable element : array) {
System.out.printf("%s ", element);
}
}

4. Пограничные случаи

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

Давайте создадим новый класс, расширяющий нашу предыдущую реализацию Stack. Обратите внимание, что это относится к классу Stack , который мы создали в разделе 3.1 , а не к классу java.util.Stack .

public class IntegerStack extends Stack<Integer> {

public IntegerStack(int capacity) {
super(capacity);
}

public void push(Integer value) {
super.push(value);
}
}

Теперь давайте посмотрим на следующий код:

IntegerStack integerStack = new IntegerStack(5);
Stack stack = integerStack;
stack.push("Hello");
Integer data = integerStack.pop();

После стирания типа имеем:

IntegerStack integerStack = new IntegerStack(5);
Stack stack = (IntegerStack) integerStack;
stack.push("Hello");
Integer data = (String) integerStack.pop();

Обратите внимание, как мы можем поместить строку S в IntegerStack , потому что IntegerStack унаследовал push(Object) от родительского класса Stack . Это, конечно, неверно, так как это должно быть целое число, так как integerStack является типом Stack<Integer> .

Поэтому неудивительно, что попытка извлечь String и присвоить Integer вызывает исключение ClassCastException из приведения , вставленного во время отправки компилятором.

4.1. Мостовые методы

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

В приведенном выше примере компилятор Java сохраняет полиморфизм универсальных типов после стирания, гарантируя отсутствие несоответствия сигнатур метода между методом push(Integer) класса IntegerStack и методом push(Object) класса Stack . ``

Следовательно, компилятор создает здесь метод моста:

public class IntegerStack extends Stack {
// Bridge method generated by the compiler

public void push(Object value) {
push((Integer)value);
}

public void push(Integer value) {
super.push(value);
}
}

Следовательно, метод push класса Stack после стирания типа делегируется исходному методу push класса IntegerStack . ``

5. Вывод

В этом руководстве мы обсудили концепцию стирания типов с примерами в переменных и методах параметров типа.

Вы можете прочитать больше об этих концепциях:

Как всегда, исходный код, сопровождающий эту статью, доступен на GitHub .