Java가 제네릭을 수정하지 않았는지 왜 신경 써야합니까?


102

이것은 후보자가 Java 언어에 추가되기를 바라는 내용으로 최근 인터뷰에서 묻는 질문으로 나왔습니다. 일반적으로 Java가 제네릭을 수정 하지 않은 것이 고통으로 알려져 있지만, 밀어 붙였을 때 후보는 실제로 그들이 거기에 있었다면 그가 달성 할 수 있었던 종류의 일을 내게 말할 수 없었습니다.

분명히 원시 유형은 Java (및 안전하지 않은 검사)에서 허용되기 때문에 제네릭을 파괴 List<Integer>하고 (예를 들어) 실제로를 포함 하는 것으로 끝날 수 있습니다 String. 이것은 유형 정보가 수정 되었다면 불가능하게 될 수있었습니다. 하지만 이것보다 더 있어야합니다 !

사람들 이 정말로하고 싶은 일의 예를 게시 할 수 있습니까? 내 말은, 분명히 당신은 List런타임 에 a의 유형을 얻을 수 있지만, 그것으로 무엇을 하시겠습니까?

public <T> void foo(List<T> l) {
   if (l.getGenericType() == Integer.class) {
       //yeah baby! err, what now?

편집 : 대답은 주로 Class매개 변수로 전달해야 할 필요성에 대해 우려하는 것 같습니다 (예 EnumSet.noneOf(TimeUnit.class):). 나는 이것이 가능하지 않은 곳에서 무언가를 더 찾고 있었다 . 예를 들면 :

List<?> l1 = api.gimmeAList();
List<?> l2 = api.gimmeAnotherList();

if (l1.getGenericType().isAssignableFrom(l2.getGenericType())) {
    l1.addAll(l2); //why on earth would I be doing this anyway?

이것은 런타임에 제네릭 유형의 클래스를 얻을 수 있음을 의미합니까? (그렇다면 예가 있습니다!)
James B

수정 가능한 제네릭에 대한 대부분의 욕구는 주로 컬렉션과 함께 제네릭을 사용하고 이러한 컬렉션이 배열처럼 작동하기를 원하는 사람들로부터 비롯된 것이라고 생각합니다.
kdgregory

2
더 흥미로운 질문 (나에게) : Java에서 C ++ 스타일 제네릭을 구현하려면 무엇이 필요합니까? 런타임에서는 확실히 할 수있는 것처럼 보이지만 기존의 모든 클래스 로더를 중단합니다 ( findClass()매개 변수화를 무시해야하지만 defineClass()할 수 없기 때문입니다). 그리고 우리가 알다시피, The Powers That Be는 이전 버전과의 호환성이 가장 중요합니다.
kdgregory

1
실제로 Java는 매우 제한된 방식으로 수정 된 제네릭을 제공합니다 . 나는이 SO 스레드에서 자세한 내용 제공 : stackoverflow.com/questions/879855/...을
리처드 고메스에게

자바 원 기조는 자바 9 구체화를 지원하는 것을 나타냅니다.
Rangi Keen 2014 년

답변:


81

내가이 "필요"를 발견 한 몇 번부터, 궁극적으로이 구조로 귀결됩니다.

public class Foo<T> {

    private T t;

    public Foo() {
        this.t = new T(); // Help?
    }

}

기본 생성자 T가 있다고 가정하면 C #에서 작동 합니다. 으로 런타임 유형을 가져오고 생성자를 가져올 수도 있습니다.typeof(T)Type.GetConstructor()

일반적인 Java 솔루션은 Class<T>as 인수 를 전달하는 것입니다.

public class Foo<T> {

    private T t;

    public Foo(Class<T> cls) throws Exception {
        this.t = cls.newInstance();
    }

}

(메서드 인자도 괜찮 기 때문에 반드시 생성자 인자로 전달할 필요는 없습니다. 위는 예시 일 뿐이며 try-catch간결성을 위해 생략되었습니다)

다른 모든 제네릭 형식 구조의 경우 약간의 리플렉션을 사용하여 실제 형식을 쉽게 확인할 수 있습니다. 아래 Q & A는 사용 사례와 가능성을 보여줍니다.


5
이것은 (예를 들어) C #에서 작동합니까? T에 기본 생성자가 있다는 것을 어떻게 알 수 있습니까?
Thomas Jung

4
네, 그렇습니다. 그리고 이것은 기본적인 예제 일뿐입니다 (자바빈으로 작업한다고 가정합시다). 요점은 자바 당신이 중에 클래스를 얻을 수 있다는 것입니다 런타임 에 의해 T.class또는 T.getClass(), 그래서 당신은 모든 필드, 생성자와 메소드에 액세스 할 수있다. 건설도 불가능합니다.
BalusC 2009

3
이것은 "큰 문제"로 저에게 정말 약한 것 같습니다. 특히 이것은 생성자 / 매개 변수 등에 대한 매우 부서지기 쉬운 반사와 관련해서 만 유용 할 가능성이 있기 때문입니다.
oxbow_lakes

33
형식을 다음과 같이 선언하면 C #으로 컴파일됩니다. public class Foo <T> where T : new (). 매개 변수없는 생성자를 포함하는 T의 유효한 유형을 제한합니다.
Martin Harris

3
@Colin 실제로 Java에게 주어진 제네릭 유형이 둘 이상의 클래스 또는 인터페이스를 확장해야 함을 알릴 수 있습니다. docs.oracle.com/javase/tutorial/java/generics/bounded.html 의 "Multiple Bounds"섹션을 참조하십시오 . (물론 객체가 인터페이스로 추상화 될 수있는 메서드를 공유하지 않는 특정 집합 중 하나가 될 것이라고 말할 수는 없습니다. 템플릿 전문화를 사용하여 C ++로 어느 정도 구현할 수 있습니다.)
JAB

99

가장 일반적으로 저를 물어 뜯는 것은 여러 일반 유형에 걸쳐 다중 디스패치를 ​​이용할 수 없다는 것입니다. 다음은 불가능하며 최상의 솔루션이되는 경우가 많습니다.

public void my_method(List<String> input) { ... }
public void my_method(List<Integer> input) { ... }

5
그렇게하기 위해 수정이 필요하지 않습니다. 메서드 선택은 컴파일 타임 유형 정보를 사용할 수있는 컴파일 타임에 수행됩니다.
Tom Hawtin-tackline

26
@Tom : 유형 삭제로 인해 컴파일되지도 않습니다. 둘 다 public void my_method(List input) {}. 그러나 나는 그들이 같은 이름을 가지지 않을 것이기 때문에 이러한 필요를 결코 발견하지 못했습니다. 이름이 같으면 public <T extends Object> void my_method(List<T> input) {}더 좋은 생각이 아닌지 질문합니다 .
BalusC 2009

1
흠, 나는 모두 매개 변수의 동일한 수의 과부하를 방지하는 경향이 있고, 같은 것을 선호 myStringsMethod(List<String> input)하고 myIntegersMethod(List<Integer> input)심지어 이러한 경우에 과부하를 경우하는 것은 자바에서 가능했다.
Fabian Steeg

4
@Fabian : 즉, 별도의 코드가 있어야 <algorithm>하며 C ++에서 얻을 수있는 이점을 방지해야합니다 .
David Thornley

나는 혼란 스럽습니다. 이것은 본질적으로 두 번째 포인트와 동일하지만 2 개의 반대 투표를 받았습니까? 누구든지 나를 계몽 해줄 사람이 있습니까?
rsp

34

형식 안전성 이 마음에 듭니다. 매개 변수화 된 유형으로 다운 캐스팅하는 것은 수정 된 제네릭 없이는 항상 안전하지 않습니다.

List<String> myFriends = new ArrayList();
myFriends.add("Alice");
getSession().put("friends", myFriends);
// later, elsewhere
List<Friend> myFriends = (List<Friend>) getSession().get("friends");
myFriends.add(new Friend("Bob")); // works like a charm!
// and so...
List<String> myFriends = (List<String>) getSession().get("friends");
for (String friend : myFriends) print(friend); // ClassCastException, wtf!? 

또한 추상화는 유출이 적습니다 . 적어도 유형 매개 변수에 대한 런타임 정보에 관심이있을 수있는 것입니다. 오늘날 일반 매개 변수 중 하나의 유형에 대한 런타임 정보가 필요한 경우 Class함께 전달해야합니다 . 이렇게하면 외부 인터페이스는 구현에 따라 달라집니다 (매개 변수에 대해 RTTI를 사용하는지 여부).


1
예- ParametrizedList소스 컬렉션 검사 유형의 데이터를 복사 하는 을 만드는 방법이 있습니다. 약간 비슷 Collections.checkedList하지만 시작할 컬렉션으로 시드 할 수 있습니다.
oxbow_lakes

@tackline-글쎄요, 몇 가지 추상화는 덜 유출 될 것입니다. 구현에서 형식 메타 데이터에 액세스해야하는 경우 클라이언트가 클래스 개체를 보내야하기 때문에 외부 인터페이스가 알려줍니다.
gustafc 2009

... 즉, reified generics를 사용하면 외부 인터페이스를 변경하지 않고 T.class.getAnnotation(MyAnnotation.class)(where Tis a generic type) 과 같은 항목을 추가 할 수 있습니다 .
gustafc 2009

@gustafc : C ++ 템플릿이 완전한 형식 안전성을 제공한다고 생각한다면 다음을 읽어보십시오. kdgregory.com/index.php?page=java.generics.cpp
kdgregory

@kdgregory : C ++가 100 % 형식 안전이라고 말한 적이 없습니다. 삭제가 형식 안전을 손상 시킨다는 것입니다. "C ++은 C 스타일 포인터 캐스트로 알려진 고유 한 유형 삭제 형식을 가지고 있습니다." 그러나 Java는 동적 캐스트 (재 해석 아님) 만 수행하므로 수정은이 전체를 유형 시스템에 연결합니다.
gustafc 2009

26

코드에서 일반 배열을 만들 수 있습니다.

public <T> static void DoStuff() {
    T[] myArray = new T[42]; // No can do
}

Object에 무슨 문제가 있습니까? 객체의 배열은 어쨌든 참조의 배열입니다. 객체 데이터가 스택에있는 것과는 다릅니다. 모두 힙에 있습니다.
Ran Biron

19
유형 안전성. 원하는 것을 Object []에 넣을 수 있지만 String []에는 String 만 넣을 수 있습니다.
Turnor

3
Ran : 비꼬는 말없이 : Java 대신 스크립팅 언어를 사용하는 것을 좋아할 수 있습니다. 그러면 어디에서나 유형이 지정되지 않은 변수의 유연성이 있습니다!
플라잉 양

2
배열은 두 언어 모두에서 공변 적이므로 형식이 안전하지 않습니다. String[] strings = new String[1]; Object[] objects = strings; objects[0] = new Object();두 언어 모두에서 잘 컴파일됩니다. notsofine을 실행합니다.
Martijn

16

이것은 오래된 질문이고 많은 답변이 있지만 기존 답변은 표시되지 않은 것 같습니다.

"reified"는 단지 실제를 의미하고 보통 유형 삭제의 반대를 의미합니다.

Java Generics와 관련된 큰 문제 :

  • 이 끔찍한 권투 요구 사항은 기본 형식과 참조 형식 간의 연결을 끊습니다. 이것은 수정 또는 유형 삭제와 직접 관련이 없습니다. C # / Scala는이 문제를 해결합니다.
  • 자기 유형이 없습니다. 이러한 이유로 JavaFX 8은 "빌더"를 제거해야했습니다. 유형 삭제와는 전혀 관련이 없습니다. Scala는이 문제를 해결하지만 C #에 대해서는 확실하지 않습니다.
  • 선언 측 유형 변형이 없습니다. C # 4.0 / Scala에는이 기능이 있습니다. 유형 삭제와는 전혀 관련이 없습니다.
  • 과부하 void method(List<A> l)method(List<B> l) . 이것은 유형 삭제로 인한 것이지만 매우 사소합니다.
  • 런타임 유형 반영을 지원하지 않습니다. 이것이 유형 삭제의 핵심입니다. 컴파일 타임에 프로그램 로직을 많이 확인하고 증명하는 고급 컴파일러를 좋아한다면 리플렉션을 가능한 한 적게 사용해야하며 이러한 유형의 삭제는 문제가되지 않아야합니다. 좀 더 고르지 않고 스크립 티하고 동적 유형 프로그래밍을 좋아하고 가능한 한 많은 논리가 올바른 것을 증명하는 컴파일러에별로 신경 쓰지 않는다면 더 나은 반영을 원하고 유형 삭제를 수정하는 것이 중요합니다.

1
대부분 직렬화 사례에서 어렵습니다. 직렬화되는 일반적인 사물의 클래스 유형을 알아낼 수 있기를 원하지만 유형 삭제로 인해 잠시 중단됩니다. 그것은 하드 같은 것을 할 수 있습니다deserialize(thingy, List<Integer>.class)
Cogman

3
이것이 최선의 답이라고 생각합니다. 특히 유형 삭제로 인해 실제로 근본적인 문제와 Java 언어 디자인 문제가 무엇인지 설명하는 부분. 제가 사람들에게 지우개를 시작한다고 말한 첫 번째 것은 Scala와 Haskell도 유형 지우기로 작동한다는 것입니다.
aemxdp 2014 년

13

직렬화는 수정으로 더 간단합니다. 우리가 원하는 것은

deserialize(thingy, List<Integer>.class);

우리가해야 할 일은

deserialize(thing, new TypeReference<List<Integer>>(){});

보기 흉하고 펑키하게 작동합니다.

다음과 같은 말을하는 것이 정말 도움이되는 경우도 있습니다.

public <T> void doThings(List<T> thingy) {
    if (T instanceof Q)
      doCrazyness();
  }

이런 것들은 자주 물지는 않지만, 일어날 때 물기도합니다.


이. 이것의 천 배. 나는 자바에서 쓰기 직렬화 코드를 시도 할 때마다 내가 뭔가 다른 작동하지있어 그날의 애도를 보내고
기본

11

Java Geneircs에 대한 나의 노출은 상당히 제한적이며 다른 답변이 이미 언급 한 점을 제외하고 Java Generics and Collections 책에 설명 된 시나리오가 있습니다. 하고 Maurice Naftalin과 Philip Walder의 , 여기서 수정 된 제네릭이 유용합니다.

유형을 수정할 수 없기 때문에 매개 변수화 된 예외를 가질 수 없습니다.

예를 들어 아래 양식의 선언은 유효하지 않습니다.

class ParametericException<T> extends Exception // compile error

이는 catch 절이 throw 된 예외가 주어진 유형과 일치하는지 여부를 확인 하기 때문 입니다. 이 검사는 인스턴스 테스트에 의해 수행되는 검사와 동일하며 유형을 수정할 수 없기 때문에 위 형식의 문은 유효하지 않습니다.

위의 코드가 유효했다면 아래 방식으로 예외 처리가 가능했을 것입니다.

try {
     throw new ParametericException<Integer>(42);
} catch (ParametericException<Integer> e) { // compile error
  ...
}

이 책은 또한 Java 제네릭이 C ++ 템플릿이 정의 된 방식 (확장)과 유사하게 정의되면 최적화를위한 더 많은 기회를 제공하므로보다 효율적인 구현으로 이어질 수 있다고 언급합니다. 그러나 이것보다 더 많은 설명을 제공하지 않으므로 지식이 풍부한 사람들의 설명 (포인터)이 도움이 될 것입니다.


1
유효한 점이지만 그렇게 매개 변수화 된 예외 클래스가 왜 유용한 지 잘 모르겠습니다. 이것이 언제 유용 할 수 있는지에 대한 간단한 예를 포함하도록 답변을 수정할 수 있습니까?
oxbow_lakes

@oxbow_lakes : 죄송합니다. Java Generics에 대한 제 지식은 상당히 제한되어 있으며이를 개선하기 위해 노력하고 있습니다. 따라서 이제 매개 변수화 된 예외가 유용 할 수있는 예를 생각할 수 없습니다. 그것에 대해 생각하려고 노력할 것입니다. 고마워.
sateesh

예외 유형의 다중 상속을 대신 할 수 있습니다.
meriton 2009

성능 향상은 현재 유형 매개 변수가 Object에서 상속되어야하므로 실행 및 메모리 오버 헤드를 부과하는 기본 유형의 boxing이 필요합니다.
meriton 2009

8

배열이 수정되면 제네릭과 함께 훨씬 더 잘 작동 할 것입니다.


2
물론입니다.하지만 여전히 문제가있을 것입니다. List<String>이 아닙니다 List<Object>.
Tom Hawtin-tackline

동의-그러나 기본 요소 (Integer, Long 등)에만 해당됩니다. "일반"개체의 경우 동일합니다. 프리미티브는 매개 변수화 된 유형이 될 수 없기 때문에 (최소한 IMHO는 훨씬 더 심각한 문제), 이것이 진짜 고통이라고 생각하지 않습니다.
Ran Biron

3
배열의 문제는 공분산이며 수정과는 관련이 없습니다.
Recurse

5

jdbc 결과 집합을 반복자로 표시하는 래퍼가 있습니다 (즉, 종속성 주입을 통해 데이터베이스에서 시작된 작업을 훨씬 쉽게 단위 테스트 할 수 있음을 의미합니다).

API는 Iterator<T>T가 생성자에서 문자열 만 사용하여 생성 할 수있는 유형 인 것처럼 보입니다 . 그런 다음 Iterator는 SQL 쿼리에서 반환되는 문자열을 살펴본 다음 T 유형의 생성자와 일치 시키려고합니다.

제네릭이 구현되는 현재 방식에서는 결과 집합에서 생성 할 객체의 클래스도 전달해야합니다. 올바르게 이해하면 제네릭이 수정되면 T.getClass ()를 호출하여 생성자를 얻은 다음 훨씬 깔끔한 Class.newInstance ()의 결과를 캐스트 할 필요가 없습니다.

기본적으로 애플리케이션을 작성하는 것이 아니라 API 작성이 더 쉬워 진다고 생각합니다. 객체에서 더 많은 것을 추론 할 수 있기 때문에 구성이 덜 필요하기 때문입니다 ... 나는 주석의 의미를 인식하지 못했습니다. config의 묶음 대신 spring 또는 xstream과 같은 것들에서 사용되는 것을 보았습니다.


그러나 수업을 통과하는 것이 나에게는 더 안전 해 보입니다. 어쨌든 데이터베이스 쿼리에서 인스턴스를 반사적으로 생성하는 것은 어쨌든 리팩토링과 같은 변경에 매우 취약합니다 (코드와 데이터베이스 모두에서). 난 그냥해진다 일을 찾고 있었다 추측 할 수 없습니다 클래스를 제공하는 것은
oxbow_lakes

5

한 가지 좋은 점은 기본 (값) 유형에 대한 권투를 피하는 것입니다. 이것은 다른 사람들이 제기 한 어레이 불만과 다소 관련이 있으며, 메모리 사용이 제한된 경우 실제로 상당한 차이를 만들 수 있습니다.

또한 매개 변수화 된 유형을 반영 할 수있는 것이 중요한 프레임 워크를 작성할 때 여러 유형의 문제가 있습니다. 물론 이것은 런타임에 클래스 객체를 전달하여 해결할 수 있지만 API를 모호하게하고 프레임 워크 사용자에게 추가적인 부담을줍니다.


2
이것은 본질적으로 new T[]T가 원시 유형 인 곳에서 할 수있는 것으로 귀결됩니다 !
oxbow_lakes

2

당신이 특별한 것을 성취 할 것이라는 것은 아닙니다. 이해하는 것이 더 간단 할 것입니다. 유형 삭제는 초보자에게 힘든 시간처럼 보이며 궁극적으로 컴파일러 작동 방식에 대한 이해가 필요합니다.

제 생각에는 제네릭은 단순히 많은 중복 캐스팅을 절약 하는 추가 항목 이라는 것입니다.


이것은 확실히 제네릭이 자바에있는 것 입니다. C # 및 다른 언어에서 그들은 강력한 도구입니다
기본

1

여기에있는 모든 답변이 놓친 것은 저에게 끊임없이 골칫거리입니다. 유형이 지워지기 때문에 일반 인터페이스를 두 번 상속 할 수 없습니다. 세밀한 인터페이스를 만들려는 경우 문제가 될 수 있습니다.

    public interface Service<KEY,VALUE> {
           VALUE get(KEY key);
    }

    public class PersonService implements Service<Long, Person>,
        Service<String, Person> //Can not do!!

0

오늘 저를 사로 잡은 것이 있습니다. 수정하지 않고, 제네릭 항목의 varargs 목록을 받아들이는 메서드를 작성하면 호출자는 유형이 안전하다고 생각할 수 있지만 실수로 오래된 크뤼를 전달하고 메서드를 날려 버릴 수 있습니다.

그런 일이 없을 것 같습니까? ... 물론입니다. Class를 데이터 유형으로 사용할 때까지. 이 시점에서 호출자는 기꺼이 많은 Class 객체를 보내지 만 간단한 오타는 T를 준수하지 않는 Class 객체를 보내어 재해가 발생합니다.

(주의 : 여기에서 실수를했을 수도 있지만 "generics varargs"를 검색해 보면 위의 내용이 예상했던 것 같습니다.이 문제를 실제 문제로 만드는 것은 Class를 사용하는 것입니다. 덜 조심하십시오 :()


예를 들어, 저는 Class 객체를지도의 키로 사용하는 패러다임을 사용하고 있습니다 (단순한지도보다 더 복잡하지만 개념적으로는 그게 진행되고 있습니다).

예를 들어 이것은 Java Generics에서 훌륭하게 작동합니다 (사소한 예).

public <T extends Component> Set<UUID> getEntitiesPossessingComponent( Class<T> componentType)
    {
        // find the entities that are mapped (somehow) from that class. Very type-safe
    }

예를 들어 Java Generics에서 수정하지 않고 이것은 모든 "Class"객체를 허용합니다. 그리고 이것은 이전 코드의 아주 작은 확장 일뿐입니다.

public <T extends Component> Set<UUID> getEntitiesPossessingComponents( Class<T>... componentType )
    {
        // find the entities that are mapped (somehow) to ALL of those classes
    }

위의 방법은 개별 프로젝트에서 수천 번 작성해야하므로 인적 오류 가능성이 높아집니다. 디버깅 실수는 "재미 있지 않음"을 증명합니다. 나는 현재 대안을 찾고 있지만 많은 희망을 가지고 있지 않습니다.

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