수색…


소개

Generics 는 Java의 유형 시스템을 확장하여 유형 또는 메소드가 다양한 유형의 객체를 조작 할 수 있도록하면서 컴파일 타임 유형 안전을 제공하는 일반 프로그래밍 기능입니다. 특히, Java 콜렉션 프레임 워크는 콜렉션 인스턴스에 저장된 오브젝트 유형을 지정하기 위해 제네릭을 지원합니다.

통사론

  • class ArrayList <E> {} // 형태 파라미터 E를 가지는 범용 클래스
  • class HashMap <K, V> {} // 2 개의 타입 파라미터 K 및 V를 가지는 범용 클래스입니다.
  • <E> void print (E element) {} // 형식 매개 변수 E를 가진 일반 메서드
  • ArrayList <String> 이름; // 일반 클래스의 선언
  • ArrayList <?> 객체; // 알 수없는 유형 매개 변수가있는 일반 클래스의 선언
  • new ArrayList <String> () // 일반 클래스의 인스턴스화
  • 새 ArrayList <> () // 유형 추론 "diamond"(Java 7 이상)이있는 인스턴스화

비고

제네릭은 유형 지우기를 통해 Java로 구현되므로 런타임 중에 일반 클래스의 인스턴스화에 지정된 유형 정보를 사용할 수 없음을 의미합니다. 예를 들어, List<String> names = new ArrayList<>(); 런타임시 요소 유형 String 을 복구 할 수없는 목록 오브젝트를 생성합니다. 그러나 목록이 List<String> 유형의 필드에 저장 List<String> 유형의 메소드 / 생성자 매개 변수에 전달되거나 해당 반환 유형의 메소드에서 반환 된 경우 런타임에 전체 유형 정보 복구 할 수 있습니다 Java Reflection API를 통해

이것은 또한 generic 형식 (예 : (List<String>) list )으로 캐스팅 할 때 캐스트가 검사되지 않은 캐스트 임을 의미합니다. <String> 매개 변수가 지워지기 때문에 JVM은 List<?> 에서 List<String> 로의 형변환이 올바른지 확인할 수 없습니다. JVM은 런타임에 List to List 에 대한 캐스트 만 볼 수 있습니다.

일반 클래스 만들기

제네릭을 사용하면 클래스, 인터페이스 및 메서드가 다른 클래스와 인터페이스를 형식 매개 변수로 사용할 수 있습니다.

이 예제에서는 제네릭 클래스 인 Param 을 사용하여 꺽쇠 괄호 ( <> )로 구분 된 단일 형식 매개 변수 T .

public class Param<T> {
    private T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

이 클래스를 인스턴스화하려면, T 대신에 type 인수 를 제공하십시오. 예 : Integer :

Param<Integer> integerParam = new Param<Integer>();

type 인수는 배열 및 기타 일반 유형을 포함하여 모든 참조 유형이 될 수 있습니다.

Param<String[]> stringArrayParam;
Param<int[][]> int2dArrayParam;
Param<Param<Object>> objectNestedParam;

Java SE 7 이상에서는 type 인수를 diamond 이라는 빈 인수 ( <> )로 바꿀 수 있습니다.

Java SE 7
Param<Integer> integerParam = new Param<>();

다른 식별자와 달리 형식 매개 변수에는 명명 제약 조건이 없습니다. 그러나 그들의 이름은 일반적으로 대문자로 된 목적의 첫 글자입니다. (공식 JavaDocs에서도 마찬가지입니다.)
예를 들면 "type"T , "element"는 E , "key"/ "value"는 K / V 를 포함 합니다.


제네릭 클래스 확장

public abstract class AbstractParam<T> {
    private T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

AbstractParam 은 type 매개 변수가 T 선언 된 추상 클래스 입니다. 이 클래스를 확장하면 해당 유형 매개 변수는 <> 내부에 작성된 유형 인수로 대체되거나 유형 매개 변수는 변경되지 않습니다. 아래의 첫 번째와 두 번째 예제에서는 StringInteger 가 type 매개 변수를 대체합니다. 세 번째 예에서 type 매개 변수는 변경되지 않습니다. 네 번째 예제는 제네릭을 전혀 사용하지 않으므로 클래스에 Object 매개 변수가있는 것과 유사합니다. 컴파일러는 AbstractParam 을 원시 형식으로 경고하지만 ObjectParam 클래스를 컴파일합니다. 다섯 번째 예제는 두 개의 매개 변수 (아래 "다중 유형 매개 변수"참조)를 가지며 두 번째 매개 변수를 수퍼 클래스에 전달 된 유형 매개 변수로 선택합니다.

public class Email extends AbstractParam<String> {
    // ...
}

public class Age extends AbstractParam<Integer> {
    // ...
}

public class Height<T> extends AbstractParam<T> {
    // ...
}

public class ObjectParam extends AbstractParam {
    // ...
}

public class MultiParam<T, E> extends AbstractParam<E> {
    // ...
}

사용법은 다음과 같습니다.

Email email = new Email();
email.setValue("[email protected]");
String retrievedEmail = email.getValue();

Age age = new Age();
age.setValue(25);
Integer retrievedAge = age.getValue();
int autounboxedAge = age.getValue();

Height<Integer> heightInInt = new Height<>();
heightInInt.setValue(125);

Height<Float> heightInFloat = new Height<>();
heightInFloat.setValue(120.3f);

MultiParam<String, Double> multiParam = new MultiParam<>();
multiParam.setValue(3.3);

Email 클래스에서 T getValue() 메소드는 String getValue() 의 서명이있는 것처럼 작동하고 void setValue(T) 메소드는 void setValue(String) 로 선언 된 것처럼 작동합니다.

빈 중괄호 ( {} )를 사용하여 익명 내부 클래스로 인스턴스화 할 수도 있습니다.

AbstractParam<Double> height = new AbstractParam<Double>(){};
height.setValue(198.6);

익명 내부 클래스가있는 다이아몬드 사용은 허용되지 않습니다.


여러 유형 매개 변수

Java는 제네릭 클래스 또는 인터페이스에서 둘 이상의 유형 매개 변수를 사용할 수있는 기능을 제공합니다. 꺽쇠 괄호 사이에 쉼표로 구분 된 유형 목록 을 배치하여 여러 유형 매개 변수를 클래스 또는 인터페이스에서 사용할 수 있습니다. 예:

public class MultiGenericParam<T, S> {
    private T firstParam;
    private S secondParam;
   
    public MultiGenericParam(T firstParam, S secondParam) {
        this.firstParam = firstParam;
        this.secondParam = secondParam;
    }
    
    public T getFirstParam() {
        return firstParam;
    }
    
    public void setFirstParam(T firstParam) {
        this.firstParam = firstParam;
    }
    
    public S getSecondParam() {
        return secondParam;
    }
    
    public void setSecondParam(S secondParam) {
        this.secondParam = secondParam;
    }
}

사용법은 아래와 같이 할 수 있습니다 :

MultiGenericParam<String, String> aParam = new MultiGenericParam<String, String>("value1", "value2");
MultiGenericParam<Integer, Double> dayOfWeekDegrees = new MultiGenericParam<Integer, Double>(1, 2.6);

일반 메소드 선언

방법도있을 수 제네릭 형식 매개 변수를.

public class Example {

    // The type parameter T is scoped to the method
    // and is independent of type parameters of other methods.
    public <T> List<T> makeList(T t1, T t2) {
        List<T> result = new ArrayList<T>();
        result.add(t1);
        result.add(t2);
        return result;
    }

    public void usage() {
        List<String> listString = makeList("Jeff", "Atwood");
        List<Integer> listInteger = makeList(1, 2);
    }
}

실제 형식 인수를 일반 메서드에 전달할 필요는 없습니다. 컴파일러는 대상 유형 (예 : 결과를 할당하는 변수) 또는 실제 인수 유형에 따라 우리를 위해 형식 인수를 유추합니다. 일반적으로 호출 형식을 올 Y 른 특정 형식 인수를 유추합니다.

때때로, 드물게, 명시 적 타입 인자로이 타입 유추를 무시할 필요가있을 수 있습니다 :

void usage() {
    consumeObjects(this.<Object>makeList("Jeff", "Atwood").stream());
}

void consumeObjects(Stream<Object> stream) { ... }

이 예제에서는 컴파일러가 stream() 을 호출 한 후 TObject 가 필요하고 makeList 인수를 기반으로 String 유추 할 것이라는 사실을 알기 위해 "미리보기"를 할 수 없으므로 필요합니다. 자바 언어 (메소드가 호출되는 클래스 또는 개체를 생략 지원하지 않습니다 this 유형의 인수가 명시 적으로 제공하는 경우 위의 예에서).

다이아몬드

Java SE 7

Java 7은 다이아몬드 1 을 도입하여 일반 클래스 인스턴스화에 대한 보일러 판을 제거했습니다. Java 7 이상에서는 다음과 같이 작성할 수 있습니다.

List<String> list = new LinkedList<>();

이전 버전에서 작성해야 할 부분은 다음과 같습니다.

List<String> list = new LinkedList<String>();

한 가지 제한 사항은 익명 클래스 (Anonymous Classes )에 대한 것이므로 인스턴스화에 type 매개 변수를 제공해야합니다.

// This will compile:

Comparator<String> caseInsensitiveComparator = new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        return s1.compareToIgnoreCase(s2);
    }
};

// But this will not:

Comparator<String> caseInsensitiveComparator = new Comparator<>() {
    @Override
    public int compare(String s1, String s2) {
        return s1.compareToIgnoreCase(s2);
    }
};
Java SE 8

익명 내부 클래스 와 함께 다이아몬드를 사용하는 것은 Java 7 및 8에서 지원되지 않지만 Java 9의 새로운 기능으로 포함됩니다 .


각주:

1 - 어떤 사람들은 <> 사용을 "다이아몬드 운전자 "라고 부릅니다. 이것은 잘못되었습니다. 다이아몬드는 연산자로 작동하지 않으며 JLS 또는 (공식적인) Java 자습서의 어디에서나 연산자로 설명되거나 나열되지 않습니다. 실제로 <> 는 별개의 Java 토큰이 아닙니다. 오히려 그것은이다 < a로 다음 토큰 > 토큰, 및 (나쁜 스타일하지만) 둘 사이의 공백이나 의견이 그것을 합법적이다. JLS와 튜토리얼에서는 <> 를 "다이아몬드"라고 일관되게 언급하기 때문에 올바른 용어입니다.

여러 개의 상한값 필요 ( "extends A & B")

여러 상한을 확장하려면 제네릭 형식이 필요할 수 있습니다.

예 : 숫자 목록을 정렬하려고하지만 NumberComparable 구현하지 않습니다.

public <T extends Number & Comparable<T>> void sortNumbers( List<T> n ) {
  Collections.sort( n );
}

이 예제에서 TNumber 를 확장 하고 Integer 또는 BigDecimal 과 같은 모든 "일반"내장 숫자 구현에 적합해야하지만 Striped64 와 같이 더 특이한 것들에는 맞지 않는 Comparable<T> 를 구현해야합니다.

다중 상속은 허용되지 않으므로 최대 하나의 클래스를 바인딩으로 사용할 수 있으며 첫 번째로 나열해야합니다. 예를 들어, <T extends Comparable<T> & Number> 는 Comparable이 클래스가 아니기 때문에 인터페이스가 아니기 때문에 허용되지 않습니다.

묶인 일반 클래스 만들기

클래스 정의에서 해당 유형을 경계하여 일반 클래스 에서 사용되는 유효한 유형을 제한 할 수 있습니다. 다음과 같은 간단한 유형 계층 구조가 제공됩니다.

public abstract class Animal {
    public abstract String getSound();
}

public class Cat extends Animal {
    public String getSound() {
        return "Meow";
    }
}

public class Dog extends Animal {
    public String getSound() {
        return "Woof";
    }
}

제한된 제네릭이 없으면 우리는 일반적이고 각 요소가 동물이라는 것을 알고있는 컨테이너 클래스를 만들 수 없습니다.

public class AnimalContainer<T> {

    private Collection<T> col;

    public AnimalContainer() {
        col = new ArrayList<T>();
    }

    public void add(T t) {
        col.add(t);
    }

    public void printAllSounds() {
        for (T t : col) {
            // Illegal, type T doesn't have makeSound()
            // it is used as an java.lang.Object here
            System.out.println(t.makeSound()); 
        }
    }
}

클래스 정의에서 제네릭 바인딩을 사용하면 이것이 가능합니다.

public class BoundedAnimalContainer<T extends Animal> { // Note bound here.

    private Collection<T> col;

    public BoundedAnimalContainer() {
        col = new ArrayList<T>();
    }

    public void add(T t) {
        col.add(t);
    }

    public void printAllSounds() {
        for (T t : col) {
            // Now works because T is extending Animal
            System.out.println(t.makeSound()); 
        }
    }
}

또한 generic 형식의 유효한 인스턴스화를 제한합니다.

// Legal
AnimalContainer<Cat> a = new AnimalContainer<Cat>();

// Legal
AnimalContainer<String> a = new AnimalContainer<String>();
// Legal because Cat extends Animal
BoundedAnimalContainer<Cat> b = new BoundedAnimalContainer<Cat>();

// Illegal because String doesn't extends Animal
BoundedAnimalContainer<String> b = new BoundedAnimalContainer<String>();

`T`,`? 슈퍼 T`,`? 연장 T`

Java generics 바운드 와일드 카드 구문은 알 수없는 형식을 나타내는 ? :

  • ? extends T 는 상한의 와일드 카드를 나타냅니다. 알 수없는 유형은 T의 부속 유형이거나 유형 T 자체 여야하는 유형을 나타냄니다.

  • ? super T 는 경계가 낮은 와일드 카드를 나타냅니다. 알 수없는 유형은 T의 4 퍼 유형이거나 T 자 유형이어야하는 유형을 나타냄니다.

경험적으로 볼 때,

  • ? extends T "읽기"액세스 ( "입력") 만 필요하면 ? extends T
  • ? super T 당신이 "쓰기"접근 ( "출력")을 원하면 ? super T
  • T 가 필요한 경우 T ( "수정")

extendssuper 사용하면 일반적으로 코드가보다 유연 해 지므로 (예 : 하위 유형 및 수퍼 유형 사용 허용) 다음과 같이 코드가보다 우수 합니다.

class Shoe {}
class IPhone {}
interface Fruit {}
class Apple implements Fruit {}
class Banana implements Fruit {}
class GrannySmith extends Apple {}

   public class FruitHelper {

        public void eatAll(Collection<? extends Fruit> fruits) {}

        public void addApple(Collection<? super Apple> apples) {}
}

컴파일러는 이제 나쁜 사용법을 감지 할 수 있습니다 :

 public class GenericsTest {
      public static void main(String[] args){
  FruitHelper fruitHelper = new FruitHelper() ;
    List<Fruit> fruits = new ArrayList<Fruit>();
    fruits.add(new Apple()); // Allowed, as Apple is a Fruit
    fruits.add(new Banana()); // Allowed, as Banana is a Fruit
    fruitHelper.addApple(fruits); // Allowed, as "Fruit super Apple"
    fruitHelper.eatAll(fruits); // Allowed

    Collection<Banana> bananas = new ArrayList<>();
    bananas.add(new Banana()); // Allowed
    //fruitHelper.addApple(bananas); // Compile error: may only contain Bananas!
    fruitHelper.eatAll(bananas); // Allowed, as all Bananas are Fruits

    Collection<Apple> apples = new ArrayList<>();
    fruitHelper.addApple(apples); // Allowed
    apples.add(new GrannySmith()); // Allowed, as this is an Apple
    fruitHelper.eatAll(apples); // Allowed, as all Apples are Fruits.
    
    Collection<GrannySmith> grannySmithApples = new ArrayList<>();
    fruitHelper.addApple(grannySmithApples); //Compile error: Not allowed.
                                   // GrannySmith is not a supertype of Apple
    apples.add(new GrannySmith()); //Still allowed, GrannySmith is an Apple
    fruitHelper.eatAll(grannySmithApples);//Still allowed, GrannySmith is a Fruit

    Collection<Object> objects = new ArrayList<>();
    fruitHelper.addApple(objects); // Allowed, as Object super Apple
    objects.add(new Shoe()); // Not a fruit
    objects.add(new IPhone()); // Not a fruit
    //fruitHelper.eatAll(objects); // Compile error: may contain a Shoe, too!
}

오른쪽 T 선택 ? super T 또는 ? extends T 는 부속 유형과 함게 사용하는 데 필요 합니다. 그러면 컴파일러는 형식 안전성을 보장 할 수 있습니다. 제대로 사용할 경우 유형을 캐스팅 할 필요가 없으며 형식 오류가 아니며 프로그래밍 오류가 발생할 수 있습니다.

이해가 쉽지 않은 경우 PECS 규칙을 기억하십시오.

P roducer는 " E xtends"를 사용하고 C onsumer는 " S uper"를 사용합니다.

(생산자는 쓰기 권한 만 가지고 있으며 소비자는 읽기 권한 만 가지고 있습니다)

일반 클래스 및 인터페이스의 이점

제네릭을 사용하는 코드는 비 제너릭 코드보다 많은 이점이 있습니다. 다음은 주요 이점입니다.


컴파일 타임에보다 강력한 유형 검사

Java 컴파일러는 일반 코드에 강력한 유형 검사를 적용하고 코드가 유형 안전을 위반하는 경우 오류를 발행합니다. 컴파일 오류를 수정하는 것은 런타임 오류를 수정하는 것보다 쉽습니다. 이는 찾기 어려울 수 있습니다.


캐스트 제거

제네릭이없는 다음 코드 스 니펫에는 형 변환이 필요합니다.

List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);

제네릭사용하도록 다시 작성하면 코드에 캐스팅이 필요하지 않습니다.

List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0);   // no cast

프로그래머가 일반 알고리즘을 구현할 수 있도록 설정

제네릭을 사용함으로써 프로그래머는 다양한 유형의 콜렉션에서 작동하고, 사용자 정의가 가능하며, 유형 안전하고 읽기 쉽도록 일반 알고리즘을 구현할 수 있습니다.

하나 이상의 유형에 제네릭 매개 변수 바인딩

일반 매개 변수는 T extends Type1 & Type2 & ... 구문을 사용하여 둘 이상의 유형에 바인딩 할 수도 있습니다.

FlushableCloseable 모두 구현해야하는 Generic 유형의 클래스를 만들고 싶다고 가정 해 보겠습니다.

class ExampleClass<T extends Flushable & Closeable> {
}

이제 ExampleClassFlushable Closeable 모두 구현하는 일반 매개 변수로만 받아들입니다.

ExampleClass<BufferedWriter> arg1; // Works because BufferedWriter implements both Flushable and Closeable

ExampleClass<Console> arg4; // Does NOT work because Console only implements Flushable
ExampleClass<ZipFile> arg5; // Does NOT work because ZipFile only implements Closeable

ExampleClass<Flushable> arg2; // Does NOT work because Closeable bound is not satisfied.
ExampleClass<Closeable> arg3; // Does NOT work because Flushable bound is not satisfied.

클래스 메소드는 제네릭 형식 인수를 Closeable 또는 Flushable 로 유추하도록 선택할 수 있습니다.

class ExampleClass<T extends Flushable & Closeable> {
    /* Assign it to a valid type as you want. */
    public void test (T param) {
        Flushable arg1 = param; // Works
        Closeable arg2 = param; // Works too.
    }

    /* You can even invoke the methods of any valid type directly. */
    public void test2 (T param) {
        param.flush(); // Method of Flushable called on T and works fine.
        param.close(); // Method of Closeable called on T and works fine too.
    }
}

노트 :

OR ( | ) 절을 사용하여 일반 매개 변수를 유형 중 하나에 바인딩 할 수 없습니다. AND ( & ) 절만 지원됩니다. 일반 유형은 하나의 클래스와 많은 인터페이스 만 확장 할 수 있습니다. 수업은 목록의 시작 부분에 위치해야합니다.

제네릭 형식 인스턴스화

유형 삭제로 인해 다음은 작동하지 않습니다.

public <T> void genericMethod() {
    T t = new T(); // Can not instantiate the type T.
}

유형 T 가 지워집니다. 런타임에 JVM은 원래 T 가 무엇인지 알지 못하기 때문에 호출 할 생성자를 알지 못합니다.


해결 방법

  1. genericMethod 호출 할 때 T 의 클래스를 전달합니다.

    public <T> void genericMethod(Class<T> cls) {
        try {
            T t = cls.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
             System.err.println("Could not instantiate: " + cls.getName());
        }
    }
    
    genericMethod(String.class);
    

    전달 된 클래스에 액세스 가능한 기본 생성자가 있는지 여부를 알 수 없으므로 예외를 throw합니다.

Java SE 8
  1. T 의 생성자에 대한 참조 전달 :

    public <T> void genericMethod(Supplier<T> cons) {
        T t = cons.get();
    }
    
    genericMethod(String::new);
    

자체 선언 내에서 선언 된 제네릭 형식을 참조하십시오.

어떻게 선언 된 제네릭 형식 자체의 메서드 선언 내에서 상속 된 제네릭 형식의 인스턴스를 사용할 것인가? 이것은 제네릭에 좀 더 깊게 파고 들지만 여전히 흔히 볼 수있는 문제 중 하나입니다.

우리가 가지고 있다고 가정 DataSeries<T> 타입 값이 포함 된 일반 데이터 계열 정의 유형 (여기 인터페이스) T . 예를 들어 double 값으로 많은 연산을 수행하고자 할 때이 타입을 직접 사용하는 것은 DoubleSeries extends DataSeries<Double> 때문에 DoubleSeries extends DataSeries<Double> 정의합니다. 이제 원래의 DataSeries<T> 유형에는 동일한 길이의 다른 시리즈를 추가하고 새 시리즈를 반환하는 add(values) 메소드가 있다고 가정합니다. 파생 클래스에서 DataSeries<Double> 가 아닌 DoubleSeries 가 될 values 의 유형과 반환 유형을 어떻게 적용합니까?

이 문제는 선언 된 유형을 다시 참조하고 유형을 확장하는 제네릭 형식 매개 변수를 추가하여 해결할 수 있습니다 (여기에는 인터페이스에 적용되지만 클래스에 대해서도 동일 함).

public interface DataSeries<T, DS extends DataSeries<T, DS>> {
    DS add(DS values);
    List<T> data();
}

여기에서 T 는 계열이 보유하는 데이터 유형을 나타냅니다 (예 : DoubleDS 는 계열 자체). 위에서 언급 한 매개 변수를 상응하는 파생 된 형식으로 대체하여 상속 된 형식을 쉽게 구현할 수 있으므로 형식에 대한 구체적인 Double 기반 정의가 생성됩니다.

public interface DoubleSeries extends DataSeries<Double, DoubleSeries> {
    static DoubleSeries instance(Collection<Double> data) {
        return new DoubleSeriesImpl(data);
    }
}

이 순간에도 IDE는 올바른 유형의 올바른 인터페이스를 구현할 것입니다. 약간의 내용을 채운 후에는 다음과 같이 보일 수 있습니다.

class DoubleSeriesImpl implements DoubleSeries {
    private final List<Double> data;

    DoubleSeriesImpl(Collection<Double> data) {
        this.data = new ArrayList<>(data);
    }

    @Override
    public DoubleSeries add(DoubleSeries values) {
        List<Double> incoming = values != null ? values.data() : null;
        if (incoming == null || incoming.size() != data.size()) {
            throw new IllegalArgumentException("bad series");
        }
        List<Double> newdata = new ArrayList<>(data.size());
        for (int i = 0; i < data.size(); i++) {
            newdata.add(this.data.get(i) + incoming.get(i)); // beware autoboxing
        }
        return DoubleSeries.instance(newdata);
    }

    @Override
    public List<Double> data() {
        return Collections.unmodifiableList(data);
    }
}

보시다시피 add 메소드는 DoubleSeries add(DoubleSeries values) 로 선언되며 컴파일러는 만족 DoubleSeries add(DoubleSeries values) .

필요한 경우 패턴을 더 중첩 할 수 있습니다.

제네릭과 함께 instanceof 사용

제네릭을 사용하여 instanceof에 유형 정의

형식 매개 변수 <T> 선언 된 다음 제네릭 클래스 Example 생각해보십시오.

class Example<T> {
    public boolean isTypeAString(String s) {
        return s instanceof T; // Compilation error, cannot use T as class type here
    }
}

컴파일러가 자바 소스Java 바이트 코드 로 컴파일하자마자, 모든 제네릭 코드를 비 제너릭 코드로 변환하는 타입 소거 (erasure) 라는 프로세스를 적용하여 런타임에 T 타입을 구별하는 것을 불가능하게하기 때문에 항상 컴파일 오류가 발생합니다. instanceof 와 함께 사용되는 형식은 다시 정의 할 수 있어야합니다 . 즉, 형식에 대한 모든 정보를 런타임에 사용할 수 있어야하며 일반적으로 제네릭 형식에 해당하지 않습니다.

다음 클래스는 제네릭이 형식 지우기에 의해 제거 된 것처럼 보이는 Example , Example<String>Example<Number> 의 두 가지 클래스를 나타냅니다.

class Example { // formal parameter is gone
    public boolean isTypeAString(String s) {
        return s instanceof Object; // Both <String> and <Number> are now Object
    }
}

유형이 없어 졌으므로 JVM이 어떤 유형이 T 인지를 알 수 없습니다.


이전 규칙에 대한 예외

다음과 같이 무제한 와일드 카드 (?)를 사용하여 instanceof 의 유형을 지정할 수 있습니다.

    public boolean isAList(Object obj) {
        return obj instanceof List<?>;
    }

이것은 인스턴스 objList 인지 여부를 평가하는 데 유용 할 수 있습니다.

System.out.println(isAList("foo")); // prints false
System.out.println(isAList(new ArrayList<String>()); // prints true
System.out.println(isAList(new ArrayList<Float>()); // prints true

사실 무한 와일드 카드는 검증 가능한 유형으로 간주됩니다.


instanceof와 함께 일반 인스턴스 사용

다른 일면은 인스턴스의 사용이다 tT 가진 instanceof 다음 예와 같이 법적 :

class Example<T> {
    public boolean isTypeAString(T t) {
        return t instanceof String; // No compilation error this time
    }
}

형식 지우기 후에 클래스는 다음과 같이 나타납니다.

class Example { // formal parameter is gone
    public boolean isTypeAString(Object t) {
        return t instanceof String; // No compilation error this time
    }
}

어쨌든 형식 지우기가 발생하더라도 JVM은 다음 스 니펫과 같이 동일한 참조 유형 ( Object )을 사용하더라도 메모리의 여러 유형을 구별 할 수 있습니다.

Object obj1 = new String("foo"); // reference type Object, object type String
Object obj2 = new Integer(11); // reference type Object, object type Integer
System.out.println(obj1 instanceof String); // true
System.out.println(obj2 instanceof String); // false, it's an Integer, not a String

일반 인터페이스 구현을위한 다양한 방법 (또는 일반 클래스 확장)

다음 일반 인터페이스가 선언되었다고 가정 해보십시오.

public interface MyGenericInterface<T> {
    public void foo(T t);
}

다음은이를 구현할 수있는 방법입니다.


특정 유형의 비 제너릭 클래스 구현

다음 예제와 같이 MyGenericClass 의 형식 유형 매개 변수 <T> 를 대체 할 특정 유형을 선택하고 구현하십시오.

public class NonGenericClass implements MyGenericInterface<String> {
    public void foo(String t) { } // type T has been replaced by String
}

이 클래스는 String 을 처리하기 MyGenericInterface 즉, Integer , Object 등 다른 매개 변수가있는 MyGenericInterface 를 사용하면 컴파일되지 않습니다. 다음 스 니펫에 나와 있습니다.

NonGenericClass myClass = new NonGenericClass();
myClass.foo("foo_string"); // OK, legal
myClass.foo(11); // NOT OK, does not compile
myClass.foo(new Object()); // NOT OK, does not compile

일반 클래스 구현

다음과 같이 MyGenericInterface 를 구현하는 형식 유형 매개 변수 <T> 를 사용하여 다른 일반 인터페이스를 선언합니다.

public class MyGenericSubclass<T> implements MyGenericInterface<T> {
    public void foo(T t) { } // type T is still the same
    // other methods...
}

다음과 같이 다른 공식 형식 매개 변수가 사용될 수 있습니다.

public class MyGenericSubclass<U> implements MyGenericInterface<U> { // equivalent to the previous declaration
    public void foo(U t) { }
    // other methods...
}

원시 타입 클래스 구현

다음과 같이 MyGenericInteface원시 형식 으로 구현하는 비 제너릭 클래스를 선언합니다 (제네릭을 전혀 사용하지 않음).

public class MyGenericSubclass implements MyGenericInterface {
    public void foo(Object t) { } // type T has been replaced by Object
    // other possible methods
}

이 방법은 (하위 클래스의) 원시 유형 과 (인터페이스의) generics를 혼합하여 혼동하기 때문에 런타임시 100 % 안전 하지 않으므로이 방법을 사용 하지 않는 것이 좋습니다. 최신 Java 컴파일러는 이런 종류의 구현으로 경고를 발생 시키지만 그럼에도 불구하고 이전 JVM (1.4 이전 버전)과의 호환성을 위해 코드가 컴파일됩니다.


위에 나열된 모든 방법은 제네릭 인터페이스 대신 제네릭 클래스를 슈퍼 유형으로 사용할 때도 허용됩니다.

제네릭을 사용하여 자동 캐스팅

generics를 사용하면 호출자가 예상하는 모든 것을 반환 할 수 있습니다.

private Map<String, Object> data;
public <T> T get(String key) {
    return (T) data.get(key);
}

이 메서드는 경고와 함께 컴파일됩니다. Java 런타임은이를 사용할 때 캐스트를 수행하기 때문에 실제로는 코드가 실제로 보이는 것보다 안전합니다.

Bar bar = foo.get("bar");

제네릭 형식을 사용하면 안전성이 떨어집니다.

List<Bar> bars = foo.get("bars");

리턴 된 형태가 List 어떤 종류 (즉, List<String> 돌려 주면 (자) ClassCastException 방아쇠되지 않는 경우), 캐스트가 동작 ClassCastException .리스트로부터 요소를 꺼낼 때 최종적으로 취득합니다.

이 문제를 해결하려면 입력 된 키를 사용하는 API를 만들 수 있습니다.

public final static Key<List<Bar>> BARS = new Key<>("BARS");

put() 메소드와 함께 :

public <T> T put(Key<T> key, T value);

이 방법을 사용하면 잘못된 유형을 맵에 넣을 수 없으므로 같은 이름이지만 다른 유형의 두 개의 키를 실수로 생성하지 않는 한 결과는 항상 정확합니다.

관련 항목 :

런타임시 일반 매개 변수를 만족하는 클래스 가져 오기

정적 메서드에서 사용되는 것과 같은 많은 언 바운드 제네릭 매개 변수는 런타임에 복구 할 수 없습니다 ( 삭제다른 스레드 참조). 그러나 런타임시 클래스의 일반 매개 변수를 충족하는 유형에 액세스하는 데 사용되는 공통 전략이 있습니다. 이렇게하면 모든 호출을 통해 유형 정보를 스레드 필요 없이 유형에 대한 액세스에 의존하는 일반 코드가 허용됩니다.

배경

클래스의 일반 매개 변수화는 익명의 내부 클래스를 작성하여 검사 할 수 있습니다. 이 클래스는 형식 정보를 캡처합니다. 일반적으로이 메커니즘은 수퍼 타입 ​​토큰 이라고하며 Neal Gafter의 블로그 게시물에 자세히 나와 있습니다.

구현

Java의 세 가지 일반적인 구현은 다음과 같습니다.

사용 예

public class DataService<MODEL_TYPE> {
     private final DataDao dataDao = new DataDao();
     private final Class<MODEL_TYPE> type = (Class<MODEL_TYPE>) new TypeToken<MODEL_TYPE>
                                                                (getClass()){}.getRawType();
     public List<MODEL_TYPE> getAll() {
         return dataDao.getAllOfType(type);
    }
}

// the subclass definitively binds the parameterization to User
// for all instances of this class, so that information can be 
// recovered at runtime
public class UserService extends DataService<User> {}

public class Main {
    public static void main(String[] args) {
          UserService service = new UserService();
          List<User> users = service.getAll();
    }
}


Modified text is an extract of the original Stack Overflow Documentation
아래 라이선스 CC BY-SA 3.0
와 제휴하지 않음 Stack Overflow