왜 배열이 공변이지만 제네릭은 변하지 않습니까?


160

Joshua Bloch의 효과적인 Java에서

  1. 배열은 두 가지 중요한 점에서 일반 유형과 다릅니다. 첫 번째 배열은 공변량입니다. 제네릭은 변하지 않습니다.
  2. 공변량은 단순히 X가 Y의 하위 유형 인 경우 X []도 Y []의 하위 유형이됨을 의미합니다. 배열이 공변량 임 string이 Object의 하위 유형이므로

    String[] is subtype of Object[]

    불변은 단순히 X가 Y의 하위 유형인지 여부에 관계없이 단순히

     List<X> will not be subType of List<Y>.

내 질문은 왜 배열을 Java에서 공변량으로 만드는 결정입니까? 왜 배열이 변하지 않습니까? 목록 공변량입니까? 와 같은 다른 SO 게시물이 있습니다 . 그러나 그들은 Scala에 초점을 맞춘 것으로 보이며 따라갈 수 없습니다.


1
나중에 제네릭이 추가 되었기 때문에 그렇지 않습니까?
Sotirios Delimanolis

1
배열과 컬렉션 간의 비교가 불공평하다고 생각합니다. 컬렉션은 백그라운드에서 배열을 사용합니다 !!
아메드 아델 이스마일

4
@ EL-conteDe-monteTereBentikh 예를 들어 모든 컬렉션이 아닙니다 LinkedList.
Paul Bellora

@PaulBellora지도가 컬렉션 구현 자와 다르다는 것을 알고 있지만 SCPJ6에서 컬렉션이 일반적으로 배열에 의존한다는 것을 읽었습니다 !!
아메드 아델 이스마일

ArrayStoreException이 없기 때문에; 배열에있는 요소를 Collection에 잘못된 요소를 삽입 할 때. 따라서 콜렉션은 검색시에만 찾을 수 있으며 캐스팅으로 인해 찾을 수도 있습니다. 따라서 제네릭은이 문제를 해결합니다.
Kanagavelu Sugumar

답변:


150

Wikipedia를 통해 :

Java 및 C #의 초기 버전에는 제네릭 (일명 파라 메트릭 다형성)이 포함되지 않았습니다.

이러한 설정에서, 배열을 변하지 않게 만드는 것은 유용한 다형성 프로그램을 배제합니다. 예를 들어, 배열을 섞기위한 함수 또는 Object.equals요소 의 메소드를 사용하여 두 배열의 동등성을 테스트하는 함수를 작성해보십시오 . 구현은 배열에 저장된 정확한 유형의 요소에 의존하지 않으므로 모든 유형의 배열에서 작동하는 단일 함수를 작성할 수 있어야합니다. 유형의 기능을 쉽게 구현

boolean equalArrays (Object[] a1, Object[] a2);
void shuffleArray(Object[] a);

그러나 배열 유형이 변하지 않는 것으로 취급되면 정확히 유형의 배열에서만 이러한 함수를 호출 할 수 있습니다 Object[]. 예를 들어 문자열 배열을 섞을 수 없었습니다.

따라서 Java와 C # 모두 배열 유형을 공변량으로 취급합니다. 예를 들어 C # string[]에서 하위 유형은 object[]이고 Java String[]에서 하위 유형은 Object[]입니다.

이것은 "왜, 더 정확하게"? 배열이 공변 왜 "라는 질문에 응답, 또는 했다 공변을 만든 배열 시간에 ?"

제네릭이 소개되었을 때 Jon Skeet 의이 답변 에서 지적한 이유로 의도적으로 공변량이되지 않았습니다 .

아니요, a List<Dog>List<Animal>입니다. 당신이 할 수있는 일을 고려하십시오 List<Animal>-고양이를 포함하여 동물을 추가 할 수 있습니다. 자, 강아지의 쓰레기에 논리적으로 고양이를 추가 할 수 있습니까? 절대적으로하지.

// Illegal code - because otherwise life would be Bad
List<Dog> dogs = new List<Dog>();
List<Animal> animals = dogs; // Awooga awooga
animals.add(new Cat());
Dog dog = dogs.get(0); // This should be safe, right?

갑자기 당신은 매우 혼란스러운 고양이 를 가지고 있습니다 .

와일드 카드 는 공분산 (및 공분산) 표현을 가능하게 했기 때문에 위키 백과 기사에 설명 된 배열을 공변량으로 만드는 원래 동기는 제네릭에 적용되지 않았습니다 .

boolean equalLists(List<?> l1, List<?> l2);
void shuffleList(List<?> l);

3
예, 배열은 다형성 동작을 허용하지만 (제네릭을 사용한 컴파일 타임 예외와 달리) 런타임 예외를 발생시킵니다. 예 :Object[] num = new Number[4]; num[1]= 5; num[2] = 5.0f; num[3]=43.4; System.out.println(Arrays.toString(num)); num[0]="hello";
eagertoLearn

21
맞습니다. 배열은 수정 가능한 유형을 가지며 필요에 따라을 던집니다 ArrayStoreException. 분명히 이것은 당시 가치있는 타협으로 간주되었습니다. 오늘날과는 대조적으로, 많은 사람들이 배열 공분산을 실수로 간주합니다.
Paul Bellora

1
"다수"가 왜 실수라고 생각합니까? 배열 공분산이없는 것보다 훨씬 유용합니다. 얼마나 자주 ArrayStoreException을 보았습니까? 그들은 매우 드 rare니다. 여기서의 아이러니는 용서할 수없는 imo입니다. 자바에서 가장 최악의 실수 중 하나는 사용 사이트 차이 일명 와일드 카드입니다.
Scott

3
@ScottMcKinney : "왜"많은 "것이 실수라고 생각합니까?" AIUI는 배열 공분산에 모든 배열 할당 작업에 대한 동적 형식 테스트가 필요하기 때문에 (컴파일러 최적화가 도움이 될 수는 있지만) 런타임 오버 헤드가 크게 발생할 수 있기 때문입니다.
Dominique Devriese

고마워, 도미니크,하지만 내 관찰에 따르면 "많은"사람이 실수라고 생각하는 이유는 다른 사람들이 말한 앵무새의 선을 따라가는 것 같습니다. 다시 배열 공분산을 다시 살펴보면 손상보다 훨씬 유용합니다. 다시 말하지만 Java가 실제로 만든 실수는 와일드 카드를 통한 사용 사이트 일반 분산입니다. 그것은 "많은"이 인정하고 싶어하는 것보다 더 많은 문제를 일으켰습니다.
Scott

30

그 이유는 모든 배열이 런타임 동안 요소 유형을 알고 있지만 일반 컬렉션은 유형 삭제 때문에 발생하지 않기 때문입니다.

예를 들면 다음과 같습니다.

String[] strings = new String[2];
Object[] objects = strings;  // valid, String[] is Object[]
objects[0] = 12; // error, would cause java.lang.ArrayStoreException: java.lang.Integer during runtime

이것이 일반 컬렉션에서 허용 된 경우 :

List<String> strings = new ArrayList<String>();
List<Object> objects = strings;  // let's say it is valid
objects.add(12);  // invalid, Integer should not be put into List<String> but there is no information during runtime to catch this

그러나 나중에 누군가가 목록에 액세스하려고 할 때 문제가 발생할 수 있습니다.

String first = strings.get(0); // would cause ClassCastException, trying to assign 12 to String

WHY Array에 대한 의견이 공변 인이므로 Paul Bellora의 답변이 더 적절하다고 생각합니다. 배열이 변하지 않으면 괜찮습니다. 당신은 그것으로 유형을 지울 것입니다. Erasure 유형의 주된 이유는 이전 버전과의 호환성이 올바른가?
eagertoLearn

@ user2708477, 예. 이전 버전과의 호환성으로 인해 유형 삭제가 도입되었습니다. 그리고 그렇습니다. 제 대답은 제목의 질문에 대답하려고하는데 왜 제네릭이 변하지 않는가.
카토 나

배열이 자신의 유형을 알고 있다는 사실은 공분산으로 인해 코드가 적합하지 않은 배열에 무언가를 저장 하도록 요청할 수는 있지만 그러한 저장소가 열릴 수는 없습니다. 결과적으로 배열을 공변량으로함으로써 발생하는 위험 수준은 유형을 알지 못하는 경우보다 훨씬 적습니다.
supercat

@supercat, 맞다. 내가 지적하고 싶은 것은 타입 삭제가있는 제네릭의 경우 런타임 검사의 안전성을 최소화하여 공분산을 구현할 수 없다는 것이다.
Katona

1
개인적 으로이 답변은 컬렉션을 사용할 수 없을 때 왜 배열이 공변량인지에 대한 올바른 설명을 제공한다고 생각합니다. 감사!
asgs

22

수 있음 도움이 : -

제네릭은 공변량이 아닙니다

Java 언어의 배열은 공변량입니다. 즉, Integer가 Number를 확장하면 (정확한) Integer도 Number 일뿐만 아니라 Integer []도 a Number[]이므로 전달하거나 할당 할 수 있습니다. Integer[]a Number[]가 필요한 곳. (공식적으로, Number가 Integer Number[]의 수퍼 타입 ​​인 경우 수퍼 타입은의 수퍼 타입 ​​인 경우도 Integer[]있습니다.) 제네릭 형식의 경우에도 마찬가지입니다. 즉 List<Number>,의 수퍼 타입 이며 예상 List<Integer>되는 List<Integer>위치를 전달할 수 있습니다 List<Number>. 불행히도, 그런 식으로 작동하지 않습니다.

그것이 그렇게 작동하지 않는 좋은 이유가 있음이 밝혀졌습니다. 에 a List<Integer>을 할당 할 수 있다고 상상해보십시오 List<Number>. 그런 다음 다음 코드를 사용하면 정수가 아닌 것을 다음에 넣을 수 있습니다 List<Integer>.

List<Integer> li = new ArrayList<Integer>();
List<Number> ln = li; // illegal
ln.add(new Float(3.1415));

ln은이므로 List<Number>Float를 추가하는 것은 완벽하게 합법적입니다. 그러나 ln이와 별칭을 li가졌다면 li의 정의에서 암시적인 타입 안전성 약속을 깨뜨릴 것입니다. 정수 타입이기 때문에 제네릭 타입은 공변량이 될 수 없습니다.


3
배열의 경우 ArrayStoreException런타임에 얻 습니다.
Sotirios Delimanolis

4
내 질문은 WHY배열이 공변량으로 만들어진 것입니다. Sotirios가 언급했듯이 Arrays를 사용하면 런타임에 ArrayStoreException이 발생합니다. 배열이 변경되지 않으면 컴파일 타임 자체 에서이 오류를 감지 할 수 있습니까?
eagertoLearn

@eagertoLearn : Java의 시맨틱 한 약점 중 하나는 유형 시스템 Animal에서 "아무것도 포함하지 않아야하는 배열"과 "다른 곳에서 수신 한 모든 항목을 받아 들일 필요가없는의 파생물"만 구별 할 수 있다는 것입니다 Animal. 과에 외부에서 공급 된 참조 받아 들일 수 있어야합니다 Animal의 배열을 받아 들여야 이전이 필요합니다. 코드를 Cat하지만, 후자를 필요 코드는 안된다. 컴파일러는 두 가지 유형을 구별 할 수 있다면, 그것은 컴파일시 검사를 제공 할 수 있습니다. 불행히도, 그들을 구별 할 수있는 유일한 것은 ...
supercat

... 코드가 실제로 무언가를 저장하려고하는지 여부이며 런타임까지 그것을 알 수있는 방법이 없습니다.
supercat

3

배열은 최소 두 가지 이유로 공변량입니다.

  • 공변량으로 변경되지 않는 정보를 보유한 컬렉션에 유용합니다. T의 집합이 공변량 인 경우, 백업 저장소도 공변량이어야합니다. 백킹 스토어로 T사용하지 않는 불변 컬렉션을 설계 할 수 있지만 T[](예 : 트리 또는 링크 된 목록 사용) 이러한 컬렉션은 어레이에서 지원하는 것만 큼 수행되지 않을 것입니다. 공변량 불변 컬렉션을 제공하는 더 좋은 방법은 백업 저장소를 사용할 수있는 "공변량 불변 배열"유형을 정의하는 것이었을 수도 있지만 단순히 배열 공분산을 허용하는 것이 더 쉬울 것입니다.

  • 배열은 어떤 유형의 항목이 있는지 알지 못하지만 동일한 배열에서 읽지 않은 것은 배열에 넣지 않는 코드에 의해 종종 변경됩니다. 이것의 주요 예는 정렬 코드입니다. 개념적으로 배열 유형에 요소를 교환하거나 치환하는 메소드 (이러한 메소드는 모든 배열 유형에 동일하게 적용 가능)를 포함하거나 배열 및 하나 이상의 항목에 대한 참조를 보유하는 "배열 조작기"오브젝트를 정의하는 것이 가능했을 수 있습니다. 그것에서 읽었으며 이전에 읽은 항목을 원래 배열에 저장하는 메소드를 포함 할 수 있습니다. 배열이 공변량이 아닌 경우 사용자 코드는 이러한 유형을 정의 할 수 없지만 런타임에는 특수화 된 메소드가 포함될 수 있습니다.

배열이 공변량이라는 사실은 추악한 해킹으로 간주 될 수 있지만 대부분의 경우 작업 코드를 쉽게 만들 수 있습니다.


1
The fact that arrays are covariant may be viewed as an ugly hack, but in most cases it facilitates the creation of working code.-좋은 지적
eagertoLearn

3

파라 메트릭 유형의 중요한 기능은 다형성 알고리즘, 즉 파라미터 값에 관계없이 데이터 구조에서 작동하는 알고리즘을 작성할 수있는 기능 Arrays.sort()입니다.

제네릭을 사용하면 와일드 카드 유형이 사용됩니다.

<E extends Comparable<E>> void sort(E[]);

실제로 유용하려면 와일드 카드 유형에는 와일드 카드 캡처가 필요하며 유형 매개 변수의 개념이 필요합니다. 배열이 Java에 추가 될 당시에는 그 중 어느 것도 사용할 수 없었으며 참조 유형 공변량의 배열을 만들면 다형성 알고리즘을 허용하는 훨씬 간단한 방법이 허용되었습니다.

void sort(Comparable[]);

그러나 이러한 단순성으로 인해 정적 유형 시스템에 허점이 생겼습니다.

String[] strings = {"hello"};
Object[] objects = strings;
objects[0] = 1; // throws ArrayStoreException

참조 유형의 배열에 대한 모든 쓰기 액세스의 런타임 검사가 필요합니다.

간단히 말해서, 제네릭에 의해 구현 된 새로운 접근 방식은 유형 시스템을 더 복잡하게 만들지 만 정적으로 유형이 더 안전하지만 이전의 접근 방식은 더 단순하고 정적으로 덜 안전합니다. 언어 설계자들은 문제를 거의 일으키지 않는 타입 시스템의 작은 허점을 막는 것보다 더 중요한 일을하는 단순한 접근 방식을 선택했습니다. 나중에, Java가 설립되고 긴급한 요구가 처리 될 때, 제네릭에 대해 올바른 작업을 수행 할 수있는 리소스가있었습니다.


2

제네릭은 변하지 않습니다 : JSL 4.10 부터 :

하위 유형은 일반 유형을 통해 확장되지 않습니다. T <: U는 C<T><: C<U>...

JLS는 또한
배열이 공변량 (첫 번째 글 머리 기호) 이라고 설명합니다 .

4.10.3 배열 유형의 하위 유형

여기에 이미지 설명을 입력하십시오


2

배열 공변량을 만든 첫 번째 장소에서 잘못된 결정을 내렸다고 생각합니다. 여기에 설명 된대로 형식 안전성을 깨뜨리고 이전 버전과의 호환성으로 인해 일반 형식에 대해 동일한 실수를 시도하지 않았습니다. 그것이 바로 Joshua Bloch가 "Effective Java (second edition)"책 25 번 항목의 목록을 선호 하는 이유 중 하나입니다.


Josh Block은 Java의 컬렉션 프레임 워크 (1.2)의 저자이자 Java의 제네릭 (1.5)의 저자였습니다. 그래서 모두가 불평하는 제네릭을 만든 사람은 우연히 그들이 더 좋은 방법이라고 말하는 책을 쓴 사람입니까? 큰 놀라움이 아닙니다!
cpurdy

1

내 코드 : 코드가 배열 A []를 기대하고 B []를 주면 B가 A의 하위 클래스 인 경우 걱정해야 할 두 가지가 있습니다. 배열 요소를 읽을 때 발생하는 일과 쓰면 발생하는 일 그것. 따라서 모든 경우에 형식 안전성을 유지하기 위해 언어 규칙을 작성하는 것은 어렵지 않습니다 (주로 규칙은 ArrayStoreExceptionA를 B []에 붙이려 고 시도하면 규칙이 던져 질 수 있음). 그러나 클래스를 선언 할 때 클래스 의 본문에는 SomeClass<T>여러 가지 방법 T이 사용될 수 있으며 가능한 모든 조합을 해결하여시기에 대한 규칙을 작성하는 것은 너무 복잡하다고 생각합니다. 일이 허용되지 않을 때.

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