Dev/Java

지네릭스(Generics) - 지네릭스를 알면 API 문서를 읽기 쉽다!

린네의 2024. 3. 25. 16:56

 

 

예제 소스 링크

 

 

지네릭스를 왜 사용해야 할까?

 

지네릭스는 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입을 체크해 주는 기능이다. 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움을 줄여준다.

 

즉, 지네릭스를 사용하면 타입의 안정성을 제공받을 수 있고, 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해진다.

 

 

 

T(Type), E(Element), K(Key), V(Value)가 의미하는 것은 무엇일까?

이들은 기호의 종류만 다를 뿐 '임의의 참조형 타입'을 의미한다.  기존에는 다양한 종류의 타입을 다루는 메서드의 매개변수나 리턴타입으로 Object 타입의 참조변수를 많이 사용했고, 그로 인해 형변환이 불가피했지만 지네릭을 사용하면 Object 타입대신 원하는 타입을 마음대로 지정해서 사용할 수 있다.

 

다음 사용 예제를 보자.

package com.example.generics;

public class GenericsTest {

    public static void main(String[] args) {

        Box<String> b = new Box<String>();
        b.setItem((String) new Object());
        b.setItem("ABC");
        String item = b.getItem();

    }

}

class Box<T> {
    T item;

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

}

 

기존에 Object 가 하던 역할을 T를 선언함으로써 대체한 것을 볼 수 있다. 이렇게 되면 타입 캐스팅을 하는 과정이 줄어든다.

( 뒤에서 형반환에 대해 보다 자세하게 기술하겠지만 타입변수가 다른경우 캐스팅은 불가능하다. 캐스팅이 가능한 것은 box 안의 인자값에 해당한다.   Box<Fruit> b = new Box<Fruit>,  Box<Grape> g = (Box<Grape>)b 가 불가능 )

 

 

 

제네릭스 용어

  • Box <T> : 지네릭 클래스, 'T의 Box' 또는 'T Box'라고 읽는다.
  • T : 타입 변수 또는 타입 매개변수 (  T는 타입 문자 )
  • Box : 원시 타입 ( raw Type )
    Box<String> b = new Box<String>();

 

추가적으로  Box를 통해 실제 객체 b를 생성할 때 String을 지정했다. 여기서 String 을 parameterized type ( 매개변수화된 타입 ) 이라고 한다

 

 

 

Static과 Generics

static 은 모든 객체에 대해 동일하게 동작해야 한다. 따라서 static 멤버에 타입 변수 T를 사용할 수 없다.  T는 인스턴스 변수이기 때문이다. 

 

지네릭을 사용하는 이유가 객체별로 다른 타입을 지정하기 위해서이고 이는 인스턴스별로 다르게 동작할 수 있게 하기 위함인데 static 은 동일하게 동작하게 하기 위함이므로  서로 의미가 모순된다.

 

인스턴스가 변수에 대해서 간단하게 짚고 넘어가자. ( 해당 게시글은 제네릭이 주된 주제이므로 접은 글로 대신했다. ) 

 

더보기

변수의 종류는 선언된 위치에 따라 달라진다.

 

종류 선언 위치 생성시기(메모리 할당 시기)
클래스 변수 클래스 영역 클래스가 메모리에 올라갈 때
인스턴스 변수 클래스 영역 인스턴스가 생성될 때
지역 변수 클래스 이외의 영역(메서드,생성자,초기화 블럭) 변수 선언문이 수행 되었을 때 

 

class Variables 
{
	int iv; // 인스턴스 변수 
    static int cv; // 클래스 변수 ( static 변수, 공유 변수 )
    
    void method() {
    int lv = 0; // 지역 변수
    }
}

 

 

  • 클래스 변수 : 인스턴스 변수 앞에 static 을 붙이면 된다. 인스턴스마다 독립적인 저장공간을 갖는 인스턴스변수와는 달리, 클래스 변수는 모든 인스턴스가 공통된 저장공간을 공유하게된다. 클래스 변수는 인스턴스 변수와 달리 인스턴스를 생성하지 않아도 언제든지 바로 사용할 수 있는 특징이 있다.  public 과 같이 사용할 경우 같은 프로그램 내에서 어디서나 접근할 수 있는 전역변수 성격을 갖는다.

 

  • 인스턴스 변수 : 클래스 영역에 선언되며, 클래스의 인스턴스를 생성할 때 만들어진다. 그렇기 때문에 인스턴스 변수의 값을 읽어 오거나 저장하기 위해서는 먼저 인스턴스를 생성해야 한다. 인스턴스는 독립적인 저장공간을 가지므로 서로 다른 값을 가질 수 있다. 인스턴스마다 고유한 상태를 유지해야하는 속성의 경우 인스턴스 변수로 선언한다.

 

  • 지역 변수 : 메서드 내에 선언되어 메서드 내에서만 사용가능하며 메서드가 종료되면 소멸되어 사용할 수 없게 된다.

 

 

 

new 연산자

 

같은 맥락에서 지네릭 타입의 배열 생성도 허용되지 않는다.

참조 변수가 지네릭인 것은 가능하지만, 배열 자체를 생성할 수는 없다. new 연산자는 컴파일 시점에 타입 T 가 무엇인지 정확히 정의되어야 하기 때문이다. 같은 맥락에서 instanceof 연산자도 T를 피연산자로 사용할 수 없다.

 

   T[] itemArr;
    
    T[] toArray() {
        T[] tmpArr = new T[itemArr.length]; // 에러 발생 
  
        return tmpArr;
    }

 

 

만약 지네릭 타입의 배열을 반드시 생성하고 싶다면  ReflectionAPI의 newInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나 Object 배열을 생성해서 복사한 다음 T []로 형변환 해야 한다.

 

 

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

  • 객체 생성 시 참조변수와 생성자에 대입된 타입이 일치해야 한다. 
  •  생성자에 대입된 타입(매개변수화된 타입) 은 상속관계에 있다고 하더라도 오류가 발생한다.
Box<Apple> appleBox = new Box<Apple> // ok
Box<Apple> appleBox = new Apple<Grape> // error
Box<Fruit> appleBox = new Box<Grape> // error -> Fruit 의 자손이 Grape 라고 해도 허용되지 않는다.

 

 

  • JDK 1.7부터는 추정이 가능한 경우 타입을 생략할 수 있게 되었다.
Box<Fruit> appleBox = new Box<>();  // Box<Fruit> appleBox = new Box<Fruit>(); 와 동일함

 

 

  • 지네릭 클래스가 상속관계에 있으면 다형성에 의해 객체 생성이 가능하다. ( 매개변수(T) 타입은 일치해야한다 ! )
// FruitBox 는 Box 의 자손이다
Box<Apple> appleBox = new FruitBox<Apple>();

 

 

  • 타입 T의 자손들은 메서드의 매개변수가 될 수 있다. 
  • 매개변수의 다형성에 해당한다. 생성은 매개변수 타입이 완전히 일치해야하지만 내부에 있는 객체들은 다형성에 의해 생성될 수 있다.
// Apple 은 Fruit 의 자손이다.
Box<Fruit> fruitBox = new Box<Fruit>();
fruitBox.add(new Fruit()); //  ok
fruitBox.add(new Apple()); // ok

 

extends를 사용해 보자

extends를 사용하면 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.

 

다음코드는 Fruit 클래스의 자손들만 담을 수 있는 예제이다.

class FruitBox<T extends Fruit> {
	ArrayList<T> list = new ArrayList<T>();
}

 

클래스가 아니라 인터페이스를 구현해야 한다는 제약이 필요하다면 아래와 같이 사용한다. implements 대신 extends를 사용하는 것에 주의하자.

 

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

 

클래스 Fruit의 자손이면서 Eatable 인터페이스도 구현해야 한다면 & 기호를 사용할 수 있다.

 

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

 

 

와일드카드 -?

와일드카드로 검색하면 공변성 불공변성에 대한 내용이 많은데 나는 해당 단어가 어려워서 아래 내용으로 이해했다. 아래 내용을 이해한 뒤 공변성과 불공변성을 읽으면 보다 이해하기 쉽다.

 

얼핏 보기에는 FuritBox <Fruit>와 FuritBox <Apple>가 다른 매개변수로 인식되어 makeJuice라는 메서드가 오버로딩 된 것처럼 보이지만, 기본적으로 지네릭 타입은 컴파일러가 컴파일할 때만 사용하고 제거해 버린다.  그래서 아래 코드는 메서드 중복 정의에 해당한다.

static Juice makeJuice<FruitBox<Fruit> box) {
	String tmp = "";
    for(Fruit f : box.getList() tmp += f + "";
    return new Juice(tmp);
}


static Juice makeJuice<FruitBox<Apple> box) {
	String tmp = "";
    for(Fruit f : box.getList() tmp += f + "";
    return new Juice(tmp);
}

 

이럴 때 사용하기 위해 고안된 것이 '와일드카드' 개념이다. 기호로는 '?'로 표현한다. 와일드카드는 어떠한 타입도 될 수 있다.

 

'?' 만으로는 Object 타입과 다를 게 없으므로 extends, super로 상한과 하한을 제한할 수 있다.

< ? extends T> 와일드 카드의 상한 제한. T와 그 자손들만 가능.
< ? super T > 와일드 카드의 하한 제한. T와 그 조상들만 가능
<?> 제한 없음. 모든 타입이 가능함. ? extends Object 와 동일한 의미

 

단, 와일드카드에는 & 사용이 불가능하다.

 

일반적으로 Comparator을 사용할 때 <?  super T > 꼴을 볼 수 있는데  Comparator에 와일드카드를 사용하는 이유는 다음과 같다.

 

package com.example.generics;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

class Fruit2 {
    String name;
    int weight;

    public Fruit2(String name, int weight) {
        this.name = name;
        this.weight = weight;
    }

    public String toString() {
        return name + " (" + weight + ")" ;
    }
}


class Apple2 extends Fruit2 {
    Apple2(String name, int weight) {
        super(name, weight);
    }
}

class Grape2 extends Fruit2 {
    Grape2(String name, int weight) {
        super(name, weight);
    }
}

class AppleComp implements Comparator<Apple2> {
    public int compare(Apple2 t1, Apple2 t2) {
        return t2.weight - t1.weight;
    }
}

class GrapeComp implements  Comparator<Grape2> {
    public int compare(Grape2 t1, Grape2 t2) {
        return t2.weight - t1.weight;
    }
}

class FruitComp implements Comparator<Fruit2> {
    public int compare(Fruit2 t1, Fruit2 t2) {
        return t1.weight - t2.weight;
    }
}


public class GenericsTest3 {
    public static void main(String[] args) {


        FruitBox2<Apple2> apple2FruitBox = new FruitBox2<Apple2>();
        FruitBox2<Grape2> grape2FruitBox = new FruitBox2<Grape2>();

        apple2FruitBox.add(new Apple2("GreenApple", 300));
        apple2FruitBox.add(new Apple2("GreenApple", 100));
        apple2FruitBox.add(new Apple2("GreenApple", 200));

        grape2FruitBox.add(new Grape2("GreenGrape", 400));
        grape2FruitBox.add(new Grape2("GreenGrape", 300));
        grape2FruitBox.add(new Grape2("GreenGrape", 200));

        Collections.sort(apple2FruitBox.getList(), new AppleComp());
        Collections.sort(grape2FruitBox.getList(), new GrapeComp());
        System.out.println(apple2FruitBox);
        System.out.println(grape2FruitBox);
        System.out.println("=============");
        Collections.sort(apple2FruitBox.getList(), new FruitComp());
        Collections.sort(grape2FruitBox.getList(), new FruitComp());

        System.out.println(apple2FruitBox);
        System.out.println(grape2FruitBox);

    }
}
    class FruitBox2<T extends  Fruit2> extends Box2<T> {}
    class Box2<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();}

        ArrayList<T> getList() { return list;}
    }

 

 

위 코드는  Collections.sort()를 이용해 정렬을 하기 위한 예제이다.

 

sort() 메서드의 선언부는 다음과 같다.

  @SuppressWarnings({"unchecked", "rawtypes"})
    public static <T> void sort(List<T> list, Comparator<? super T> c) {
        list.sort(c);
    }

 

인수로 Comparator 가 필요한데, 만약 와일드카드를 사용하지 않는다면 List <T>가 가지는 매개변수 타입별로 Comparator을 모두 만들어야 할 것이다. 이 내용이 위에 구현된 AppleComp, GrapeComp에 해당한다. 지금은 단순하게 Apple, Grape로 나뉘었지만 만약 Fruit (부모) 하위에 있는 자식객체가 훨씬 많다면 그 자식객체만큼 '자식객체형태 + Comp'에 해당하는 메서드가 각각 필요한 상황이 만들어진다.  이런 문제를 해결하기 위해 와일드카드가 필요하다.

 

super 키워드는햐한제한으로,  Comparator <? super Apple2>는 Apple2와 조상이 모두 가능하다는 뜻이 된다.   즉, Comparater <Apple>, Comparator <Fruit>, Comparator <Object>가 해당한다. (? super T 꼴이 어렵다면 T로 치환해서 생각하면 쉽다! ) 

 

 

지네릭 메서드

 

메서드의 선언부에 지네릭 타입이 선언된 메서드를 지네릭 메서드라고 한다. 이 때 주의해야 하는 것이 지네릭 클래스에 정의된 타입 매개변수지네릭 메서드에 정의된 타입 매개변수는 전혀 별개의 것이라는 것이다. 같은 문자 T를 사용해도 완전히 별개의 것이다.

 

또한 static 멤버에는 타입 매개변수를 사용할 수 없지만, 메서드가 제네릭 타입을 선언하고 사용하는 것은 가능하다.

 

// 가능
class FruitBox<T> {  
	static <T> void sort(List<T> list, Comparator<? super T> c) {}
}

//에러
class Box<T> {
 static T item;
 static int compare(T t1, T t2) {}
 }

 

 

메서드에 선언된 지네릭 타입은 지역 변수를 선언한 것과 같다고 생각하면 이해하기 쉽다. ( 이 타입 매개 변수는 메서드 내에서만 지역적으로 사용될 것이므로 메서드가 static 이건 아니건 상관이 없다.)

 

예제 코드를 보자.

 

// ex1
static Juice makeJuice(FruitBox<? extends Fruit> box) {

	String tmp = "";
    for(Fruit f : box.getList()) temp += f + " ";
    return new Juice(tmp);
}


static <T extends Fruit> Juice makeJuice(FruitBox<T> Box) {
	String tmp = "";
    for(Fruit f : box.getList()) tmp += f + " ";
    return new Juice(tmp);
}

// ex2
public static void printAll(ArrayList<? extends Product> list, ArrayList<? extends Product> list2 {
 for(Uinit u : list ) {
 	System.out.println(u);
 }
}

public static <T extends Product> void printAll(ArrayList<T> list, ArrayList<T> list2) {
 for(Uinit u : list ) {
 	System.out.println(u);
 }
}


// ex3

public static <T extends Comparable<? super T>> void sort(List<T> list)
-> 타입 T 를 요소로하는 List 를 매개변수로 허용한다.
-> T 는 Comparable 을 구현한 클래스여야 함. 여기서 Comparable 은 T 또는 그 조상의 타입을 비교하는 Comparable 여야 함

 

지네릭 메서드로 검색하면 복사 붙여넣기한 것 처럼 다 동일한 내용만 나오고 정확하게 뭔지 시원하게 기재되어있는 글이 없어서 이해하는데 힘들었다. 내가 정리한 내용에 의하면 메서드에 정의한 타입 매개변수의 값을 앞에 미리 정의 해두는 것이다.

 

지네릭 타입의 형변환

  • 지네릭 타입과 원시 타입 간의 형변환은 가능하다. Box, Box <String> 또는 Box, Box<String>
  • 대입된 타입이 다른 지네릭 타입 간에는 형변환이 불가능하다. Box<String> , Box <Object>
  • Box <String> 은 Box <? extends Object>로 형변환이 된다.
// 매개변수로 FruitBox<Fruit>, FruitBox<Apple>, FuitBox<Grape> 등이 가능하다
static Juice makeJuice(FruitBox<? extends Fruit> box) {...}

FruitBox<? extends Fruit> box = new FruitBox<Fruit>(); // ok 
FruitBox<? extends Fruit> box = new FruitBox<Apple>(); // ok
FruitBox<? extends Fruit> box = new FruitBox<Grape>(); // ok