JAVA Generic 2 +

KUN·2025년 4월 1일

Java Generic

목록 보기
2/3

컨디션 조절에 실패하여 오늘의 기분은 매우 안좋다. ㅜ ㅜ


시작하기 전

제네릭을 알려주면서 바운드, 언바운드 제네릭에 대해서 새롭게 알아보았다.
물론 와일드카드를 알려준다고 했지만, 와일드 카드를 먼저 알려주면 순서가 뒤죽박죽 일 것 같아
제네릭에 대한 추가적인 설명으로 갈 것 같다.

Unbounded Generic ?

public class Box<T> {

    private T t;          

    public void set(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }
}

여기서 언바운드 제네릭<T> 타입 매개변수로 어떤 것이든 받을 수 있음을 의미 한다.
이전 시간에 제네릭이 뭔가에 대해 배웠지만 하나하나 찾으면 찾을 수록 다양한 것이 나온다.


Bounded Generic

상한 바운드 ( Upper Bounded )

오라클 자바

public class PrintSound<T extends Animal> { <- Animal 이 구현된 클레스만 받겠다는 의미

    private T n;

    public PrintSound(T n)  { this.n = n }
    
    public void sound(){
    	this.n.hour(); <- 해당 기능을 쓸 수 있는 이유도 Animal 에 hour() 이라는 메서드를 명시했기 때문
        //만약 언바운드 였다면 불가능 했을 것.
    }
}
public class Cat implement Animal{ <-
	@Override
    public void hour(){
    	// 냐용
    }
}
public class Dog implement Animal{
	@Override
    public void hour(){
    	// 멍멍
    }
}

새로운 개념 등장에 머리를 쎄게 맞는 듯한 기분이다.
기분은 좋지 않지만 새로운 지식에 또 한번 즐거움을 느낀다.

public class BoundGeneric <T extends Payment & Comparable<T> & PaymentMethod>{
    private T t;

    public BoundGeneric(T t) {
        this.t = t;
    }
    public void print(){
        t.getAmount(); // 이건 Payment
        t.pay(); // 이건 PaymentMethod
        t.compareTo(t); // 이건 Comparable
    }
}

이런 것도 된다.
만약 클레스를 넣고 싶다면 클레스는 맨 앞에 넣도록하자. 클레스가 인터페이스 뒤로 오게되면 오류를 일으킨다.

하한 바운드 ( Lower Bounded )

이 개념이 살짝 어렵다.
보통 WildCard 에사용 되며 다음과 같은 예시가 있다.

public void addInteger(List<? super Integer> list) {
    list.add(100);
}

사실 와일드카드는 다뤄야할 내용이 크다.
그래서 이번에는 다루지 않고, 다음에 다시 다루기로 한다.


제네릭의 주의할 점?

	런타임에는 타입 정보가 사라진다 (타입 소거, Type Erasure)
Box<String> box1 = new Box<>();
Box<Integer> box2 = new Box<>();

System.out.println(box1.getClass() == box2.getClass()); // true!
String 과 Integer 은 타입이 다른데 런타임하게 되면 타입이 일치한다.

이유 : 하위 호환성을 위해 도입된 설계입니다 (제네릭이 자바 5에서 도입되었을 때 기존 코드와 충돌하지 않게 하기 위해).
	정적(static) 영역에서는 제네릭 타입 매개변수를 사용할 수 없다
public class MyClass<T> {
    private T value;
    // static T instance; // static에서는 T 사용 불가
}
이유 : static은 클래스 전체에서 공유되는 요소이다.
하지만 T는 인스턴스 생성 시점에 결정되는 타입이기 때문에 클래스 수준(static)에서는 어떤 T가 올지 알 수 없다.
	배열과 제네릭은 잘 안 맞는다.
List<String>[] arr = new ArrayList<String>[10]; <- 에러가 발생한다.

이유 : 배열은 런타임에 타입을 체크하는 반면,
제네릭은 컴파일 타임에 타입이 사라지는 특성(타입 소거)이 있어
배열과 제네릭을 섞으면 타입 안정성이 깨질 수 있다.
	제네릭 타입으로는 예외를 던질 수 없다.
public class MyException<T> extends Exception {
    // catch(T e) 불가, throw new T() 불가
}

이유 : 제네릭 타입 T는 런타임에 타입이 사라지기 때문에,
자바 예외 시스템은 구체적인 타입 정보가 있어야 throw, catch 등을 처리할 수 있다.
	오버로딩에 제네릭만 다른 경우 충돌 가능
public void print(List<String> list) {}
public void print(List<Integer> list) {} // 타입 소거 때문에 컴파일 에러

이유 : 컴파일 후 타입 소거가 일어나면서 둘 다 print(List list)로 바뀌기 때문에
오버로딩 시 메서드 시그니처가 충돌하게 된다.
자바는 메서드 시그니처를 이름 + 파라미터 타입으로 구분하기 때문에, 충돌이 발생한다.

결론 : 타입소거로 인해서 이와 같은 일이 일어난다.


제네릭을 남발하면 안되는 이유

이렇게만 보면 제네릭이 정말 확장성이 좋고 유연한 도구라고 생각이 될 것이다.
하지만, 모든것에는 과유불급이라고 너무 많이쓰면 안좋다.

쓸데없이 많이 쓰게 되거나 괜히 쓰는 건 코드가 더 복잡해지고 애매해지고 가독성과 유지보수성이 떨어질 수 있다.
제네릭을 "잘" 써야 하는거지 많이 쓰면 안된다는 것이다.
  1. 타입 추론이 가능할 때에도 괜히 명시적으로 <T>를 남발하는 경우
  <T> void print(T data) {
      System.out.println(data);
  }

  void print(Object data) {
      System.out.println(data);
  }
  1. 지나친 제네릭 선언
  public class Something<
    T extends Comparable<? super T> & Cloneable,
    U extends Map<String, List<T>>
  > { ... }
  1. 제네릭 썼는데 결국 오브젝트로 할꺼라면?
  <T> void doSomething(T t) {
      Object obj = (Object) t;  // ???
  }
  1. 모든 클래스에 무조건 <T> 붙이는 습관
public class UserService<T> {
    // 실은 전혀 T 쓸 일이 없음
    public void login(String id, String pw) { ... }
}

오케이! 그러면 대체 언제 사용해야할까?


제네릭은 언제?

타입에 따라 다르게 작동해야할 때 OK
컨테이너를 만들거나 다뤄야 할 때 OK
타입 일관성이 필요한 복잡한 메서드가 필요할 때 OK

좋은 예시

public class GenericUtils {

    // 어떤 타입이든 배열을 출력할 수 있음
    public static <T> void printArray(T[] array) {
        for (T elem : array) {
            System.out.print(elem + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        String[] strArray = {"Java", "is", "fun"};
        Integer[] intArray = {1, 2, 3};

        printArray(strArray); // String도 가능
        printArray(intArray); // Integer도 가능
    }
}
public class Box<T> {
    private T value;

    public void set(T value) { this.value = value; }
    public T get() { return value; }
}

public class Main {
    public static void main(String[] args) {
        Box<String> strBox = new Box<>();
        strBox.set("Hello");
        System.out.println(strBox.get());

        Box<Integer> intBox = new Box<>();
        intBox.set(123);
        System.out.println(intBox.get());
    }
}

나쁜 예시

public class UselessGeneric<T> {
    public void greet(String name) {
        System.out.println("Hello, " + name);
    }

    public static void main(String[] args) {
        UselessGeneric<Integer> g = new UselessGeneric<>(); // Integer 왜씀?
        g.greet("World");
    }
}
public class BadBox<T> {
    private Object value;

    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return (T) value; // 위험한 캐스팅
    }
}

새롭게 알게된 사실

  1. 내가 지금까지 알던 건 언바운드 뿐 이였는데 바운드를 알게 되었다.
  2. public class BoundGeneric <T extends Payment & Comparable<T> & PaymentMethod> 이런식으로도 사용이 된다는 점
  3. 과한 제네릭은 금물!
  4. "필요할 때만 사용할 것!"
  5. 스스로에게 "이건 유연하게 작동해야 하는가? 라고 묻기 "
profile
배우노라, 실험하노라, 기록하노라

0개의 댓글