제네릭 프로그래밍

LaStella·2022년 11월 25일
0

📌제네릭 자료형 정의

  • 클래스에서 사용하는 변수의 자료형이 여러개 일 수 있고, 그 기능(메서드)는 동일한 경우 클래스의 자료형을 특정하지 않고 추후 해당 클래스를 사용할 때 지정 할 수 있도록 선언합니다.
  • 실제 사용되는 자료형의 변환은 컴파일러에 의해 검증되므로 안정적인 프로그래밍 방식입니다.
public class GenericTest {
    public static void main(String[] args) {
        ObjectPrinter obejctPrinter = new ObjectPrinter();

        Powder powder = new Powder();
        obejctPrinter.setMaterial(powder); // Powder -> Object (형 변환)
        Powder p = (Powder)obejctPrinter.getMaterial();

        Plastic plastic = new Plastic();
        obejctPrinter.setMaterial(plastic); // Plastic -> Object (형 변환)
        p = (Powder)obejctPrinter.getMaterial(); // 컴파일 단계에서 에러 검출이 되지 않으나 런타임 에러 발생
				
        GenericPrinter<Powder> genericPrinter = new GenericPrinter();

		genericPrinter.setMaterial(powder);
        p = genericPrinter.getMaterial();
        genericPrinter.setMaterial(plastic); // 컴파일 단계에서 에러 검출
    }
}

class ObjectPrinter{
    private Object material;

    public void setMaterial(Object material) {
        this.material = material;
    }

    public Object getMaterial() {
        return material;
    }
}

class GenericPrinter<T>{
    private T material;

    public void setMaterial(T material) {
        this.material = material;
    }

    public T getMaterial() {
        return material;
    }
}

class Powder{ ... }

class Plastic{ ... }
  • 위의 예제와 같이 ObjectPrinter에서 material을 자바 객체의 최상위 클래스인 Object타입으로 지정하여 사용할 수 있습니다.
  • Object 타입을 사용하면 모든 종류의 자바 객체를 저장할 수 있지만, 원하는 타입으로 형 변환하여 사용해야 합니다.
  • 형 변환이 자주 사용되면 프로그램 성능이 저하되며 잘못된 형 변환을 시도하는 경우 컴파일에서 이를 검출할 수 없는 문제가 있습니다.
  • 제네릭을 사용하면 클래스 외부에서 타입을 지정해주기 때문에 별도의 타입 검사나 형 변환을 해줄 필요가 없습니다. 또한, 잘못된 타입이 들어오는 오류를 컴파일 단계에서 검출이 가능합니다.

📌제네릭 메서드

public static <T> T genericMethod(T param) { return param; }
  • 첫번째 T는 제네릭 타입, 두번째는 리턴타입이며 세번째는 파라미터 타입입니다.
public class GenericPrinter<T>{
    static T material; // 컴파일 에러
    
    static T getMaterial(T material) { // 컴파일 에러
        return material;
    }

		static <T> T getMaterial(T material) {
        return material;
    }
}
  • 제네릭 클래스에서 static 변수와 메소드에는 제네릭을 사용할 수 없습니다. GenericPrinter가 인스턴스 되기 전에 static은 메모리에 올라가야 하나 타입 T가 정해지지 않았기 때문입니다.
  • 제네릭 메소드는 호출 시 매게 타입을 지정하기 때문에 static이 가능합니다.
  • 제네릭 타입은 리턴타입 이전에 <>(다이아몬드 연산자)으로 선언합니다.
  • 클래스의 제네릭 타입이 전역변수처럼 사용된다면 메소드의 제네릭 타입은 해당 메소드에서만 사용할 수 있는 지역변수와 같습니다. 따라서 GenericPrinter<T>에서 <T>와 getMatarial 메소드에서의 <T>는 별개의 타입입니다.

📌제한된 제네릭

  • 제네릭 타입(T)의 범위를 제한할 수 있습니다.
  • 제한에는 extends(상한제한), super(하한제한), ?(와일드카드)가 사용됩니다.

📖<T extends S>, <? extends T>

<T extends S>	// T는 S와 S의 자식 타입이 가능하며 T의 타입을 참조할 수 있습니다.
<? extends S>	// S와 S의 자식 타입이 가능합니다.

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
    List<String> list2 = new ArrayList<>(Arrays.asList("1", "2", "3"));
        testMethod1(list);
        testMethod1(list2); // 컴파일 에러
}

public static <T extends Number> void testMethod1(List<T> list) {
    T element = list.get(0);
}

public static void testMethod2(List<? extends Number> list) {
    Number element = list.get(0);
}
  • 두 메소드 모두 받는 타입을 Number와 이를 상속하는 자식으로 제한하였습니다. Number 클래스의 자식인 Integer타입 리스트인 list를 매개변수로 testMethod1호출하여도 문제없지만, String타입 리스트인 list2를 매개변수로 호출하는 경우 String 클래스는 Number 클래스의 자식이 아니므로 컴파일 에러가 발생합니다.
  • 두 메소드는 모두 list에서 첫번째 값을 가져오며 첫번째 메소드는 타입 T를 지정하여 지역변수에 저장할 수 있으나 두번째 메소드는 와일드카드를 사용하므로 Number 클래스 또는 Number 클래스의 자식인 것만을 알 수 있어 Number 타입으로 저장할 수 있습니다. 물론 객체 최상위 클래스인 Object타입도 가능합니다. 특정 타입을 지정하여 사용하기를 원한다면 <T> 타입을 사용해야합니다.
  • 타입을 제한하여 상위 타입에서 선언하거나 정의하는 메서드를 활용할 수 있습니다. 위의 예제에서는 Number의 메소드를 사용할 수 있습니다.

📖<T>와 <Object>, <?> 의 차이

<?>		// 모든 타입 가능합니다. <? extends Object>와 같은 의미입니다.

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
	testMethod1(list);
    testMethod2(list); // 컴파일 에러
    testMethod3(list); 
}

public static <T> void testMethod1(List<T> list) {
    list.add(list.get(0));
}

public static void testMethod2(List<Object> list) {
    list.add(list.get(0));
}

public static void testMethod3(List<? > list) {
    list.add(list.get(0));
}
  • 3개의 메소드는 모두 list에서 첫번째 값을 list에 넣는 메소드로 모두 어떠한 타입이 전달되더라도 오류가 안날거같지만 아닙니다.
  • List<Object>와 List<Integer> 전혀 다른 타입이며 서로 상속관계가 성립하지 않습니다. 따라서 메소드를 호출하는 부분에서 컴파일 에러가 발생합니다.
  • List<?>는 타입에 대해 확정하지 않으며 타입을 신경쓰지 않습니다. list에 get을 호출하여 첫번째 값을 가져와도 타입을 알 수 없으며, 이를 list에 add로 삽입하면 알 수 없는 타입의 값을 알 수 없는 타입의 List에 넣는 것이므로 컴파일 에러가 발생합니다.

📖<? extends T>, <? super T> 상한제한과 하한제한의 차이

<T super S>	// 불가능한 타입입니다.
<? super S>	// S와 S의 부모 타입이 가능합니다.

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
		List<Object> list2 = new ArrayList<>(Arrays.asList(1, 2, 3));
    testMethod1(list);
    testMethod2(list); // 컴파일 에러
    testMethod2(list2);
}

// 상한제한
public static void testMethod1(List<? extends Number> list) {
    Number element = list.get(0);
    Number n = new Integer(1);
    list.add(n); // 컴파일 에러
}

// 하한제한
public static void testMethod2(List<? super Number> list) {
    Object element = list.get(0);
    Number n = new Integer(1);
    list.add(n);
}
  • < ? super Number>하한 제한이므로 가능한 타입은 Number 클래스와 Number 클래스의 부모 타입입니다.
  • 요소를 Number 타입으로 저장한다면 Object타입으로 들어오는 경우를 저장할 수 없으므로 컴파일 에러가 발생합니다.
  • 상한제한에서 list는 Number클래스의 자식타입일 수 있기때문에 Number객체 n을 넣을 수 없지만, 하한제한에서 list는 Number클래스가 최하위클래스이므로 n을 넣을 수 있습니다.

📖<T super S>가 불가능한 이유

class TestClass<T super Number> {
    T item;
}
public static <T super Number> void testMethod(List<T> list) {
    T item;
}
  • T에는 Number 클래스와 Number 클래스의 부모만이 올 수 있으므로 최대 Object 클래스가 올 수 있습니다.
  • T에는 항상 최상위 클래스가 지정되므로 super를 사용하여 타입을 제한하는 것이 의미가 없게 됩니다. 따라서 JAVA는 super로 타입을 지정하는 것을 허용하지 않습니다.

📖<? super T>를 사용하는 이유

// Collections 클래스의 sort 메소드입니다. 
public static <T extends Comparable<? super T>> void sort(List<T> list) {...}
  • <T extends Comparable>에서 T객체는 Comparable을 구현한 객체라는 것을 의미합니다.
class Powder implements Comparable<Powder>{
    @Override
    public int compareTo(Powder o) { ... };
}
  • 예를 들어 T객체에 Powder타입을 넣는다면 Powder클래스는 Comparable<Powder>의 하위 클래스가 되어야 하므로 Comparable을 구현해야 한다는 의미입니다.
  • 위 경우 Comparable<T>로 제한하여도 문제가 없으나 Comparable<? super T> 로 제한하는 이유는 다음과 같습니다.
class SortClass {
    public static <T extends Comparable<T>> void sort(List<T> list){ ... }
    public static <T extends Comparable<? super T>> void sort2(List<T> list){ ... }
}

class Material { ... }
class Powder extends Material implements Comparable<Material>{
    @Override
    public int compareTo(Material o) { ... }
}

public static void main(String[] args) {
    List<Powder> list = new ArrayList<>();
    SortClass.sort1(list); // 컴파일 에러
    SortClass.sort2(list);
}
  • Powder클래스가 Material클래스를 상속받으며 Comparable의 구현부인 compreTo에서 Material 타입으로 업캐스팅한 객체를 비교하는 경우입니다.
  • Powder 타입 객체가아닌 업캐스팅한 Material 타입 객체를 비교하므로 정렬이 안되거나 에러가 발생할 수 있습니다.
  • <T extends Comparable<? super T>>는 T클래스 또는 T클래스의 부모클래스가 Comparable을 의무적으로 구현해야한다는 뜻으로 T타입이 부모클래스타입으로 업캐스팅이 발생하더라도 안정성을 보장받을 수 있습니다.

참고글

자바 제네릭(Generics) 기초 https://tecoble.techcourse.co.kr/post/2020-11-09-generics-basic/
자바 [JAVA] - 제네릭(Generic)의 이해 https://st-lab.tistory.com/153
[Java] 제네릭 메소드(Generic Method)란? https://devlog-wjdrbs96.tistory.com/201
제네릭이란? https://vvshinevv.tistory.com/54
제네릭에서 T super ... 사용이 불가능한 이유 Generic-Type Erasure https://velog.io/@sunaookamisiroko/%EC%A0%9C%EB%84%A4%EB%A6%AD-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%97%90%EC%84%9C-super-%EC%82%AC%EC%9A%A9%EC%9D%B4-%EB%B6%88%EA%B0%80%EB%8A%A5%ED%95%9C-%EC%9D%B4%EC%9C%A0-Generic-Type-Erasure

profile
개발자가 되어가는 중...

0개의 댓글