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정확히 MapStringDate일치하지 않는 클래스 객체 MapString어떤이의에 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.