본문 바로가기
Backend/Java

[Java의 정석] 제너릭스

by 박상윤 2024. 3. 29.

Chatper 12. 지네릭스, 열거형, 애너테이션

 

1.1 지네릭스?

지네릭스는 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크를 해주는 기능이다.

객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.

 

타입의 안정성을 높인다는 것?

의도하지 않은 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄어준다는 뜻

 

제너릭스의 장점
1. 타입 안정성을 제공한다.
2. 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해진다.

 

타입을 미리 명시해줌으로써 번거로운 형변환을 줄여준다.

 

1.2 지네릭 클래스의 선언

지네릭 타입은 클래스와 메서드에 선언할 수 있다.

 

class Box {
	Object item;
    
    void setItem(Object item){
    	this.item = item;
    }
    
    Object getItem(){
    	return item;
    }
}

 

 

클래스 옆에 <T>를 붙이면 된다.

package com.example.javabible.ch12_지네릭스;

public class Box<T> {
    T item;

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

    T getItem(){
        return item;
    }
}

 

BOX<T>에서 T를 타입 변수라고 하며, Type의 첫 글자에서 따온 것이다.

타입 변수는 T가 아닌 다른 것을 사용해도 된다. ArrayList<E>의 경우, 타입 변수 E는 'Element(요소)'의 첫 글자를 따서 사용했다.

타입 변수가 여러 개인 경우에는 Map<K,V>와 같이 콤마 ','를 구분자로 나열하면 된다.

K는 Key(키)를 의미하고, V는 Value(값)을 의미한다.

기호의 종류만 다를 뿐 임의의 참조형 타입을 의미한다는 것은 모두 같다.

 

 

지네릭 클래스가 된 Box 클래스의 객체 생성하기

package com.example.javabible.ch12_지네릭스;

public class Main {
    public static void main(String[] args) {
        Box<String> b = new Box<>();
        //b.setItem(new Object()); String 이외의 타입은 지정 불가
        b.setItem("ABC");
        String item = b.getItem();
    }
}

 

타입 T 대신에 String타입을 지정해줬으므로, 지네릭 클래스 Box<T>는 다음과 같이 정의된 것과 같다.

public class Box {
    String item;

    void setItem(String item){
        this.item = item;
    }

    String getItem(){
        return item;
    }
}

 

지네릭 클래스인데도 에전의 방식으로 객체를 생성하는 것이 허용된다.

Box b = new Box();
b.setItem("ABC"); // warring : unchecked or unsafe operation
b.setItem(new Object());

 

타입 변수 T에 Object타입을 지정하면, 타입을 지정하지 않은 것이 아니라 알고 적은 것이므로 경고는 발생하지 않는다.

Box<Object> b = new Box<Object>();
b.setItem("ABC"); // 경고발생 안함
b.setItem(new Obejct()); // 경고발생 안함

 

지네릭 클래스를 사용할 때는 반드시 타입을 지정해서 제너릭스와 관련된 경고가 나오지 않도록 하자!

 

제너릭스의 용어

class Box<T> {}

 

Box<T> : 지네릭 클래스, T의 Box 또는 T Box라고 읽는다.

T : 타입 변수 또는 타입 매개변수

Box : 원시 타입(raw type)

 

타입 매개변수에 타입을 지정하는 것 : 지네릭 타입 호출

지정된 타입 : 매개변수화된 타입 = 대입된 타입

 

지네릭의 제한

지네릭 클래스를 생성할 때, 객체별로 다른 타입을 지정하는 것은 적절하다.

 

모든 객체에 대해 동일하게 동작해야하는 static 멤버에 타입 변수 T를 사용할 수 없다.

T는 인스턴스 변수로 간주되기 때문이다. static 멤버는 인스턴스변수를 참조할 수 없다.

 

인스턴스 변수 : 클래스에 정의된 변수

 

지네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만, new T[10]과 같이 배열을 생성하는 것은 안된다.

왜? new 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 한다.

제너릭 배열을 생성해야할 필요가 있을 때는, new연산자 대신에 Reflection API의 newInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나, Object 배열을 생성해서 복사한 다음 T[]로 형변환하는 방법을 사용한다.

 

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

 

ArrayList를 이용해서 여러 객체를 저장할 수 있도록 하자

package com.example.javabible.ch12_지네릭스;


import java.util.ArrayList;

class Fruit {
    public String toString(){
        return "Fruit";
    }
}

class Apple extends Fruit {
    public String toString(){
        return "Apple";
    }
}

class Grape extends Fruit {
    public String toString(){
        return "Grape";
    }
}

class Toy extends Fruit {
    public String toString(){
        return "Toy";
    }
}

public class FruitBoxEx1 {
    public static void main(String[] args) {
        Box<Fruit> fruitBox = new Box<Fruit>();
        Box<Apple> appleBox = new Box<Apple>();
        Box<Toy> toyBox = new Box<>(); // 타입 생략 가능
        // Box<Grape> grapeBox = new Box<Apple>(); - 타입 불일치

        fruitBox.add(new Fruit());
        fruitBox.add(new Apple()); // 다형성

        appleBox.add(new Apple());
        //appleBox.add(new Toy()); - Apple만 담을 수 있음

        System.out.println(fruitBox);
        System.out.println(appleBox);
    }
}

class Box<T> {

    ArrayList<T> list = new ArrayList<>();

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

 

출력 결과

 

Box<T>의 객체를 생성할 때는 참조변수와 생성자에 대입된 타입(매개변수화된 타입)이 일치해야 한다.

단, 두 지네릭 클래스의 타입이 상속관계에 있고, 대입된 타입이 같은 것은 괜찮다.

Box<Apple> appleBox = new FruitBox<Apple>();

 

JDK1.7 부터는 추정이 가능한 경우 타입을 생략할 수 있게 되었다.

 

제한된 지네릭 클래스

 

타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한할 수 있는 방법은 없을까?

지네릭 타입에 extends를 사용하면 특정 타입의 자손들만 대입할 수 있게 해준다.

 

만일 클래스가 아니라 인터페이스를 구현해야 한다는 제약이 필요하다면, 이때도 extends를 사용한다. implements를 사용하지 않는다는 점에 주의하라!

interface Eatable {}
class FruitBox<T extends Eatable> { ... }

 

클래스 Fruit의 자손이면서 Eatable인터페이스도 구현해야한다면 아래와 같이 & 기호로 연결한다.

class FruitBox<T extends Fruit & Eatable> {...}

 

package com.example.javabible.ch12_지네릭스;

interface Eatable {}

public class FruitBoxEx2 {
    public static void main(String[] args) {
        FruitBox<Fruit> fruitBox = new FruitBox<>();
        FruitBox<Apple> appleBox = new FruitBox<>();
        FruitBox<Grape> grapeBox = new FruitBox<>();
        //FruitBox<Grape> grapeBox2 = new FruitBox<Apple>(); 에러, 타입 불일치
        // FruitBox<Toy> toyBox = new FruitBox<Toy>(); 에러 - 제너릭 제한

        fruitBox.add(new Fruit());
        fruitBox.add(new Apple());
        fruitBox.add(new Grape());
        appleBox.add(new Apple());
        //appleBox.add(new Grape()); - 에러, 타입 불일치
        grapeBox.add(new Grape());

        System.out.println("fruit-Box" + fruitBox);
        System.out.println("apple-Box" + appleBox);
        System.out.println("grapeBox" + grapeBox);
    }
}

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

 

출력 결과