林's

[JAVA] 제네릭 본문

프로그래밍/웹백앤드

[JAVA] 제네릭

풀림이 2019. 10. 6. 23:10

예를 들어, a + b 를 하는데 a와 b의 타입이 서로 다른 여러 함수를 오버로딩하고 싶을 때가 있다. 이를 일일이 오버로딩하는 것은 매우 번거로우므로, 언어적 차원에서 제네릭(일반화)이라는 문법을 통해 프로그래머들의 불편함을 해소시켜주고 있다.

이번 시간에는, 간단한 제네릭이 사용될 수 있는 곳과 제네릭의 와일드카드 기호('?')에 대해 살펴볼 것이다.

 

예제를 위해 JUnit 이라는 단위테스트 도구가 쓰인다. 잘 모르는 사람은 아래 링크를 통해 공부하고 오자.

http://www.nextree.co.kr/p11104/

 

1. 제네릭의 문법

우선 제네릭(일반화)란 무엇일까?

다음과 같이 두 정수를 더하여 정수로 된 결과를 반환하는 함수가 있다고 치자.

public int plus(int a, int b) {
    return a + b;
}

이 때, 정수 뿐만 아니라, 정수 + 실수, 실수 + 실수 연산도 가능하게 하려면, plus 함수를 그에 맞춰서 같은 이름으로 여러개를 만들어야한다. ( 이를 오버로딩이라고 한다. )

우리가 오버로딩 하려는 plus 함수의 형식을 보면,

반환 타입 int, 매개변수 a, b 가 필요하다는 것을 알 수 있다.

어떤 타입이든 일반적으로 저 자리는 변함이 없다.

이러한 원리에서 시작한 것이 바로 제네릭(일반화) 인 것이다.

제네릭을 사용하면 다음과 같이 우리의 귀찮음을 해소할 수 있다.

   public R plus(T a, U b) {
       return a + b;
   }

반환 값은 타입이 뭐가 됐든간에, R

매개변수도 뭐가 됐던 T와 U면 OK인 것이다.

cf. 단점
내부적으로는 모든 경우에 따른 plus 함수를 컴파일러가 만들어준다.
필요없는 메소드들도 생성되므로 프로그램의 크기가 커진다.

 

이제 제네릭이 어디에 쓰이는 지 충분히 이해를 했을 것이라 생각한다.

다음은, 제네릭을 class, method, interface 에 사용하는 방법을 알아보도록 하겠다.

 

 

2. 제네릭 클래스

  1. 타입변수(Type Variable)
class AnimalList <T> { ... }

< > 사이에 들어가는 TType variable (타입 변수) 라 칭한다.

T는 클래스 내부에서 어디든지 쓰일 수 있다.

 

  1. 타입제한자로서의 extends

    class AnimalList <T extends LandAnimal> { ... }

    제네릭에서 extends 는 상속의 extends 가 아닌, 타입 변수의 타입을 제한하는 용도로 쓰인다.

    다시 말해서, T로 LandAnimal 혹은 이를 상속한 객체들만 사용할 수 있다는 뜻이다.

이 때 extends 뒤에 인터페이스도 들어올 수도 있다.

주의할점은 이 때도 extends 를 쓴다는 사실이다.

즉, 인터페이스를 구현하듯이 implements 를 사용하지 않고 extends 를 쓴다.

 

아래는 이해를 돕기 위해, 온열동물(WarmBlood) 인터페이스의 구현체들로 타입을 제한한 코드이다.

   interface WarmBlood { ... }
   ...
   class AnimalList <T extends WarmBlood> { ... }

 

클래스인터페이스동시에 상속받고 구현해야한다면, 다음과 같이 & 기호를 사용하면 된다.

   class AnimalList <T extends LandAnimal & WarmBlood> { ... }

아래의 코드는 위의 설명들을 종합해 놓은 것이다.

개인의 IDE에 붙여넣기해서 눈디버깅을 하거나 따라쳐보면 좋을 것이다.

 

예를 들기위해, 동물 목록을 담는 클래스를 만들 것이므로 제네릭타입의 ArrayList를 멤버로 추가하겠다.

class AnimalList<T extends LandAnimal> {
    ArrayList<T> list = new ArrayList<>();
    ...
}

육지동물과 이를 상속한 클래스를 정의하겠다.

class LandAnimal {
    public void crying() {
        System.out.println("육지동물의 울음소리");
    }
}
class Cat extends LandAnimal {
    public void crying() {
        System.out.println("냥~");
    }
}
class Dog extends LandAnimal {
    public void crying() {
        System.out.println("멍!");
    }
}

 

첫 번째로 보여준 코드를 완성해보면 다음과 같다.

class AnimalList<T extends LandAnimal> {
    ArrayList<T> list = new ArrayList<>();

    void add(T animal) {
        list.add(animal);
    }

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

    boolean remove(T animal) {
        return list.remove(animal);
    }

    int size() {
        return list.size();
    }
}

 

마지막으로 JUnit 으로 테스트를 해보자.

public class GenericTest {

    @Test
    public void genericTest() {
        AnimalList<LandAnimal> landAnimals = new AnimalList<>();
        landAnimals.add(new LandAnimal());
        landAnimals.add(new Cat());
        landAnimals.add(new Dog());
//        landAnimals.add(new Baby());

        for (LandAnimal anim : landAnimals.list) {
            anim.crying();
        }
    }
}

주석으로 처리한 부분은, 컴파일 타임에 에러가 나는 곳이다. extends 키워드를 사용하여 타입제한을 걸어두었으므로, LandAnimal 이 아닌 객체는 넣을 수 없다.

 

출력을 해보면 다음과 같이 육지동물들의 울음소리?를 들을 수 있다.

육지동물의 울음소리
냥~
멍!

 

 

3. 제네릭 메소드

제네릭 메소드는 다음과 같이 타입변수를 클래스 전체에 적용하지않고

특정 메소드 안에서만 제한적으로 사용하기 위해 쓰인다.

사용법은, 함수의 선언부에서, 반환 타입 앞에 타입 변수를 적어주면 된다.

   public static <T> void sort(...) { ... }

 

예). 아래의 예제에서 AnimalList의 T와 sort함수의 T가 서로 별개의 것임에 유의하면 된다.

   class AnimalList<T> {
       ...
       public static <T> void sort(List<T> list, Comparator<? super T> comp) {
           ...
       }
       ...
   }

이제 위의 예제에서 등장한 ?가 무엇인지 자연스럽게 궁금증이 생길것이다.

이를 무엇이든지 올 수있다는 의미에서 와일드 카드라고 칭한다.

 

 

4. 와일드 카드 ( <?> )

제네릭에서 와일드 카드의 의미는, 타입 변수로 모든 타입을 사용할 수 있다는 것을 의미한다.

빠른 이해를 위해, 위의 예제에서 다뤘던, AnimalList를 여러개 만들고 자신이 갖고 있는 동물들의 울음소리를 모두 출력해주는 static 함수를 만들어 호출해보겠다.

 

위에서 만든 AnimalList의 이름을 CryingAnimalList 로 바꾼 뒤, static 함수를 추가해주자.

   public static void cryingAnimalList(CryingAnimalList<? extends LandAnimal> animalList) {
       for (LandAnimal animal : animalList.list) {
           animal.crying();
       }
   }

이 함수는 ? 를 통해 CryingAnimalList의 타입변수로 아무거나 올 수 있지만,

extends 를 통해 그 중에서도 LandAnimal 의 자손들만 사용할 수 있도록 제한을 둔 것이다. 이 함수를 호출하면, 해당 리스트에 존재하는 동물들의 울음소리를 들려준다.

 

테스트는 다음과 같이 할 수 있을 것이다.

   CryingAnimalList<Cat> catList = new CryingAnimalList<>();
   CryingAnimalList<Dog> dogList = new CryingAnimalList<>();
   CryingAnimalList<Baby> babyList = new CryingAnimalList<>();
   catList.add(new Cat());
   dogList.add(new Dog());
   babyList.add(new Baby());

   CryingAnimalList.cryingAnimalList(catList);
   CryingAnimalList.cryingAnimalList(dogList);
   //CryingAnimalList.cryingAnimalList(babyList);

주석이 쳐진 곳은, babyList 가 extends 로 인해 컴파일 오류를 발생하는 부분이다.

 

여기까지 오느라 정말 고생이 많았다.

제네릭을 이해했다면, 다음에 공부할 내용은 바로 JAVA8에 제대로 도입이 된, 람다식과 스트림 문법이다!

궁금한 사람들은 아래 주소로 가서 공부하면 좋을 것이다.

http://wiki.sys4u.co.kr/pages/viewpage.action?pageId=7766426#FunctionalInterface%EC%99%80LambdaExpression-1.FunctionalInterface

 

http://wiki.sys4u.co.kr/pages/viewpage.action?pageId=7766426#FunctionalInterface%EC%99%80LambdaExpression-1.FunctionalInterface

페이지 … SYS4U OPEN WIKI Programming Tips JSDK 배너의 맨 끝으로 배너의 맨 처음으로 Functional Interface와 Lambda Expression 메타 데이터의 끝으로 건너뛰기 HS Kim님이 작성, 7월 18, 2017에 최종 변경 메타 데이터의 시작으로 이동 Introduction 이 글에서는 Java Standard Development Kit Specification 8(JSDK 8)에서 제안된 Functi

wiki.sys4u.co.kr

 

궁금한 점이나 잘못 된 부분이 있다면 언제든지 지적해주시면 감사하겠습니다. ^^

 

Comments