본문 바로가기

Java

[Java] 제네릭에 대한 공부

제네릭을 공부하기 전에 다형성에 대해서 알아야 한다. 다형성에 대해서 모른다면 이 글을 참고 하도록 하자.

이제 다형성을 알고 있다고 생각하고 제네릭에 대해서 설명하겠다.

 

제네릭이란?

클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법이다.

이렇게만 보면 이게 무슨 소리지? 또는 어떻게 이게 가능하지?라는 생각을 하게 된다. 

ArrayList<Integer> list=new ArrayList<Integer>();

위 코드를 보면 Integer을 감싸고있는 <>가 사용한 클래스를 제네릭 클래스라고 한다. 

public class ExGeneric<T>{

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

}

다른 코드들을 보다보면 위 코드처럼 <T>라는 코드를 봤을 텐데 이게 바로 타입매개변수이다. 이런 T에는 Integer, String같이 미리 타입을 정하지 않는다. 여기서 T를 타입 매개변수라고 한다. 이 타입 매개변수는 이후에 원하는 Integer, String으로 타입을 정할 수 있다.

왜 필요한가?

그냥 Object를 사용하면 이 문제를 해결할 수 있지 않을까? Object는 모든 클래스의 부모니까 이게 더 편한거 아닐까?라는 생각을 할 수 있다.

public class ExObject{

    private Object object;
    
    public void set(Object object){
    	this.object=object;
    }
    
    public Object get(){
    	return object;
    }
}

위 코드는 제네릭 역할을 하는 Object로 만든 클래스 코드이다.

public class ObjectMain{
	
    public static void main(String[] args){
    	ExObject integerObject = new ExObject();
        integerObject.set(10);
        Integer integer = (Integer) integerObject.get(); 
        
        integerObject.set("문자입니다.");
        Integer string = (Integer) integerObject.get();// String문자열 이기에 예외 출력
        
    }
}

위 코드에서 보듯이 Object클래스를 사용하는데 문제는 이 데이터를 넣을때 문제가 생긴다. 제네릭에서는 무슨 값을 넣어도 그 값 타입으로 변경되었지만 Object에서는  (Integer) integerObject.get()에서처럼 코딩을 짤 때 미리 선언을 한다. 그런데 그 값 타입이 아닌 값이 들어오면 예외가 발생한다. 그래서 이 문제를 해결하기 위해서 제네릭이 필요하다.

 

타입 추론

ExGeneric<String> str = new ExGeneric<String>(); 
ExGeneric<String> strNull = new ExGeneric<>();

위 코드에서 보듯이  new ExGeneric<String>() 처럼 직접 타입을 입력할 수 있지만 new ExGeneric<>() 에서 처럼 왼쪽에 <String>을 보고 오른쪽 <>은 타입 정보가 생략이 가능하다. 이렇게 생략이 가능한걸 타입 추론이라고 한다.

 

타입 매개변수 제한

코드를 사용하다 보면 실제로 사용하려는 타입 매개변수의 메서드를 사용해야하는경우가 생긴다. 이때 제네릭은 이를 해결하기 위해서 타입 매개변수에 제한을 하면 된다.

public class Product{

    private String name;
    private int price;
    
    public void set(String name, Long price){
    	this.name=name;
        this.price=price;
    }
    
    public String getName(){
    	return name;
    }
    
    public int getPrice(){
    	return price;
    }
}

public class Snack extends Product{
	
    public Snack(String name, int price){
    	super(name, price);
    }
}
public class Candy extends Product{
	
    public Candy(String name, int price){
    	super(name, price);
    }
}

위 코드는 예시를 들기위한 예제 코드이다. Snack, Candy 클래스는 Product 클래스는 상속하고 있다. 

public class Store<T extends Product>{ // 타입을 Product 클래스와 그 하위 타입으로 제한

    int T product;
    
    public void set(T product){
    	this.product=product;
    }
    
    public T getExpensivePrice(T target){
    	return product.price() > target.price() ? product : target;
    }
}

public class Main{
	public static void main(String[] args){
    	Store<Snack> store = new Store<>();
        
        Snack snack = new Snack("과자",1000);
        
        store.set(snack); 
        store.getName(); // Product를 상한으로 제한 하고있기에 사용이 가능하다.
    }
}

 

위 코드에서 getName은 Product에서 가지고 있는 메서드이다. Store에서는 존재하지 않는다. 원래는 사용하지 못하지만 여기 코드에서는 extends를 이용하여 제네릭이 Product 클래스와 그 하위 타입만 받도록 범위를 제한했다. 그래서 Product에 메서드인 getName() 메서드가 사용 가능하다.

제네릭 메서드 

제네릭은 메서드에서도 사용이 가능하다.

public class Method{
    public static <T> T get(T t){
        return t;
    }

    public static <T extends Product> T getProduct(T t){
        return t;
    }
}

제네릭 메서드는 특정 메서드 단위로 제네릭을 도입할 때 사용한다. 제네릭 메서드는 <T> T로 선언한다. 또한 제네릭 메서드는 제한도 가능하다.

public static void main(String[] args){
	Integer result = Method.<Integer>get(10);
	String resultStr = Method.get("문자열 입니다.");
	Product product = Method.<Product>getProduct(new Product("과자",1000));
}

위 제네릭 메서드를 호출하는 값을 보면 <Integer>처럼 중간에 값을 넣어줘야 하지만 타입 추론으로 들어갈 값의 타입을 미리 알기에 생략이 가능하다.

공변과 불공변

공변 : A가 B의 하위 타입일때, T <A>가 T <B>의 하위 타입인 경우

불공변 : A가 B의 하위 타입일때, T <A>가 T <B>의 하위 타입이 아닌 경우

위에 대한 설명으로만 보면 이해하기 어렵다. 

public void example1(){
    String[] str = new String[]{"하위","타입","입니다"}
    printArr(str);
}

public void printArr(Object[] objects){
    for(Object object : objects){
    	System.out.println(object);
    }
}

위 코드처럼 String은 Object의 하위 타입이다. printArr메서드를 호출하면 파라미터가 Object 배열이지만 배열은 공변이기에 호출이 가능하다.

public void example2(){
    List<String> str = List.of("하위","타입","입니다");
    printList(str); //컴파일 에러 발생
}

public void printList(List<Object> objects){
    for(Object object : objects){
    	System.out.println(object);
    }
}

위 코드처럼 String은 Object의 하위 타입이다. 하지만 printList 메서드를 호출하면 아래처럼 예외를 발생한다. 이는 제네릭이 불공변이기 때문에 호출이 불가능하다.

와일드카드 

위와 같은 문제를 해결하기 위해서 나온 게 모든 타입을 대신할 수 있는 와일드카드 타입이다. 와일드카드는 컴퓨터 프로그래밍에서 *,? 와 같은 하나 이상의 문자들을 상징하는 특수 문자를 뜻한다. 

public <T> void printGeneric(Store<T> store){
	System.out.println("T =" + store.get());
}

public void printWildcard(Store<?> store){
	System.out.println("? =" + store.get());
}

위 코드처럼 와일드카드를 표현할 수 있다. 와일드카드인 ?는 모든 타입을 받을 수 있다.  이를 비제한 와일드카드라고 한다.

public void printList(List<?> objects){
    for(Object object : objects){
    	System.out.println(object);
    }
}

와일드카드를 사용하면 제네릭이 불공변이여서 불가능했던 코드가 사용이 가능하다.

public void printWildcardExtends(Store<? extends Product> store){ //상한 경계 와일드카드
    Product product=store.get();
    Snack snack=store.get();
}

public void printWildcardSuper(Store<? super Product> store){ //하한 경계 와이드카드
    Product product=store.get();
    Snack snack=store.get(); // 컴파일 에러
}

위 코드처럼 extends는 상한 경계 와이드카드로 Product를 상속하는 클래스는 모두 받을 수 있다. 반대로 super는 하한 경계 와일드카드로 Product 타입의 상위 타입만 받을 수 있다. 즉 Store<Object>는 허용된다.

 

 

출처

https://www.inflearn.com/course/%EA%B9%80%EC%98%81%ED%95%9C%EC%9D%98-%EC%8B%A4%EC%A0%84-%EC%9E%90%EB%B0%94-%EC%A4%91%EA%B8%89-2/dashboard

https://mangkyu.tistory.com/241