Java - Generics

Dasole Kwon·2023년 1월 18일
0

지네릭스, 열거형, 어노테이션

Generics란?

: 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크(compile-time type check)를 해주는 기능

  • 타입 안정성을 제공한다.
    • 의도하지 않은 타입의 객체가 저장되는 것을 막아 잘못 형변환 되는 오류를 줄여준다.
  • 형변환의 번거로움을 줄여준다.(타입체크와 형변환 생략 -> 코드간결)
    • ex) ArrayList클래스: 지네릭스 도입 이전에는 각 객체의 형을 체크해야 했음
/* Box<T>클래스 선언 */

// 지네릭 타입 T를 선언
class Box<T> { // class Box {
  // Object item;
  T item;

  void setItem(T item) { // void setItem(Object item) {
    this.item = item;
  }

  T getItem() { // Object getItem() {
    return item;
  }
}

/* Box<T>클래스 사용 */ 

Box<String> b = new Box<String>(); // T 대신 실제 타입 지정

b.setItem(new Object()); // ERROR: String타입 이외의 타입은 지정불가
b.setItem("ABC"); // OK: String타입만 가능

// String item = (String)b.getItem(); 
String item = b.getItem(); // 형변환 필요없음

/* 호환성 */

Box b2 = new Box(); // OK: T는 Object로 간주된다.
b2.setItem("ABC"); // WARN: unchecked or unsafe operation.
b2.setItem(new Object()); // WARN: unchecked or unsafe operation.

T: 타입 변수(type variable)

  • T가 아닌 다른것도 사용 가능하다. (ex.ArrayList< E>, Map<K,V>)
  • 상황에 맞게 의미 있는 문자를 선택해서 사용 하는 것이 좋다.
  • 기호만 다를 뿐 '임의의 참조형 타입'을 의미한다.

Box< String> 클래스

  • Box< T>클래스: 어떤 타입이든 한가지 타입을 정해서 넣을 수 있다.(T:type, E:element, K:key, V:value, ...)
  • Box< String>클래스: String 타입만 담을 수 있다.
class Box<String> { // 지네릭 타입을 String으로 지정
  String item;
  void setItem(String item) {
    this.item = item;
  }
  String getItem() {
    return item;
  }
}

지네릭스의 용어

  • Box< T>: 지네릭 클래스, 'T의 Box'또는 'T Box'라고 읽는다.
  • T: 타입 변수 또는 타입매개변수, T는 타입문자
  • Box: 원시타입(raw type)
  • Box< String>과 Box< Integer>는 지네릭 클래스 Box< T>에서 서로 다른 타입을 대입하여 호출한 것일 뿐 동일한 클래스이며, 컴파일 후에 모두 원시타입(Box)로 바뀌어 지네릭 타입이 제거된다.

지네릭스의 제한

  • 모든 객체에 동일하게 동작해야 하는 static멤버에는 타입변수 T를 사용할 수 없다.
    • T는 인스턴스변수로 간주되기 때문이다. static에는 인스턴스변수를 참조할 수 없다.
  • 지네릭 타입의 배열을 (선언은 가능하지만) 생성 하는 것은 불가능하다.
    • new연산자, instanceof연산자는 컴파일 시점에 타입T가 뭔지 정확하게 알아야 하기 때문에 T를 피연산자로 사용할 수 없다.
/* 지네릭스는 인스턴스별로 다르게 동작하려고 만든 기능 */
Box<Apple> appleBox = new Box<Apple>(); // OK: Apple객체만 저장가능
Box<Grape> grapeBox = new Box<Grape>(); // OK: Grape객체만 저장가능

/* 타입변수는 static에는 사용불가 */
class Box<T> {
  static T item; // ERROR
  static int compare(T t1, T t2) { ... } // ERROR
}

/* 지네릭타입배열 생성불가 */
class Box<T> {
  T[] itemArr; // OK: T타입의 배열을 위한 참조변수
  ...
  T[] toArray() {
    T[] tmpArr = new T[itemArr.length]; // ERROR: 지네릭 배열 생성 불가
    ...
    return tmpArr;
  }
}

참고: 지네릭 배열을 꼭 생성해야 할 경우 방법
1. new 연산자 대신 'Reflection API'의 newInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열 생성
2. Object배열을 생성해서 복사한 다음에 T[]로 형변환

지네릭 클래스의 객체 생성과 사용

Box< T> 지네릭 클래스 정의

class Box<T> {
  ArrayList<T> list = new ArrayList<T>();

  void add(T item) { list.add(item); }
  T get(int i) { return list.get(i); }
  ArrayList<T> getList() { return list; }
  int size() { return list.size(); }
  public String toString() { return list.toString(); }
}

Box< T> 객체 생성

Box<Apple> appleBox = new Box<Apple>(); // OK
// ERROR: 참조변수와 생성자에 대입된 타입이 일치해야 한다.
Box<Apple> appleBox = new Box<Grape>(); 

// 타입 상속관계: Apple이 Fruit의 자손일 경우
// ERROR: 상속관계여도 일치된 타입이여야 한다.
Box<Fruit> appleBox = new Box<Apple>(); 

// 지네릭 클래스 상속관계: FruitBox<T>가 Box<T>의 자손인 경우
Box<Apple> appleBox = new FruitBox<Apple>(); // OK: 다형성

// 추정가능한 타입 생략 가능 (JDK1.7부터)
Box<Apple> appleBox = new Box<Apple>(); // OK
Box<Apple> appleBox = new Box<>(); // JDK1.7부터 OK

Box< T>.add() 사용

// 생성시 대입된 타입과 다른 타입의 객체 추가 불가
Box<Apple> appleBox = new Box<Apple>();
appleBox.add(new Apple()); // OK
appleBox.add(new Grape()); // ERROR

// 타입 상속관계: Apple이 Fruit의 자손
Box<Fruit> fruitBox = new Box<Fruit>();
fruitBox.add(new Fruit()); // OK
fruitBox.add(new Apple()); // ERROR: Fruit만 가능

제한된 지네릭 클래스

FruitBox<Toy> fruitBox = new FruitBox<Toy>();
fruitBox.add(new Toy()); // OK

// Fruit의 자손만 타입으로 지정
class FruitBox<T extends Fruit> {
    ArrayList<T> list = new ArrayList<T>();
    ...
}

class Apple extends Fruit { ... }
class Grape extends Fruit { ... }
class Toy { ... }

FruitBox<Apple> appleBox = new FruitBox<Apple>(); // OK: Apple은 Fruit의 자손
FruitBox<Toy> toyBox = new FruitBox<Toy>(); // ERROR: Toy는 Fruit의 자손이 아님

// 다형성: 매개변수의 타입 T도 Fruit과 그 자손타입이 가능해짐
// (따라서, T에 Object를 대입하면 모든 종류의 객체를 저장할 수 있게 됨)
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
fruitBox.add(new Apple()); // OK: Apple이 Fruit의 자손
fruitBox.add(new Grape()); // OK: Grape가 Fruit의 자손

// 특정 인터페이스를 구현해야 한다는 제약 (인터페이스도 implements가 아니라 extends)
interface Edible { }
class FruitBox<T extends Edible> { ... }

// Fruit의 자손이면서 Eatable 인터페이스도 구현해야 한다는 제약
class FruitBox<T extends Fruit & Edible> { ... }

와일드 카드
static 메서드에는 타입 매개변수 T를 매개변수에 사용할 수 없다.

  • 지네릭스를 사용하지 않거나, 특정타입을 지정해서 써야 한다.
    • 하지만, 특정 타입을 지정하면 여러가지 타입을 받을 수가 없다.
class Juicer {
  static Juice makeJuice(FruitBox<Fruit> box) {
    String tmp = "";
    for(Fruit f: box.getList()) tmp += f + " ";
    return new Juice(tmp);
  }
}

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();

Juicer.makeJuice(fruitBox); // OK
Juicer.makeJuice(appleBox); // ERROR: FruitBox<Apple> -> FruitBox<Fruit> 형변환 안됨
  • 특정타입을 고정하면 여러가지 타입의 매개변수를 갖도록 오버로딩 해야 한다.
    • 하지만, 지네릭 타입이 다른것만으로는 오버로딩이 성립되지 않는다.
    • 이때 사용하기 위해 고안된 것이 '와일드카드'
class Juicer {
  static Juice makeJuice(FruitBox<Fruit> box) {
    String tmp = "";
    for(Fruit f: box.getList()) tmp += f + " ";
    return new Juice(tmp);
  }

  static Juice makeJuice(FruitBox<Apple> box) {
    String tmp = "";
    for(Fruit f: box.getList()) tmp += f + " ";
    return new Juice(tmp);
  }
}

와일드카드 기호 ?

  • 와일드카드는 어떠한 타입도 될 수 있다.
  • ?는 Object와 다름 없으므로 extends, super로 상한과 하한을 제한 할 수 있다.
    • : 와일드카드의 상한 제한(T와 그 자손들만 가능)
    • : 와일드카드의 하한 제한 (T와 그 조상들만 가능)
    • : 제한없음(모든 타입 가능), 와 동일
  • 제네릭 클래스와 달리 와일드카드에는 &를 사용 할 수 없다.
    • 즉, <? extends T & E> 와 같이 쓸 수 없음
class Juicer {
  static Juice makeJuice(FruitBox<? extends Fruit> box) {
    String tmp = "";
    for(Fruit f: box.getList()) tmp += f + " ";
    return new Juice(tmp);
  }
}

< ? extends Object>

  • 모든 타입이 가능
class Juicer {
  static Juice makeJuice(FruitBox<? extends Object> box) {
    String tmp = "";
    for(Fruit f: box.getList()) tmp += f + " ";
    return new Juice(tmp);
  }
}
  • makeJuice()의 매개변수로 모든 타입의 FruitBox가 올 수 있지만, for문에서는 Fruit만을 받기 때문에 Fruit타입만을 받을 수 있다.
  • 그러나, FruitBox는 지네릭 클래스로 Fruit을 제한하고 있기 때문에 컴파일에 문제가 생기지 않는다.
    • 컴파일러는 FruitBox의 요소가 모두 Fruit의 자손이라는 것을 알기 때문
class FruitBox<T extends Fruit> extends Box<T> { }

java.util.Optional 클래스

public final class Optional<T> {
  private static final Optional<?> EMPTY = new Optional<>();

  private final T value;
  ...
  public static <T> Optional<T> empty() {
    Optional<T> t = (Optional<T>) EMPTY;
    return t;
  }
}
  • < ?>: <? extends Object>를 줄여서 쓴 것

  • <>: Object를 줄여서 쓴것

  • Empty타입을 Optional로 선언한 이유
    - empty()에서 Optional로 형변환이 가능하기 때문
    - Optional -> Optional: 형변환 불가능
    - Optional -> Optional<?> -> Optional: 형변환 가능
    (경고)


    링크텍스트

0개의 댓글

관련 채용 정보