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


141

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

답변:


200

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

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

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

기타 자료 :


6
@Rogerio : 아니요, 객체 에는 다른 일반 유형이 없습니다. 필드 유형을 알고 있지만, 객체하지 않습니다.
Jon Skeet

8
@Rogerio : 물론-실행 시간에 Object(약한 형식의 시나리오에서) 제공되는 것이 실제로) 인지 쉽게 알 수 List<String>있습니다. Java에서는 실현 가능하지 않습니다. Java 인 ArrayList경우 원래의 제네릭 형식이 아니라는 것을 알 수 있습니다 . 이러한 종류의 일은 예를 들어 직렬화 / 직렬화 해제 상황에서 발생할 수 있습니다. 또 다른 예는 컨테이너가 제네릭 형식의 인스턴스를 구성 할 수 있어야하는 경우입니다 Class<T>. Java에서 해당 형식을 개별적으로 전달해야합니다 (as ).
Jon Skeet

6
나는 그것이 항상 또는 거의 항상 문제라고 주장하지는 않았지만 적어도 내 경험에있어서 합리적으로 자주 발생하는 문제입니다. Class<T>Java가 해당 정보를 보유하지 않기 때문에 생성자 (또는 일반 메소드)에 매개 변수 를 추가 해야하는 다양한 장소 가 있습니다. 봐 EnumSet.allOf- 예를 들어, 메서드의 제네릭 형식 인수가 충분해야한다; 왜 "정상적인"인수를 지정해야합니까? 답 : 유형 삭제. 이런 종류의 것은 API를 오염시킵니다. 관심이 없다면 .NET 제네릭을 많이 사용 했습니까? (계속)
Jon Skeet

5
.NET 제네릭을 사용하기 전에 Java 제네릭을 여러 가지 방식으로 어색한 것으로 나타났습니다. "호출자 지정"형태의 분산에는 분명히 이점이 있지만 와일드 카드는 여전히 골치 아픈 일입니다. 그러나 .NET 제네릭을 사용한 후에 만 한동안 Java 제네릭으로 몇 개의 패턴이 어색하거나 불가능 해졌습니다. 다시 Blub 역설입니다. .NET 제네릭에는 단점도 없습니다. 불행히도 표현할 수없는 다양한 유형 관계가 있습니다.하지만 Java 제네릭보다 훨씬 선호합니다.
Jon Skeet

5
@Rogerio : 리플렉션으로 할 있는 일이 많지만 Java 제네릭으로 할 수없는 것만 큼 ​​자주 그런 일을 하고 싶지 는 않습니다 . 나는 필드에 대한 형식 인수를 발견하지 않으려는 거의 내가 실제 개체의 형식 인수를 찾아 원하는만큼 자주.
Jon Skeet

41

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

javac -XD-printflat -d output_dir SomeFile.java

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

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


29

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

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();

    }
} 

java decompiler를 사용하여 .class 파일에서 유형 삭제 후 코드를 보려고했지만 .class 파일에는 여전히 유형 정보가 있습니다. 나는 jigawot말했다, 그것은 작동합니다.
Frank

25

이미 매우 완전한 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();
}

2
이전 버전과의 호환성은 유형을 지우지 않고 달성 할 수 있었지만 Java 프로그래머가 새로운 컬렉션 집합을 배우지 않으면 불가능합니다. 이것이 바로 .NET의 경로입니다. 다시 말해,이 세 번째 글 머리 기호는 중요한 것입니다. (계속)
Jon Skeet

15
개인적으로 이것은 근시안적 실수라고 생각합니다. 단기적인 장점과 장기적인 단점이 있습니다.
Jon Skeet

8

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

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

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

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


6
실행 시간에 "이상한"일을하고 싶을 때는 불편하지 않습니다. 실행 시간에 완벽하게 합리적인 일을하고 싶을 때입니다. 실제로 유형 삭제를 사용하면 List <String>을 List로 캐스팅 한 다음 경고만으로 List <Date>로 캐스팅하는 등 훨씬 더 이상한 일을 할 수 있습니다.
Jon Skeet

5

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

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


3
Java 제네릭은 어쨌든 값 유형을 나타낼 수 없습니다. List <int>와 같은 것은 없습니다. 그러나 Java에는 참조를 통한 전달이 전혀 없습니다. 값을 기준으로 엄격하게 전달됩니다 (값이 참조 일 수 있음)
Jon Skeet

2

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

오리지널 수업,

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);


   }
}

2

제네릭을 사용하는 이유

간단히 말해 제네릭을 사용하면 클래스, 인터페이스 및 메서드를 정의 할 때 형식 (클래스 및 인터페이스)이 매개 변수가 될 수 있습니다. 메소드 선언에 사용되는 친숙한 형식 매개 변수와 마찬가지로 유형 매개 변수는 다른 입력으로 동일한 코드를 재사용 할 수있는 방법을 제공합니다. 차이점은 형식 매개 변수에 대한 입력은 값이고 유형 매개 변수에 대한 입력은 유형입니다. 제네릭을 사용하는 ode는 제네릭이 아닌 코드에 비해 많은 이점이 있습니다.

  • 컴파일 타임에 더 강력한 유형 검사.
  • 캐스트 제거.
  • 프로그래머가 일반 알고리즘을 구현할 수 있도록합니다.

타입 소거 란?

컴파일시에 더 엄격한 유형 검사를 제공하고 일반 프로그래밍을 지원하기 위해 Java 언어에 제네릭이 도입되었습니다. 제네릭을 구현하기 위해 Java 컴파일러는 유형 삭제를 다음에 적용합니다.

  • 제네릭 형식의 모든 형식 매개 변수를 해당 범위 또는 Object 형식 매개 변수가 바인딩되지 않은 경우 Object로 바꿉니다. 따라서 생성 된 바이트 코드에는 일반 클래스, 인터페이스 및 메소드 만 포함됩니다.
  • 유형 안전을 유지하기 위해 필요한 경우 유형 캐스트를 삽입하십시오.
  • 확장 된 제네릭 형식에서 다형성을 유지하기 위해 브리지 방법을 생성합니다.

[NB]-브릿지 방식이란 무엇입니까? 간단히 말해,와 같은 매개 변수화 된 인터페이스의 경우 Comparable<T>컴파일러에서 추가 메소드를 삽입 할 수 있습니다. 이러한 추가 방법을 브리지라고합니다.

지우는 방법

유형의 삭제는 다음과 같이 정의됩니다. 매개 변수화 된 유형에서 모든 유형 매개 변수를 삭제하고 유형 변수를 해당 경계의 삭제로, 또는 경계가없는 경우 Object로 바꾸거나 가장 왼쪽에있는 경계로 지 웁니다. 여러 경계. 여기 몇 가지 예가 있어요.

  • 의 소거는 List<Integer>, List<String>, 및 List<List<String>>이다 List.
  • 삭제는 List<Integer>[]입니다 List[].
  • 소거 List는 자체 유형이며 모든 원시 유형과 유사합니다.
  • int의 소거 자체는 모든 기본 유형과 유사합니다.
  • Integer유형 매개 변수가없는 모든 유형의 경우와 마찬가지로 삭제도 마찬가지입니다 .
  • 의 삭제 T의 정의에 asListIS는 Object, 때문에이 T 더 바인딩 없다.
  • 의 삭제 T의 정의에 maxIS는 Comparable, 때문에이 T 결합했다 Comparable<? super T>.
  • T최종 정의에서 삭제는 max입니다 Object. 왜냐하면 & T는 바인딩 되었고 우리는 가장 왼쪽 바인딩을 삭제 하기 때문 입니다.ObjectComparable<T>

제네릭 사용시주의해야 함

Java에서 두 가지 고유 한 메소드는 동일한 서명을 가질 수 없습니다. 제네릭은 삭제에 의해 구현되기 때문에 두 가지 고유 한 메소드가 동일한 삭제를 갖는 서명을 가질 수 없다는 점도 따릅니다. 클래스는 서명이 동일한 삭제를 갖는 두 개의 메소드를 오버로드 할 수 없으며 클래스는 동일한 삭제를 갖는 두 개의 인터페이스를 구현할 수 없습니다.

    class Overloaded2 {
        // compile-time error, cannot overload two methods with same erasure
        public static boolean allZero(List<Integer> ints) {
            for (int i : ints) if (i != 0) return false;
            return true;
        }
        public static boolean allZero(List<String> strings) {
            for (String s : strings) if (s.length() != 0) return false;
            return true;
        }
    }

이 코드는 다음과 같이 작동합니다.

assert allZero(Arrays.asList(0,0,0));
assert allZero(Arrays.asList("","",""));

그러나이 경우 두 방법의 서명 삭제는 동일합니다.

boolean allZero(List)

따라서 컴파일시 이름 충돌이보고됩니다. 두 메소드 모두에 동일한 이름을 부여 할 수 없으며 오버로드를 통해 두 메소드를 구별하려고 시도 할 수 없습니다. 삭제 후 하나의 메소드 호출을 다른 메소드 호출과 구별 할 수 없기 때문입니다.

잘만되면, 독자는 즐길 것이다 :)

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