공부해봅시당

[JAVA] Generic을 간단하게 알아보자 본문

STUDY/JAVA

[JAVA] Generic을 간단하게 알아보자

tngus 2024. 3. 6. 20:07

1. 제네릭(Generic)이란?

자바 제네릭은 generic 타입 및 메소드를 정의하고 사용 할 수 있는 언어적 특성

List<Integer> list = new ArrayList<>();
Map<String, Integer> map = new HashMap<>();

위와같이 꺽쇠(< >)안에 클래스 타입이 명시된 패턴을 자주 발견할 수 있다. 

이걸 제네릭(Generic) 이라고 부르며, 제네릭 파라미터는 꺽쇠안에 포함하여 전달한다.

 

특징

- 파라미터 타입이나 리턴 타입에 대한 정의를 외부로 미룸
- 타입에 대해 유연성과 안정성을 확보함
- 런타임 환경에 아무런 영향이 없는 컴파일 시점의 전처리 기술

 

2. 제네릭을 왜 사용할까?

타입을 유연하게 처리하며, 잘못된 타입 사용으로 발생할 수 있는 런타임 타입 에러를 컴파일 과정에 검출한다.

자바 컴파일러는 코드에서 잘못 사용된 타입 때문에 발생하는 문제점을 제거하기 위해

제네릭 코드에 대해 강한 타입 체크를 한다.

 

실행 시 타입 에러가 나는것보다는 컴파일 시에 미리 타입을 강하게 체크해서 에러를 사전에 방지하는 것이 좋다.

또한 제네릭 코드를 사용하면 타입을 국한하기 때문에 요소를 찾아올 때 타입 변환을 할 필요가 없어 프로그램 성능이 향상되는 효과를 얻을 수 있다.

 

3. 제네릭 사용법

- 클래스, 인터페이스 또는 메소드에 선언할 수 있음
- 동시에 여러 타입을 선언할 수 있음
- 와일드 카드를 이용하여 타입에 대하여 유연한 처리를 가능하게 함
- 제네릭 선언 및 정의시에 타입의 상속 관계를 지정할 수 있음

제네릭 타입은 타입을 파라미터로 가지는 클래스와 인터페이스를 말한다.

제네릭 타입은 클래스 또는 인터페이스 이름 뒤에 < > 부호가 붙고 사이에 타입 파라미터가 위치한다.

public class MyClass<T>
public interface MyInterface<T>

 

타입 파라미터는 정해진 규칙은 없지만 일반적으로 대문자 알파벳 한글자로 표현한다.

 

자주 사용하는 타입인자

 

 

3-1. 제네릭 클래스

class ExClassGeneric<T> {
    private T t;

    public void setT(T t) {
        this.t = t;
    }
			
    public T getT() {
        return t;
    }
}

위와 같이 클래스를 설계할 때 구체적인 타입을 명시하지 않고 타입 파라미터로 넣어두었다가 

실제 설계한 클래스가 사용되어질 때 ExClassGeneric exGeneric = new ExClassGeneric<>(); 

이런식으로 구체적인 타입을 지정하면서 사용하면 타입 변환을 최소화 시킬 수 있다.

 

3-2. 제네릭 인터페이스

interface ExInterfaceGeneric<T> {
    T example();
}

class ExGeneric implements ExInterfaceGeneric<String> {

    @Override
    public String example() {
        return null;
    }
}

인터페이스도 위와 같이 클래스처럼 제네릭으로 설정해두고 활용할 수 있다.

3-3. 멀티 타입 파라미터 사용

class ExMultiTypeGeneric<K, V> implements Map.Entry<K,V>{

    private K key;
    private V value;

    @Override
    public K getKey() {
        return this.key;
    }

    @Override
    public V getValue() {
        return this.value;
    }

    @Override
    public V setValue(V value) {
        this.value = value;
        return value;
    }
}

 

타입은 두개 이상의 멀티 타입 파라미터를 사용할수 있고 이 경우 각 타입 파라미터를 콤마로 구분한다.

 

3-4. 제네릭 메소드

class People<T,M>{
    private T name;
    private M age;
	
    People(T name, M age){
        this.name = name;
        this.age = age;
    }

    public T getName() {
        return name;
    }
    public void setName(T name) {
        this.name = name;
    }
    public M getAge() {
        return age;
    }
    public void setAge(M age) {
        this.age = age;
    }
	
    //Generic Mothod
    public static<T,V> boolean compare(People<T,V>p1, People<T,V>p2) {
        boolean nameCompare = p1.getName().equals(p2.getName());
        boolean ageCompare =p1.getAge().equals(p2.getAge());
        return nameCompare && ageCompare;
    }
}

public class ExGeneric {
    public static void main(String []args){
        //타입 파라미터 지정
        People<String,Integer> p1 = new People<String,Integer>("Jack",20);
        //타입 파라미터 추정
        People<String,Integer> p2 = new People("Steve",30);
        //GenericMothod 호출
        boolean result = p1.compare(p1,p2);
        System.out.println(result);
    }
}

 

제네릭 메서드를 정의할때는 리턴타입이 무엇인지와는 상관없이 내가 제네릭 메서드라는 것을 컴파일러에게 알려줘야 한다.

그러기 위해서 리턴타입을 정의하기 전에 제네릭 타입에 대한 정의를 반드시 명시해야한다.

 

그리고 중요한 점이 제네릭 클래스가 아닌 일반 클래스 내부에도 제네릭 메서드를 정의할 수 있다.

그 말은, 클래스에 지정된 타입 파라미터와 제네릭 메서드에 정의된 타입 파라미터는 상관이 없다는 것이다.

 

3-5. 제네릭 와일드카드

public class Calc {
    public void printList(List<?> list) {
       for (Object obj : list) {
    	   System.out.println(obj + " ");  
       }
    }

    public int sum(List<? extends Number> list) {
      int sum = 0;
      for (Number i : list) {
    	  sum += i.doubleValue();  
      }
      return sum;
    }

   public List<? super Integer> addList(List<? super Integer> list) {
      for (int i = 1; i < 5; i++) {
    	 list.add(i); 
      }
      return list;
    }
}

 

와일드카드 타입에는 총 세가지의 형태가 있으며 물음표(?)라는 키워드로 표현된다.


- 제네릭타입<?>

   타입 파라미터를 대치하는 것으로 모든 클래스나 인터페이스타입이 올 수 있음
- 제네릭타입<? extends 상위타입>

   와일드카드의 범위를 특정 객체의 하위 클래스만 올 수 있음
- 제네릭타입<? super 하위타입>

   와일드카드의 범위를 특정 객체의 상위 클래스만 올 수 있음

4. 제네릭 사용 주의사항

4-1. 제네릭 타입의 객체는 생성 불가

제네릭 타입 자체로 타입을 지정하여 객체를 생성하는 것은 불가능하다.

즉, new 연산자 뒤에 제네릭 타입 파라미터가 올수는 없다.

class Sample<T> {
    public void someMethod() {
        // Type parameter 'T' cannot be instantiated directly
        T t = new T();
    }
}

 

 

4-2. static 멤버에 제네릭 타입이 올 수 없음

static 변수의 데이터 타입으로 제네릭 타입 파라미터가 올수는 없다. 

 

왜냐하면 static 멤버는 클래스가 동일하게 공유하는 변수로서 

제네릭 객체가 생성되기도 전에 이미 자료 타입이 정해져 있어야 하기 때문이다. 

즉, 논리적인 오류인 것이다.

 

 

4-3. 제네릭으로 배열 선언 주의점

기본적으로 제네릭 클래스 자체를 배열로 만들 수는 없다.

Java는 타입 소거(Type Erasure) 정책이 있기 때문이다.

 

타입 소거란 제네릭 정보가 컴파일 시에만 존재하고, 런타임에는 제네릭 타입이 일반 객체로 변환되어 처리되는 과정을 말한다.

이로 인해, 런타임에는 정확한 제네릭 타입 정보를 알 수 없게 되므로, 제네릭 타입의 배열을 직접적으로 생성하는 것은 허용되지 않는다.

class Sample<T> { 
}

public class Main {
    public static void main(String[] args) {
        Sample<Integer>[] arr1 = new Sample<>[10];
    }
}

new Sample<>[10]; 구문에서 발생하는 문제는 다이아몬드 연산자(<>)를 사용하여 제네릭 배열을 직접 생성하려고 시도하기 때문이다.

Java에서는 제네릭 타입의 배열을 직접 생성할 수 없으며,

이는 제네릭 타입 정보가 컴파일 후에 소거되기 때문에 런타임에 타입 안전성을 보장할 수 없다는 이유 때문이다.

배열은 런타임 시에 자신이 담고 있는 요소의 타입을 알아야 하는데, 타입 소거로 인해 이 정보가 런타임에는 존재하지 않게 된다.

 

 

 

하지만 제네릭 타입의 배열 선언 자체는 허용된다.

단, 직접적으로 제네릭 배열을 초기화하는 것은 불가능하며, 일반적으로 우회적인 방법을 사용해야 한다.

 

위의 식과 차이점은 배열에 저장할 Sample 객체의 타입 파라미터를 Integer 로 지정한다는 뜻이다. 

new Sample<Integer>() 인스턴스는 저장이 가능하며, new Sample<String>() 인스턴스는 저장이 불가능하다.

class Sample<T> { 
    // Sample 클래스 내용
}

public class Main {
    public static void main(String[] args) {
        // 제네릭 타입 Sample<Integer>의 배열 선언 및 비제네릭 배열로 초기화
        Sample<Integer>[] arr = new Sample[10]; 
        
        // 배열의 첫 번째와 두 번째 요소에 Sample<Integer> 인스턴스 할당
        arr[0] = new Sample<Integer>(); // 명시적 제네릭 타입
        arr[1] = new Sample<>(); // 다이아몬드 연산자를 사용하여 타입 추론
        
        // 컴파일 오류: 타입 불일치
        // arr[2] = new Sample<String>(); // 이 줄은 컴파일되지 않음
    }
}

 

다시 말해서, Java에서는 타입 안정성을 유지하기 위해 제네릭 타입의 배열 생성을 직접적으로 허용하지 않지만,

제네릭 타입의 배열을 선언하고, 이를 비제네릭 배열로 초기화한 다음 특정 타입의 제네릭 인스턴스를 할당하는 것은 가능하다.

 


참조

https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EC%A0%9C%EB%84%A4%EB%A6%ADGenerics-%EA%B0%9C%EB%85%90-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%B3%B5%ED%95%98%EA%B8%B0

 

☕ 자바 제네릭(Generics) 개념 & 문법 정복하기

제네릭 (Generics) 이란 자바에서 제네릭(Generics)은 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미한다. 객체별로 다른 타입의 자료가 저장될 수 있도록 한다. 자바에서 배

inpa.tistory.com

https://hahahoho5915.tistory.com/69

 

[간단정리] JAVA - 제네릭(Generic)이란?

개요 제네릭(Generic)에 대해 알아보기 제네릭(Generic)? 자바 제네릭은 generic 타입 및 메소드를 정의하고 사용 할 수 있는 언어적 특성입니다. List list = new ArrayList(); Map map = new HashMap(); 위와같이 꺽쇠

hahahoho5915.tistory.com

https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EC%A0%9C%EB%84%A4%EB%A6%AD-%EC%99%80%EC%9D%BC%EB%93%9C-%EC%B9%B4%EB%93%9C-extends-super-T-%EC%99%84%EB%B2%BD-%EC%9D%B4%ED%95%B4

 

☕ 자바 제네릭의 공변성 & 와일드카드 완벽 이해

자바의 공변성 / 반공변성 제네릭의 와일드카드를 배우기 앞서 선수 지식으로 알고 넘어가야할 개념이 있다. 조금 난이도 있는 프로그래밍 부분을 학습 하다보면 한번쯤은 들어볼수 있는 공변

inpa.tistory.com