Post

[JAVA] Generic

[JAVA] Generic

Generic

Generic 을 사용하는 이유

  • 코드 재사용성 증가: 동일한 로직을 다양한 타입에 대해 반복해서 작성하지 않아도 됨

    List<String> List<Integer> 등 동일한 List 로직 재사용

  • 타입 안정성 향상: 컴파일 타임에 타입 체크 가능

    → 잘못된 타입 사용 시 컴파일 에러 발생 → 런타임 오류 방지

  • 불필요한 캐스팅 제거: 형 변환이 필요없어 가독성 ⬆️, 오류 가능성 ⬇️

    String s = (String) list.get(0); 대신에 String s = list.get(0);

  • 유연한 API 설계 가능: 사용할 타입을 외부에서 받아오기 때문에 다양한 상황에 대응 가능
  • 한번에 여러 타입 매개변수를 선언 가능

    Map<K,V>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class StringBox {
    //문자를 보관하고 꺼내는 단순한 기능
    private String value;

    public void set(String value) {
        this.value = value;
    }

    public String get() {
        return value;
    }
}

public class IntegerBox {
    //숫자를 보관하고 꺼내는 단순한 기능
    private Integer value;

    public void set(Integer value) {
        this.value = value;
    }

    public Integer get() {
        return value;
    }
}

public static void main(String[] args) {
    IntegerBox integerBox = new IntegerBox();
    integerBox.set(111); //오토 박싱으로 int -> Integer 자동변환
    Integer integer = integerBox.get();
    System.out.println("integer = " + integer);

    StringBox stringBox = new StringBox();
    stringBox.set("hello");
    String string = stringBox.get();
    System.out.println("string = " + string);

    //double, boolean 을 포함한 다양한 타입을 담는 박스가 필요하다면 각각의 타입별로 클래스를 새로 만들어야 한다
    //담는 타입이 수십개라면 어떻게 이 문제를 해결할까?
}

다형성으로 중복 해결 시도

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ObjectBox {
    private Object value;

    public void set(Object object) {
        this.value = object;
    }

    public Object get() {
        return this.value;
    }
}

public static void main(String[] args) {
    ObjectBox integerBox = new ObjectBox();
    integerBox.set(10);
    Object object = (Integer) integerBox.get();  //다운 캐스팅
    System.out.println("integer = " + object);

    ObjectBox stringBox = new ObjectBox();
    stringBox.set("hello");
    String str = (String) stringBox.get(); //다운 캐스팅
    System.out.println("str = " + str);

    //잘못된 타입의 인수 전달
    integerBox.set("문자100");
    Integer result = (Integer) integerBox.get();    //String -> Integer 캐스팅 예외
    System.out.println("result = " + result);
}

문제점

  • 반환 타입이 맞지 않음

    Integer = Object 자식은 부모를 담을 수 없다, 즉 성립하지 않는다. 다운캐스팅 필요

  • 잘못된 타입의 인수 전달

    → 값을 꺼낼 때 문제 발생

  • 타입 안정성 문제 발생

제네릭 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class GenericBox<T> { //<T>를 선언하면 제네릭 클래스 T를 타입 매개변수라 함.
    private T value;

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

    public T get() {
        return value;
    }
}

public static void main(String[] args) {
    //GenericBox<T>는 원하는 타입(T)으로 만들 수 있음
    //생성 시점에 T의 타입 결정
    GenericBox<Integer> integerBox = new GenericBox<>();
    /*integerBox.set("1000"); //Integer 타입만 허용, 컴파일 오류*/
    integerBox.set(1000);
    Integer integer = integerBox.get();  //Integer 타입 반환 (캐스팅 X)
    System.out.println("integer = " + integer);

    GenericBox<String> stringBox = new GenericBox<>();
    stringBox.set("스트리 잉");
    /*stringBox.set(2000);    //String 타입만 허옹*/
    String string = stringBox.get();
    System.out.println("string = " + string);
    
    GenericBox<Double> doubleBox = new GenericBox<>();
    doubleBox.set(100.0); //Double 타입만 허옹*/
    System.out.println("doubleValue = " + doubleBox.get());

    //타입 직접 입력
    GenericBox<Integer> integerBox2 = new GenericBox<Integer>();
    //타입 추론: 생성하는 제네릭 타입 생략 가능
    GenericBox<Integer> integerBox3 = new GenericBox<>();
}

//출력 결과
integer = 1000
string = 스트리 
doubleValue = 100.0

제네릭 클래스는 생성시점에 **** 에 원하는 타입을 지정

1
new GenericBox<Integer>()

제네릭 클래스를 사용하면 GenericBox 객체를 생성하는 시점에 원하는 타입을 지정할 수 있다.

한번에 여러 타입 매개변수를 선언 가능

1
public class GenericBox<K, V> {}

GenericBox<Integer> 와 같은 코드는 실제로 만들어지는게 아니라 컴파일러에서 내가 입력한 타입 정보를 기반으로 이러한 코드가 있다 가정하고 컴파일 과정에 타입 정보를 반영해준다. 이 과정에서 타입이 맞지 않으면 컴파일 오류 발생

제네릭 타입 매개변수와 타입 인자

메서드의 매개변수는 사용할 값에 대한 결정을 나중으로 미루고, 제네릭 타입 매개변수는 사용할 타입에 대한 결정을 나중으로 미루는 것 메서드는 매개변수에 인자를 전달해 사용하는 값 결정


제네릭 클래스는 타입 매개변수에 타입 인자를 전달해 사용할 타입 결정


제네릭 타입(Generic Type) → GenericBox<T> 타입 매개변수(type parameter) → GenericBox<T> 의 T 타입 인자(type argument) → GenericBox<Integer> 의 Integer


타입 인자로 기본형은 사용할 수 없다. 래퍼 클래스(Integer, Double)를 사용해야 한다.

제네릭 메서드

제네릭 타입과 제네릭 메서드는 제네릭을 사용하지만 서로 다른 기능 제공.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class GenericMethod {

    public static Object objectMethod(Object obj) {
        System.out.println("objectMethod " + obj);
        return obj;
    }

    //재네릭 메서드를 정의할 땐 메서드의 반환 타입 왼쪽 <>를 사용해 <T>와 같이 타입 매개변수를 적어줌
    public static <T> T geneticMethod(T t) {
        System.out.println("geneticMethod " + t);
        return t;
    }

    /**
     * 제네릭 메서드도 타입 매개변수 제한 가능 Number와 그 자식만 받을 수 있음!
     */
    public static <T extends Number> T numberMethod(T t) {
        System.out.println("numberMethod " + t);
        return t;
    }
}

/**
 *  제네릭 메서드는 인스턴스 메서드와 스테틱 메서드에 모두 적용할 수 있당
 */
class StBox<T> {    //제네릭 타입
    static <V> V staticMethod(V v) {return v;}  //static 메서드에 제네릭 메서드 도입
    <Z> Z instanceMethod(Z z) {return z;}   //인스턴스 메서드에 제네릭 메서드 도입 가능
}

class Box<T> {
    /**
     * 제네릭 타입은 static 메서드에 타입 매개변수를 사용할 수 없다.
     * 제네릭 타입은 객체를 생성하는 시점에 타입이 정해지지만
     * static 메서드는 인스턴스 단위가 아니라 클래스 단위로 작동하기 때문에 제네릭과 무관
     */
    T instanceMethod(T t) {return t;}   //인스턴스 메서드에선 가능
    //static T staticMethod(T t) {return t;} 재네릭 타입의 T 사용 불가
}

  • 클래스 전체가 아닌, 메서드 단위로 제네릭을 도입할 때 사용
  • 메서드를 호출하는 시점에 <Object> 와 같이 타입을 정하고 호출
  • 인스턴스 메서드와 static 메서드 모두 적용 가능
  • 제네릭 타입보다 제네릭 메서드가 높은 우선순위를 가짐

메서드를 호출하는 시점에 타입 인자를 전달해 타입을 지정한다. 즉 타입을 지정하면서 메서드를 호출

제네릭 메서드의 타입 매개변수 제한

1
2
3
4
5
6
7
/**
 * 제니릭 메서드도 타입 매개변수 제한 가능 Number와 그 자식만 받을 수 있음!
 */
public static <T extends Number> T numberMethod(T t) {
    System.out.println("numberMethod " + t);
    return t;
}

제네릭 메서드 실행 순서

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//1. 전달
WhildcardEx.printGenericV1(dogBox);

//제네릭 메서드
//2. Box<Dog> dogBox를 전달. 타입 추론에 의해 타입 T가 Dog가 된다.
static <T> void printGenericV1(Box<T> box) {
    //Box<T> box 제네릭 타입을 받아 출력
    System.out.println("T = " +box.get());
}

//3. 타입 인자 결정
static <Dog> void printGenericV1(Box<Dog> box) {
    System.out.println("T = " +box.get());
}

//4. 최종 실행 메서드
static void printGenericV1(Box<Dog> box) {
	System.out.println("T = " +box.get());
}

와일드 카드

프로그래밍에서 * , ? 과 같이 하나 이상의 문자들을 상징하는 특수 문자를 뜻한다. 즉 여러 타입이 들어올 수 있음.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class WhildcardEx {

    //제네릭 메서드
    //Box<Dog> dogBox를 전달. 타입 추론에 의해 타입 T가 Dog가 된다.
    static <T> void printGenericV1(Box<T> box) {
        //Box<T> box 제네릭 타입을 받아 출력
        System.out.println("T = " +box.get());
    }

    //제네릭 메서드 X 일반적인 메서드
    //Box<Dog> dogBox를 전달. 와일드 카드 ?는 모든 타입을 받을 수 있음.
    //?만 사용한 다 받는 와일드카드를 비제한 와일드카드라 함.
    static void printWildcardV1(Box<?> box) {
        // ? = 아무거나 ex) Box<Dog> Box<Cat> Box<Object>
        System.out.println("? = " +box.get());
    }

    static <T extends Animal> void printGenericV2(Box<T> box) {
        //T에 상한을 Animal로 해서 Animal의 기능 사용 가능
        T t = box.get();
        System.out.println("이름 = " +t.getName());
    }

    static void printWildcardV2(Box<? extends Animal> box) {
        Animal animal = box.get();
        System.out.println("이름 = " + animal.getName());
    }

    static <T extends Animal> T printAndReturnGeneric(Box<T> box) {
        //T에 상한을 Animal로 해서 Animal의 기능 사용 가능
        T t = box.get();
        System.out.println("이름 = " +t.getName());
        return t;   //반환까지
    }
  • 와일드 카드는 ? 를 사용해 정의
  • 와일드 카드는 제네릭 타입이나, 제네릭 메서드를 선언하는 것이 아니다.
  • 와일드 카드는 제네릭 타입을 활용할 때 사용.

비제한 와일드카드

? 만 사용해서 제한 없이 모든 타입을 다 받는 와일드카드

상한 와일드 카드

1
2
3
4
5
6
7
//와일드카드에도 상한을 걸 수 있다.
static Animal printAndReturnWildcard(Box<? extends Animal> box) {
    Animal animal = box.get();
    System.out.println("이름 = " +animal.getName());
    return animal;   //반환까지
}
//와일드 카드는 이미 만들어진 지네릭 타입을 가져다 활용할 때 사용
  • 제네릭 메서드와 같이 와일드카드에도 상한 제한 가능
  • ? extends Animal 사용 즉 Animal 과 그 하위 타입만 입력 받는다.
  • box.get(); 을 통해 꺼낼 수 있는 타입의 최대부모는 Animal 따라서 Animal 타입으로 조회 가능
  • Animal 타입의 기능을 호출할 수 있다.

하안 와일드 카드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
    Box<Object> objectBox = new Box<>();
    Box<Animal> animalBox = new Box<>();
    Box<Dog> dogBox = new Box<>();
    Box<Cat> catBox = new Box<>();

    // Animal 포함 상위 타입 전달가능
    writeBox(objectBox); //가능
    writeBox(animalBox); //가능
    //writeBox(dogBox); //하한이 Animal
    //writeBox(catBox); //하한이 Animal

    Animal animal = animalBox.get();
    System.out.println("animal = " + animal );
}

static void writeBox(Box<? super Animal> box) { //Animal 이상만 가능
    box.set(new Dog("멍멍이",100));
}

와일드 카드 실행 순서

1
2
3
4
5
6
7
8
9
10
11
//1. 전달
WhildcardEx.printWildcardV1(dogBox);

//2. 최종 실행 메서드
//제니릭 메서드 X 일반적인 메서드
//Box<Dog> dogBox를 전달. 와일드 카드 ?는 모든 타입을 받을 수 있음.
//?만 사용한 다 받는 와일드카드를 비제한 와일드카드라 함.
static void printWildcardV1(Box<?> box) {
    // ? = 아무거나 ex) Box<Dog> Box<Cat> Box<Object>
    System.out.println("? = " +box.get());
}

타입 매개변수가 필요한 경우

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static <T extends Animal> T printAndReturnGeneric(Box<T> box) {
    //T에 상한을 Animal로 해서 Animal의 기능 사용 가능
    T t = box.get();
    System.out.println("이름 = " +t.getName());
    return t;   //반환까지
}

//와일드카드에도 상한을 걸 수 있다.
static Animal printAndReturnWildcard(Box<? extends Animal> box) {
    Animal animal = box.get();
    System.out.println("이름 = " +animal.getName());
    return animal;   //반환까지
}

//전달한 타입을 반환 가능
Dog dog = WildcardEx.printAndReturnGeneric(dogBox)

//전달한 타입 반환 불가 Animal 타입으로 반환
Animal animal = WildcardEx.printAndReturnWildcard(dogBox)
  • 메서드의 타입을 특정 시점에 변경하려면 제네릭 타입이나, 제네릭 메서드 사용
  • 와일드카드는 이미 만들어진 제네릭 타입을 받아 활용할때 사용
  • 메서드의 타입들을 타입인자를 통해 변경 불가 (일반적인 메서드라 생각)

제네릭 메서드가 필요한 상황이면 <T> 를 사용 그렇지 않으면 와일드 카드 사용

제네릭 메서드 vs 와일드카드

타입 매개변수가 존재하는 제네릭 메서드는 특정 시점에 타입 매개변수에 타입 인자를 전달 해 타입을 결정해야 한다. 이런 과정은 매우 복잡하다. 반면 와일드 카드를 사용하는 printWildcardV1 메서드는 일반적인 메서드에 사용가능하고 매개변수로 제네릭 타입을 받을 수 있다는 것 뿐 제네릭 메서드보다 덜 복잡하게 동작하지 않는다.

단순 일반 메서드에 제네릭 타입을 받을 수 있는 매개변수가 있을 뿐

제네릭 타입이나 제네릭 메서드가 꼭 필요한 상황이 아니라면 와일드 카드를 사용하자!

타입 이레이저

  • 제네릭은 자바 컴파일에서만 사용하고 컴파일 이후에는 삭제. 즉 제네릭 타입 매개변수가 사라짐
  • 컴파일 전 .Java 에는 제네릭의 타입 매개변수가 존재하지만 컴파일 이후 자바 바이트코드 .class 에는 타입 매개변수가 존재하지 않음.
  • 따라서 런타임에 타입을 활용하는 코드는 작성 불가



출처: 김영한의 실전 자바 - 중급 2편

This post is licensed under CC BY 4.0 by the author.