Java 제네릭 형식 삭제 : 언제 그리고 어떻게됩니까?


238

Oracle 웹 사이트에서 Java 유형 삭제 대해 읽었습니다 .

타입 삭제는 언제 발생합니까? 컴파일 타임이나 런타임에? 수업이로드되면? 수업이 언제 시작됩니까?

많은 사이트 (위에서 언급 한 공식 튜토리얼 포함)는 컴파일시에 타입 삭제가 발생한다고 말합니다. 컴파일시에 타입 정보가 완전히 제거되면, 타입 정보가 없거나 잘못된 타입 정보로 제네릭을 사용하는 메소드가 호출 될 때 JDK는 어떻게 타입 호환성을 검사합니까?

다음 예제를 고려하십시오. 클래스 A에 메소드가 있다고 가정하십시오 empty(Box<? extends Number> b). 우리는 컴파일 A.java하고 클래스 파일을 얻습니다 A.class.

public class A {
    public static void empty(Box<? extends Number> b) {}
}
public class Box<T> {}

이제 매개 변수가없는 인수 (원시 유형)로 B메서드를 호출하는 다른 클래스 를 만듭니다 . 우리가 컴파일하는 경우 와 클래스 패스에, javac의 경고를 높이기 위해 스마트 충분히입니다. 그래서emptyempty(new Box())B.javaA.classA.class 그 안에 저장된 어떤 종류의 정보를.

public class B {
    public static void invoke() {
        // java: unchecked method invocation:
        //  method empty in class A is applied to given types
        //  required: Box<? extends java.lang.Number>
        //  found:    Box
        // java: unchecked conversion
        //  required: Box<? extends java.lang.Number>
        //  found:    Box
        A.empty(new Box());
    }
}

내 생각 엔 클래스가로드 될 때 유형 지우기가 발생하지만 추측 일뿐입니다. 언제 발생합니까?



@afryingpan : 내 답변에 언급 된 기사는 유형 삭제가 언제 어떻게 발생하는지 자세히 설명합니다. 유형 정보가 보관되는시기도 설명합니다. 다시 말해, 통일 된 제네릭은 광범위한 신념과 달리 Java로 제공됩니다. 참조 : rgomes.info/using-typetokens-to-retrieve-generic-parameters
Richard Gomes

답변:


240

유형 삭제는 제네릭 사용 에 적용됩니다 . 클래스 파일에는 메서드 / 유형 일반적 인지 여부 와 제약 조건 등 을 나타내는 메타 데이터 가 있습니다. 그러나 generics가 사용 되면 컴파일 타임 검사 및 실행 시간 캐스트로 변환됩니다. 따라서이 코드 :

List<String> list = new ArrayList<String>();
list.add("Hi");
String x = list.get(0);

에 컴파일

List list = new ArrayList();
list.add("Hi");
String x = (String) list.get(0);

실행 시간에는 T=String목록 객체에 대한 정보 를 찾을 방법이 없습니다 .

...하지만 List<T>인터페이스 자체는 여전히 일반적인 것으로 광고합니다.

편집 : 명확히하기 위해 컴파일러는 변수 에 대한 정보를 a로 유지 List<String>하지만 여전히 T=String목록 객체 자체에 대한 정보를 찾을 수는 없습니다 .


6
아니요, 일반 유형을 사용하더라도 런타임시 메타 데이터가 제공 될 수 있습니다. 리플렉션을 통해 로컬 변수에 액세스 할 수 없지만 "List <String> l"로 선언 된 메서드 매개 변수의 경우 리플렉션 API를 통해 사용 가능한 메타 데이터 형식이 런타임에 있습니다. 그렇다. "유형 소거"는 많은 사람들이 생각하는 것처럼 간단하지 않다 ...
Rogério

4
@Rogerio : 귀하의 의견에 대답하면서 변수 유형을 얻는 것과 객체 유형을 얻는 것은 혼란 스럽다고 생각 합니다 . 필드 자체는 있지만 개체 자체는 형식 인수를 알지 못합니다.
Jon Skeet

물론 객체 자체를 보면 객체가 List <String>이라는 것을 알 수 없습니다. 그러나 물체는 아무데도 나타나지 않습니다. 그것들은 로컬로 작성되고, 메소드 호출 인수로 전달되거나, 메소드 호출에서 리턴 값으로 리턴되거나, 일부 오브젝트의 필드에서 읽습니다.이 모든 경우 런타임에서 일반 유형이 무엇인지 알 수 있습니다. 암시 적으로 또는 Java Reflection API를 사용하여.
Rogério

13
@Rogerio : 객체가 어디에서 왔는지 어떻게 알 수 있습니까? 유형의 매개 변수가있는 경우 해당 List<? extends InputStream>유형이 작성된 유형을 어떻게 알 수 있습니까? 참조가 저장된 필드의 유형을 찾을 있다고하더라도 왜해야합니까? 실행 시간에 객체에 대한 나머지 정보를 모두 얻을 수 있지만 일반적인 유형 인수는 왜 얻을 수 없습니까? 개발자에게 실제로 영향을 미치지 않는이 작은 것으로 유형을 지우려고하는 것처럼 보이지만 어떤 경우에는 매우 중요한 문제 라고 생각합니다 .
Jon Skeet

그러나 유형 삭제는 개발자에게 실제로 영향을 미치지 않는 아주 작은 것입니다! 물론, 나는 다른 사람들과 대화 할 수 없지만 내 경험으로는 결코 큰 문제가되지 않았습니다. 실제로 Java Mocking API (JMockit) 디자인에서 런타임 유형 정보를 활용합니다. 아이러니하게도 .NET mocking API는 C #에서 사용할 수있는 제네릭 형식 시스템을 덜 활용하는 것 같습니다.
Rogério

99

컴파일러는 컴파일 타임에 제네릭을 이해해야합니다. 컴파일러는 타입 삭제 라고 부르는 프로세스에서 일반 클래스의 "이해"를 버리는 책임도 있습니다. . 모든 컴파일 시간에 발생합니다.

참고 : 대부분의 Java 개발자의 생각 달리 컴파일 타임 유형 정보를 유지하고 런타임에이 정보를 매우 제한적인 방식으로 검색 할 수 있습니다. 즉, Java는 매우 제한된 방식으로 통합 제네릭을 제공합니다 .

타입 소거에 대하여

컴파일 타임에 컴파일러는 전체 유형 정보를 사용할 수 있지만이 정보는 일반적으로 바이트 코드가 생성 될 때 유형 삭제 라고 알려진 프로세스에서 의도적 으로 삭제 됩니다. 호환성 문제로 인해 이러한 방식으로 수행됩니다. 언어 디자이너의 의도는 플랫폼 버전간에 전체 소스 코드 호환성과 전체 바이트 코드 호환성을 제공하는 것이 었습니다. 다르게 구현 된 경우 최신 버전의 플랫폼으로 마이그레이션 할 때 레거시 응용 프로그램을 다시 컴파일해야합니다. 완료된 방식으로 모든 메소드 서명이 유지되고 (소스 코드 호환성) 아무것도 다시 컴파일 할 필요가 없습니다 (이진 호환성).

Java의 통합 제네릭 관련

컴파일 타임 유형 정보를 유지해야하는 경우 익명 클래스를 사용해야합니다. 요점은 다음과 같습니다. 매우 특수한 익명 클래스의 경우 런타임에 전체 컴파일 타임 유형 정보를 검색 할 수 있습니다. 즉, 즉 일반화 된 제네릭입니다. 이것은 익명 클래스가 관련 될 때 컴파일러가 유형 정보를 버리지 않음을 의미합니다. 이 정보는 생성 된 이진 코드로 유지되며 런타임 시스템을 통해이 정보를 검색 할 수 있습니다.

이 주제에 관한 기사를 작성했습니다.

https://rgomes.info/using-typetokens-to-retrieve-generic-parameters/

위 기사에서 설명한 기술에 대한 참고 사항은 대부분의 개발자에게는이 기술이 모호하다는 것입니다. 작동하고 잘 작동하지만 대부분의 개발자는이 기술에 대해 혼동되거나 불편 함을 느낍니다. 공유 코드 기반이 있거나 코드를 공개 할 계획이라면 위의 기술을 권장하지 않습니다. 반면, 코드의 유일한 사용자 인 경우이 기술이 제공하는 강력한 기능을 활용할 수 있습니다.

샘플 코드

위의 기사에는 샘플 코드에 대한 링크가 있습니다.


1
@ will824 : 나는 대답을 크게 개선했으며 일부 테스트 사례에 대한 링크를 추가했습니다. 건배 :)
Richard Gomes

1
실제로 바이너리와 소스 호환성을 모두 유지하지 않았습니다. oracle.com/technetwork/java/javase/compatibility-137462.html 그들의 의도에 대한 자세한 내용은 어디서 볼 있습니까? 문서에서는 유형 삭제를 사용하지만 그 이유는 밝히지 않습니다.
Dzmitry Lazerka

@Richard 실제로, 훌륭한 기사! 로컬 클래스도 작동하고 두 경우 모두 (익명 클래스와 로컬 클래스) 컴파일러가 형식 정보를 유지하지 않기 때문에 new Box<String>() {};간접 액세스의 경우가 아닌 직접 액세스의 경우에만 원하는 형식 인수에 대한 정보가 유지되도록 추가 할 수 있습니다 void foo(T) {...new Box<T>() {};...}둘러싸는 메소드 선언
Yann-Gaël Guéhéneuc

내 기사에 대한 깨진 링크를 수정했습니다. 나는 천천히 내 인생을 인터넷 검색하고 데이터를 되찾고 있습니다. :-)
Richard Gomes

33

제네릭 형식 인 필드가 있으면 해당 형식 매개 변수가 클래스로 컴파일됩니다.

제네릭 형식을 가져 오거나 반환하는 메서드가 있으면 해당 형식 매개 변수가 클래스로 컴파일됩니다.

이 정보는 컴파일러가 당신이 통과 할 수없는 당신에게 사용하는 것입니다 Box<String>받는 empty(Box<T extends Number>)방법.

API는 복잡하지만, 같은 방법과 반사 API를 통해 이러한 유형의 정보를 검사 할 수 있습니다 getGenericParameterTypes, getGenericReturnType, 필드, 및 getGenericType.

제네릭 형식을 사용하는 코드가있는 경우 컴파일러는 필요에 따라 캐스트를 삽입하여 호출자를 검사하여 형식을 확인합니다. 일반 객체 자체는 단지 원시 유형입니다. 매개 변수화 된 유형은 "삭제됨"입니다. 따라서를 만들 때 객체 new Box<Integer>()Integer클래스에 대한 정보가 없습니다 Box.

Angelika Langer의 FAQ 는 Java Generics에서 본 최고의 참조입니다.


2
실제로, 클래스로 컴파일되는 필드 및 메소드 의 형식적인 일반 유형입니다 (일반적으로 "T"). 제네릭 형식 의 실제 형식 을 얻으려면 "익명 클래스 트릭"을 사용해야합니다 .
Yann-Gaël Guéhéneuc

13

Java 언어의 제네릭은 이 주제에 대한 훌륭한 안내서입니다.

제네릭은 Java 컴파일러에 의해 삭제라는 프론트 엔드 변환으로 구현됩니다. 당신은 (거의) 그것을 소스-소스 변환으로 생각할 수 있으며, 이로 인해 일반 버전은 일반 버전이 loophole()아닌 버전으로 변환됩니다.

따라서 컴파일 타임입니다. JVM은 ArrayList사용자가 사용한 것을 절대 알 수 없습니다 .

Java에서 제네릭의 삭제 개념은 무엇입니까?에 대한 Skeet의 답변을 추천합니다.


6

컴파일시 타입 삭제가 발생합니다. 유형 삭제의 의미는 모든 유형이 아니라 일반 유형을 잊어 버린다는 것입니다. 게다가, 제네릭 형식에 대한 메타 데이터는 여전히 존재합니다. 예를 들어

Box<String> b = new Box<String>();
String x = b.getDefault();

로 변환

Box b = new Box();
String x = (String) b.getDefault();

컴파일 타임에. 컴파일러가 어떤 유형의 제네릭을 알기 때문에 경고를받을 수는 없지만, 충분히 알지 못하기 때문에 타입 안전을 보장 할 수 없기 때문에 경고가 표시 될 수 있습니다.

또한 컴파일러는 리플렉션을 통해 검색 할 수있는 메서드 호출의 매개 변수에 대한 형식 정보를 유지합니다.

안내서 는 내가 찾은 주제 중 최고입니다.


6

"유형 삭제"라는 용어는 실제로 제네릭에 대한 Java의 문제점에 대한 올바른 설명이 아닙니다. 유형 삭제는 그 자체로 나쁜 것이 아니며 실제로 성능에 매우 필요하며 종종 C ++, Haskell, D와 같은 여러 언어에서 사용됩니다.

역겨워하기 전에 위키 에서 올바른 유형 삭제 정의를 기억하십시오.

유형 삭제 란 무엇입니까?

유형 삭제는 런타임에 실행되기 전에 명시 적 유형 주석이 프로그램에서 제거되는로드 프로세스입니다.

타입 삭제는 디자인 타임에 생성 된 타입 태그 나 컴파일 타임에 유추 된 타입 태그를 버려 바이너리 코드로 컴파일 된 프로그램에 타입 태그가 포함되지 않도록하는 것을 의미합니다. 런타임 태그가 필요한 경우를 제외하고 모든 프로그래밍 언어가 이진 코드로 컴파일하는 경우가 여기에 해당합니다. 이러한 예외에는 모든 존재 유형 (하위 유형이 가능한 Java 참조 유형, 여러 언어의 모든 유형, 공용체 유형)이 포함됩니다. 타입 소거의 이유는 타입이 추상화 일 뿐이고 값을위한 구조와 그 의미를 처리하는 적절한 의미를 가지기 때문에 프로그램이 어떤 종류의 단일 타입 언어 (비트 만 허용하는 이진 언어)로 변환되기 때문입니다.

그래서 이것은 정상적인 자연의 결과입니다.

Java의 문제점은 다르며이를 재정의하는 방법으로 인해 발생합니다.

Java에 대해 자주 작성된 진술에는 제네릭 제네릭이 없습니다.

Java는 이전 버전과의 호환성으로 인해 잘못된 방식으로 수정됩니다.

통일이란 무엇입니까?

우리 위키에서

Reification은 컴퓨터 프로그램에 대한 추상적 인 아이디어를 명시 적 데이터 모델 또는 프로그래밍 언어로 작성된 다른 개체로 변환하는 프로세스입니다.

Reification은 전문화에 의해 추상 (Parametric Type)을 구체적인 (Concrete Type)으로 변환하는 것을 의미합니다.

간단한 예를 통해이를 설명합니다.

정의가있는 ArrayList :

ArrayList<T>
{
    T[] elems;
    ...//methods
}

Integer는 구체적인 유형으로 특수화 될 때 "통합"되는 추상화 된 유형 생성자입니다.

ArrayList<Integer>
{
    Integer[] elems;
}

ArrayList<Integer>실제로 유형은 어디 입니까?

그러나 이것은 정확히 Java 가하지 않는 것입니다 !!! 대신에, 그것들은 한계를 가지고 끊임없이 추상적 타입을 구체화한다.

ArrayList
{
    Object[] elems;
}

여기서는 암시 적 바운드 Object ( ArrayList<T extends Object>== ArrayList<T>)로 수정되었습니다.

그럼에도 불구하고 일반 배열을 사용할 수 없게 만들고 원시 유형에 대해 이상한 오류가 발생합니다.

List<String> l= List.<String>of("h","s");
List lRaw=l
l.add(new Object())
String s=l.get(2) //Cast Exception

그것은 다음과 같이 많은 모호성을 유발합니다

void function(ArrayList<Integer> list){}
void function(ArrayList<Float> list){}
void function(ArrayList<String> list){}

동일한 기능을 참조하십시오.

void function(ArrayList list)

따라서 일반적인 메소드 오버로드는 Java에서 사용할 수 없습니다.


2

Android에서 유형 삭제가 발생했습니다. 프로덕션에서는 minify 옵션과 함께 gradle을 사용합니다. 축소 후 치명적인 예외가 있습니다. 객체의 상속 체인을 표시하는 간단한 기능을 만들었습니다.

public static void printSuperclasses(Class clazz) {
    Type superClass = clazz.getGenericSuperclass();

    Log.d("Reflection", "this class: " + (clazz == null ? "null" : clazz.getName()));
    Log.d("Reflection", "superClass: " + (superClass == null ? "null" : superClass.toString()));

    while (superClass != null && clazz != null) {
        clazz = clazz.getSuperclass();
        superClass = clazz.getGenericSuperclass();

        Log.d("Reflection", "this class: " + (clazz == null ? "null" : clazz.getName()));
        Log.d("Reflection", "superClass: " + (superClass == null ? "null" : superClass.toString()));
    }
}

이 기능에는 두 가지 결과가 있습니다.

축소되지 않은 코드 :

D/Reflection: this class: com.example.App.UsersList
D/Reflection: superClass: com.example.App.SortedListWrapper<com.example.App.Models.User>

D/Reflection: this class: com.example.App.SortedListWrapper
D/Reflection: superClass: android.support.v7.util.SortedList$Callback<T>

D/Reflection: this class: android.support.v7.util.SortedList$Callback
D/Reflection: superClass: class java.lang.Object

D/Reflection: this class: java.lang.Object
D/Reflection: superClass: null

축소 된 코드 :

D/Reflection: this class: com.example.App.UsersList
D/Reflection: superClass: class com.example.App.SortedListWrapper

D/Reflection: this class: com.example.App.SortedListWrapper
D/Reflection: superClass: class android.support.v7.g.e

D/Reflection: this class: android.support.v7.g.e
D/Reflection: superClass: class java.lang.Object

D/Reflection: this class: java.lang.Object
D/Reflection: superClass: null

따라서 축소 된 코드에서 실제 매개 변수화 된 클래스는 유형 정보가없는 원시 클래스 유형으로 대체됩니다. 내 프로젝트의 솔루션으로 모든 리플렉션 호출을 제거하고 함수 인수에 전달 된 명시 적 params 유형으로 다시 작성했습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.