제네릭에 대한 헷갈리는 개념들

MINJU·2023년 3월 30일
1

제네릭에 대해 공부하며 헷갈렸던 부분들에 대해 정리해놓으려고 한다.
해당 포스팅은 이 참조 블로그를 많이 참조했음을 알린다 :)


1. List<String>List<Object>의 하위타입이 아니다.

당연하지 않은가? 싶다가도 헷갈리기 쉬운 개념이다.

List<String>List의 하위 타입이지만, List<Object>의 하위 타입은 아니다!

이 개념은 참조 블로그의 예시를 보면 더 명확히 이해할 수 있다.

class Box<T> {
	protected T obj;
    
    public void setObj(T obj) {
    	this.obj = obj;
    }
    
    public T getObj() {
    	return obj;
    }
}

class SteelBox<T> extends Box<T> {
	public SteelBox(T obj) {
    	this.obj = obj;
    }
}

Box<Integer> IBox = new SteelBox<>(7959);
Box<String> SBox = new SteelBox<>("simple");

해당 예시에서도 살펴볼 수 있듯 "제네릭 클래스 Steel Box"는 "제네릭 클래스 Box"를 상속받고 있다.

이 경우에 Integer를 매개타입 인자로 받는 SteelBox 제네킬 클래스는 Integer를 매개타입 인자로 받는 Box 제네릭 클래스를 상속 받을 수 있고, String을 매개타입 인자로 받는 SteelBox 제네릭 클래스는 String을 인자로 받는 Box 제네릭 클래스를 상속 받을 수 있다.

Box<String>Box<Integer>는 서로 다른 타입이기 때문이다!

이러한 논리로 인해
List<String>은 List의 하위 클래스이지만, List<Object>의 하위 클래스는 아님을 이해하게 된다.

따라서 아래의 예제도 성립하게 된다.


public void static method1(Box<Object> box) {
	...
}

이러한 메소드가 있다고 생각해보자.
그렇다면 해당 메소드에 method1(new Box<Integer>())을 집어넣으면 올바르게 동작할까 ?

전혀 그렇지 않다.

왜냐하면 Box<Integer>Box<Object>의 하위 타입이 아니기 때문이다!!

이러한 부분에서 헷갈릴 수 있기 때문에 제네릭 클래스 간의 상속 개념을 잘 이해해 두는 것이 좋다.


2. static 메소드와 제네릭에 대한 이해

다음 예제를 살펴보자

public class MyClass1<T> {
	
    public static void method1(T t) {
    	..
	}
}

이는 제네릭 클래스 내부에 선언된 메소드이고

public class MyClass2 {
	pbulic static void <T> method2(T t) {
    	...
    }
}

이는 일반 클래스 내부에 선언된 메소드이다.

이 둘 중 첫 번째, 즉 제네릭 클래스 내부에 선언된 메소드에서 컴파일 에러가 발생한다.

왜냐하면 MyClass1의 제네릭 타입 T는 인스턴스가 생성될 때 결정이 되는데, static 메소드는 인스턴스 생성과 별도로 메모리에 올라와 있다.

그렇다면 MyClass1이 생성되는 시점에 결정되는 제네릭 타입을 메서드가 매개변수로 받아낼 방법이 없다.

하지만 MyClass2의 메소드는, 호출되는 시점에 타입이 결정되기 때문에 컴파일 에러가 발생하지 않는다.

3. List<T>List<?>는 같은 개념이 아니다

<?>는 타입 매개변수가 무엇이든 난 모른다! 알아서 해라!는 뜻으로 이해하면 좋다 (일단 나는 그렇게 이해했다💦)

List<?>
-> 원소 꺼내서! 해당 원소에 정의 된 기능만 사용할게
-> List 인터페이스에 정의 된 기능만 사용할게. 근데 나는 타입 매개변수는 몰라서 타입 매개변수랑 관련 있는 작업은 못해~

를 의미한다. 타입 매개변수를 모르기 때문에! 애초에 타입 매개변수 관련 작업을 진행하지 않아서 혹시 모를 타입 무결성 이슈를 예방하고자 관련 작업을 막아놓은 것이다.

List<T>도 비슷한 의미를 가지지만 한 가지 다른 부분은 나는 타입 매개변수를 곧 알 것이기 때문에 관련된 작업도 가능해라는 것이다.


그렇다면 타입 매개변수랑 관련있는 작업은Listadd 등이 있다.

4. 상한제한과 하한제한은 언제 사용하는 것이 적합한가?

상한 제한과 하한 제한이 실무에서 어떻게 사용하는지에 대해 ! 마찬가지로 참조 블로그에서 좋은 예시를 제공해준다. 내 의견을 덧붙힌 설명을 작성하고 싶지만 해당 블로그에 너무 잘 정리가 되어있어서 링크를 걸어두는 것이 최선일 것 같다😂 당장 나도 까먹지 않게 자주 봐야지

결론은 Box<? extends Toy> box에서는 box넣는 것이 불가능해지고
Box<? super Toy> box에서는 box에서 꺼내는 것이 불가능해진다.

5. 제네릭 오버로딩은 불가능하다.

아래와 같은 코드가 있다고 해보자.

public static void outBox(Box<? extends Toy> box) {
	...
}
public static void ouBox(Box<? extends Robot> box) {
	...
}

일단 매개변수 타입은 분명히 다르다. 그래서 오버로딩이 될 것 같긴한데..😶

일단 오버로딩의 조건을 살펴보자.

  1. 메서드 이름이 같아야 함
  2. 매개변수의 개수가 다르거나 타입이 달라야 함

그렇다면 타입이 같아서 가능할 것 같아보이는데!

결론은 불가능하다

이를 알기 위해선 제네릭의 등장에 대해 알아야한다.

제네릭은 등장 이전 코드와 잘 융합되기 위해 타입 소거라는 개념을 활용한다.

제네릭이 등장한 자바 1.5 버전 이하에서는 <>라는 다이아몬드 연산자가 존재하지 않는다. 따라서 이를 호환성있게 활용하기 위해서 자바 컴파일러는 다이아몬드 연산자를 지워버리는 선택을 한다.

그래서 결국 컴파일 된 코드는 아래와 같게 된다.

public static void outBox(Box box) {
	...
}

public static void outBox(Box box) {
	...
}

이리봐도 저리봐도 같은 코드다.
결국 오버로딩이 될 수 없는 것이다.

하지만 당연하게도 방법이 없는 것은 아니다.
와일드 카드와 제네릭을 혼합하면 가능해지기 때문이다!

6. 와일드 카드와 제네릭을 혼합하여 오버로딩을 구현할 수 있다.

오버로딩을 구현하고 싶은 이유는 outBox 매개변수에 Robot도 넣고싶고 Toy도 넣고 싶어서 일 것이다.
그러면! 타입을 제네릭으로 바꿔주면 된다.

public static <T> void outBox(Box<? extends T> box {
	T toy = box.get();
    System.out.println(toy.toString());
}

이렇게 되면! set()은 못하기 때문에 논리적인 오류도 막을 수 있고 그 안에 다양한 매개변수 타입을 집어넣을 수 있게 된다.



추가적으로 참조 블로그를 활용하여 복잡한 제네릭 메소드를 해석하는 과정을 따라가보자.

아래의 내용은 공부를 위해 작성하는 글이니, 자세한 내용은 참조 블로그를 확인하는 것이 더 좋을 것 같다!

업로드중..

0개의 댓글