티스토리 뷰

이 콘텐츠는 아래와 같은 사전 지식이 필요합니다.

  • 상속

이 콘텐츠는 아래와 같은 내용을 담고 있습니다.

  • 제네릭(Generic) 이란?
  • 제네릭의 장점
  • 제네릭 클래스와 제네릭 메서드
  • 제네릭 타입 제한

이 콘텐츠는 다음과 같은 포스팅과 연관이 있습니다. 참조하십시오.


1. 제네릭이란?

 JAVA의 collection Framework를 사용하다 보면, sort() 메서드를 자주 사용하게 될 것이다. sort()메소드를 사용하려고 해당 API를 찾아보면, 이게 뭐지 싶은 내용들이 나온다.

 

 

클래스 타입 제한에서 마주하게될 최종 보스

Comparator <? super T>? ㅋㅋㅋㅋ 뭐고 이게 ㅋㅋㅋㅋㅋㅋ

(지금도 실력이 잣밥이지만,) 그 옛날 collection framework가 뭔지도 잘 몰랐던 시절 정렬 조건을 만드려다가, 저걸 보고 웃음밖에 안 나왔다. 아마 한 번쯤 문서 보면서 저게 뭘까 야발! 하고 지나간 경험이 있을 것이라고 본다. 오늘 공부할 주제는 저놈이다. 저놈을 우리는 제네릭(Generic)이라고 부른다.

 

그래서 제네릭이 뭔데 씹덕아

 제네릭은 JDK1.5에 처음 도입되었으며, 인스턴스를 생성하거나 메서드를 호출할 때, 데이터 타입을 결정하는 기법이다.

미리 데이터 타입을 지정할 필요 없이, 사용자가 타입을 지정하여 사용할 수 있게끔 일반화(Generic)하는 것이다.

 

 

제네릭 클래스 선언 형식은 다음과 같다.

//접근제어자 class 클래스명 <제네릭 파라미터>

public class MyGeneric <한글,도쌉가능>{
    한글 val1;
    도쌉가능 val2;

}

 

 위와 같이 <> 내부에 파라미터를 지정할 수 있으며, 콤마(', ')를 이용하여 여러 개의 파라미터를 지정할 수 있다. 각각의 파라미터가 데이터 타입을 의미한다. ( 포스팅하려고 준비하다 보니, 쓸 일은 없겠지만..  한글도 잘 들어간다 -_-;)

위 예제에서는 '한글'이라는 데이터 타입, '도쌉가능'이라는 데이터 타입 두 종류가 존재하는 제네릭 클래스이다. 

 

 

JDK1.7부터 클래스를 인스턴스화 할 때, new 클래스명 <파라미터 타입>()의 '파라미터 타입'을 생략할 수 있다.

public class MyGeneric <E>{
    E val;

    public MyGeneric(E val) {
        this.val = val;
    }

    public static void main(String[] args) {
    	//생성시 제네릭 파라미터는 생략할 수 있다.
        MyGeneric<Integer> sample = new MyGeneric<>();
    }
}

 

 제네릭에 구성된 파라미터들에는 지정 제약조건은 없다. 따라서 위처럼 '한글'이라고 지어서 써도 문제 될 것은 없다. 다만, 일반적으로는 아래와 같이 쓰이므로, 아래와 같이 쓰는 것을 권고한다.

 

 

2. 제네릭의 장점

-컴파일 시 타입 체크가 가능하기 때문에 Type safety(런타임 전에 에러를 방지할 수 있음)

-형 변환을 불 필요

-코드의 중복 제거

 

제네릭을 이용하지 않는다면, 타입에 따라 클래스를 다음과 같이 여러 개 지정해야 할 수도 있다.

class NoGeneric_String {
    String a;

    public NoGeneric_String(String a) {
        this.a = a;
    }
}

class NoGeneric_int {
    int a;

    public NoGeneric_int(int a) {
        this.a = a;
    }
}

//... 타입에 따른 수백가지의 클래스를 만들어야 할 수도 있다..

제네릭을 이용하면, 이러한 코드의 중복을 제거할 수 있다.

public class MyGeneric <E>{
    E val;

    public MyGeneric(E val) {
        this.val = val;
    }
}

 

※사용 시 주의 사항

제네릭 파라미터에 들어가는 데이터 타입은 모두 참조형 변수만 가능하다. 기본형 타입인 int, char 등은 쓸 수 없다는 의미이다.

int는 제네릭 파라미터로 사용할 수없다

제네릭 문법이 모든 데이터 타입에 일관된 성질을 갖기 위해서는 어찌 보면 당연한 의미이기도 하다. 그럼 기본형을 쓰고 싶을 때는 어떻게 하면 되냐고? Wrapper class를 이용하면 된다. 

 

 

 

3. 제네릭 클래스와  제네릭 메서드

 제네릭을 모를 때는 사실 이 두 개가 뭐가 다른가 몰랐다... 전혀 제네릭 클래스랑 제네릭 메서드는 전혀 상관이 없다! 제네릭 클래스라고 해서, 반드시 제네릭 메서드를 사용할 이유도 없고, 그 반대도 마찬가지이다.

 

제네릭 클래스 

클래스 명 뒤에 제네릭 파라미터를 사용한다.

멀티 파라미터를 사용할 수 있다.

제네릭 파라미터는 static variable, static method에서는 사용이 불가능하다.

정적으로 사용 불가능한 것은 어찌 보면 당연하다. static 멤버라면,  실행 시 객체가 생성되기 전에 이미 메모리에 올라와 있어야 하는데, 해당 데이터 타입을 가져올 수 있는 방법이 없기 때문이다. 

 

static 멤버로는 제네릭 타입을 쓸 수 없다.  

 

제네릭 메서드

반환 값 앞에 제네릭 파라미터를 사용한다.

이 역시 멀티 파라미터를 사용할 수 있다.

선언된 제네릭 파라미터를 이용해, 함수 파라미터 데이터 타입으로 사용할 수 있다.

이때, 제네릭 파라미터와  함수 파라미터 데이터 타입의 이름은 같아야 한다.

//접근제어자 제네릭파라미터 반환형 함수이름 (파라미터)
public <T> void setGen(T a){
        System.out.println("해당 파라미터의 클래스 : " + a.getClass().getName());
    }

 

 

 

 

 

4. 제네릭 타입 제한

 기본적인 내용들은 끝났다. 정리하지면, 제네릭은 데이터 타입을 일반화하여 코드의 재사용성을 높이고, 타입 안정성 있게 사용할 수 있는 기법이다. 하지만 의문이 들 것이다.

 

클래스는 정의하기 나름인데, 모든 클래스에 통용되는 제네릭 클래스(or메서드)를 만드는 게 가능할까?

 

정답은 '아니오'다. 말 만들어도 말이 안 된다.  정수형 타입을 위해서 만든 클래스에 String 타입이 들어간다고 생각해보자. 생성 자체엔 문제가 없을지 몰라도, 경우에 따라 Type safety를 보장하지 않는 경우가 생길 수 있다.

 

그래서 타입의 제네릭 파라미터에 어느 정도 제약을 걸어두겠다는 것이 제네릭 타입 제한에 관한 내용이다.

 

타입은 다음과 같이 두 종류로 제한조건을 걸 수 있다.

 

  1. < T extends K>
  2. < T super K >

두 제한 조건을 설명하기 위해 다음과 같은 상속관계를 들고 나왔다. 

 

 

<T extends K>

  제네릭 파라미터 T가 K의 자식 타입이라면, 제네릭 타입으로 사용 가능함

K 보다 부모는 올 수 없으므로, 상한 경계를 의미한다. 

즉, extneds K에는 타입 파라미터로 올 수 있는 가장 넓은 범위의 최상위 타입이 기술된다.

 

 

<T super K>

 제네릭 파라미터 T가 K의 부모 타입이라면, 제네릭 타입으로 사용 가능함

K 보다 자식은 올 수 없으므로, 하한 경계를 의미한다.

즉, super K에는 타입 파라미터로 올 수 있는 가장 작은 범위의 최하위 타입이 기술된다. 

 

 

cf) 와일드카드

 레퍼런스를 보다 보면, 제네릭 파라미터에 물음표가 들어가 있는 것을 본 적이 있을 것이다. 와일드카드는 해당 타입 파라미터에 어떤 데이터 타입이든 들어올 수 있음을 말한다. 다시 말해, 뭐든 들어갈 수 있다는 의미이다.

이제 위에서 사용했던,  Collections.sort 메서드 API를 다시 보자. 

최종 보스

 해당 메서드에 타입 파라미터인 Comparator <? super T> c를 해석해본다면, '리스트와 같은 데이터 타입 이상의 부모 타입으로 정의된 Comparator 임의의 객체' 정도가 되겠다.

 

 

5. 예제

 제네릭 학습을 위해 간단한 예제를 만들어 보았다.

 인간과 오크 두 종류의 캐릭터가 있다. 각 종족마다 군대를 만들고, 해당 군대에 공격을 명령하면, 모두가 공격하게 만들어보려고 한다. 이때의 다이어그램은 아래와 같다.

 

클래스 다이어그램

 추상 클래스 캐릭터를 통해 인간과 오크라는 구체화된 클래스를 만들었고, 해당 캐릭터가 지닐 무기 인터페이스와 구체적인 무기 클래스들을 구현하였다. GenericApp이라는 클래스의 main 메서드에서 군대를 만들고, 출력해보자.

 

 필자는 캐릭터가 만드는 군대는 종족이 다를 뿐 같다고 생각했으므로, MyArmy라는 클래스를 만들었다. 해당 종족만 군대에 들어갈 수 있되, 최소 Character이하의 클래스라는 상한 경계를 만들었다. 

import java.util.ArrayList;

//군대 조직 클래스 캐릭터 이하의 자식클래스만 파라미터로 가능하다.
public class MyArmy<T extends Character>{
    private ArrayList<T> people;
	//생성자
    public MyArmy() {
        this.people = new ArrayList<>();
    }

	//해당 타입의 캐릭터
    public void addSolider (T one){
        people.add(one);
    }

	//캐릭터 클래스는 추상클래스이며 attack을 가지고 있으므로,
    //반드시 하위클래스에서는 attack override 되어있다.즉, 사용가능
    public void attackAll(){
        for (int i = 0 ; i < people.size();i++){
            people.get(i).attack();
        }
    }

	//해당 군인 선택
    public T getSolider(int i){
        return people.get(i);
    }

}

 

 이제 휴먼과 오크가 들어갈 수 있는 군대 클래스를 만들었다. 두 군대를 따로 만들 필요도 없으며, 설령 다른 캐릭터가 추가된다 하여도, 캐릭터를 상속받을 것이므로, 군대를 만드는 행위는 달라질 것이 없다. 제네릭을 이용해 이렇게 효율적으로 코드를 재사용할 수 있다.

 

public class GenericApp {

    public static void main(String[] args) {

        //오크 4명생성
        MyArmy<Orc> ors = new MyArmy<>();
        ors.addSolider(new Orc(new Axe()));
        ors.addSolider(new Orc(new Axe()));
        ors.addSolider(new Orc(new Sword()));
        ors.addSolider(new Orc(new Axe()));


        //휴먼 3명 생성
        MyArmy<Human> hus = new MyArmy<>();
        hus.addSolider(new Human(new Sword()));
        hus.addSolider(new Human(new Sword()));
        hus.addSolider(new Human(new Sword()));

		//오크 공격
        ors.getSolider(0).showSelf();
        ors.attackAll();

		//인간 공격
        hus.getSolider(0).showSelf();
        hus.attackAll();

    }
}

결과

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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
글 보관함