Java 제네릭에는 언제 <가 필요합니까? <T> 대신 T>를 확장하고 스위칭의 단점이 있습니까?


205

다음 예에서 Hamcrest 매처와 함께 JUnit 사용 :

Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<java.util.Date>> result = null;
assertThat(result, is(expected));  

다음과 같은 JUnit assertThat메소드 서명으로 컴파일되지 않습니다 .

public static <T> void assertThat(T actual, Matcher<T> matcher)

컴파일러 오류 메시지는 다음과 같습니다.

Error:Error:line (102)cannot find symbol method
assertThat(java.util.Map<java.lang.String,java.lang.Class<java.util.Date>>,
org.hamcrest.Matcher<java.util.Map<java.lang.String,java.lang.Class
    <? extends java.io.Serializable>>>)

그러나 assertThat메소드 서명을 다음과 같이 변경하면

public static <T> void assertThat(T result, Matcher<? extends T> matcher)

그런 다음 컴파일이 작동합니다.

따라서 세 가지 질문이 있습니다.

  1. 현재 버전이 정확히 컴파일되지 않는 이유는 무엇입니까? 여기서 공분산 문제를 모호하게 이해했지만, 필요한 경우 설명 할 수 없었습니다.
  2. assertThat방법을 변경하면 단점 이 Matcher<? extends T>있습니까? 그렇게하면 깨질 다른 사례가 있습니까?
  3. assertThatJUnit 에서 메소드 의 일반화에 대한 요점이 있습니까? MatcherJUnit을 아무것도하지 않는 형태의 안전을 강제하기 위해 원하는 일반, 그냥 외모에 입력되지 않은 일치하는 메소드를 호출하기 때문에이 같은 클래스는, 그것을 필요로하지 않는 것 Matcher사실 만하지 않습니다 일치하면 테스트가 실패합니다. 안전하지 않은 작업이 필요하지 않습니다 (또는 그렇게 보입니다).

참고로 다음은 JUnit 구현입니다 assertThat.

public static <T> void assertThat(T actual, Matcher<T> matcher) {
    assertThat("", actual, matcher);
}

public static <T> void assertThat(String reason, T actual, Matcher<T> matcher) {
    if (!matcher.matches(actual)) {
        Description description = new StringDescription();
        description.appendText(reason);
        description.appendText("\nExpected: ");
        matcher.describeTo(description);
        description
            .appendText("\n     got: ")
            .appendValue(actual)
            .appendText("\n");

        throw new java.lang.AssertionError(description.toString());
    }
}

이 링크가 매우 유용합니다 (제네릭, 상속 및 하위 유형)입니다 : docs.oracle.com/javase/tutorial/java/generics/inheritance.html
— 다리우스 Jafari

답변:


145

먼저 http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html 로 안내해야합니다 . 그녀는 놀라운 일을합니다.

기본 아이디어는 당신이 사용하는 것입니다

<T extends SomeClass>

실제 매개 변수가 될 수 SomeClass있거나 하위 유형일 수 있습니다 .

귀하의 예에서

Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<java.util.Date>> result = null;
assertThat(result, is(expected));

당신은 expected을 구현하는 모든 클래스를 나타내는 Class 객체를 포함 할 수 있다고 말하고 있습니다 Serializable. 결과 맵은 Date클래스 객체 만 보유 할 수 있다고 말합니다 .

당신이 결과를 전달하면있는 거 설정 T정확히 Map의 String에 Date일치하지 않는 클래스 객체 Map의 String어떤이의에 Serializable.

한 가지 확인해야 할 사항-확실 Class<Date>하지 Date않습니까? 의지도 String로는 Class<Date>일반적으로 대단히 유용 소리가 나지 않는다 (이 보유 할 수있는 모든입니다 Date.class값이 아닌 인스턴스로 Date)

일반화 assertThat와 관련 하여이 메소드는 메소드가 Matcher결과 유형에 맞는를 전달 하도록 보장 할 수 있습니다 .


이 경우에는 클래스 맵을 원합니다. 내가 준 예제는 내 사용자 정의 클래스 대신 표준 JDK 클래스를 사용하도록 고안되었지만이 경우 클래스는 실제로 리플렉션을 통해 인스턴스화되고 키를 기반으로 사용됩니다. (클라이언트가 사용할 수있는 서버 클래스가없고 서버 측 작업을 수행하는 데 사용할 클래스의 키만있는 분산 앱).
— Yishai

6
내 두뇌가 어디에 붙어 있는지에 대해 Date 유형의 클래스를 포함하는지도가 Serializable 유형의 클래스를 포함하는지도 유형에 잘 맞지 않는 이유가 무엇인지 생각합니다. Serializable 유형의 클래스는 다른 클래스도 될 수 있지만 확실히 Date 유형을 포함합니다.
— Yishai

캐스트가 당신을 위해 수행되도록하는 assertThat에서 matcher.matches () 메소드는 신경 쓰지 않습니다. 따라서 T가 사용되지 않기 때문에 왜 관련이 있습니까? (메소드 리턴 타입은 void)
— Yishai

아아-그것이 assertTdef를 충분히 읽지 않아 얻은 것입니다. 피팅 매 처가 통과되도록하는 것만으로 보입니다 ...
— Scott Stanchfield

28

질문에 답변 한 모든 사람들에게 감사의 말을 전하는 것이 정말 도움이되었습니다. 결국 Scott Stanchfield의 답변은 내가 그것을 이해하는 방법에 가장 가깝습니다. 그러나 그가 처음 썼을 때 그를 이해하지 못했기 때문에 문제를 다시 언급하여 다른 누군가가 혜택을 볼 수 있도록 노력하고 있습니다.

하나의 일반적인 매개 변수 만 가지고 있으므로 이해하기 쉽기 때문에 List 측면에서 질문을 다시 설명하겠습니다.

매개 변수화 된 클래스 (예 : 목록 <Date>또는 맵 과 같은)의 목적은 다운 캐스트<K, V> 를 강제 실행 하고 컴파일러가 이것이 안전한지 (런타임 예외 없음) 보장하도록하는 것입니다.

List의 경우를 고려하십시오. 내 질문의 본질은 유형 T와 목록을 취하는 메소드가 T보다 상속 체인보다 더 아래에있는 목록을 허용하지 않는 이유입니다.이 고안된 예를 고려하십시오.

List<java.util.Date> dateList = new ArrayList<java.util.Date>();
Serializable s = new String();
addGeneric(s, dateList);

....
private <T> void addGeneric(T element, List<T> list) {
    list.add(element);
}

list 매개 변수는 문자열 목록이 아닌 날짜 목록이므로 컴파일되지 않습니다. 이것이 컴파일되면 제네릭은별로 유용하지 않습니다.

동일한 내용이지도에 적용됩니다.지도 <String, Class<? extends Serializable>>와 같은 것은 아닙니다 <String, Class<java.util.Date>>. 그들은 공변량이 아니므로 날짜 클래스가 포함 된 맵에서 값을 가져 와서 직렬화 가능한 요소가 포함 된 맵에 넣으려면 괜찮지 만 메소드 서명은 다음과 같습니다.

private <T> void genericAdd(T value, List<T> list)

둘 다 할 수 있기를 원합니다.

T x = list.get(0);

과

list.add(value);

이 경우 junit 메소드는 실제로 이러한 사항을 신경 쓰지 않지만 메소드 서명에는 공분산이 필요하므로 가져 오지 않습니다. 따라서 컴파일되지 않습니다.

두 번째 질문에서

Matcher<? extends T>

T가 API 의도가 아닌 객체 일 때 실제로 아무것도 받아들이지 않는 단점이 있습니다. 의도는 매 처가 실제 오브젝트와 일치하는지 정적으로 확인하는 것이며 해당 계산에서 오브젝트를 제외 할 방법이 없습니다.

세 번째 질문에 대한 답변은 확인되지 않은 기능면에서 아무것도 손실되지 않을 것입니다 (이 방법이 일반화되지 않은 경우 JUnit API 내에 안전하지 않은 유형 캐스팅이 없음). 그러나 다른 것을 달성하려고합니다. 두 매개 변수가 일치 할 가능성이 있습니다.

편집 (추가 숙고 및 경험 후) :

assertThat 메소드 서명의 큰 문제 중 하나는 변수 T를 일반 매개 변수 T와 동일시하려는 시도입니다. 공변량이 아니기 때문에 작동하지 않습니다. 따라서 예를 들어 T는 List<String>있지만 컴파일러가 수행하는 일치 항목을 전달할 수 Matcher<ArrayList<T>>있습니다. 타입 매개 변수가 아니라면 List와 ArrayList는 공변량이기 때문에 문제가 없지만 컴파일러가 ArrayList를 요구하는 한 Generics 때문에 List가 허용되지 않기 때문에 분명한 이유가 있습니다. 위에서.


그래도 왜 내가 업 캐스트 할 수 없는지 모르겠습니다. 날짜 목록을 직렬화 가능 목록으로 전환 할 수없는 이유는 무엇입니까?
— Thomas Ahle

@ThomasAhle, 날짜 목록이라고 생각하는 참조는 문자열 또는 기타 직렬화 가능 항목을 찾을 때 캐스팅 오류가 발생합니다.
— Yishai

알지만, 어떻게 든 List<Date>유형이있는 메소드에서 from을 반환 한 것처럼 어떻게 든 이전 참조를 제거하면 어떻게 List<Object>됩니까? Java에서 허용하지 않더라도 안전합니다.
— Thomas Ahle

14

그것은 다음과 같이 요약됩니다 :

Class<? extends Serializable> c1 = null;
Class<java.util.Date> d1 = null;
c1 = d1; // compiles
d1 = c1; // wont compile - would require cast to Date

클래스 참조 c1이 Long 인스턴스를 포함 할 수 있음을 알 수 있습니다 (주어진 시간에 기본 오브젝트가 List<Long>있었기 때문에). "알 수없는"클래스가 Date라는 보장이 없기 때문에 분명히 Date로 캐스트 할 수 없습니다. 일반적으로 안전하지 않으므로 컴파일러에서 허용하지 않습니다.

그러나 List와 같은 다른 객체를 소개하면 (예 :이 객체는 Matcher) 다음이 적용됩니다.

List<Class<? extends Serializable>> l1 = null;
List<Class<java.util.Date>> l2 = null;
l1 = l2; // wont compile
l2 = l1; // wont compile

그러나리스트의 유형이? T ... 대신 T를 확장합니다.

List<? extends Class<? extends Serializable>> l1 = null;
List<? extends Class<java.util.Date>> l2 = null;
l1 = l2; // compiles
l2 = l1; // won't compile

나는 Matcher<T> to Matcher<? extends T>당신이 기본적으로 l1 = l2를 할당하는 것과 유사한 시나리오를 소개 한다고 생각합니다 .

중첩 된 와일드 카드를 사용하는 것은 여전히 ​​매우 혼란 스럽지만, 일반적으로 서로에게 참조를 할당 할 수있는 방법을 살펴보면 제네릭을 이해하는 데 도움이되는 이유가 이해되기를 바랍니다. 함수 호출을 할 때 컴파일러가 T 유형을 유추하기 때문에 더 혼란 스럽습니다 (명백하게 T라고 말하지는 않습니다).


9

원래 코드는 컴파일하지 않는 이유는 즉 <? extends Serializable>수행 하지 , "직렬화를 확장하는 모든 클래스,"하지만, "직렬화를 확장 알 수없는하지만 특정 클래스."평균

예를 들어 작성된 코드가 지정된 경우에 할당하는 것이 완벽하게 유효 new TreeMap<String, Long.class>()>합니다 expected. 컴파일러가 코드 컴파일을 허용 한 경우 맵에서 찾은 객체 대신 객체를 assertThat()기대하기 때문에 중단 될 수 있습니다.DateLong


1
나는 꽤 따르지 않습니다-당신이 "의미하지는 않지만 ..."이라고 말할 때 : 차이점은 무엇입니까? (예를 들어, 이전의 정의에는 맞지만 후자의 것은 아닌 "알려졌지만 비특이적"클래스의 예는 무엇입니까?)
— poundifdef

예, 조금 어색합니다. 더 잘 표현하는 방법을 모르겠다 ... " '?' "알 수없는 유형이며 어떤 것과도 일치하는 유형이 아닌가?"
— erickson

1
설명하는 데 도움이 될 수있는 것은 "Serializable을 확장하는 모든 클래스"에 대해 간단히 사용할 수 있다는 것입니다.<Serializable>
— c0der

8

와일드 카드를 이해하는 한 가지 방법은 와일드 카드가 제네릭 참조가 제공 할 수있는 가능한 객체의 유형을 지정하지 않고 호환 가능한 다른 제네릭 참조의 유형을 지정한다고 생각하는 것입니다. ...) 따라서 첫 번째 대답은 말로 잘못 해석됩니다.

즉, List<? extends Serializable>유형이 알 수없는 유형이거나 Serializable의 하위 클래스 인 다른 목록에 해당 참조를 할당 할 수 있음을 의미합니다. SINGLE LIST가 Serializable의 서브 클래스를 보유 할 수 있다고 생각하지 마십시오.


확실히 도움이 되겠지만 "혼란스러운 소리"는 "혼란스러운 소리"로 대체됩니다. 후속 조치로, 왜이 설명에 따르면 Matcher를 <? extends T>사용 하여 메소드를 컴파일합니까?
— Yishai

List <Serializable>로 정의하면 똑같은 일을합니까? 나는 당신의 두 번째 파라에 대해 이야기하고 있습니다. 즉 다형성이 그것을 처리 할 것인가?
— Supun Wijerathne

3

나는 이것이 오래된 질문이라는 것을 알고 있지만 경계 와일드 카드를 잘 설명한다고 생각되는 예를 공유하고 싶습니다. java.util.Collections이 방법을 제공합니다 :

public static <T> void sort(List<T> list, Comparator<? super T> c) {
    list.sort(c);
}

List가 있으면 TList는 확장하는 유형의 인스턴스를 포함 할 수 있습니다 T. 목록에 동물이 포함 된 경우 목록에는 개와 고양이 (동물 모두)가 모두 포함될 수 있습니다. 개에는 "woofVolume"속성이 있고 고양이에는 "meowVolume"속성이 있습니다. 의 하위 클래스에 따라 이러한 속성을 기준으로 정렬하고 T싶지만이 방법으로 어떻게 할 수 있습니까? Comparator의 한계는 하나의 유형 ( T) 만 두 가지만 비교할 수 있다는 것입니다 . 따라서 단순히 a를 요구하면 Comparator<T>이 방법을 사용할 수있게됩니다. 그러나이 방법을 만든 사람은 무언가가 T이면 수퍼 클래스의 인스턴스 라는 것을 인식했습니다 T. 따라서, 그는의 비교기 사용할 수있게 해준다 T또는 슈퍼 클래스 T즉, ? super T.


1

당신이 사용하면 어떻게

Map<String, ? extends Class<? extends Serializable>> expected = null;

예, 이것은 위의 답변이 추진하고있는 것입니다.
— GreenieMeanie

아니요, 적어도 내가 시도한 방식으로 상황에 도움이되지 않습니다.
— Yishai
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.