여기서 컬렉션을 몰라도 크게 지장은 없으나 예제에서 사용된 List가 무엇인지 궁금하시다면 컬렉션 편이랑 같이 병행해서 보시기 바랍니다.

제네릭(Generic)

타입 안정성(type safety)

제네릭의 주 목적은 바로 타입 안정성을 제공하고 타입 캐스팅 문제를 해결하는 것입니다. 예를 들면, 아래의 코드를 생각해봅시다. 리스트에 값을 추가할 때는 문제가 없어보이지만, 리스트에서 값을 꺼낼 때는 컴파일러가 꺼낸 값이 무슨 타입인지 모르기 때문에 명시적으로 캐스팅을 해야 한다는 번거로움이 있습니다. 여기서 캐스팅을 잘못하면 캐스팅을 할 수 없다는 런타임 예외가 발생하므로 주의해야 합니다.

List list = new ArrayList();
list.add(123);
list.add("가나다");

// 타입 캐스팅을 하지 않으면 에러가 발생함
Integer a = (Integer)list.get(0);
// 모르고 String 타입을 Integer 타입으로 변환하려고 하면 런타임 에러가 발생함
Integer b = (Integer)list.get(1);

이를 해결하기 위해 프로그래머가 명시적으로 타입을 지정하고, 컴파일러가 컴파일 타임에 더 엄격하게 타입 검사를 한다면 런타임에 저런 불상사가 일어나는 일을 막을 수 있을 것입니다. 아래의 예시에서는 리스트가 정수 값만 허용하므로 문자열을 인수로 넘기면 컴파일 에러가 발생합니다. 그리고 아래의 리스트 객체에는 정수형 값만 들어가는 것이 보장되므로 타입 캐스팅을 하지 않아도 됩니다.

List<Integer> list = new ArrayList<Integer>();
list.add(123);
list.add("가나다"); // 컴파일 에러

Integer a = list.get(0);

서브타이핑(Subtyping)

제네릭을 살펴보기 전에 자바의 서브타이핑을 잠시 살펴보고 가도록 하겠습니다. 자바에서의 하위 타입(subtype)은 어떤 인터페이스나 클래스를 상속받거나 구현하는 인터페이스 혹은 클래스를 말합니다. 덧붙여서, 지금 설명하는 자바의 하위 타입은 리스코프 치환 원칙에서 말하는 하위 타입과는 좀 다릅니다.

class A { /* ... */ }
class B extends A { /* ... */ }

위의 코드에서는 상위 타입이 A이고 하위 타입이 B라는 것을 쉽게 알 수 있습니다. 따라서, 아래와 같이 클래스 A 타입의 참조형 변수로 클래스 B 타입의 객체를 가리키고 마치 A의 객체인 것처럼 다룰 수 있습니다.

B b = new B();
A a = b;

그리고 B가 A의 하위 타입이고 C가 B의 하위 타입이라면 C도 A의 하위 타입이라고 할 수 있습니다. 이를 상속 계층도로 확인하면 다음과 같습니다.

아래의 클래스 계층에서, Object는 루트 클래스로 모든 자바 클래스의 부모 클래스입니다. Object 클래스를 상속받는 Number 클래스는 다양한 숫자 타입으로 변환할 수 있는 메서드를 제공합니다. Number 클래스를 상속받는 Integer 클래스는 기본 타입 int의 래퍼 클래스입니다.

따라서 아래와 같이 Integer 타입의 객체에 대한 참조를 Number 타입의 참조형 변수에 할당해도 문제가 없습니다.

Integer someInteger = 10;
Number someNumber = someInteger;

아래와 같은 코드에서도 물론 문제가 없습니다. 이제 제네릭을 잠시만 살펴보고 나서 다시 서브타이핑과 관련된 문제로 돌아오도록 하겠습니다.

public void someMethod(Number n) { /* ... */ }

someMethod(new Integer(10));   // OK
someMethod(new Double(10.1));   // OK

제네릭 클래스와 인터페이스

제네릭 클래스(Generic class)

제네릭 클래스는 보통 아래와 같이 정의합니다. 여기서 T를 타입 매개변수(type parameter)라고 하며, 괄호 <와 > 사이에 타입 매개변수를 쉼표로 구분하여 늘어놓습니다. 이런 클래스를 매개변수화된 클래스(parameterized classes)라고 하기도 합니다.

class 이름<T1, T2, ..., Tn> {
	// ...
}

그리고 제네릭 클래스의 객체를 만드려면 아래와 같이 타입 인수(type argument)로 원하는 타입을 적습니다.

타입 매개변수(type parameter) vs 타입 인수(type argument)

타입 매개변수는 제네릭 클래스를 정의할 때 사용하며, 타입 인수는 제네릭 클래스를 사용할 때 사용합니다. 이 문서에서는 두 용어를 구분해서 설명합니다.

class Box<T> { ... } // T = 타입 매개변수

Box<Integer> box = new Box<Integer>(); // Integer = 타입 인수
Box<String> box = new Box<String>();

타입을 적을 때 주의해야 하는 것은 기본 타입이 아니라 참조 타입만 적을 수 있다는 것입니다. 아래와 같이 기본 타입을 작성하면 에러가 발생합니다.

Box<int> box = new Box<int>();

제네릭 인터페이스(Generic interface)

제네릭 인터페이스도 제네릭 클래스와 마찬가지 방법으로 선언할 수 있습니다.

interface 이름<T1, T2, ..., Tn> {
    // ...
}

타입 매개변수명 작성 규칙

보통 타입 매개변수명은 하나의 대문자입니다. 앞에서 살펴본 클래스명이나 변수명 작성 규칙과는 크게 다릅니다. 만약에 타입 매개변수명도 클래스명 작성 규칙을 따른다면 타입 매개변수명과 클래스명, 인터페이스명을 구분하기가 힘들어집니다. 가장 일반적으로 쓰이는 타입 매개변수명은 아래와 같습니다.

  • E - 요소(Element)
  • K - 키(Key)
  • N - 숫자(Number)
  • T - 타입(Type)
  • V - 값(Value)
  • S, U, V 등 - 2번째, 3번째, 4번째 타입

예시 살펴보기

처음에 살펴봤던 박스 클래스 예제를 제네릭 클래스로 바꿔보도록 하겠습니다.

class Box<T> {
	private T t;
	
	public void set(T t) {
		this.t = t;
	}
	
	public T get() {
		return t;
	}
}

class JavaTutorial26 {	
	public static void main(String[] args) {
		Box<Integer> box = new Box<Integer>();
		
		box.set(10);
        // box.set("가나");
		System.out.println(box.get());
	}
}

위를 살펴보면 타입 인수로 Integer가 온 것을 볼 수 있습니다. 따라서, 제네릭 클래스 Box에서 사용된 타입 매개변수 T는 Integer로 대체됩니다. 따라서, 주석 처리된 코드와 같이 Integer로 변환될 수 없는 데이터가 오면 에러가 발생합니다.

제네릭 메서드와 생성자

제네릭 메서드(Generic method)

제네릭 클래스와 비슷한 방법으로 제네릭 메서드를 만들 수 있습니다. 제네릭 메서드는 제네릭 클래스가 아닌 일반 클래스에서도 정의할 수 있습니다. 이때, 타입 매개변수는 반환형 앞에 적어야 하며, 이를 반환형으로 사용할 수도 있습니다.

class JavaTutorial26 {
	public static <T> void printAll(T[] arr) {
		for (T t : arr) {
			System.out.println(t);
		}
	}
	
	public static void main(String[] args) {
		Integer[] nums = {1, 2, 3};
		String[] names = {"철수", "영희", "길동"};
		
		printAll(nums);
		printAll(names);
	}
}

제네릭 생성자(Generic constructor)

물론 제네릭 생성자도 제네릭 메서드와 마찬가지 방법으로 만들 수 있습니다. 생성자에는 반환형이 없으므로 생성자명 앞에 적습니다.

class 클래스명 {
	public <T> 생성자명(T t) {
		// ...
	}
}

제한된 타입 매개변수(Bounded Type Parameters)

앞에서 살펴본 예시들은 타입 인수로 모든 타입이 올 수 있었습니다. 하지만 올 수 있는 타입 인수에 제한을 걸고 싶을 때도 있습니다. 예를 들어서, 숫자만 담을 수 있는 클래스를 만들고 싶다면 아래와 같이 쓸 수 있습니다. 타입 매개변수명, extends, 상위 타입 경계(upper bound)를 차례대로 적습니다. 제네릭에서 extends는 단지 상속이 아니라 클래스의 상속(extends), 인터페이스의 구현(implements)을 아우르는 일반적인 의미이므로 주의하세요. 

// T = 타입 매개변수, Number = 상위 타입 경계(upper bound)
class Box<T extends Number> {
	// ...
}

위의 예시에서는 타입 매개변수 T가 상위 타입 경계인 Number이거나 Number의 하위 타입이어야 함을 의미합니다.

class Box<T extends Number> {
	// ...
}

class JavaTutorial26 {	
	public static void main(String[] args) {
		Box<Integer> box1 = new Box<Integer>();
		Box<Short> box2 = new Box<Short>();
		Box<Double> box3 = new Box<Double>();
		// Box<String> box4 = new Box<String>();
	}
}

위의 예시에서는 Integer, Short, Double은 Number의 하위 타입이지만 String은 Number의 하위 타입이 아니므로 에러가 발생합니다.

다중 경계(Multiple bounds)

하나의 경계가 아니라 여러 개의 경계를 가질 수도 있습니다. 이때, 타입 매개변수 T는 경계로 나열된 모든 타입의 하위 타입입니다. 아래와 같이 상위 타입 경계를 & 기호로 구분합니다.

class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }

class D<T extends A & B & C> { /* ... */ }

경계 중 하나가 클래스인 경우에는 클래스가 순서상으로 맨 앞에 와야 합니다. 예를 들어서, 아래와 같이 순서를 바꾸면 에러가 발생합니다.

class D <T extends B & A & C> { /* ... */ }

와일드카드(Wildcards)

제네릭에서 알 수 없는 타입을 나타낼 때 와일드카드(?)를 사용할 수 있습니다. 상위 타입 경계만 사용할 수 있는 타입 매개변수와는 다르게 와일드카드는 하위 타입 경계도 사용할 수 있습니다. 이를 차례대로 살펴보도록 하겠습니다.

와일드카드(wildcard)

wildcard는 카드 게임에서 만능 카드라는 의미로 흔히 조커(Joker)라고 불리는 카드를 떠올릴 수 있습니다. 비슷하게 컴퓨터 세계에선 보통 알 수 없는 문자나 문자열을 대체할 수 있는 만능 기호를 말합니다. 와일드카드는 주로 검색에 사용되는데, 예를 들어서 이름이 '이'로 시작하는 사람들을 모두 나열하고 싶을 때 '이*'라고 적을 수 있습니다. 여기서 *를 와일드카드라고 부릅니다. 자바의 제네릭에서는 알 수 없는 타입을 대체하고 싶을 때 와일드카드 문자인 ?를 사용합니다.

List<?> // 경계가 없는 와일드카드(Unbounded Wildcards)
List<? extends Foo> // 상위 타입 경계 와일드카드(Upper Bounded Wildcards)
List<? super Bar> // 하위 타입 경계 와일드카드(Lower Bounded Wildcards)

경계가 없는 와일드카드(Unbounded Wildcards)

말 그대로 경계가 없으며, 어떤 타입이 와도 상관이 없을 때 사용합니다. 예를 들어서 Object 클래스가 제공하는 메서드를 사용해서 메서드를 구현할 수 있거나, 코드가 타입 매개변수와는 상관이 없을 때 사용할 수 있습니다.

import java.util.Arrays;
import java.util.List;

class JavaTutorial26 {
	public static void printList(List<?> list) {
    	// System.out.print() 메서드는 어떤 타입이든 상관없이 출력할 수 있다.
	    for (Object elem: list)
	        System.out.print(elem + " ");
	    System.out.println();
	}
	
	public static void main(String[] args) {
		List<Integer> li = Arrays.asList(1, 2, 3);
		List<String> ls = Arrays.asList("하나", "둘", "셋");
		
		printList(li);
		printList(ls);
	}
}

상위 타입 경계 와일드카드(Upper Bounded Wildcards)

경계가 없는 와일드카드는 어떤 타입이든 올 수 있었는데, 여기서 제한을 걸고 싶다면 아래와 같이 상위 타입 경계 와일드카드를 사용할 수 있습니다. 와일드카드 문자(?), extends, 상위 타입 경계를 차례대로 적습니다.

public static double sum(List<? extends Number> list) {
    double total = 0;
    for (Number elem: list)
        total += elem.doubleValue();
    return total;
}

여기서 와일드카드 ?에 오는 타입은 상위 타입 경계인 Number이거나 Number의 하위 타입이어야 합니다. 따라서, ?에 오는 타입은 Number 클래스를 상속받으므로 Number 클래스의 typeValue() 메서드를 사용할 수 있습니다.

List<Integer> li = Arrays.asList(1, 2, 3);
System.out.println(sum(li)); // 6.0

List<Double> ld = Arrays.asList(3.2, 4.3, 5.5, 6.6);
System.out.println(sum(ld)); // 19.6

하위 타입 경계 와일드카드(Lower Bounded Wildcards)

하위 타입 경계 와일드카드도 있습니다. 사용하려면 와일드카드 문자(?), super, 하위 타입 경계를 차례대로 적습니다.

public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}

여기서 와일드카드 ?에 오는 타입은 하위 타입 경계인 Integer이거나 Integer의 상위 타입이어야 합니다. 예를 들어서 ?에 Number, Object 등이 들어갈 수 있습니다.

서브타이핑(Subtyping)

다시 서브타이핑으로 돌아와서, 위에서 이미 살펴봤지만 제네릭에서도 Number가 Integer와 Double의 상위 타입이므로 아래와 같이 코드를 작성할 수 있습니다.

List<Number> list = new ArrayList<Number>();

list.add(new Integer(10)); // OK
list.add(new Double(10.1));  // OK

이제는 다음과 같은 메서드를 생각해봅시다. listTest() 메서드의 매개변수에 어떤 타입들이 넘어갈 수 있을까요? 왠지 모르게 List<Integer>와 같은 타입이 넘어갈 수 있을 것은 느낌이 듭니다.

public void listTest(List<Number> n) {
    // ...
}
...
// 에러: JavaTutorial26 타입 내의 listTest(List<Number>) 메서드는 인수 List<Integer>에 들어맞지 않습니다.
List<Integer> nums = new ArrayList<Integer>();
listTest(nums);

하지만 기대와는 다르게 넘어갈 수 없으므로 주의해야 합니다. Integer는 Number의 하위 타입이 맞지만 List<Integer>는 List<Number>의 하위 타입이 아니기 때문입니다. 만약에 List<A>와 List<B>가 있을 때 A가 B의 상위 타입이든 하위 타입이든 상관없이, List<A>와 List<B>는 오로지 공통 부모가 Object라는 것을 제외하고는 서로 아무런 관계가 없습니다.  물론 List<?>도 공통 부모지만 아직 와일드카드는 생각하지 않습니다. 상속 계층도를 살펴보면 다음과 같습니다.

공통 부모는 Object

여기서 와일드카드가 끼어들면 얘기가 조금 달라집니다. 와일드카드가 섞여있을 때는 경계 없는 와일드카드, 상위 타입 경계 와일드카드, 하위 타입 경계 와일드카드와 같이 세 가지의 경우를 고려해야 합니다.

위로 올라갈수록 더 일반적이며, 아래로 내려갈수록 구체적입니다. 예를 들어서, 매개변수 타입이 List<? extends Number>라면 List<? extends Number>, List<? extends Integer>, List<Integer>, List<Number> 타입이 들어갈 수 있습니다.

타입 추론(Type Inference)

표현식(Expression)

표현식(Expression)은 하나 이상의 식별자, 리터럴, 연산자로 이루어져 있습니다. 대부분의 언어에서 표현식은 항상 결과를 반환하며 부수적으로 무언가를 일으키지는 않는 경우가 많습니다. 예를 들어서 a + b라는 표현식은 두 변수의 값을 합한 결과를 반환하지만, 이 표현식 자체로는 아무런 일도 일어나지 않습니다. 그리고 이 표현식은 a, b와 같이 더 작은 표현식으로 나타낼 수 있습니다. 자바에서 볼 수 있는 표현식의 예로는 다음과 같은 것들이 있습니다.

1
x = 5
a * 100
x * y + x * z
a < b
a > b ? a : b
func()
"this is expression"
getUTCFullYear() + "/" + (getUTCMonth() + 1) + "/" + getUTCDate()

하지만 자바의 세계에서는 위의 예에서 볼 수 있듯이 표현식이 결과를 반환하지 않을 수도 있고, 부수적으로 무언가를 일으킬 수도 있습니다. 표현식 func()는 반환형이 void라면 아무 것도 반환하지 않으며, 표현식 x = 5는 부수적으로 x의 값을 변경합니다. 아래의 코드도 역시 표현식입니다.

System.out.println("Hello, world!")

흔히 표현식을 평가(evaluate)한다는 말을 많이 쓰는데, 이는 표현식에 있는 각 변수를 실제 값으로 대체한 후 연산자 우선순위에 따라서 식을 단순화하는 것을 말합니다. 따라서 표현식 5+10을 평가하면 15라는 결과 값을 얻게 됩니다. 

문장(Statement)

자바에서 문장은 자바 인터프리터가 실행하는 하나의 명령을 말합니다. 인터프리터는 나타난 순서대로 문장을 실행하며, 이 문장은 보통 문장의 끝을 나타내는 세미콜론(;)으로 끝납니다. 하나의 문장에는 0개 이상의 표현식이 들어갈 수 있으며, 자바에는 다양한 종류의 문장들이 있습니다. 여기까지 오면서 살펴본 문장들이 많으므로 여기에서는 몇 가지 문장들만 살펴보겠습니다.

표현식 문장(Expression Statements)

표현식 끝에 세미콜론(;)이 붙으면 표현식 문장이라고 부릅니다. 표현식 문장은 공통적으로 어떤 값을 반환할 뿐만 아니라 부수적으로 어떤 일을 하고 있습니다.

num++; // 증감
myValue = 10; // 할당
System.out.println("Hello, world!"); // 메서드 호출

하지만 아래와 같은 표현식에 세미콜론을 붙이면 에러가 발생합니다. 표현식이 결과를 반환하지만 부수적으로 아무런 일도 하지 않으므로 의미가 없기 때문입니다.

a / 3;
a < b;

메서드 호출의 경우는 결과를 반환할 수도 있지만 반환된 값을 사용하지 않을 수 있는데, 이 경우 값은 조용히 버려집니다.

int doSomething(int x) {
    // ...
    return result;
}
doSomething(someInteger); // 값이 버려짐

복합문(Compound Statements)

복합문은 말 그대로 중괄호({...})를 사용하여 여러 개의 문장을 하나의 문장으로 묶은 것을 말합니다. 문장을 쓸 수 있는 곳이면 어디에서든 복합문을 쓸 수 있습니다.

if (houseIsOnFire) { // 복합문의 시작
    scream();
    runAway();
} // 복합문의 끝

빈 문장(Empty Statement)

자바에서 빈 문장은 세미콜론(;) 하나로 표현되어 있으며 아무것도 하지 않습니다. 널 문장(null statement)이라고 부르기도 합니다. 하지만 때로는 유용할 수도 있습니다. 

/* 비어있음 */;

아래 코드는 사실 뒤에 빈 문장이 들어간 것입니다.

while (true);

좀 더 보기 쉽게 이를 분리하면 아래와 같이 작성할 수 있습니다.

while (true)
	/* 비어 있음 */;

타입 추론(Type Inference)

자, 이제 타입 추론으로 돌아오겠습니다. 자바를 설계한 사람들은 개발자가 더욱 간결한 코드를 작성할 수 있도록 돕기 위해서 타입 추론을 점진적으로 도입하기 시작했습니다. 자바 5부터 생겨난 제네릭 메서드는 호출할 때 따로 타입 인수를 명시하지 않아도 컴파일러가 적절한 타입을 유추할 수 있습니다.

List<String> strList = Collections.<String>emptyList();
List<Integer> intList = Collections.<Integer>emptyList();

따라서 위의 코드는 아래와 같이 생략하여 적을 수 있습니다. 할당식의 왼쪽에 오는 타입을 보고 오른쪽의 타입을 유추할 수 있기 때문입니다.

List<String> strList = Collections.emptyList();
List<Integer> intList = Collections.emptyList();

그리고 자바 7로 넘어와서는 아래와 같이 제네릭 클래스의 생성자를 호출할 때 타입 인수를 생략할 수 있게 되었습니다.

List<String> names = new ArrayList<>();

타겟 타입(Target Type)

자바 8로 넘어와서는 타입 추론의 범위가 좀 더 확장되었습니다. 자바 8부터는 메서드를 호출할 때 타겟 타입을 통한 타입 추론을 지원합니다. 표현식의 타겟 타입은 표현식이 어디서 사용되느냐에 따라서 자바 컴파일러가 예상하는 타입을 말합니다. 간단한 예시를 하나 들면, 아래의 코드에서 컴파일러는 할당 연산자의 왼쪽에 있는 변수의 타입을 보고 오른쪽에 있는 표현식의 타입을 유추합니다.

int a = b;

변수 a는 int형이므로 컴파일러는 표현식 b도 int형일 것이라 예상합니다. 여기서 타겟 타입은 int형이라 할 수 있습니다. 그러면 아래의 코드를 살펴봅시다.

import java.util.ArrayList;

class TargetTypeExample {
	public static void processStringList(ArrayList<String> list) {
		// ...
	}

	public static void main(String[] args) {
		processStringList(new ArrayList<>());
	}
}

processStringList() 메서드의 매개변수는 ArrayList<String> 타입입니다. 따라서 우리가 processStringList() 메서드를 호출할 때마다 컴파일러는 ArrayList<String> 타입이 넘어올 것이라 예상합니다. 따라서 아래에서 생략된 타입 인수가 String임을 유추할 수 있습니다.

processStringList(new ArrayList<>()); // = new ArrayList<String>()

하지만 이 기능은 자바 8 이후부터 지원하므로 자바 7 이전 버전에서 코드를 작성하면 아래와 같은 에러가 발생합니다.

// 에러: TargetTypeExample 타입 내의 processStringList(ArrayList<String>) 메서드는 인수 ArrayList<Object>에 들어맞지 않습니다.
processStringList(new ArrayList<>());

타입 소거(Type Erasure)

컴파일러는 제네릭을 지원하지 않는 이전 버전의 자바와 호환될 수 있도록 타입 소거(Type Erasure)를 진행합니다. 자세한 내용은 아래와 같습니다.

  • 제네릭 타입의 모든 타입 매개변수를 경계 타입으로 변경하거나, 경계 제한이 없는 경우에는 Object로 바꾼다.
  • 타입 안정성을 유지하기 위해서 필요한 경우에는 타입 캐스트를 삽입한다.
  • 제네릭 클래스나 제네릭 인터페이스를 상속받을 때 다형성을 유지하기 위해서 브리지 메서드(bridge method)를 삽입한다.

차례대로 이를 살펴보도록 하겠습니다.

경계 타입 있음

예를 들어서, 아래와 같이 Box 제네릭 클래스의 코드를 작성했다고 해봅시다.

class Box<T extends String> {
	private T t;
	
	public void set(T t) {
		this.t = t;
	}
	
	public T get() {
		return t;
	}
}

여기서 제네릭 타입에서의 모든 타입 매개변수를 경계 타입으로 변경한다고 했으므로, 컴파일을 하고 나면 아래와 같이 대체됩니다.

class Box {
	private String t;
	
	public void set(String t) {
		this.t = t;
	}
	
	public String get() {
		return t;
	}
}

경계 타입 없음

만약에 경계 제한이 없다면 타입 매개변수 T는 Object 타입으로 변경됩니다. 아래와 같은 예시를 생각해봅시다.

class Box<T> {
	private T t;

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

public class JavaTutorial26 {
    public static void main(String[] args) {
        Box<String> messageBox = new Box<String>("Hello");
        String contents = messageBox.get();
        System.out.println(contents);
    }
}

컴파일을 하고 나면 아래의 코드로 대체됩니다.

class Box {
	private Object t;

    public Box(Object t) {
        this.t = t;
    }
	
	public void set(Object t) {
		this.t = t;
	}
	
	public Object get() {
		return t;
	}
}

public class JavaTutorial26 {
    public static void main(String[] args) {
        Box messageBox = new Box("Hello");
        String contents = (String)messageBox.get();
        System.out.println(contents);
    }
}

이와 비슷하게, 아래와 같이 작성하면 에러가 발생하니 주의하세요. 타입 소거가 일어나면 매개변수 list의 타입은 List로 두 메서드의 시그니처는 사실상 같기 때문입니다.

class Foo {
	// 타입 소거가 일어나면 두 매개변수의 타입은 List로 대체된다.
	public void bar(List<Object> list) { /* ... */ }
	
	public void bar(List<String> list) { /* ... */ }
}

브리지 메서드(bridge method)

어떤 경우에는 타입 소거로 인해서 메서드 오버라이딩 시 문제가 일어날 수 있습니다. 컴파일러는 이러한 문제를 방지하기 위해서 브리지 메서드(bridge method)를 만들기도 합니다. 브리지 메서드는 bridge에서 알 수 있듯이 서로 떨어져 있는 것을 이어주는 다리 역할을 하는 메서드라고 생각할 수 있습니다. 이 메서드는 구체적으로 어떤 역할을 하는지 살펴보기 위해서, 이번에는 Box<T> 클래스와 Box<Integer>를 상속받는 IntegerBox 클래스를 생각해보도록 하겠습니다.

class Box<T> {
	private T t;
	
	public void set(T t) {
		this.t = t;
	}
	
	public T get() {
		return t;
	}
}

class IntegerBox extends Box<Integer> {
    public void set(Integer num) {
    	System.out.println("IntegerBox.set(Integer)가 호출됨");
    	super.set(num);
    }
}

여기서 타입 소거가 일어나면 아래와 같이 대체될 것입니다. 하지만 부모 클래스와 자식 클래스에 있는 메서드 시그니처가 서로 다르므로 오버라이딩은 일어나지 않습니다.

class Box {
	private Object t;
	
	public void set(Object t) {
		this.t = t;
	}
	
	public Object get() {
		return t;
	}
}

class IntegerBox extends Box<Integer> {
    public void set(Integer num) {
    	System.out.println("IntegerBox.set(Integer)가 호출됨");
    	super.set(num);
    }
}

따라서 컴파일러는 아래와 같이 Box.set(Object)와 IntegerBox.set(Integer)를 잇는 브리지 메서드를 만들어서 다형성을 유지할 수 있도록 해줍니다.

class IntegerBox extends Box<Integer> {
    public void set(Integer num) {
    	System.out.println("IntegerBox.set(Integer)가 호출됨");
    	super.set(num);
    }

    // 브리지 메서드
    public void set(Object num) {
        this.set((Integer)num); // set(Integer)를 호출함
    }
}

직접 아래와 같이 바이트코드에서도 브리지 메서드 set(java.lang.Object)을 확인해볼 수 있습니다.