일부 사람들은 Java의 제네릭 구현이 나쁘다고 주장하는 이유는 무엇입니까?


115

제네릭의 경우 Java가 제대로 작동하지 않는다는 말을 가끔 들었습니다. (가장 가까운 참조, 여기 )

내 경험이 없어서 용서하지만 무엇이 그들을 더 좋게 만들었 을까요?


23
나는 이것을 닫을 이유가 없다. 제네릭에 대한 모든 쓰레기와 함께 Java와 C #의 차이점을 명확하게 이해하면 사람들이 정보가 부족한 혼란을 피하려는 사람들에게 도움이 될 수 있습니다.
Rob

답변:


146

나쁜:

  • 유형 정보는 컴파일 시간에 손실되므로 실행 시간에 "의미"되는 유형을 알 수 없습니다.
  • 값 유형에 사용할 수 없습니다 (이것은 큰 문제입니다. .NET에서는 List<byte>실제로 byte[]예를 들어 지원되며 권투가 필요하지 않습니다)
  • 제네릭 메서드 호출 구문 (IMO)
  • 제약 조건에 대한 구문이 혼동 될 수 있습니다.
  • 와일드 카드는 일반적으로 혼란 스럽습니다.
  • 위의 사항으로 인한 다양한 제한-캐스팅 등

좋은:

  • 와일드 카드를 사용하면 호출 측에서 공분산 / 반공 분산을 지정할 수 있습니다. 이는 많은 상황에서 매우 깔끔합니다.
  • 없는 것보다 낫다!

51
@Paul : 저는 실제로 일시적인 파손을 선호했지만 나중에 괜찮은 API를 선호했을 것입니다. 또는 .NET 경로를 사용하여 새로운 컬렉션 유형을 도입하십시오. (2.0의 .NET 제네릭은 1.1 앱을 손상시키지 않았습니다.)
Jon Skeet

6
기존의 큰 코드 기반을 사용하면 호출하는 모든 사람을 변경하지 않고 제네릭을 사용하도록 한 메서드의 서명을 변경할 수 있다는 사실이 마음에 듭니다.
Paul Tomblin

38
그러나 그 단점은 거대하고 영구적입니다. 큰 단기 승리와 장기 손실 IMO가 있습니다.
Jon Skeet

11
Jon-

6
@Rogerio : 내 첫 번째 "나쁜"항목이 올바르지 않다고 주장하셨습니다. 내 첫 번째 "Bad"항목은 컴파일 시간에 정보가 손실된다는 것을 말합니다. 그것이 틀리기 위해서는 컴파일 타임에 정보 손실 되지 않는다고 말해야합니다. 다른 개발자들을 혼란스럽게하는 것에 관해서는, 내가 말하는 것에 혼란스러워하는 유일한 사람인 것 같습니다.
Jon Skeet

26

가장 큰 문제는 Java 제네릭이 컴파일 타임에만 해당되며 런타임에이를 전복 할 수 있다는 것입니다. C #은 더 많은 런타임 검사를 수행하기 때문에 칭찬을받습니다. 이 게시물 에는 정말 좋은 토론 있으며 다른 토론으로 연결됩니다.


그것은 실제로 문제가되지 않으며, 런타임 용으로 만들어지지 않았습니다. 배가 산을 오를 수 없기 때문에 문제가된다고 말하는 것과 같습니다. 언어로서 Java는 많은 사람들이 필요로하는 것을 수행합니다. 의미있는 방식으로 유형을 표현합니다. 런타임 유형 적용의 경우 사람들은 여전히 Class객체를 전달할 수 있습니다 .
Vincent Cantin

1
그가 맞아. 배를 설계하는 사람들이 산을 오를 수 있도록 설계 했어야했기 때문에 당신의 신학은 잘못되었습니다. 그들의 디자인 목표는 필요한 것에 대해 잘 생각되지 않았습니다. 이제 우리는 모두 보트에만 갇혀 산을 오를 수 없습니다.
WiegleyJ

1
@VincentCantin-가장 확실한 문제입니다. 따라서 우리 모두는 그것에 대해 불평하고 있습니다. Java 제네릭은 반쯤 구워졌습니다.
Josh M.

17

주요 문제는 Java가 실제로 런타임에 제네릭을 가지고 있지 않다는 것입니다. 컴파일 타임 기능입니다.

Java에서 제네릭 클래스를 만들 때 "Type Erasure"라는 메서드를 사용하여 실제로 클래스에서 모든 제네릭 유형을 제거하고 기본적으로 Object로 대체합니다. 제네릭의 마일 높은 버전은 컴파일러가 메서드 본문에 나타날 때마다 지정된 제네릭 유형에 캐스트를 삽입하는 것입니다.

이것은 많은 단점이 있습니다. 가장 큰 IMHO 중 하나는 리플렉션을 사용하여 제네릭 유형을 검사 할 수 없다는 것입니다. 유형은 실제로 바이트 코드에서 제네릭이 아니므로 제네릭으로 검사 할 수 없습니다.

차이점에 대한 개요 : http://www.jprl.com/Blog/archive/development/2007/Aug-31.html


2
왜 당신이 반대표를 받았는지 모르겠습니다. 내가 이해하는 바에 따르면 당신이 옳고 그 링크는 매우 유익합니다. 찬성.
Jason Jackson

@Jason, 감사합니다. 내 원래 버전에 오타가 있었는데 그에 대해 비추천을받은 것 같습니다.
JaredPar

3
리플렉션을 사용하여 제네릭 메서드 서명 인 제네릭 형식을 볼 있기 때문 입니다. 리플렉션을 사용하여 매개 변수화 된 인스턴스와 관련된 일반 정보를 검사 할 수는 없습니다. 예를 들어 foo (List <String>) 메서드가있는 클래스가있는 경우 "String"을 반사적으로 찾을 수 있습니다.
oxbow_lakes

유형 삭제로 인해 실제 유형 매개 변수를 찾을 수 없다는 기사가 많이 있습니다. 예 : Object x = new X [Y] (); x가 주어지면 Y 유형을 찾는 방법을 보여줄 수 있습니까?
user48956 dec. 06 2013

@oxbow_lakes-제네릭 유형을 찾기 위해 리플렉션을 사용해야하는 것은 그렇게 할 수없는 것만큼이나 나쁩니다. 예를 들어 그것은 나쁜 해결책입니다.
Josh M.

14
  1. 런타임 구현 (즉, 유형 삭제가 아님)
  2. 기본 유형을 사용하는 능력 (이는 (1)과 관련됨)
  3. 와일드 카드는 유용하지만 구문과 사용시기를 아는 것은 많은 사람들을 괴롭히는 일입니다. 과
  4. 성능 향상 없음 ((1 때문에), Java 제네릭은 Casti Objects의 구문 설탕입니다).

(1) 매우 이상한 행동을합니다. 제가 생각할 수있는 가장 좋은 예는 다음과 같습니다. 취하다:

public class MyClass<T> {
  T getStuff() { ... }
  List<String> getOtherStuff() { ... }
}

그런 다음 두 개의 변수를 선언하십시오.

MyClass<T> m1 = ...
MyClass m2 = ...

이제 전화하십시오 getOtherStuff():

List<String> list1 = m1.getOtherStuff(); 
List<String> list2 = m2.getOtherStuff(); 

그것이이없는 경우에도 원시 유형 (파라미터 화 된 형태를 의미가 제공되지 않음) 때문에 두 번째는 컴파일러에 의해 벗겨 자사의 제네릭 형식 인수를 가지고 아무것도 파라미터 화 된 형태로 할 수있다.

또한 JDK에서 가장 좋아하는 선언을 언급하겠습니다.

public class Enum<T extends Enum<T>>

와일드 카드 (혼합 된 백)를 제외하고는 .Net 제네릭이 더 낫다고 생각합니다.


그러나 컴파일러는 특히 rawtypes lint 옵션을 사용하여 알려줍니다.
Tom Hawtin-tackline

죄송합니다. 마지막 줄이 왜 컴파일 오류입니까? 나는 Eclipse에서 그것을 엉망으로 만들고 있고 거기에서 실패하게 할 수 없다-나머지 부분을 컴파일하기에 충분한 것들을 추가한다면.
oconnor0 2010

public class Redundancy<R extends Redundancy<R>>;)
java.is.for.desktop

@ oconnor0 컴파일 실패가 아니라 컴파일러 경고입니다. 명백한 이유가 없습니다.The expression of type List needs unchecked conversion to conform to List<String>
artbristol

Enum<T extends Enum<T>>처음에는 이상하거나 중복되어 보일 수 있지만, 적어도 Java / 그것의 제네릭의 제약 내에서 실제로는 꽤 흥미 롭습니다. 열거 형에는 열거 형이 아니라 열거 형으로 values()형식화 된 요소의 배열을 제공하는 정적 메서드가 Enum있으며 해당 형식은 원하는을 의미하는 제네릭 매개 변수에 의해 결정됩니다 Enum<T>. 물론 그 타이핑은 열거 형의 컨텍스트에서만 의미가 있으며 모든 열거 형은의 하위 클래스 Enum이므로 Enum<T extends Enum>. 그러나 Java는 Enum<T extends Enum<T>>일관성 을 위해 원시 유형을 제네릭과 혼합하는 것을 좋아하지 않습니다 .
JAB 2014 년

10

정말 논란의 여지가있는 의견을 버리려고합니다. 제네릭은 언어를 복잡하게하고 코드를 복잡하게 만듭니다. 예를 들어, 문자열을 문자열 목록에 매핑하는 맵이 있다고 가정 해 보겠습니다. 예전에는 이것을 간단히 다음과 같이 선언 할 수있었습니다.

Map someMap;

이제 다음과 같이 선언해야합니다.

Map<String, List<String>> someMap;

그리고 내가 그것을 어떤 방법으로 전달할 때마다 나는 그 크고 긴 선언을 다시 반복해야한다. 제 생각에, 그 모든 추가 타이핑은 개발자의주의를 산만하게하고 그를 "영역"밖으로 데려갑니다. 또한 코드가 엉망진창으로 가득 차면 나중에 다시 돌아와서 중요한 논리를 찾기 위해 모든 엉뚱한 부분을 빠르게 살펴보기가 어려울 때가 있습니다.

Java는 이미 일반적으로 사용되는 가장 장황한 언어 중 하나라는 평판이 좋지 않으며 제네릭은 그 문제를 추가합니다.

그리고 그 모든 추가 장황함에 대해 실제로 무엇을 구입합니까? 누군가가 Strings를 보유해야하는 컬렉션에 Integer를 넣거나 누군가 Integer 컬렉션에서 String을 꺼내려고하는 곳에서 실제로 몇 번이나 문제를 겪었습니까? 상용 Java 응용 프로그램을 빌드하는 데 10 년 동안 일한 경험을 통해 이것이 큰 오류 원인이 된 적이 없습니다. 그래서 나는 당신이 여분의 장황함을 위해 무엇을 얻고 있는지 정말로 잘 모르겠습니다. 그것은 정말 관료적 인 짐으로 나를 때립니다.

이제 저는 정말 논란이 될 것입니다. Java 1.4에서 컬렉션의 가장 큰 문제는 어디에서나 타입 캐스트가 필요하다는 것입니다. 나는 이러한 타입 캐스트를 제네릭과 동일한 문제가 많이있는 추가적이고 장황한 것으로 봅니다. 예를 들어, 저는

List someList = someMap.get("some key");

나는해야한다

List someList = (List) someMap.get("some key");

물론 그 이유는 get ()이 List의 상위 유형 인 Object를 반환하기 때문입니다. 따라서 typecast 없이는 할당 할 수 없습니다. 다시 말하지만, 그 규칙이 얼마나 당신을 사는지 생각해보십시오. 내 경험으로는 그리 많지 않습니다.

나는 1) 제네릭을 추가하지 않았지만 2) 대신 수퍼 유형에서 하위 유형으로의 암시 적 캐스팅을 허용했다면 Java가 더 나았을 것이라고 생각합니다. 런타임에 잘못된 캐스트를 잡으십시오. 그러면 간단하게 정의 할 수있었습니다.

Map someMap;

그리고 나중에

List someList = someMap.get("some key");

모든 엉터리가 사라질 것이고, 나는 정말로 큰 버그 소스를 내 코드에 도입 할 것이라고 생각하지 않습니다.


15
죄송합니다. 말씀하신 내용에 동의하지 않습니다. 그러나 당신이 그것을 잘 주장하기 때문에 나는 그것을 투표하지 않을 것입니다.
Paul Tomblin

2
물론 내가 대답에서 제안한 것을 정확히 수행하는 Python 및 Ruby와 같은 언어로 구축 된 대규모 성공적인 시스템의 예가 많이 있습니다.
Clint Miller

이런 유형의 아이디어에 대한 저의 모든 반대는 그 자체가 나쁜 아이디어가 아니라 Python 및 Ruby와 같은 현대 언어가 개발자의 삶을 더 쉽고 편하게 만들어 주므로 개발자는 지적 만족감을 느끼고 결국에는 자신의 코드에 대한 이해.
Dan Tao

4
"그리고 당신은 그 모든 추가 장황함을 위해 정말로 무엇을 구입합니까? ..."그것은 열악한 유지 보수 프로그래머에게 그 컬렉션에 무엇이 있어야하는지 알려줍니다. 나는 이것이 상용 Java 응용 프로그램에서 작동하는 문제라는 것을 발견했습니다.
richj 2010

4
"누군가 [y] s를 보관해야하는 컬렉션에 [x]를 넣는 문제가 실제로 몇 번이나 있었습니까?" -오, 내가 카운트를 잃었 어! 또한 버그가 없더라도 가독성 킬러입니다. 그리고 개체가 무엇인지 (또는 디버깅) 있는지 찾기 위해 많은 파일을 스캔하면 실제로 나를 영역에서 벗어날 수 있습니다. Haskell을 좋아할 수도 있습니다. 타이핑은 강력하지만 덜 깔끔합니다 (유형이 유추되기 때문에).
Martin Capodici

8

런타임이 아닌 컴파일 타임의 또 다른 부작용은 제네릭 형식의 생성자를 호출 할 수 없다는 것입니다. 따라서 일반 공장을 구현하는 데 사용할 수 없습니다.


   public class MyClass {
     public T getStuff() {
       return new T();
     }
    }

--jeffk ++


6

Java 제네릭은 컴파일 시간에 정확성을 검사 한 다음 모든 유형 정보가 제거됩니다 (프로세스를 유형 삭제 라고 합니다 . 따라서 제네릭 List<Integer>은 임의 클래스의 객체를 포함 할 수있는 원시 유형 , 비 제네릭 으로 축소됩니다 List.

이로 인해 런타임에 목록에 임의의 개체를 삽입 할 수있을뿐만 아니라 이제 어떤 유형이 일반 매개 변수로 사용되었는지 알 수 없습니다. 후자는 차례로 결과

ArrayList<Integer> li = new ArrayList<Integer>();
ArrayList<Float> lf = new ArrayList<Float>();
if(li.getClass() == lf.getClass()) // evaluates to true
  System.out.println("Equal");

5

다른 사람에게 추가 할 수 있도록 위키 였으면 좋겠는데 ...하지만 ...

문제점 :

  • 유형 삭제 (런타임 가용성 없음)
  • 원시 유형에 대한 지원 없음
  • 주석과의 비 호환성 (둘 다 1.5에 추가되었습니다. 주석이 기능을 서두르는 것 외에 제네릭을 허용하지 않는 이유를 모르겠습니다)
  • 어레이와의 비 호환성. (때때로 Class와 같은 일을하고 싶 <습니까? MyObject >[]를 확장 하지만 허용되지 않습니다)
  • 이상한 와일드 카드 구문 및 동작
  • 일반적인 지원이 Java 클래스에서 일관되지 않는다는 사실. 그들은 그것을 대부분의 컬렉션 메서드에 추가했지만 가끔씩 인스턴스가없는 인스턴스를 실행합니다.

5

전체 유형 지우기 혼란을 무시하면 지정된 제네릭이 작동하지 않습니다.

이것은 다음을 컴파일합니다.

List<Integer> x = Collections.emptyList();

그러나 이것은 구문 오류입니다.

foo(Collections.emptyList());

여기서 foo는 다음과 같이 정의됩니다.

void foo(List<Integer> x) { /* method body not important */ }

따라서 표현식 유형이 검사되는지 여부는 로컬 변수에 할당되는지 아니면 메서드 호출의 실제 매개 변수에 할당되는지에 따라 다릅니다. 얼마나 미쳤어?


3
javac에 의한 일관되지 않은 추론은 쓰레기입니다.
mP.

3
후자의 형식이 거부 된 이유는 메서드 오버로딩으로 인해 "foo"버전이 두 개 이상있을 수 있기 때문이라고 생각합니다.
Jason Creighton

2
이 비판은 더 이상 자바 (8)는 개선 대상 타입 추론을 도입으로 적용되지 않습니다
oldrinb

4

Java에 제네릭을 도입하는 것은 어려운 작업이었습니다. 설계자가 기능, 사용 편의성 및 레거시 코드와의 하위 호환성 간의 균형을 맞추려고했기 때문입니다. 예상대로 타협을해야했습니다.

Java의 제네릭 구현이 언어의 복잡성을 허용 할 수없는 수준으로 증가 시켰다고 생각하는 사람들도 있습니다 (Ken Arnold의 " Generics Thoughmful "참조). Angelika Langer의 Generics FAQs 는 상황이 얼마나 복잡해질 수 있는지에 대한 좋은 아이디어를 제공합니다.


3

Java는 런타임에 Generics를 적용하지 않고 컴파일 시간에만 적용합니다.

이것은 제네릭 컬렉션에 잘못된 유형을 추가하는 것과 같은 흥미로운 일을 할 수 있음을 의미합니다.


1
컴파일러는 당신이 그것을 관리한다면 당신이 망쳤다 고 말할 것입니다.
Tom Hawtin-tackline

@Tom-반드시 그런 것은 아닙니다. 컴파일러를 속이는 방법이 있습니다.
Paul Tomblin

2
컴파일러가 말하는 것을 듣지 않습니까 ?? (내 첫 번째 댓글 참조)
Tom Hawtin-tackline

1
@Tom, ArrayList <String>이 ​​있고 간단한 List를 사용하는 이전 스타일의 메서드 (예 : 레거시 또는 타사 코드)에 전달하면 해당 메서드는 원하는 모든 항목을 해당 List에 추가 할 수 있습니다. 런타임에 적용되면 예외가 발생합니다.
Paul Tomblin

1
static void addToList (목록 목록) {list.add (1); } 목록 <문자열> 목록 = 새 ArrayList <문자열> (); addToList (목록); 그것은 컴파일되고 심지어 런타임에도 작동합니다. 문제가 발생하는 유일한 경우는 문자열을 예상하여 목록에서 제거하고 int를 얻을 때입니다.
Laplie Anderson


2

Java Posse # 279-Interview with Joe Darcy 및 Alex Buckley 를 들으면 이 문제에 대해 이야기합니다. 또한 Reified Generics for Java 라는 제목의 Neal Gafter 블로그 게시물로 연결됩니다 .

많은 사람들이 제네릭이 Java에서 구현되는 방식으로 인해 발생하는 제한에 만족하지 않습니다. 특히, 제네릭 유형 매개 변수가 수정되지 않은 것에 불만이 있습니다. 런타임에 사용할 수 없습니다. 제네릭은 삭제를 사용하여 구현되며, 제네릭 유형 매개 변수는 런타임에 간단히 제거됩니다.

이 블로그 게시물 은 요구 사항에서 마이그레이션 호환성 에 대한 요점을 강조한 이전 항목 인 Puzzling Through Erasure : 답변 섹션을 참조 합니다.

목표는 소스 및 개체 코드의 하위 호환성과 마이그레이션 호환성을 제공하는 것이 었습니다.

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