Java

[Java] 자바 제네릭(Generic)이란, 제네릭 쉽게 이해하기

tmkimm 2023. 8. 8. 15:28

평소 제네릭을 사용은 했었지만 왜 사용하는 것인지, 어떤 역할인지 이해하고 있지 않은 것 같아 완벽히 이해하는 것을 목표로 정리해 봤습니다.

 

제네릭이란?

제네릭은 클래스, 메소드에서 사용할 타입을 외부에서 주입받는 것을 말합니다.

우리가 HashMap에서 <String>으로 사용했던 것이 바로 제네릭입니다.

HashMap<String, String> map = new HashMap();

ArrayList<String> arrList = new ArrayList<>();

정의로는 제네릭이 무엇인지 감이 안 오지만 예제를 통해 쉽게 이해할 수 있습니다.

예를 들어, 우리가 숫자 리스트를 저장할 수 있는 ArrayList라는 클래스를 만들었다고 가정하겠습니다.

public class main{
    public static void main(String[] args)  {
			ArrayList arrList = new ArrayList();
			arrList.add(100); // int만 가능
			arrList.add(200);
			arrList.add(300);
		}
}

public class ArrayList implements List {
	int[] elementData;
	public boolean add(int e) { ... } // int
	public int set(int index, int e) { ... }
}

 

기능이 변경되어 String이나 사용자 정의 클래스를 ArrayList에 담을 수 있도록 변경하려면 어떡할까요?

타입 별로 클래스를 만들거나 Object 객체(모든 객체의 조상)를 사용할 수도 있겠지만 비효율적입니다.

이때 필요한 게 제네릭입니다. 제네릭을 이용하면 타입을 클래스 내부에서 지정하는 것이 아니라 외부에서 타입을 지정하게 할 수 있습니다.

클래스 선언 시 제네릭 **<E>**를 통해 외부에서 전달받을 타입을 지정하면 클래스 영역 안에서 타입을 사용할 수 있습니다.

제네릭 클래스는 객체 생성 시 타입을 꼭 전달해야 합니다.

public class main{
    public static void main(String[] args)  {
			ArrayList<int> intList = new ArrayList<int>();
			intList.add(100); 

			ArrayList<String> strList = new ArrayList<String>();
			strList.add("aaa"); 
		}
}

public class ArrayList<E> implements List<E> {
	public boolean add(E e) { ...	}  // int e가 아닌 E e
	public void set(E e) { ... }
}

 

제네릭의 선언

우리가 자주 사용하는 HashMap의 실제 클래스 선언 부분을 보면 public class HashMap<K,V>로 K, V 타입을 선언하고 있습니다.

HashMap<Int, String> map = new HashMap(); 으로 HashMap 객체를 생성할 경우 K = Int, V = String 타입이 됩니다.

public class HashMap<K,V> { // 불필요한 부분 생략
		public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
}

제네릭은 어떤 문자로든 사용할 수 있고 주로 의미를 알 수 있는 영어 1글자 대문자를 사용합니다.

  • T : type
  • E : Element
  • K : key
  • V : value
  • N : number
ClassName<String> a = new ClassName<String>();

class ClassName<홍길동> {    // 에러 X
}

클래스에서 선언된 제네릭은 클래스 영역 어디서든 사용할 수 있습니다.(코드 출처)

// 제네릭 클래스
class ClassName<E> {
    private E element;    // 제네릭 타입 변수

    void set(E element) {    // 제네릭 파라미터 메소드
        this.element = element;
    }

    E get() {    // 제네릭 타입 반환 메소드
        return element;
    }
}

class Main {
	public static void main(String[] args) {
		
		ClassName<String> a = new ClassName<String>();
		ClassName<Integer> b = new ClassName<Integer>();
		
		a.set("10");
		b.set(10);
   }
}

제네릭을 사용하는 이유

  1. 재사용성이 증가하며 타입 변환, 타입 검사에 들어가는 노력을 줄일 수 있다.
    JDK 1.5에 제네릭이 도입되었고 그 전까지 여러 타입을 사용하는 클래스나 메소드에서 모든 객체의 조상인 Object 타입을 사용했습니다. Object 객체를 반환받을 경우 원하는 타입인지 체크해야하기 때문에 번거럽고, 변환하는 과정에서 에러가 발생할 수 도 있습니다.
    제네릭을 사용할 경우 미리 타입이 정해지므로 번거로운 작업을 생략할 수 있습니다.
  2. 클래스, 메소드에서 사용되는 타입 안정성을 높일 수 있다.
    제네릭을 이용하면 타입에 조건을 줄 수 있습니다.

 

타입의 제약

extends와 super를 이용해 제네릭에 사용 가능한 타입에 제약을 줄 수 있습니다.

타입 제약을 이용하여 안정성을 높일 수 있고 컴파일 단계에서 에러를 발견할 수 있습니다.

  • <E extends A> : E는 A의 Child만 가능
  • <E super A> : E는 A의 Parent만 가능
  • <? super A> : A의 Parent만 가능. 타입을 지정하지 않아 참조는 불가능하지만 제약은 줄 수 있음

 

 

 

참고글