본문 바로가기
  • 오늘도 한걸음. 수고많았어요.^^
  • 조금씩 꾸준히 오래 가자.ㅎ
IT기술/JAVA

[Java] 제네릭스(Generics) 개념 정리하기

by 미노드 2023. 7. 21.

제네릭스(Generics)란?

다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시 타입체크를 해주는 기능이다.
객테의 타입을 컴파일 시 체크하기에 객체의 타입 안정성을 높인다.
지정되지 않은 타입의 객체가 저장되는 것을 막는다.

ArrayList같은 컬렉션 클래스는 다양한 종류의 객체를 담을 수 있다.
만약 한 종류의 객체만 담기를 원할 때 제네릭스를 사용하면
약속한 형식대로만 사용하도록 개발하다보니 이후에 꺼낼때 형변환이 필요 없어진다.

 

- 제네릭스(Generics)를 사용하지 않을 때

public static void main(String[] args) {
	ArrayList list = new ArrayList();
	list.add(10);
	list.add("10");

	String word = (String)list.get(1);
}
list 는 ArrayList로 이루어져 있으며 Object 타입이 들어가다보니, 객체를 꺼낼 때마다 필요한 형으로 변환이 필요하다.

 

- 제네릭스(Generics)를 사용할 때

public static void main(String[] args) {
	// 제네릭 사용
	ArrayList<String> list = new ArrayList<String>();
	list.add("10");
	list.add("20");

	String word = list.get(0);
}
제네릭스를 사용한다면 타입지정을 해주고, 원하는 곳에 사용할 때 형변환을 사용하지 않아도 된다.

 

제네릭스(Generics) 사용 장점

  • 타입 안정성을 제공한다. ( 코드를 잘못 입력하게 되면 컴파일 에러가 발생하므로 오류를 바로바로  체크 할 수 있다.)
  • 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해 진다.

 

1. 제네릭스 클래스 선언

제네릭 타입은 클래스와 메소드에 선언할 수 있다.
클래스에 선언하는 제네릭 타입이란?

1
2
3
4
5
6
class Box{
    Object item;
        
    void setItem(Object item){}
    Object getItem(){}
}
cs

위의 클래스는 일반적인 클래스이다.
이를 제네릭 클래스로 변경하고자 한다면 '<T>' 를 붙이고 Object는 모두 T로 변경해야한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Box<T> {
    ArrayList<T> list = new ArrayList<T>();
    
    void add(T item) {
        list.add(item);
    }
    
    T get(int i) {
        return list.get(i);
    }
    
    int size() {
        return list.size();
    }
    
    @Override
    public String toString() {
        return list.toString();
    }
 
}
cs
  • Box<T>에서 'T'를 타입변수( type variable )라고 하며 'Type'에서 첫글자를 따온 것이다.( 타입변수는 T가 아닌 다른 것을 사용해도 된다. )
  • 타입변수가 여러개인 경우에는 Map<K,V>와 같이 콤마를 구분자로 나열하면 된다.
  • T, K, V 이들은 기호의 종류만 다를 뿐, '임의의 참조형 타입'을 의미한다는 것은 모두 같다.
  • 기존에는 다양한 종류의 타입을 다루는 메소드의 매개변수나 리턴 타입으로 Object타입의 참조변수를 많이 사용했고, 그로 인해 형변환이 불가피했지만, 이젠 Object타입 대신 원하는 타입을 지정하기만 하면 되는 것이다.
Box b = new Box(); 는 Box<Object> b = new Box<>(); 와 같다.
b.setItem(new Object()); // 정상
b.setItem("ABC");        // 정상, Object Type이다보니 어느 형이든 들어가는게 가능


Box<String> b = new Box<String>(); // 객체생성 제제릭스 String 형으로 지정
b.setItem(new Object());    // 에러, Object Type은 들어갈 수 없다.
b.setItem("ABC");               // 정상
String item = b.getItem      // 형변환을 별도로 할 필요가 없다.

※ 제네릭스 용어 정리

class Box {}
Box<T> 제네릭클래스, 'T의 Box' 또는 'T Box'라고 읽는다.
T 타입변수 또는 타입 매개변수( T는 타입문자 )
Box 원시타입( raw type )
  • 타입문자 T는 제네릭 클래스 Box<T>의 타입변수 또는 타입 매개변수라고 하는데 메소드의 매개변수와 유사한 점이 있기 때문이다.
  • 그래서 아래와 같이 타입 매개변수에 타입을 지정하는 것을 '제네릭 타입 호출'이라고 하고 지정된 타입을 '매개변수화된 타입( parameterized type )'이라고 한다.
1
2
Box<String> b = new Box<String>();
Box<Integer> b1 = new Box<Integer>();
cs
  • 위처럼 Box<String>, Box<Integer>는 서로 다른 타입을 대입하여 객체를 만든 것이다.
    Box라는 같은 클래스를 활용해서 다른 타입의 객체를 만들 수 있음.

※ 제네릭스의 제한

    • 제네릭 클래스 Box의 객체를 생성할 때는 객체 별로 다른 타입을 지정해주는 것이 적절하다.
      왜? 제네릭은 인스턴스 별로 다르게 동작하도록 만든 것이기 때문이다.
    • 그러나 모든 객체에 대해 동일하게 동작해야 하는 static멤버에 타입변수 T를 사용할 수 없다.
      왜? T는 인스턴스 변수로 간주되기 때문이다.
  • static 멤버는 타입 변수에 지정된 타입의 종류에 관계없이 동일한 것이어야 하기 때문이다.
  • 또한 제네릭 타입의 배열을 생성하는 것도 허용되지 않는다.
  • 제네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만 new T[10]; 과 같은 배열을 생성하는 것은 안된다.
    ==> new 연산자로 인해 불가능한 것인데, new 연산자는 컴파일 시점에 타입 T가 무엇인지 정확히 알아야 하기 때문이다.
1
2
3
4
5
class Box<T>{
    static T item;           // 에러
    static int compare(T t1, T t2){};        // 에러
}
 
cs
  • 타입문자로 사용할 타입을 명시하면 한 종류의 타입만 지정할 수 있도록 제한할 수 있지만,
    그래도 여전히 모든 종류의 타입을 지정할 수 있다는 것에는 변함이 없다.
  • 타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한할 수 있는 방법도 있다.

    Box<Toy> toyBox = new Box<>();
    toyBox.add(new Toy());        // Ok, 과일상자에 장난감을 담을 수 있다.

  • 아래와 같이 제네릭 타입에 extends를 사용하면 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.

    class FruitBox<extends Fruit>{/* 내용 생략 */}

  • 여전히 한 종류의 타입만 담을 수 있지만, Fruit 클래스의 자손들만 담을 수 있다는 제한이 걸린다.

 

2. 와일드 카드( wild card ? 물음표 = 와일드카드 )

매개변수에 과일박스를 대입하면 주스를 만들어 반환하는 Jucier라는 클래스가 있고
이 클래스에는 과일을 주스로 만들어서 반환하는 static 메소드 makeJuice()가 있다고 가정하자.

1
2
3
class Juicer{
    static Juice makeJuice(FruitBox<T> box){}
}
 
cs

Juicer 클래스는 제네릭 클래스가 아닌데다가 제네릭 클래스라고 해도 static 메소드에는 타입 매개변수 T가 사용할 수 없으므로 아예 제네릭스를 적용하지 않던가 아래와 같이 매개변수 T 대신 특정 타입 Fruit를 정해줘야 한다.

1
2
3
4
5
6
7
8
9
10
class Juicer{
    static Juice makeJuice(FruitBox<Fruit> box){}
    // static Juice makeJuice(FruitBox<Apple> box){}    // 에러
}
 
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();        
FruitBox<Apple> appleBox = new FruitBox<Apple>();    
    
Juicer.makeJuice(fruitBox);        // 성공   
//Juicer.makeJuice(appleBox);        // 에러  
cs

이렇게 제네릭 타입을 'FruitBox<Fruit>'로 고정해 놓으면 위의 코드에서도 알 수 있듯이 'FruitBox<Apple>' 타입의 객체는 메소드의 매개변수가 될 수 없다.
여기서 FruitBox<Apple>' 도 매개변수로 사용하려면 오버로딩을 해야한다. 그러나.....

"제네릭 타입이 다른 것만으로는 오버로딩이 성립되지 않는다."라는 규칙때문에 우리가 생각한 것처럼 오버로딩을 하면 에러가 발생한다.
이럴 때 사용하는 것이 와일드카드이다. 와일드카드는 기호 ?를 사용한다.

1
2
3
4
5
6
7
8
9
class Juicer{
    static Juice makeJuice(FruitBox<extends Fruit> box){}    // Fruit 상속 받은 애 다 할 수 있음
}
 
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();        
FruitBox<Apple> appleBox = new FruitBox<Apple>(); 
   
Juicer.makeJuice(fruitBox);        // 성공 
Juicer.makeJuice(appleBox);        // 
cs
  • 와일드카드는 기호 ?로 표현하는데 와일드카드는 어떠한 타입도 될 수 있다.
  • ? 만으로는 Object 타입과 다를게 없으므로 아래와 같이 제한을 할 수 있다.

    <? extends T> 와일드카드 상한제한, T와 그 자손들만 가능
    <? super T> 와일드카드 하한제한, T와 그 조상들만 가능
    <?> 제한 없음 모든 타입 가능( <? extends Object> )

3. 제네릭 메소드

  • 메소드 선언부에 제네릭 타입이 선언된 메소드를 제네릭 메소드라 한다.
  • 제네릭 타입의 선언 위치는 반환 타입 바로 앞이다.
  • 보통 메소드는 제네릭스를 쓸 일이 없을텐데, 제네릭 메소드라는 것도 존재하긴한다.
  • 하나의 파라미터로 여러 파라미터가 들어오거나, 리턴을 여러 형식이 되도록 설정하는데 쓰인다.

    1
    static<T> void sort(List<T> list, Comparator<super T> c);
    cs
  • 제네릭 클래스에 정의된 타입 매개변수T제네릭 메소드에 정의된 타입 매개변수T는 전혀 별개인 것이다.
  • 같은 타입문자 T를 사용해도 같은 것이 아니다. 혼용되어 사용되는 경우도 있으니 주의해야 한다.
  • 메소드에 선언된 제네릭 타입은 지역변수를 선언한 것과 같다고 생각하면 편한데, 이 타입 매개변수는 메소드 내에서만 지역적으로 사용될 것이므로 메소드가 static이건 아니건 상관없다.
1
2
3
4
5
6
7
8
9
static Juice makeJuice(FruitBox<extends Fruit> box){}
 
static<extends Fruit> Juice makeJuice(FruitBox<T> box){}
 
FruitBox<Fruit> fruitBox = new FruitBox<>();
FruitBox<Apple> appleBox = new FruitBox<>();
 
Juicer.<Fruit>makeJuice(fruitBox);
Juicer.<Apple>makeJuice(appleBox);
cs

static<T extends Fruit> Juice 부분을 주목하자.
제네릭 메소드를 만들 때, 제네릭 타입의 선언 위치는 반환타입(Juice) 바로 앞이다.
이상한 위치인데, 쨋든 선언하면 파리미터 영역에서 Object T
여기서 extends Fruit 이면 메소드를 통해 Fruit 상속된 모든 객체들로 생성 가능하다는 뜻이다.

* 두 개는 같은 것이다.

static Juice makeJuice(FruitBox<extends Fruit> box){}
static<extends Fruit> Juice makeJuice(FruitBox<T> box){}

 

참조

https://devlog-wjdrbs96.tistory.com/201

 

[Java] 제네릭 메소드(Generic Method)란?

제너릭 메소드 제네릭 메소드는 메소드의 선언 부에 적은 제네릭으로 리턴 타입, 파라미터의 타입이 정해지는 메소드이다. 제너릭에 대한 예시를 보면서 이해해보자. public class Student { static T nam

devlog-wjdrbs96.tistory.com

https://tadaktadak-it.tistory.com/24

 

[JAVA] 제네릭스( generics )

1. 제네릭스( generics ) 제네릭스는 다양한 타입의 객체들을 다루는 메소드나 컬렉션 클래스에 컴파일 시 타입체크( compile-time type check )를 해주는 기능이다. 객체의 타입은 컴파일 시에 체크하기

tadaktadak-it.tistory.com

https://dahliachoi.tistory.com/m/46

 

JAVA :: 제네릭스(Generics) 개념 정리

제네릭스(Generics)란? 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시 타입체크를 해주는 기능이다. ArrayList같은 컬렉션 클래스는 다양한 종류의 객체를 담을 수 있는데 보통

dahliachoi.tistory.com