[윤성우의 열혈 Java 프로그래밍] Chapter 21 - 제네릭(Generics) 1

Update:     Updated:

카테고리:

태그:

21-1. 제네릭의 이해

클래스를 정의하는데 있어 자료형을 결정짓지 않고 틀만 만들어놓는 형태의 클래스 정의 형태

- 제네릭 이전의 코드

제네릭 문법이 등장하기 이전에는 일반적인 클래스를 정의할 때 다음과 같이 Object를 사용했다.

class Apple {
    @Override
    public String toString() {
        return "I am an apple";
    }
}

class Orange {
    @Override
    public String toString() {
        return "I am an Orange";
    }
}

class GeneralBox {  // 무엇이든지 담을 수 있는 박스
    private Object object;

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

    public Object get() {
        return object;
    }
}

public class BeforeGeneric {
    public static void main(String[] args) {
        GeneralBox appleBox = new GeneralBox();
        GeneralBox orangeBox = new GeneralBox();

        appleBox.set(new Apple());
        orangeBox.set(new Orange());

        // 명시적 형 변환이 필요함
        Apple apple = (Apple) appleBox.get();
        Orange orange = (Orange) orangeBox.get();

        System.out.println(apple);
        System.out.println(orange);
    }
}


Object로 클래스를 디자인 할 시 인스턴스를 꺼내는 과정에서 필수적으로 명시적 형변환이 필요하다.
그리고 이러한 번거로운 과정으로 인해 다음과 같은 실수가 발생할 수도 있다.

class Apple {
    @Override
    public String toString() {
        return "I am an apple";
    }
}

class Orange {
    @Override
    public String toString() {
        return "I am an Orange";
    }
}

class GeneralBox {  // 무엇이든지 담을 수 있는 박스
    private Object object;

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

    public Object get() {
        return object;
    }
}

public class BeforeGenericFault {
    public static void main(String[] args) {
        GeneralBox appleBox = new GeneralBox();
        GeneralBox orangeBox = new GeneralBox();

        // 아래 두 문장에서는 사과와 오렌지가 아닌 '문자열'을 담았다.(개발자의 실수)
        appleBox.set("Apple");
        orangeBox.set("Orange");

        // 상자에 과일이 담기지 않았는데 과일을 꺼내려 한다.
        Apple apple = (Apple) appleBox.get();
        Orange orange = (Orange) orangeBox.get();

        System.out.println(apple);
        System.out.println(orange);
    }
}

위 코드에서 가장 큰 문제점은 개발자의 실수가 컴파일 과정에서 발견되지 않았다는데 있다.
즉, 제네릭 등장 이전의 자바코드는

  1. 물건을 꺼낼 때, 명시적 형변환이 필요하다는 점과
  2. 이로 인해 프로그래머가 실수를 해도 그 실수가 드러나지 않을 수 있다
    는 점이 불편함과 문제점으로 있다.

- 제네릭 기반의 클래스 정의하기

제네릭이 등장하면서 자료형에 의존적이지 않은 클래스를 정의할 수 있게 되었다.

class Box<T> {
    private T ob;

    public void set(T o) {
        ob = o;
    }

    public T get() {
        return ob;
    }
}

자료형에 의존적이지 않은 형태로 위의 클래스를 정의하였다.
T는 인스턴스를 생성할 때 결정하면 된다.
이렇게 인스턴스 생성 시 T의 자료형을 결정하는 것이 ‘제네릭’이다.

Box<Apple> aBox = new Box<Apple>();
  • 타입 매개변수 (Type Parameter): Box 에서 T
  • 타입 인자 (Type Argument): Box 에서 Apple
  • 매개변수화 타입 (Parameterized Type), 제네릭 타입(Generic Type): Box

- 제네릭 이후의 코드

다음의 예제를 통해 제네릭을 이해해보자.

class Apple {
    public String toString() {
        return "I am an apple";
    }
}

class Orange {
    public String toString() {
        return "I am an Orange";
    }
}

class Box<T> {
    private T ob;

    public void set(T o) {
        ob = o;
    }

    public T get() {
        return ob;
    }
}

public class GenericExample {
    public static void main(String[] args) {
        Box<Apple> aBox = new Box<>();
        Box<Orange> oBox = new Box<>();

        aBox.set(new Apple());
        oBox.set(new Orange());

        Apple apple = aBox.get();
        Orange orange = oBox.get();

        System.out.println(apple);
        System.out.println(orange);
    }
}

21-2. 제네릭의 기본 문법

- 다중 매개변수 기반 제네릭 클래스의 정의

다음과 같이 둘 이상의 타입 매개변수에 대한 제네릭 클래스도 정의할 수 있다.

class DBox<L, R> {
    private L left;
    private R right;

    public void set(L l, R r) {
        left = l;
        right = r;
    }

    @Override
    public String toString() {
        return left + " & " + right;
    }
}

public class MultiTypeParam {
    public static void main(String[] args) {
        DBox<String, Integer> box = new DBox<>();

        box.set("Apple", 25);
        System.out.println(box);
    }
}

타입 매개변수의 이름은 일반적으로 다음 두 가지 규칙을 지켜서 이름을 짓는다.

  • 한 문자로 이름을 짓는다.
  • 대문자로 이름을 짓는다.

보편적으로 자주 사용하는 타입 매개변수의 이름과 그 의미는 다음과 같다.

  • E Element
  • K Key
  • N Number
  • T Type
  • V Value

- 기본 자료형에 대한 제한 그리고 래퍼 클래스

기본 자료형의 이름은 ‘타입 인자’로 쓸 수 없다.

// 타입 인자로 기본 자료형이 올 수 없으므로 컴파일 오류 발생
Box<int> box = new Box<>();

- 타입 인자의 생략: 다이아몬드(Diamond) 기호

컴파일러는 제네릭 관련 문장에서 자료형의 이름을 추론하는 능력을 갖고 있다.
따라서 다음과 같은 두 가지 방법 모두 가능하다.

Box<Apple> aBox = new Box<Apple>();
Box<Apple> aBox = new Box<>();

- '매개변수화 타입'을 '타입 인자'로 전달하기

‘매개변수화 타입’이 다음과 같이 ‘타입 인자’로 사용이 될 수도 있다.
image

- 제네릭 클래스의 타입 인자 제한하기

extends 키워드를 이용해서 제네릭 클래스의 타입 인자를 제한할 수 있다.

// 클래스나 인터페이스로 제한
class Box<T extends Number> {...}   // 클래스
class Box<T extends Eatable> {...}  // 인터페이스

// 하나의 클래스와 하나 이상의 인터페이스에 대해 동시에 제한을 하는 경우
class Box<T extends Number & Eatable> {...}

image

- 제네릭 메소드의 정의

클래스를 대상으로 하는 것이 아닌 메소드에 대해서 제네릭으로 정의하는 것도 가능하다.
제네릭 메소드는 인스턴스 메소드 뿐만 아니라 클래스 메소드에 대해서도 정의가 가능하다.
즉, static 선언의 유무에 상관없이 제네릭 메소드의 정의가 가능하다.

class BoxFactory {
    public static <T> makeBox(T o) {
        Box<T> box = new Box<>();   // 상자를 생성하고
        box.set(o); // 전달된 인스턴스를 상자에 담아서
        return box; // 상자를 반환한다.
    }
}

제네릭 클래스는 ‘인스턴스 생성 시’ 자료형이 결정된다.
반면, 제네릭 메소드는 ‘메소드 호출 시’ 자료형이 결정된다.

// 1. 기본형태
Box<String> sBox = BoxFactory.<String>makeBox("Sweet");
Box<String> dBox = BoxFactory.<Double>makeBox(7.59);

// 2. 컴파일러는 'makeBox에 전달되는 인자'를 보고 T를 String과 Double로 유추한다.
Box<String> sBox = BoxFactory.makeBox("Sweet");
Box<String> dBox = BoxFactory.makeBox(7.59);

- 제네릭 메소드의 제한된 타입 매개변수 선언

제네릭 클래스와 같이 제네릭 메소드도 타입 제한이 가능하다.

public static <T extends Number> Box<T> makeBox(T o) {... }

Java lang 카테고리 내 다른 글 보러가기

댓글 남기기