[Java 기초] 제네릭 - Generics

Updated:

이번 포스트에서는 자바의 제네릭에 대해서 알아보도록 하겠다. 👨‍🏫

📋 목차

  • 제네릭 사용법
  • 제네릭 주요 개념 $($바운디드 타입, 와일드 카드)
  • 제네릭 메서드 만들기
  • Erasure

제네릭 사용법

제네릭 : 컴파일 시에 메서드나 컬랙션 클래스타입 체크를 해주는 기능이다.

  • 객체의 타입을 컴파일 시에 체크
    • 타입 안정성 증가 - 의도치 않은 타입의 객체가 저장되는 것을 방지.
    • 형변환 생략 가능 - 저장된 객체를 꺼낼때 다른 타입으로 형변환되는 실수를 방지


  • 타입체크, 형변환 생략으로 코드가 간결해진다.
  • 제네릭 타입은 클래스와 메서드에 선언 가능.

간단히 요약하자면

제네릭이 객체의 타입을 미리 명시해주어서 번거로운 형변환을 줄여주는 역할을 한다.

제네릭 클래스 생성하기

클래스이름 옆에 <T>를 붙여서 제네릭 클래스를 생성한다.

< >안에는 어떤 문자든 올 수 있고 여러 문자가 ,를 통해 구분되어 올 수 있다.

class Thing<T> { // 제네릭 타입 T 선언
  T thing;

  void setThing(T thing) { this.thing = thing; }
  T getThing() { return thing; }
}

제네릭 클래스의 객체를 생성할 떄는 참조변수와 생성자에 타입 T 대신 사용될 실제 타입을 지정해 줘야 한다.

Thing<String> t = new Thing<String>();  // 타입 T 대신 실제 타입을 지정

위와 같이 String을 지정하면 T의 자리에 String 타입이 선언되게 된다.

제네릭 용어

Thing<T> : 제네릭 클래스.
T의 Thing이라고 읽는다.

T : 타입 매개변수

Thing : 원시 타입

Thing<String> : 제네릭 타입 호출.
타입 매개변수 T에 타입을 지정하는 것.

<String> : 매개변수화된 타입. 대입된 타입.
타입 매개변수에 지정된 타입.


바운디드 타입

Bounded의 의미는 제한된다는 의미이다.

타입 매개변수에 사용할 타입을 지정하면 한 종류의 타입만 저장할 수 있도록 제한한다는 것을 앞에서 보았다.

하지만 타입 매개변수에 사용할 타입을 지정하는것 또한 제한하는것이 필요하다.

class CookingBox<Toy> cookingBox = new CookingBox<Toy>();
cookingBox.add(new Toy()); // 음식 상자에 동물을 담을 수 있다... ?

제네릭 타입에 extends를 넣어서 특정타입의 자손들만 대입가능하게 제한해야한다.

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

CookingBox<Apple> appleBox = new CookingBox<Apple>();

CookingBox<Toy> ToyBox = new ToyBox<Toy>(); // Toy는 Food의 자손이 아니어서 에러!

인터페이스 구현제한 시에도 implements 대신 extends를 쓴다.

다중 바운디드

CookingBox에 Food의 자손이면서 Eatable을 구현한 클래스만 T에 대입되게 제한하기 위해서는 & 를 사용한다.

class CookingBox<T extends Food & Eatable> { ... }

예제

import java.util.ArrayList;

interface Eatable {}

class Example {
    public static void main(String[] args) {
        CookingBox<Food> cookingBox = new CookingBox<Food>();
        CookingBox<Apple> appleBox = new CookingBox<Apple>();
        CookingBox<Grape> grapeBox = new CookingBox<Grape>();
        // CookingBox<Toy> toyBox = new CookingBox<Toy>();

        cookingBox.add(new Food());
        cookingBox.add(new Apple());
        cookingBox.add(new Grape());
        appleBox.add(new Apple());

        System.out.println("cookingBox -" + cookingBox);
        System.out.println("appleBox -" + appleBox);
        System.out.println("grapeBox -" + grapeBox);
    }
}

class Food implements Eatable {
    public String toString() { return "Food"; }
}

class CookingBox<T extends Food & Eatable> extends Box<T> { }

class Apple extends Food { public String toString() { return "Apple"; }}
class Grape extends Food { public String toString() {return "Grape"; }}
class Toy { public String toString() {return "Toy"; }}

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

출력

cookingBox -[Food, Apple, Grape]
appleBox -[Apple]
grapeBox -[]

와일드 카드 - ?

와일드 카드 등장배경

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

때문에 static 메서드에 한해서는 타입 매개변수 대신, 특정 타입을 지정해서 사용해야 한다.

아래의 코드의 경우 다음과 같이 작성될 수 밖에 없다.

static Food cookFood(CookingBox<Egg> box){ ... }
static Food cookFood(CookingBox<Cabbage> box) { ... }

하지만 제네릭 타입이 다른 것만으로는 오버로딩이 성립되지 않는다.

때문에 위의 코드는 잘못되었으며 이런 상황 떄문에 와일드카드가 고안되었다.

<? extends T> : 와일드 카드의 상한 제한. T와 그 자손들만 가능.
<? super T> : 와일드 카드의 하한 제한. T와 그 자손들만 가능.
<?> : 제한없음. 모든 타입이 가능.

와일드 카드 상한 예제

import java.util.ArrayList;

class Ingredients { public String toString() { return "Ingredients"; }}
class Egg extends Ingredients { public String toString() { return "Egg"; }}
class Cabbage extends Ingredients { public String toString() { return "Cabbage"; }}

class Food {
    String name;

    Food(String name) { this.name = name; }
    public String toString() { return name; }
}

class Cooker {
    // cook(CookingBox<Egg>), cook(CookingBox<Cabbage>) 가 가능
    static Food cook(CookingBox<? extends Ingredients> box) {
        String tmp = "";

        for(Ingredients i : box.getList()) tmp += i + " ";
        return new Food(tmp);
    }
}

class Example {
    public static void main(String[] args) {
        CookingBox<Ingredients> cookingBox = new CookingBox<>(); // new CookingBox<Ingredients>();
        CookingBox<Egg> eggBox = new CookingBox<>();

        cookingBox.add(new Egg());
        cookingBox.add(new Cabbage());
        eggBox.add(new Egg());

        System.out.println(Cooker.cook(cookingBox));
        System.out.println(Cooker.cook(eggBox));
    }
}

class CookingBox<T extends Ingredients> extends 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(); }
}

출력

Egg Cabbage 
Egg 

와일드 카드 하한 예제

Collection.sort()를 사용하여 appleBox와 grapeBox에 담긴 과일을 무게별로 내림차순, 오름차순 정렬한 예제이다.

Collection.sort() 메서드의 선언부는 다음과 같은 하한 제한 와일드 카드로 되어있다.

static <T> void sort(List<T> list, Comparator<? super T> c)

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

class Example {
    public static void main(String[] args) {
        FruitBox<Apple> appleBox = new FruitBox<Apple>();
        FruitBox<Grape> grapeBox = new FruitBox<Grape>();

        appleBox.add(new Apple("GreenApple", 300));
        appleBox.add(new Apple("GreenApple", 100));
        appleBox.add(new Apple("GreenApple", 200));

        grapeBox.add(new Grape("GreenGrape", 400));
        grapeBox.add(new Grape("GreenGrape", 300));
        grapeBox.add(new Grape("GreenGrape", 200));

        Collections.sort(appleBox.getList(), new AppleComp());
        Collections.sort(grapeBox.getList(), new GrapeComp());
        System.out.println(appleBox);
        System.out.println(grapeBox);
        System.out.println();
        Collections.sort(appleBox.getList(), new FruitComp());
        Collections.sort(grapeBox.getList(), new FruitComp());
        System.out.println(appleBox);
        System.out.println(grapeBox);
    }
}

class Fruit {
    String name;
    int weight;

    Fruit(String name, int weight) {
        this.name = name;
        this.weight = weight;
    }

    public String toString() { return name + "(" + weight + ")"; }
}

class Apple extends Fruit {
    Apple(String name, int weight) {
        super(name, weight);
    }
}

class Grape extends Fruit {
    Grape(String name, int weight) {
        super(name, weight);
    }
}

class AppleComp implements Comparator<Apple> {
    public int compare(Apple t1, Apple t2) {
        return t2.weight - t1.weight;
    }
}

class GrapeComp implements Comparator<Grape> {
    public int compare(Grape t1, Grape t2) {
        return t2.weight - t1.weight;
    }
}

class FruitComp implements Comparator<Fruit> {
    public int compare(Fruit t1, Fruit t2) {
        return t1.weight - t2.weight;
    }
}

class FruitBox<T extends Fruit> extends Box<T> { }

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

    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(); }
}

출력

[GreenApple(300), GreenApple(200), GreenApple(100)]
[GreenGrape(400), GreenGrape(300), GreenGrape(200)]

[GreenApple(100), GreenApple(200), GreenApple(300)]
[GreenGrape(200), GreenGrape(300), GreenGrape(400)]

제네릭 메서드 만들기

메서드 선언부$($반환타입 앞)에 <T>를 붙여서 제네릭 메서드를 생성한다.

static 메서드에는 타입 매개변수를 사용할 수 없었지만 제네릭 타입을 선언한 static 메서드는 타입 매개변수가 사용 가능하다.

타입 매개변수 T는 메서드 내에서만 유효한 지역 변수 선언과 같다고 생각하면 된다.

앞서 와일드 카드로 선언한 cook()을 제네릭 메서드로 바꾸면 다음과 같다.

static Food cook(CookingBox<? extends Ingredients> box) 

제네릭 메서드

static <T extends Ingredients> Food cook(CookingBox<T> box)

// 제네릭 메서드 호출 할 때는 타입 변수에 타입을 대입해야 한다.
CookingBox<Ingredients> cookingBox = new CookingBox<Ingredients>();
...

System.out.println(Cooker.<Ingredients>cook(cookingBox));

복잡한 제네릭 메서드

public static <T extends Comparable<? super T>> void sort(List<T> list)

List<T> 타입 T를 요소로 하는 List를 매개변수로 허용한다.

<T extends Comparable<? super T>> T는 Comparable을 구현한 클래스이어야 하며 (<T extends Comparable>)

T또는 그 조상의 타입을 비교하는 Comparable이어야 한다. (Comparable<? super T>)

Erasure

컴파일러는 제네릭 타입을 통해 형변환을 넣어주고 컴파일이 끝나면 제네릭 타입을 제거한다.

때문에 클래스파일 $($.class)에는 제네릭 타입 정보가 존재하지 않는다.

1. 제네릭 타입의 경계를 제거 타입 매개변수T를 제거하고 그 자리를 제한하고 있는 타입으로 변경.

  • <T extends Fruit> -> T를 Fruit으로 치환.

  • <T> -> Object로 치환.

class Box<T extends Fruit> {
  void add(T t) {
    ...
  }
}

가 아래와 같이 치환.

class Box {
  void add(Fruit t) {
    ...
  }
}

2. 제네릭 타입을 제거한 후 타입이 일치하지 않으면 형변환 추가.

T get(int i) {
  return list.get(i);
}

가 아래와 같이 치환.

Fruit get(int i) {
  return (Fruit)list.get(i);
}

List의 get()은 Object 타입을 반환하기 때문에 형변환이 필요하다.


참조
Java의 정석 $($남궁 성)
whiteship 자바 라이브 스터디

Categories:

Updated:

Leave a comment