☕ Java/이론

[Java] 제네릭1 (제네릭 클래스, 제네릭 메소드)

an2z 2022. 1. 13. 16:08

제네릭

  • 제네릭(Generic)이란 데이터의 타입을 일반화(generalize)한다는 의미로 클래스나 메소드에서 사용할 데이터의 타입을 컴파일 시 미리 지정하는 방법이다.
  • 제네릭을 사용 시 장점
    1. 타입 변환 및 타입 검사같은 번거로운 작업을 제거할 수 있다.
    2. 클래스나 메소드 내부에서 사용되는 객체의 타입 안정성을 높일 수 있다. → 실수가 컴파일 오류로 이어짐

 

제네릭 선언 및 생성

💡 제네릭 클래스 정의

인스턴스 생성시 결정이 되는 자료형의 정보를 <T>로 대체한다.

class Box<T> {
    private T ob;
    public void set(T o) {
        ob = o;
    }
    public T get( ) {
        return ab;
   }
}

 

💡 제네릭 인스턴스 생성

Box<Apple> aBox = new Box<Apple>( );   // T를 Apple로 결정하여 인스턴스 생성
Box<Orange> oBox = new Box<Orange>( ); // T를 Orange로 결정하여 인스턴스 생성

 

❓ 관련 용어

용어 예시
타입 매개변수 (Type Parameter) Box<T>에서 T
타입 인자 (Type Argument) Box<Apple>에서 Apple
매개변수화 타입 (Parameterized Type)
= 제네릭 타입 (Generic Type)
Box<Appel>

 

제네릭 이전의 코드의 문제점

  • 여러 타입을 사용하는 대부분의 클래스나 메소드에서 인수나 반환값으로 Object 타입을 사용할 경우 반환된 Object 객체를 다시 원하는 타입으로 타입 변환을 해줘야 한다.
  • 프로그래머가 실수를 해도 그 실수가 드러나지 않을 수 있다.
    • 오류의 발견
      • 컴파일 과정에서 오류 발견 → 컴파일 오류는 원인을 바로 찾을 수 있다.
      • 실행 과정에서 예외 발견 → 예외의 원인은 쉽게 발견되지 않는 경우도 많다.
      • 발견되지 않음 → 오류가 있는지도 모른채 넘어갈 수 있다.
class Apple { // 사과를 단순히 표현한 클래스
    public String toString() {
        return "I am an apple";
    }
}

class Orange { // 오렌지를 단순히 표현한 클래스
    public String toString() {
        return "I am an arange";
    }
}

class AppleBox { // 사과를 담는 상자를 표현한 클래스
    private Apple ap;

    public void set(Apple a) { // 사과를 담는다.
        ap = a;
    }

    public Apple get() { // 사과를 꺼낸다.
        return ap;
    }
}

class OrangeBox { // 오렌지를 담는 상자를 표현한 클래스
    private Orange or;

    public void set(Orange o) { // 오렌지를 담는다.
        or = o;
    }

    public Orange get() { // 오렌지를 꺼낸다.
        return or;
    }
}

class FruitBox {
    public static void main(String[] args) {
        AppleBox aBox = new AppleBox(); // 사과 상자 생성
        OrangeBox oBox = new OrangeBox(); // 오렌지 상자 생성

        aBox.set(new Apple()); // 사과를 사과 상자에 담는다.
        oBox.set(new Orange()); // 오렌지를 오렌지 상자에 담는다.

        Apple ap = aBox.get(); // 사과 상자에서 사과를 꺼낸다.
        Orange or = oBox.get(); // 오렌지 상자에서 오렌지를 꺼낸다.

        System.out.println(ap);
        System.out.println(or);
    }
}

// 실행 결과
I am an apple
I am an arange
class Apple { // 사과를 단순히 표현한 클래스
    public String toString() {
        return "I am an apple";
    }
}

class Orange { // 오렌지를 단순히 표현한 클래스
    public String toString() {
        return "I am an arange";
    }
}

class Box { // 무엇이든 담을 수 있는 상자
    private Object ob;
    public void set(Object o) {
        ob = o;
    }
    public Object get() {
        return ob;
    }
}

class FruitBox {
    public static void main(String[] args) {
        Box aBox = new Box();
        Box oBox = new Box();

        aBox.set(new Apple()); // 상자에 사과를 담는다
        oBox.set(new Orange()); // 상자에 오렌지를 담는다

        // Box 인스턴스에서 내용물을 꺼낼 때 형 변환을 해야한다
        Apple ap = (Apple)aBox.get(); // 상자에서 사과를 꺼낸다
        Orange or = (Orange)oBox.get(); // 상자에서 오렌지를 꺼낸다

        System.out.println(ap);
        System.out.println(or);
    }
}

// 실행 결과
I am an apple
I am an arange
class Apple {
    public String toString() {
        return "I am an apple";
    }
}

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

class Box {
    private Object ob;
    public void set(Object o) {
        ob = o;
    }
    public Object get() {
        return ob;
    }
}

class FruitBoxFault {
    public static void main(String[] args) {
        Box aBox = new Box();
        Box oBox = new Box();

        /* 프로그래머의 실수 */
        // 아래 두 문장에서는 사과와 오렌지가 아닌 "문자열"을 담았다.
        aBox.set("Apple");
        oBox.set("Orange");

        /* 실수가 드러나지 않음 */
        System.out.println(aBox.get());
        System.out.println(oBox.get());
    }
}

// 실행 결과
Apple
Orange

 

제네릭 이후의 코드

👉🏻 제네릭 이후 예제(1)

  • 형 변환을 해줘야 한다는 번거러움이 없다.
/* 관련 예제 */

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

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

// 제네릭 기반의 코드
class Box<T> {
    private T ob;
    public void set(T o) {
        ob = o;
    }
    public T get() {
        return ob;
    }
}

class FruitBox_Generic {
    public static void main(String[] args) {
        Box<Apple> aBox = new Box<Apple>(); // T를 Apple로 결정
        Box<Orange> oBox = new Box<Orange>(); // T를 Orange로 결정

        aBox.set(new Apple()); // 사과를 상자에 담는다
        oBox.set(new Orange()); // 오렌지를 상자에 담는다

        Apple ap = aBox.get(); // 사과를 꺼내는데 형 변환이 필요 없다
        Orange or = oBox.get(); // 오렌지를 꺼내는데 형 변환이 필요 없다

        System.out.println(ap);
        System.out.println(or);
    }
}

// 실행 결과
I am an apple
I am an arange

 

👉🏻 제네릭 이후 예제(2)

  • 실수가 컴파일 오류로 이어져 코드의 안정성이 높아진다.
class Apple {
    public String toString() {
        return "I am an apple";
    }
}

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

class Box<T> {
    private T ob;
    public void set(T o) {
        ob = o;
    }
    public T get() {
        return ob;
    }
}

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

        aBox.set("Apple"); // 프로그래머의 실수가 컴파일 오류로 이어짐
        oBox.set("Orange"); // 프로그래머의 실수가 컴파일 오류로 이어짐

        Apple ap = aBox.get();
        Orange or = oBox.get();

        System.out.println(ap);
        System.out.println(or);
    }
}

// 실행 결과
Exception in thread "main" java.lang.Error: Unresolved compilation problems: 
	The method set(Apple) in the type Box<Apple> is not applicable for the arguments (String)
	The method set(Orange) in the type Box<Orange> is not applicable for the arguments (String)

	at ch21.FruitBoxFault_Generic.main(FruitBoxFault_Generic.java:34)

 

 

제네릭 기본 문법


타입 매개변수의 이름

  • 한 문자로 이름을 짓는다.
  • 대문자로 이름을 짓는다.
  • 보편적인 선택
    E Element
    K Key
    N Number
    T Type
    V Value

 

다중 매개변수 기반의 제네릭

  • 타입 매개변수가 둘 이상인 제네릭 클래스를 정의할 경우 쉼표( , )로 구분하여 명시해준다.
class MBox<L,R> {
    private L left;
    private R right;

    public void set(L l, R r) {
        left = l;
        right = r;
    }
}
Box<String, Integer> box = new Box<String, Integer>();

 

기본 자료형에 대한 제한

  • 제네릭 클래스에 대하여 매개변수화 타입을 구성할때 기본 자료형의 이름은 타입인자로 쓸 수 없다.
  • 이때는 기본 자료형에 대한 레퍼 클래스를 사용해야만 한다.
class Box<T> {
    private T ob;

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

class PrimitiveDataGeneric {
    public static void main(String[] args) {
        Box<Integer> IBox = new Box<Integer>();
        IBox.set(125); // 오토 박싱 진행
        int num = IBox.get(); // 오토 언박싱 진행
        System.out.println(num);
    }
}

// 실행 결과
125

 

다이아몬드 기호

  • <>를 다이아몬드 기호라 한다.
  • 자바 7부터 인스턴스 생성시 타입을 추정할 수 있는 경우 타입을 생략할 수 있다.
    • 컴파일러가 프로그래머가 작성하는 제네릭 관련 문장에서 자료형의 이름을 추론하는 능력을 갖고 있기 때문이다.
// Box<Apple> aBox = new Box<Apple>();
Box<Apple> aBox = new Box<>();

 

매개변수화 타입을 타입인자로 전달

  • Box<String>과 같은 매개변수화 타입이 타입 인자로 사용될 수 있다.
Box<Box<String>> wBox = new Box<>();
더보기
class Box<T> {
    private T ob;

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

public class BoxInBox {
    public static void main(String[] args) {
        Box<String> sBox = new Box<>();
        sBox.set("I am so happy");

        Box<Box<String>> wBox = new Box<>();
        wBox.set(sBox);

        Box<Box<Box<String>>> zBox = new Box<>();
        zBox.set(wBox);

        System.out.println(zBox.get().get().get());
    }
}

// 실행 결과
I am so happy

 

타입 매개변수의 제한

💡 클래스로 제한

  • 제네릭은 'T'와 같은 타입 매개변수를 사용하여 타입을 제한한다.
  • 이때 extends를 사용해 타입 매개변수에 특정 타입만을 사용하도록 제한할 수 있다.
// Number 또는 이를 상속하는 클래스만이 타입 매개변수(T)로 올 수 있다.
class Box<T extends Number> {...}
더보기
class Box<T extends Number> { // Number 또는 이를 상속하는 클래스만 T로 올 수 있음
    private T ob;

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

class TypeArguLimit {
    public static void main(String[] args) {
        Box<Integer> iBox = new Box<>(); // Integer는 Number를 상속
        iBox.set(24);

        Box<Double> dBox = new Box<>(); // Double은 Number를 상속
        dBox.set(5.97);

        System.out.println(iBox.get());
        System.out.println(dBox.get());
    }
}

// 실행 결과
24
5.97

 

💡 인터페이스로 제한

  • 제네릭 클래스의 타입 매개변수를 인터페이스의 이름으로 제한할 수 있다.
  • 클래스가 아닌 인터페이스를 구현할 경우에도 implements 키워드가 아닌 extends를 사용해야 한다.
// Eatable 인터페이스를 구현하는 클래스만이 타입 매개변수(T)로 올 수 있다.
class Box<T extends Eatable> {...}
더보기
interface Eatable {
	public String eat();
}

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

    @Override
    public String eat() {
        return "It tastes so good";
	}
}

class Box<T extends Eatable> { // 타입 인자를 Eatable 인터페이스로 제한
    T ob;

    public void set(T o) {
        ob = o;
    }
    public T get() {
        System.out.println(ob.eat()); // Eatable로 제한했기에 eat메소드 호출 가능
        return ob;
    }
}

class TypeArguLimit_Interface {
    public static void main(String[] args) {
        Box<Apple> box = new Box<>();
        box.set(new Apple()); // 사과 저장

        Apple ap = box.get(); // 사과 꺼내기
        System.out.println(ap);
    }
}

// 실행 결과
It tastes so good
I am an apple

 

💡 인터페이스로 제한

  • 제네릭 클래스의 타입 매개변수를 인터페이스의 이름으로 제한할 수 있다.
  • 클래스가 아닌 인터페이스를 구현할 경우에도 implements 키워드가 아닌 extends를 사용해야 한다.
// Number 클래스를 상속하면서 동시에 Eatable 인터페이스를 구현하는 클래스만이 타입 매개변수(T)로 올 수 있다.
class Box<T extends Number & Eatable> {...}

 

 

제네릭 메소드

  • 메소드가 호출 시점에 제네릭 메소드의 T를 결정한다.
  • 전달받는 타입 인자를 통해 타입 인자 생략 가능 

 


Reference

http://www.tcpschool.com/java/java_generic_concept