Background: how we got the generics we have

Background: how we got the generics we have (Or, how I learned to stop worrying and love erasure)

Brian Goetz, June 2020

제네릭이 어디로 가는지 이야기하기 전에 먼저 그들이 어디에 있는지, 어떻게 거기에 도달했는지 이야기해야합니다.
이 문서는 주로 우리가 현재 가지고있는 제네릭에 도달 한 방법과 현재 보유하고있는 제네릭이
우리가 구축하려는 “더 나은” 제네릭에 어떻게 영향을 미치는지에 대한 토대를 설정하는 방법에 초점을 맞출 것입니다.

특히 2004 년에 Java에 제네릭을 추가하는 데있어 erasure가 실제로 합리적이고 실용적인 선택 이었음을 강조합니다.
erasure를 통해 translation을 선택하게 된 많은 힘은 오늘날에도 여전히 작동 할 수 있습니다.

Erasure

개발자에게 Java 제네릭에 대해 물어 보면 erasure 에 대해 화를 낼 수 있습니다.
erasure 는 아마도 Java에서 가장 광범위하고 깊이 오해받는 개념 일 것입니다.

erasure는 Java 나 제네릭에만 국한되지 않습니다.
이것은 한 레벨의 코드를 더 낮은 레벨로 변환하기위한 유비쿼터스이며 종종 필요한 도구입니다
(예 : Java 소스에서 바이트 코드로 컴파일하거나 C 소스를 네이티브 코드로 컴파일 할 때)

이는 stack이 아래로 이동하기 때문입니다. 고수준 언어에서 하드웨어에 대한 네이티브 코드의 중간 표현으로,
하위 수준에서 제공하는 유형 추상화는 거의 항상 상위 수준의 추상화보다 더 간단하고 약합니다.
(가상 디스패치의 의미를 X86 명령어 세트로 굽거나 레지스터에서 Java의 기본 유형 세트를 모방하는 것을 원하지 않습니다.)

erasure는 한 레벨에서 더 풍부한 유형을 더 낮은 레벨에서 단조로운 유형으로 매핑하는 기술입니다.
(이상적으로는 더 높은 수준에서 사운드 유형 검사를 수행 한 후)

예를 들어, 자바 바이트 코드 집합 (스택 및 로컬 변수 집합의 정수 값을 이동시키기위한 명령들을 포함 iload, istore)
및 (int 치의 산술 수행하는 iadd, imul floats에 대한 유사한 명령어 fload, fstore, fmul등),
longs(lload, lstore, lmul), doubles (dload, dstore, dmul), 그리고 object references (aload, astore.)
그러나 bytes, shorts, chars, 또는 booleans에 대한 내용은 없다.
러한 유형의 컴파일러의 int로 삭제되기 때문에, 그리고 사용int-움직임 및 산술 지침.
이것은 바이트 코드 명령어 세트의 디자인에서 실용적인 디자인 절충안입니다.

이는 명령어 세트의 복잡성을 줄여 주어 런타임의 효율성을 향상시킬 수 있습니다.
Java 언어의 다른 많은 기능 (예 : 확인 된 예외, 메서드 오버로딩, 열거 형, 명확한 할당 분석, 중첩 된 클래스, 람다 또는 로컬 클래스에 의한 로컬 변수 캡처 등)은 “언어 허구”입니다.
Java 컴파일러에서 확인됩니다. 그러나 클래스 파일로의 번역에서 지워졌습니다.

마찬가지로 C를 네이티브 코드로 컴파일 할 때 부호있는 정수와 부호없는 정수는 모두 범용 레지스터로 지워지고
(부호없는 레지스터와 부호없는 별도의 레지스터가 없음) const변수는 가변 레지스터와 메모리 위치에 저장됩니다.
이런 종류의 삭제는 전혀 이상하지 않습니다.

Homogeneous vs heterogeneous translations

매개 변수 다형성을 가진 언어에서 제네릭 유형을 번역하는 데는 두 가지 일반적인 접근 방식이 있습니다. 동종 번역(homogeneous translations) 과
이종 번역(heterogeneous translations)입니다.

동종 번역에서 generic 클래스 Foo는 다음 Foo.class과 같은 단일 아티팩트로 번역됩니다 (일반 메서드에서도 동일).
이종 번역에서 제네릭 형식 또는 메서드 ( Foo, Foo) 의 각 인스턴스화 는 별도의 엔터티로 처리되고 별도의 아티팩트를 생성합니다.

예를 들어 C++는 이기종 번역을 사용합니다.
템플릿의 다른 인스턴스화는 의미 체계와 생성 된 코드가 다른 완전히 다른 유형입니다.
유형 vector 및 vector 별도의 유형입니다.

한편으로 이것은 유형 안전성(type safety) (각 인스턴스화는 확장 후 개별적으로 유형 검사 할 수 있음) 및
생성 된 코드의 품질 (각 인스턴스화를 개별적으로 최적화 할 수 있음)에 유용합니다.

반면에,이 방법 큰 코드 풋 프린트 (이후 vector및 vector각 인스턴스는 완전히 관련이없는 타입이기 때문에, 별도의 코드가)
와 (자바 와일드 카드를 통해처럼) 우리는 “무엇인가의 벡터”에 대해 이야기 할 수 없다.
(가능한 공간 비용 극단적 입증 된 바와 같이, 스칼라는 실험 @@specialized annotation 입력에인가 될 때 compiler는
모든 primitive types 버전을 전문화하기 위해, 컴파일러에 의한 것으로 주석.
좋은 이야기인것 같지만 결과는 9N 폭발 발생의 generated classes,
여기서 n 클래스에있는 특수 유형 변수의 수이므로 몇 줄의 코드로 100MB JAR 파일을 쉽게 생성 할 수 있습니다.)

동종 번역과 이종 번역 사이에서 선택하려면 언어 디자이너가 항상 만드는 일종의 절충안을 만들어야합니다.
이기종 번역은 더 많은 유형 특이성을 제공하고 더 큰 정적 및 동적 풋 프린트를 제공하고 런타임시 공유를 줄입니다.
모두 성능에 영향을 미칩니다. 동종 번역은 Java의 와일드 카드 또는 C#의 선언 사이트 분산
( vector 와 vector 사이에 C++ 과 공통점이없는 경우 모두 부족함)과 같은 매개 변수 계열 유형을 추상화하는 데 더 적합합니다 .

Erased generics in Java

Java는 동종 번역을 사용하여 제네릭을 번역합니다.
제네릭은 컴파일 타임에 유형 검사되지만 바이트 코드를 생성 List할 List때 같은 제네릭 유형 이 지워지고,
이러한 유형 변수 는 해당 경계를 지울 때 지워집니다 (이 경우 Object).

If we have:

1
2
3
4
5
6
7
8
9
10
class Box<T> {
private T t;

public T(T t) { this.t = t; }

public Box<T> copy() { return new Box<>(t); }

public T t() { return t; }
}

javac컴파일러는 단일 클래스 파일 출사 Box.class모든 예증의 구현 역할을 Box와일드 카드(포함 - Box<?>) 및 원시 타입 ( Box).
필드, 메소드 및 수퍼 타입 설명자는 지워집니다.
유형 변수는 경계까지 지워지고 제네릭 유형은 다음과 같이 헤드까지 List 지워집니다 ( erases to List).

1
2
3
4
5
6
7
8
9
10
11

class Box {
private Object t;

public Box(Object t) { this.t = t; }

public Box copy() { return new Box(t); }

public Object t() { return t; }
}

일반 서명은 ( Signature속성에) 유지 되므로 컴파일러는 클래스 파일을 읽을 때 일반 서명을 볼 수 있지만
JVM은 링크에서 지워진 설명 자만 사용합니다. 이 변환 체계는 클래스 파일 수준에서의 레이아웃 과 API 가 모두 Box지워짐을 의미합니다.
상기 사용 사이트, 같은 일이 발생합니다 참조를하기 Box에 삭제됩니다 Box에 합성 캐스트, String사용 부위에 삽입.

왜? 대안은 무엇 이었습니까?

이 시점에서 이것이 분명히 어리 석거나 게으른 선택이거나 삭제가 더러운 해킹이라고 선언하고 선언하려는 유혹이 있습니다.
결국 컴파일러가 완벽하게 좋은 유형 정보를 버리는 이유는 무엇입니까?

질문을 더 잘 이해하기 위해 우리는 또한 다음과 같은 질문을해야합니다.
유형 정보를 수정해야했는지, 그 정보로 무엇을 할 수있을 것이며, 그와 관련된 비용은 얼마입니까?
수정 된 유형 매개 변수 정보를 사용하여 구상 할 수있는 여러 가지 방법이 있습니다.

  • Reflection : 일부의 경우 “수정 된 제네릭” List은 instanceof 유형 변수 와 같은 언어 도구 또는
    패턴 일치를 사용하거나 유형 매개 변수에 대해 문의하기 위해 반사 라이브러리를 사용하여 목록이 무엇인지 물어볼 수 있음을 의미합니다 .

  • Layout or API specialization : 기본 유형 또는 인라인 클래스가있는 언어에서는 Pair<int, int>박스형 객체에 대한
    두 개의 참조가 아닌 두 개의 int를 보유하도록 a의 레이아웃을 평면화하는 것이 좋습니다.

  • Runtime type checking : 클라이언트가 힙 오염을 일으킬 수 Integer있는 List(예를 들어, 원시 List참조를 통해)
    in 을 넣으려고 시도 할 때 (아마도)가 아니라 힙 오염이 발생할 지점에서이를 포착하고 실패하는 것이 좋습니다.
    나중에 합성 캐스트에 부딪히면 감지합니다.

상호 배타적이지는 않지만이 세 가지 가능성 (Reflection, specialization 및 type checking)은
서로 다른 목표 (각각 프로그래머의 편의성, 성능 및 안전)를 지원하며 의미와 비용이 다릅니다.
“우리는 수정을 원합니다”라고 말하기는 쉽지만 더 자세히 살펴보면 이들 중 가장 중요한 부분과
상대적인 비용 및 이점에 대한 중요한 구분을 찾을 수 있습니다.

여기에서 삭제가 어떻게 현명하고 실용적인 선택인지 이해하려면 당시의 목표, 우선 순위 및 제약 조건, 대안이 무엇인지 이해해야합니다.

Goal: 점진적 마이그레이션 호환성

Java 제네릭은 야심 찬 요구 사항을 채택했습니다.

1
기존의 비 제네릭 클래스를 바이너리 호환 및 소스 호환 방식으로 제네릭으로 발전시킬 수 있어야합니다.

즉, 기존 클라이언트 및의 하위 클래스가 ArrayList로 생성 된 ArrayList에 대해 변경없이 계속 재컴파일 할 수 있으며 기존 클래스 파일이 생성 된 ArrayList.
이를 지원한다는 것은 생성 된 클래스의 클라이언트와 하위 클래스가
즉시, 나중에 또는 전혀 생성하지 않도록 선택할 수 있으며 다른 클라이언트
또는 하위 클래스의 유지 관리자가 선택한 작업과 독립적으로 수행 할 수 있음을 의미합니다.

이 요구 사항이 없으면 클래스를 생성하려면 모든 클라이언트와 하위 클래스를 수정하지 않으면 적어도 한 번에
다시 컴파일해야하는 “플래그 데이”가 필요합니다. 와 같은 핵심 클래스의 ArrayList 경우 본질적으로
전 세계의 모든 Java 코드를 한 번에 다시 컴파일해야합니다
(또는 Java 1.4에 유지하기 위해 영구적으로 강등되어야합니다.)
전체 Java 에코 시스템에 걸쳐 이러한 “플래그 데이”가 벗어 났기 때문에
질문에, 우리는 클라이언트가 생성을 인식하지 않고도 핵심 플랫폼 클래스 (및 인기있는 타사 라이브러리)를 생성 할 수있는
일반 유형 시스템이 필요했습니다.
(더 나쁜 것은 하나의 플래그 데이가 아니었을 것입니다. 그러나 전 세계의 모든 코드가 단일 원자 트랜잭션으로 생성 된 경우가 아니기 때문에 많은 날이었습니다.)

이 요구 사항을 설명하는 또 다른 방법은 생성 될 수있는 모든 코드를 고아로 만들거나
개발자가 제네릭을 선택하고 기존 코드에 이미 투자 한 금액을 유지하도록하는 것은 허용되지 않는 것으로 간주되었습니다.
생성을 호환 가능한 작업으로 만들면 해당 코드에 대한 투자가 무효화되지 않고 유지 될 수 있습니다.

“플래그 데이”에 대한 혐오감은 Java 디자인의 필수 측면에서 비롯됩니다.
Java는 별도로 컴파일 되고 동적으로 연결 됩니다.
별도의 컴파일은 모든 소스 파일이 소스 그룹을 단일 아티팩트로 컴파일하는 것이
아니라 하나 이상의 클래스 파일로 컴파일됨을 의미합니다.
동적 연결은 클래스 간의 참조가 심볼 정보를 기반으로 런타임에 연결됨을 의미합니다.
클래스 C가에서 메서드 void m(int x)를 호출 하면 클래스 D파일 에서 호출하는 메서드 C의 이름과 설명자 ( (I)V)를 기록하고
링크 타임 D에이 이름과 설명자를 사용 하여 메서드를 찾고 일치 항목이 발견되면 통화 사이트가 연결되었습니다.

이것은 많은 작업처럼 들릴 수 있지만 별도의 컴파일과 동적 연결은 Java의 가장 큰 장점 중 하나 입니다.
클래스 경로에서의 한 버전에 대해 컴파일 D하고 다른 버전 D으로 실행할 수 있습니다 (아무것도 만들지 않는 한 .)의 바이너리 비 호환 변경 D.

1
2
3
동적 연결에 대한 보편적 인 노력은 아무것도 재 컴파일 할 필요없이 
새 JAR을 클래스 경로에 간단히 드롭하여 종속성의 새 버전으로 업데이트 할 수 있도록합니다.
우리는이 작업을 너무 자주 수행하지만 눈치 채지도 못합니다.하지만 이것이 작동을 멈춘다면 실제로 눈치 채게 될 것입니다.

제네릭이 Java에 도입되었을 때 이미 세계에 많은 Java 코드가 있었고 클래스 파일은 java.util.ArrayList.
이러한 API를 호환 가능하게 생성 할 수 없다면이 를 대체 할 새 API를 작성 해야하고,
더 나쁜 것은 이전 API의 모든 클라이언트 코드가 계속해서 1.4에 머 무르거나 새로운 API를 동시에 사용하도록 다시 작성하십시오
(애플리케이션 코드뿐만 아니라 애플리케이션이 의존하는 모든 타사 라이브러리 포함).
이것은 당시 존재하는 거의 모든 Java 코드의 가치를 떨어 뜨렸을 것입니다.

C #은 반대로 VM을 업데이트하고 기존 라이브러리와 여기에 의존하는 모든 사용자 코드를 무효화하는 선택을했습니다.
그들은 세계에 비교적 적은 C # 코드가 있었기 때문에이 작업을 할 수있었습니다. 당시 Java 에는 이 옵션이 없었습니다.

그러나이 선택의 한 가지 결과는 제네릭 클래스가 제네릭 및 비 제네릭 클라이언트 또는 하위 클래스를 동시에 가질 것으로 예상되는 발생입니다.
이는 소프트웨어 개발 프로세스에 도움이되지만 이러한 혼합 사용에서 유형 안전성에 잠재적 인 결과를 초래할 수 있습니다.

Heap pollution

이러한 방식으로 지우고 제네릭 클라이언트와 비 제네릭 클라이언트 간의 상호 운용성을 지원하면 힙 오염 가능성이 발생합니다.
즉, 상자에 저장된 항목이 예상했던 컴파일 타임 유형과 호환되지 않는 런타임 유형을 갖게됩니다.
클라이언트가를 사용할 때는에 할당 Box될 때마다 캐스트가 삽입 되므로 데이터가 유형 변수의 세계 (의 구현 )에서
구체적인 유형의 세계로 전환되는 지점에서 힙 오염이 감지됩니다.
힙 오염이있는 경우 이러한 캐스트가 실패 할 수 있습니다.TStringBox

힙 오염은 비 제네릭 코드가 제네릭 클래스를 사용하거나 잘못된 제네릭 유형의 변수에 대한 참조를 위조하기 위해
검사되지 않은 캐스트 또는 원시 유형을 사용할 때 발생할 수 있습니다.
(확인되지 않은 캐스트 또는 원시 유형을 사용할 때 컴파일러는 힙 오염이 발생할 수 있음을 경고합니다.)
예를 들면 다음과 같습니다.

1
2
3
4
5
6

Box<String> bs = new Box<>("hi!"); // safe
Box<?> bq = bs; // safe, via subtyping
Box<Integer> bi = (Box<Integer>) bq; // unchecked cast -- warning issued
Integer i = bi.get(); // ClassCastException in synthetic cast to Integer

이 코드의 죄는 체크되지 않은 캐스트 from Box<?>to입니다 Box.
개발자는 지정된 box것이 실제로 Box. 그러나 힙 오염은 즉시 잡히지 않습니다.
우리가하려고하는 경우에만 사용 String AS를 상자에 있던 것을 Integer, 우리가 뭔가 잘못 감지 할.
번역에서 우리는 우리가 우리의 상자를 캐스팅하는 경우가 Box다시에 Box우리는
그것을 사용하기 전에 Box이종 번역에서 아무것도 나쁜 (좋든 나쁘.) 발생,
Box및 Box다른 런타임 형식을 가질 것이며,이 캐스트가 실패합니다.

이 언어는 실제로 규칙을 따르는 한 제네릭에 대해 매우 강력한 안전 보장을 제공합니다.

1
프로그램이 확인되지 않거나 원시 경고없이 컴파일되는 경우 컴파일러에서 삽입 한 합성 캐스트는 실패하지 않습니다.

즉, 힙 오염은 우리가 비 제네릭 코드와 상호 운용 할 때 또는 컴파일러에 거짓말을 할 때만 발생할 수 있습니다.
힙 오염이 발견 된 지점에서 예상 한 유형과 실제로 발견 된 유형을 알려주는 깨끗한 예외가 발생합니다.

컨텍스트 : JVM 구현 및 언어의 생태계

제네릭을 둘러싼 디자인 선택은 JVM 구현 생태계와 JVM 에서 실행되는 언어의 구조에 영향을 받았습니다.
대부분의 개발자에게 “Java”는 모 놀리 식 엔티티이지만 실제로 Java 언어와 JVM (Java Virtual Machine)은
각각 고유 한 사양을 가진 별도의 엔티티입니다. Java 컴파일러는 JVM 용 클래스 파일을 생성하지만
(그 형식과 의미는 Java 가상 머신 사양에 배치되어 있음)
JVM은 원래 소스 언어에 관계없이 유효한 클래스 파일을 실행합니다.
일부 계산에 따르면 JVM을 컴파일 대상으로 사용하는 200 개 이상의 언어가 있으며,
그중 일부는 Java 언어 (예 : Scala, Kotlin) 및 매우 다른 언어 (예 : JRuby, Jython, Jaskell.)

JVM이 Java 와는 상당히 다른 언어에서도 컴파일 대상으로 성공한 한 가지 이유는
Java 언어의 영향을 제한하면서 컴퓨팅을위한 상당히 추상적 인 모델을 제공하기 때문입니다.
언어와 가상 머신 사이의 추상화 계층은 JVM 에서 실행되는 다른 언어의 에코 시스템을 자극하는 데
유용 할뿐만 아니라 JVM의 독립적 인 구현 에코 시스템에도 유용했습니다.
오늘날 시장은 실질적으로 통합되었지만 제네릭이 Java에 추가되었을 당시에는 상업적으로 실행 가능한 JVM 구현이 12 개가 넘었습니다.
제네릭을 수정하면 제네릭을 지원하기 위해 언어를 향상시킬뿐만 아니라 JVM도 향상시켜야합니다.

당시 JVM에 일반 지원을 추가하는 것이 기술적으로 가능했을 수도 있지만 많은 구현 자 간의 상당한
조정과 합의가 필요한 상당한 엔지니어링 투자 였을뿐만 아니라 JVM의 언어 생태계에도 수정 된 제네릭에 대한 의견.
예를 들어 수정의 해석에 런타임시 유형 검사가 포함 된 경우 Scala (선언 사이트 제네릭 포함)가
JVM이 Java의 (불변) 제네릭 하위 유형 규칙을 적용하게하는 것에 만족할까요?

Erasure는 실용적인 타협이었다

종합하면 이러한 제약 (기술 및 생태계 모두)은 컴파일 타임에 일반 유형 정보가 삭제되는
동종 번역 전략으로 우리를 밀어 붙이는 강력한 힘으로 작용했습니다. 요약하면 이 결정을 내리게 한 요인은 다음과 같습니다.

  • 런타임 비용. 이기종 변환에는 모든 종류의 런타임 비용이 수반됩니다.
    더 큰 정적 및 동적 풋 프린트, 더 큰 클래스 로딩 비용, 더 큰 JIT 비용 및 코드 캐시 압력 등. 이로 인해 개발자는 유형 안전성과 성능.

  • 마이그레이션 호환성. 당시에는 소스 및 바이너리 호환이 가능하도록 제네릭을 수정하여 마이그레이션을
    허용하여 플래그 데이를 만들고 기존 코드에 대한 개발자의 상당한 투자를 무효화 할 수있는 알려진 번역 체계가 없었습니다.

  • 런타임 비용, 보너스 에디션. 수정이 런타임시 유형을 확인하는 것으로 해석되는 경우
    (Java의 공변 배열에 대한 저장소가 동적으로 확인되는 것과 마찬가지로)
    JVM이 런타임에 모든 필드 또는 배열 요소 저장소에서 일반 하위 유형 검사를 수행해야하므로 런타임에 상당한 영향을 미칩니다.
    언어의 일반 유형 시스템을 사용합니다.
    (유형이과 같은 단순한 경우 쉽고 저렴하게 들릴 수 List있지만 과 같은 경우 금방 비싸 질 수 있습니다
    Map<? extends List<? super Foo>>, ? super Set<? extends Bar>>.
    (사실 이후 연구에서는 일반 하위 유형의 결정 가능성에 의문을 제기합니다.).

  • JVM 생태계. 런타임시 유형 정보가 수정되는지 여부와 방법에 대해 수십 개의 JVM 공급 업체가 동의하도록하는 것은
    매우 의심스러운 제안이었습니다.

  • 배포의 실용화. 실제로 작동 할 수있는 계획에 동의하도록 12 개의 JVM 공급 업체를 확보 할 수 있었다
    하더라도 이미 상당하고 위험한 노력의 복잡성, 일정 및 위험이 크게 증가했을 것입니다.

  • 언어 생태계. Scala와 같은 언어는 Java의 불변 제네릭을 JVM의 의미 체계에 태우는 것에 만족하지 않았을 수 있습니다.
    JVM의 제네릭에 대해 허용 가능한 교차 언어 의미 체계 집합에 동의하면 이미 상당하고 위험한 노력의 복잡성,
    시간표 및 위험이 다시 증가했을 것입니다.

  • 사용자는 어쨌든 삭제 (따라서 힙 오염)를 처리해야합니다.
    유형 정보가 런타임에 보존 될 수 있더라도 클래스가 생성되기 전에 컴파일 된 더티 클래스 파일이 항상 존재하므로
    ArrayList 힙에있는 어떤 항목에도 유형 정보가 첨부되지 않았을 가능성이 있으며 이에 따른 힙 위험도 있습니다.

  • 어떤 유용한 관용구는 표현할 수 없었을 것입니다.
    기존의 제네릭 코드는 컴파일러가 알지 못하는 런타임 유형에 대해 알고있을 때 체크되지 않은 캐스트에 의지
    할 수 있으며 제네릭 유형 시스템에서 쉽게 표현할 수있는 방법이 없습니다.
    이러한 기술의 대부분은 reified generics로는 불가능했을 것입니다.
    즉, 다른 방식으로 표현되어야하고 종종 훨씬 더 비싼 방식으로 표현되어야했습니다.

비용과 위험이 상당했을 것이 분명합니다. 혜택은 무엇 이었을까요? 앞서 우리는 리플렉션, 레이아웃 전문화 및 런타임 유형 검사라는
세 가지 가능한 수정 이점을 언급했습니다.
위의 주장은 우리가 런타임 유형 검사 (런타임 비용, 결정 불가능 위험, 생태계 위험 및 삭제 된 인스턴스의 존재)를받을 가능성을 크게 배제합니다.

분명히 그것 List의 요소 유형이 무엇인지 물어볼 수 있다면 좋을 것 입니다
(그리고 대답 할 수 있었을 수도 있지만 아닐 수도 있습니다)
– 이것은 분명히 0이 아닌 이점입니다.
비용과 이점이 수십 배 이상 범위를 벗어났습니다.
(선택한 번역 전략의 또 다른 비용은 프리미티브를 유형 매개 변수로 지원할 수 없다는 것입니다.
대신 List을 사용해야 List합니다.)

삭제가 “더티 해킹”이라는 일반적인 오해는 일반적으로 엔지니어링 노력,
시장 출시 시간, 제공 위험, 성능, 생태계 영향 및 프로그래머 편의성 측면에서 대안의 실제 비용이 얼마인지에 대한 인식 부족에서 비롯됩니다.
이미 작성된 많은 양의 Java 코드와 JVM에서 실행되는 JVM 구현 및 언어의 다양한 에코 시스템을 고려할 때.

참조