[java] Java의 제네릭에서 지우기 개념은 무엇입니까?

Java의 제네릭에서 지우기 개념은 무엇입니까?



답변

기본적으로 컴파일러 속임수를 통해 제네릭이 Java로 구현되는 방식입니다. 컴파일 된 제네릭 코드는 실제로java.lang.Object 당신이 이야기 하는 곳마다 T(또는 다른 유형 매개 변수) 사용합니다. 컴파일러에 실제로 제네릭 유형이라는 것을 알려주는 메타 데이터가 있습니다.

제네릭 형식이나 메서드에 대해 일부 코드를 컴파일하면 컴파일러는 실제로 의미하는 바를 확인하고 형식 인수가 무엇인지 T확인하고 컴파일 타임에 올바른 일을하고 있는지 확인 하지만 방출 된 코드는 다시 대화합니다. 측면에서 java.lang.Object-컴파일러는 필요한 경우 추가 캐스트를 생성합니다. 실행 시간에서 a List<String> 와 a List<Date>는 정확히 동일합니다. 추가 유형 정보가 컴파일러에 의해 지워졌 습니다.

같은 코드가 식을 포함 할 수 있도록 정보가 실행 시간에 유지 C #을, 말과 비교해 typeof(T)에 해당되는T.class 후자가 유효하지 않은 것을 제외을 -. .NET 제네릭과 Java 제네릭 사이에는 추가 차이점이 있습니다. 유형 삭제는 Java 제네릭을 처리 할 때 많은 “홀수”경고 / 오류 메시지의 소스입니다.

기타 자료 :


답변

부수적으로, 소거를 수행 할 때 컴파일러가 실제로 무엇을하고 있는지 확인하는 것은 흥미로운 연습입니다. 전체 개념을 이해하기 쉽게 만듭니다. 제네릭이 지워지고 캐스트가 삽입 된 Java 파일을 출력하도록 컴파일러에 전달할 수있는 특수 플래그가 있습니다. 예를 들면 :

javac -XD-printflat -d output_dir SomeFile.java

-printflat파일을 생성하는 컴파일러에 넘겨 도착 플래그입니다. (이 -XD부분은 javac실제로 컴파일하는 것이 아니라 실행 가능한 jar 파일로 전달하는 것입니다 javac. 그러나 나는 -d output_dir벗어납니다 …) 컴파일러는 새로운 .java 파일을 넣을 장소가 필요하기 때문에 필요합니다.

물론 이것은 단순히 지우는 것 이상의 역할을합니다. 컴파일러가 수행하는 모든 자동 작업은 여기에서 수행됩니다. 예를 들어, 기본 생성자가 삽입되고 새로운 foreach 스타일 for루프가 일반 for루프 등으로 확장됩니다 . 자동으로 발생하는 작은 일을 보는 것이 좋습니다.


답변

삭제는 말 그대로 소스 코드에 존재하는 유형 정보가 컴파일 된 바이트 코드에서 지워짐을 의미합니다. 몇 가지 코드로 이것을 이해하자.

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class GenericsErasure {
    public static void main(String args[]) {
        List<String> list = new ArrayList<String>();
        list.add("Hello");
        Iterator<String> iter = list.iterator();
        while(iter.hasNext()) {
            String s = iter.next();
            System.out.println(s);
        }
    }
}

이 코드를 컴파일 한 다음 Java 디 컴파일러로 디 컴파일하면 다음과 같은 결과가 나타납니다. 디 컴파일 된 코드에는 원본 소스 코드에 존재하는 유형 정보의 흔적이 포함되어 있지 않습니다.

import java.io.PrintStream;
import java.util.*;

public class GenericsErasure
{

    public GenericsErasure()
    {
    }

    public static void main(String args[])
    {
        List list = new ArrayList();
        list.add("Hello");
        String s;
        for(Iterator iter = list.iterator(); iter.hasNext(); System.out.println(s))
            s = (String)iter.next();

    }
} 


답변

이미 매우 완전한 Jon Skeet의 답변을 완성하려면 유형 삭제 개념이 이전 버전의 Java와의 호환성이 필요 하다는 것을 알아야합니다 .

EclipseCon 2007에서 처음 제공 (더 이상 사용할 수 없음) 한 호환성은 다음과 같습니다.

  • 소스 호환성 (좋아요 …)
  • 이진 호환성 (필수)
  • 마이그레이션 호환성
    • 기존 프로그램은 계속 작동해야합니다
    • 기존 라이브러리는 일반 유형을 사용할 수 있어야합니다
    • 있어야합니다!

원래 답변 :

그 후:

new ArrayList<String>() => new ArrayList()

더 큰 통일을 위한 제안이 있습니다 . 언어 구조가 구문 설탕뿐만 아니라 개념이어야하는 “추상적 인 개념을 실제 개념으로 간주”로 되십시오.

또한 checkCollection지정된 컬렉션의 동적 형식 안전 뷰를 반환하는 Java 6 의 방법에 대해서도 언급해야 합니다. 잘못된 유형의 요소를 삽입하려고하면 즉시 발생합니다 ClassCastException.

언어의 제네릭 메커니즘 은 컴파일 타임 (정적) 유형 검사를 제공하지만, 확인되지 않은 캐스트로이 메커니즘을 물리 칠 수 있습니다 .

컴파일러는 검사되지 않은 모든 작업에 대해 경고를 발행하므로 일반적으로 문제가되지 않습니다.

그러나 정적 유형 검사만으로는 충분하지 않은 경우가 있습니다.

  • 콜렉션이 써드 파티 라이브러리로 전달 될 때 라이브러리 코드가 잘못된 유형의 요소를 삽입하여 콜렉션을 손상시키지 않아야합니다.
  • ClassCastException잘못 입력 된 요소가 매개 변수화 된 콜렉션에 입력되었음을 나타내는 프로그램이로 실패합니다 . 불행히도, 잘못된 요소가 삽입 된 후 언제든지 예외가 발생할 수 있으므로 일반적으로 문제의 실제 원인에 대한 정보를 거의 또는 전혀 제공하지 않습니다.

거의 4 년 후 2012 년 7 월 업데이트 :

이제는 ” API 마이그레이션 호환성 규칙 (서명 테스트) “에 자세히 설명되어 있습니다 (2012).

Java 프로그래밍 언어는 삭제를 사용하여 제네릭을 구현하므로 레거시 버전과 제네릭 버전은 일반적으로 유형에 대한 일부 보조 정보를 제외하고 동일한 클래스 파일을 생성합니다. 클라이언트 코드를 변경하거나 다시 컴파일하지 않고도 레거시 클래스 파일을 일반 클래스 파일로 바꿀 수 있기 때문에 이진 호환성이 손상되지 않습니다.

제네릭이 아닌 레거시 코드와의 인터페이스를 용이하게하기 위해 매개 변수화 된 유형의 삭제를 유형으로 사용할 수도 있습니다. 이러한 유형을 원시 유형 ( Java Language Specification 3 / 4.8 )이라고합니다. 원시 유형을 허용하면 소스 코드와의 호환성도 보장됩니다.

이에 따르면, 다음 버전의 java.util.Iterator클래스는 이진 및 소스 코드 모두 이전 버전과 호환됩니다.

Class java.util.Iterator as it is defined in Java SE version 1.4:

public interface Iterator {
    boolean hasNext();
    Object next();
    void remove();
}

Class java.util.Iterator as it is defined in Java SE version 5.0:

public interface Iterator<E> {
    boolean hasNext();
    E next();
    void remove();
}


답변

이미 보완 된 Jon Skeet 답변을 보완하는 중 …

소거를 통해 제네릭을 구현하면 성가신 한계가 발생한다고 언급했습니다 (예 : no new T[42]). 이런 식으로 작업을 수행하는 주된 이유는 바이트 코드에서 이전 버전과의 호환성이라고 언급되었습니다. 이것은 또한 (대부분) 사실입니다. -target 1.5로 생성 된 바이트 코드는 설탕 제거 캐스팅 -target 1.4와 약간 다릅니다. 기술적으로, 심지어 엄청나게 많은 속임수를 통해 런타임 에 일반 유형 인스턴스화 액세스 하여 바이트 코드에 실제로 무언가가 있음을 증명할 수도 있습니다.

더 흥미로운 점은 제기되지 않았지만 삭제를 사용하여 제네릭을 구현하면 고수준 유형 시스템이 달성 할 수있는 것보다 훨씬 더 유연성이 있다는 것입니다. 이에 대한 좋은 예는 Scala의 JVM 구현 대 CLR입니다. JVM에서는 JVM 자체가 일반 유형에 대한 제한을 부과하지 않기 때문에 더 높은 종류를 직접 구현할 수 있습니다 (이러한 “유형”은 사실상 없기 때문에). 이는 매개 변수 인스턴스화에 대한 런타임 지식이있는 CLR과 대조됩니다. 이 때문에 CLR 자체에는 제네릭을 사용하는 방법에 대한 개념이 있어야합니다. 예상치 못한 규칙으로 시스템을 확장하려는 시도는 무효화됩니다. 결과적으로 CLR에 대한 스칼라의 높은 종류는 컴파일러 자체 내에서 모방 된 이상한 형태의 소거를 사용하여 구현됩니다.

런타임에 나쁜 일을하고 싶을 때는 삭제가 불편할 수 있지만 컴파일러 작성자에게 가장 큰 유연성을 제공합니다. 나는 그것이 곧 사라지지 않는 이유의 일부라고 생각합니다.


답변

내가 이해하는 것처럼 ( .NET 사람 이기 때문에 ) JVM 에는 제네릭 개념이 없으므로 컴파일러는 유형 매개 변수를 Object로 바꾸고 모든 캐스팅을 수행합니다.

이것은 Java 제네릭이 구문 설탕 일 뿐이며 참조로 전달 될 때 boxing / unboxing이 필요한 값 유형에 대한 성능 향상을 제공하지 않음을 의미합니다.


답변

좋은 설명이 있습니다. 유형 삭제가 디 컴파일러에서 작동하는 방법을 보여주는 예제 만 추가합니다.

오리지널 수업,

import java.util.ArrayList;
import java.util.List;


public class S<T> {

    T obj;

    S(T o) {
        obj = o;
    }

    T getob() {
        return obj;
    }

    public static void main(String args[]) {
        List<String> list = new ArrayList<>();
        list.add("Hello");

        // for-each
        for(String s : list) {
            String temp = s;
            System.out.println(temp);
        }

        // stream
        list.forEach(System.out::println);
    }
}

바이트 코드에서 디 컴파일 된 코드

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Objects;
import java.util.function.Consumer;

public class S {

   Object obj;


   S(Object var1) {
      this.obj = var1;
   }

   Object getob() {
      return this.obj;
   }

   public static void main(String[] var0) {

   ArrayList var1 = new ArrayList();
   var1.add("Hello");


   // for-each
   Iterator iterator = var1.iterator();

   while (iterator.hasNext()) {
         String string;
         String string2 = string = (String)iterator.next();
         System.out.println(string2);
   }


   // stream
   PrintStream printStream = System.out;
   Objects.requireNonNull(printStream);
   var1.forEach(printStream::println);


   }
}