✅ 제네릭(Generic)이란?
제네릭(Generic)은 자바에서 타입 안정성을 확보하고 재사용성을 높이기 위해 도입된 기능입니다. 제네릭은 클래스, 인터페이스, 메서드 등에서 사용할 수 있으며, 컴파일 시에 타입 체크를 수행하여 타입 안정성을 보장합니다.
🔹제네릭이점
- 타입 안정성(Type Safety)
제네릭은 컴파일 시에 타입 체크를 수행하여 타입 안정성을 보장합니다. 즉, 잘못된 타입 사용으로 인한 런타임 에러를 사전에 방지할 수 있습니다. 타입 체크를 통해 컴파일러는 프로그램에서 발생할 수 있는 타입 관련 오류를 찾아내고 경고나 에러를 발생시킵니다.
- 재사용성(Reusability)
제네릭을 사용하면 타입을 일반화하여 재사용 가능한 코드를 작성할 수 있습니다. 타입 매개변수를 사용하여 클래스, 인터페이스, 메서드 등을 정의하고, 실제 사용 시에 타입을 지정함으로써 코드의 재사용성을 높일 수 있습니다. 이로 인해 코드의 중복을 줄이고 유지 보수성을 향상시킬 수 있습니다.
- 타입 변환 감소(Decreased Type Casting)
제네릭을 사용하면 타입 매개변수를 통해 원하는 타입을 직접 사용할 수 있습니다. 이로써 타입 변환을 하지 않고도 타입에 안전하게 접근할 수 있으며, 코드의 가독성을 높일 수 있습니다.
- 컬렉션 안정성(Collection Safety)
제네릭은 컬렉션 프레임워크에서 특히 유용하게 사용됩니다. 컬렉션 클래스에 제네릭을 적용하면 컴파일러가 컬렉션에 잘못된 타입의 요소를 추가하는 것을 방지해줍니다. 이로써 컬렉션에서 발생할 수 있는 런타임 에러를 사전에 방지할 수 있습니다.
👉 제네릭을 결국 컬렉션 프레임워크의 타입 안정성과 밀접한 관계를 지니고 있습니다. 컬렉션 프레임워크는 다양한 타입의 객체를 저장할 수 있습니다. 이때 제네릭을 통해 특정한 타입의 객체만 저장될 수 있도록하여 타입의 안정성을 보장하는 것입니다. 제네릭을 활용하면 컴파일 단계에서 타입 관련 오류를 찾아내고(코드를 작성하는 중에, 프로그램을 실행하지 않아도) 경고나 에러를 발생시켜줍니다.
🔹제네릭 문법
- 클래스 또는 인터페이스의 제네릭 선언
class 이름<타입 매개변수> { // 코드 } interface 이름<타입 매개변수> { // 코드 }
class Box<T> { private T content; public void setContent(T content) { this.content = content; } public T getContent() { return content; } }
제네릭은 <> 꺽쇠 괄호 키워드를 사용해서 타입 매개변수를 지정합니다. 이를 다이아몬드 연산자라고 합니다. 이 꺽쇠 괄호 안에 식별자 기호를 넣음으로써 제네릭으로 선언한 클래스의 객체를 생성하거나, 메소드를 사용할 때 사용할 수 있습니다. 마치 파라미터처럼 타입을 받아서 사용한다고 하여 타입 매개변수(타입 파라미터)라고 부릅니다. 다음은 Java에서 제네릭을 선언할 때 일반적으로 타입 매개변수로 작성하는 식별자 기호입니다.
E
: Element의 약자로 주로 컬렉션 클래스에서 사용됩니다. 일반적으로 임의의 객체를 나타냅니다.
K
: Key의 약자로 맵(Map)에서 사용되는 키(key)의 타입을 나타냅니다.
V
: Value의 약자로 맵(Map)에서 사용되는 값(value)의 타입을 나타냅니다.
T
: Type의 약자로 임의의 타입을 나타냅니다.
S
,U
,V
등: 여러 개의 타입 매개변수가 필요한 경우에 순차적으로S
,U
,V
등의 알파벳을 사용합니다.
👉 일반적으로 위와 같이 선언하여 사용합니다. 개발자가 임의로 다른 알파벳을 사용하여 지정할 수도 있습니다.
🔹제네릭의 사용
- 변수와 메소드 선언
타입<타입 매개변수> 변수이름; <타입 매개변수> 반환타입 메서드이름(매개변수...) { // 코드 }
변수에 선언할 때는 위와 같이 <>를 사용하여 제네릭 타입을 사용할 수 있습니다. 이를 사용할 때 타입 매개변수로 String으로 선언했을 때는 해당 제네릭으로 선언한 해당 필드의 변수나 메소드가 String 타입이 됩니다.
Box<String> box = new Box<>(); Box<Integer> box = new Box<>();
jdk 1.7 버전 이후부터, new 키워드와 함께 생성자가 있는 부분의 제네릭을 생략할 수 있게 되었습니다. 객체의 타입과 함께 제네릭을 지정해주었기 때문에 굳이 생성자까지 지정할 필요가 없기 때문입니다.
class Box<String> { private String content; public void setContent(String content) { this.content = content; } public String getContent() { return content; } }
위와 같이 제네릭으로 선언한 타입이 전파되어 변수와 메소드의 타입이 결정되는 것입니다. Integer로 선언한 경우엔 Integer로, 해당 클래스의 객체를 생성하는 쪽에서 결정하게 됩니다.
- 컬렉션 타입의 제네릭
컬렉션 타입<타입 매개변수> 변수이름 = new 실제타입<>();
ArrayList<String> stringList = new ArrayList<>(); stringList.add("Apple"); stringList.add("Banana"); stringList.add("Cherry"); System.out.println(stringList); // 출력: [Apple, Banana, Cherry]
위의 예제에서 ArrayList<String>은 stringList 변수가 ArrayList 클래스의 인스턴스를 참조하며, new ArrayList<>()를 통해 ArrayList 객체를 동적으로 생성하여 변수에 할당하고 있습니다. 이후에 stringList 변수를 사용하여 문자열 요소를 추가하고 출력하는 예제입니다. 이를 통해 ArrayList 컬렉션은 String 타입의 객체만 저장할 수 있게 됩니다.
✅ 타입 경계(Type Bounds)
타입 경계(Type Bounds)는 제네릭 타입 매개변수가 특정 타입 또는 타입들의 상위 또는 하위 타입으로 제한되는 것을 말합니다. 타입 경계는 제네릭 클래스나 제네릭 메서드에서 사용될 수 있습니다. 타입 경계를 사용함으로써 특정 타입에 대한 제약 조건을 부여할 수 있고, 보다 구체적인 타입 체크를 수행할 수 있습니다. 타입 바운드에는 크게 두 가지 종류가 있습니다. 상한 바운드(Upper Bound)와 하한 바운드(Lower Bound)입니다.
- 상위 타입 경계(Upper bound) → 타입의 최상단 경계를 지정한다.
- 상한 바운드는 타입 매개변수가 특정 클래스 또는 인터페이스의 하위 타입(subtype)이어야 한다는 제약을 설정합니다. 그렇기에 가장 상위 타입을 경계로 지정함으로써 그 하위 타입이 타입 매개변수로 올 수 있음을 알립니다.
- 상한 바운드를 설정하려면 타입 매개변수 뒤에
extends
키워드를 사용하고, 상한 타입을 지정합니다.
- 예를 들어,
<T extends Number>
는 타입 매개변수T
가Number
클래스 또는Number
를 상속하는 클래스들만을 사용할 수 있음을 의미합니다.// 상한 바운드 예시 public class Box<T extends Number> { private T contents; public T getContents() { return contents; } public void setContents(T contents) { this.contents = contents; } }
위의 예시 코드에서 Box 클래스의 타입 매개변수 T는 Number 클래스 또는 Number의 하위 클래스로 제한됩니다. 이로써 Box 객체를 생성할 때 T에는 숫자 타입만 사용할 수 있게 됩니다.
- 하위 타입 경계(Lower Bound) → 타입의 최하단 경계를 지정한다.
- 하한 바운드는 타입 매개변수가 특정 클래스 또는 인터페이스의 상위 타입(supertype)이어야 한다는 제약을 설정합니다. 그렇기에 가장 하위 타입을 경계로 지정함으로써 그 상위 타입이 타입 매개변수로 올 수 있음을 알립니다.
- 하한 바운드를 설정하려면 타입 매개변수 뒤에
super
키워드를 사용하고, 하한 타입을 지정합니다.
- 예를 들어,
<T super Integer>
는 타입 매개변수T
가Integer
클래스 또는Integer
클래스가 상속받 클래스들만을 사용할 수 있음을 의미합니다.// 하한 바운드 예시 public class Box<T super Integer> { private T contents; public T getContents() { return contents; } public void setContents(T contents) { this.contents = contents; } }
위의 예시 코드에서 Box 클래스의 타입 매개변수 T는 Integer 클래스 또는 Integer의 상위 클래스로 제한됩니다. 이로써 Box 객체를 생성할 때 T에는 Integer과 그 상위에 해당하는 Object 등도 사용할 수 있게 됩니다.
👉 상위, 하위 타입 경계가 헷갈릴 수 있습니다. 상위 타입 경계는 타입 매개변수가 하위 타입이어야 하고, 하위 타입 경계는 타입 매개변수가 상위 타입이어야 합니다. 이를 상위 타입 경계는 타입 매개변수의 최상단, 즉 가장 조상의 타입을 정의하는 것, 하위 타입 경계는 타입 매개변수의 최하단, 즉 가장 자식의 타입을 정의하는 것이라고 이해하면 쉽습니다.
✅ 타입 소거(Type Erasure)
제네릭의 타입 소거(Type Erasure)는 자바 컴파일러가 제네릭 타입 정보를 컴파일 시간에만 사용하고, 실제 실행 시에는 제거하는 프로세스를 말합니다. 이 과정에서 제네릭 타입의 매개변수 정보는 삭제되고, 타입 매개변수가 포함된 부분은 타입이 Object로 변환됩니다.
타입 소거는 제네릭이 도입되기 이전의 호환성을 유지하기 위해 이루어지는 메커니즘입니다. 제네릭이 도입되기 이전의 자바 코드와 제네릭을 도입한 자바 코드 간의 호환성을 유지하기 위해 제네릭 타입에 대한 정보를 컴파일 후에는 삭제하고, 호환성을 위해 필요한 경우 캐스팅을 추가합니다.
➡️타입 소거의 결과는 다음과 같습니다.
- 타입 매개변수 제거
컴파일 시점에 타입 매개변수는 모두 삭제됩니다. 제네릭 클래스나 제네릭 메서드의 인스턴스화된 타입에 대한 정보는 사라지고, 모든 타입 매개변수는 Object 타입으로 대체됩니다.
- 타입 캐스팅 추가
컴파일러는 타입 소거 후에도 호환성을 유지하기 위해 필요한 경우 캐스팅을 추가합니다. 이는 제네릭 타입이나 타입 매개변수를 사용하는 코드를 기존의 호환되는 형태로 변환하는 역할을 합니다.
타입 소거는 컴파일러가 제네릭 코드를 처리하는 방식이므로, 실행 시점에는 제네릭 타입에 대한 정보를 직접적으로 확인할 수 없습니다. 따라서 타입 소거로 인해 다음과 같은 몇 가지 제한 사항이 발생할 수 있습니다.
- 타입 검사와 캐스팅 제한
타입 소거로 인해 실행 시점에 제네릭 타입에 대한 타입 검사 및 캐스팅이 제한됩니다. 컴파일 시점에는 타입 검사가 이루어지지만, 실행 시점에는 모든 제네릭 타입이 Object로 처리되므로 컴파일러가 경고를 표시할 수 있습니다.
- 타입 정보의 손실
타입 소거로 인해 실행 시점에는 제네릭 타입에 대한 정보가 손실됩니다. 따라서 제네릭 타입에 대한 실제 타입 정보를 동적으로 확인하는 것은 어렵습니다.
- 정적(static) 필드의 사용 제한
타입 소거는 인스턴스화된 제네릭 타입에만 영향을 미치므로, 정적 메서드는 타입 소거의 영향을 받지 않습니다. 정적 메서드는 제네릭 타입 매개변수를 사용할 수 없으며, 타입 매개변수가 있는 제네릭 클래스의 정적 멤버에는 타입 변수를 사용할 수 없습니다. 이는 제네릭이 참조형 변수가 객체를 참조하는 규칙을 정하는 것이기 때문에 static 변수, 메서드에는 사용되지 않습니다.
👉이러한 제한 사항과 함께, 타입 소거는 자바에서 제네릭 타입의 호환성을 유지하면서 이전 버전의 자바 코드와의 상호 운용성을 가능하게 합니다.
🏷️이미지 출처 및 참고한 사이트
Uploaded by N2T