제네릭(Generic)이란?
제네릭은 자바의 타입 안정성을 위한 프로그래밍 기법으로 제네릭을 통해 컬렉션에 저장될 데이터의 타입을 컴파일 시점에 체크 가능하여 런타임 시 발생하는 에러를 방지할 수 있습니다.
제네릭은 클래스나 인터페이스, 메서드를 정의할 때 타입을 명시적으로 고정하지 않고 외부에서 지정할 수 있도록 함으로써 다양한 타입에서의 동작이 가능합니다. 이렇게 일반화된 코드는 중복 코드를 줄이고 코드의 재사용성을 높일 수 있습니다.
제네릭을 사용하지 않는 코드에서는 명시적으로 데이터의 타입을 캐스팅 해주어야 하지만, 제네릭의 사용함으로써 특정 타입을 지정하여 타입 캐스팅 과정을 생략 하는 등 형변환의 번거로움을 줄여줍니다.
제네릭(Generic)은 포괄적인, 일반적인이라는 뜻을 가지고 있다.
클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법이다. 어떤 데이터 타입을 인스턴스화 할 때 사용하는 것으로 다양한 타입의 매개변수를 받을 수 있도록 해준다.
제네릭은 generic의 어원처럼 제네릭의 사용으로 코드의 일반화를 시켜 효율적인 사용을 가능하게 한다. 아래는 제네릭을 사용하여 나타낸 코드 예시이다.
🧚 제네릭의 사용 이유와 방법은 아래에서 더욱 자세하게 설명
class Animal<T>{
public T info;
}
Animal<String> a1 = new Animal<String>();
Animal<Integer> a2 = new Animal<Integer>();
info의 타입을 지정하지 않고 있다가 인스턴스화 할 때 String이라는 구체적인 타입을 지정하면 클래스가 타입을 가지게 된다. a1의 데이터 타입 또한 String 을 가지게 된다.
두 번째로 생성된 인스턴스 a2는 Integer타입으로 선언되었으며, a2의 타입은 Integer를 가진다.
제네릭(Generic) 사용 이유
제네릭은 타입 안정성을 확보하고 코드 재사용성을 높이기 위해 사용한다. 제네릭을 사용하면 데이터 타입에 의존하지 않고 일반화된 코드를 작성할 수 있으며 컴파일 시점에서 타입 체크를 수행하여 타입 안정성을 강화한다.
만약위의 코드에서 제너릭을 사용하지 않는다면 아래처럼 각 타입별로 크래스를 만들어 줘야 한다.
🔽 제네릭을 사용하지 않았을 경우
class AnimalString {
private String species;
AnimalString(String species){
this.species = species)
}
}
class AnimalDog{
public AnimalString info;
AnimalDog(AnimalString info){
this.info = info;
}
}
class AnimalInt {
private int i;
AnimalInt(int i){
this.i = i}
}
}
class AnimalIntDog{
public AnimalInt info;
AnimalDog(AnimalInt info){
this.info = info;
}
}
...
public class Animal {
public static void main(String args[]){
AnimalString as = new AnimalString("강아지");
AnimalInt ai = new AnimalInt(2);
AnimalDog ad = new AnimalDog(as);
...
위의 코드는 **AnimalDog**와 AnimalIntDog 클래스의 생성자에서 중복이 존재한다. 두 클래스는 ‘AnimalString’과 ‘AnimalInt’ 객체를 변수로 가지며 하는 일은 거의 동일하다.
AnimalDog 클래스의 생성자는 AnimalString 객체를 받아서 ‘info’멤버 변수에 할당하는 작업을 수행한다. 마찬가지로 AnimalIntDog 클래스의 생성자는 AnimalInt 객체를 받아서 info 멤버 변수에 할당한다.
두 클래스의 생성자는 거의 동일한 작업을 수행하므로 중복이 발생하고 중복을 제거하고 코드를 개선하기 위해서는 공통된 동작을 수행하는 일반화된 코드를 작성함으로써 코드를 더욱 효율적으로 만들 수 있다. ❗코드 일반화란? 특정한 타입에 종속되지 않고 범용적으로 사용할 수 있는 코드를 작성하는 것을 의미
🔽 제네릭을 사용한 경우
class AnimalString {
private String species;
AnimalString(String species){
this.species = species;
}
}
class AnimalInt {
private int i;
AnimalInt(int i){
this.i = i;
}
}
// 제네릭 사용으로 데이터 타입에 의존하지 않을 수 있음
class AnimalInfo<T> {
public T info;
AnimalInfo(T info){
this.info = info;
}
}
public class Animal {
public static void main(String[] args) {
AnimalInfo<AnimalString> asInfo = new AnimalInfo<>(new AnimalString("강아지"));
AnimalInfo<AnimalInt> aiInfo = new AnimalInfo<>(new AnimalInt(2));
AnimalInfo<AnimalString> adInfo = new AnimalInfo<>(asInfo.info);
//...
}
}
이처럼 제네릭을 사용하면 특정 타입에 종속되지 않고 범용적으로 사용할 수 있는 코를 작성함으로 다양한 타입에 대해 재사용 가능한 코드를 만들 수 있다.
제네릭(Generic)의 특징
- 알파벳 대문자로 된 단수형 변수이름 사용
class MyClass<T> {
// T는 제네릭 타입 매개변수
// 클래스 내부에서 T 사용가능
}
제네릭에서 변수를 지정하는 경우 일반적으로 알파벳 대문자로 된 단수형 변수 이름을 사용한다. 개발자의 선택에 따라 자유롭게 정할 수도 있다. 하나의 변수를 사용할 때는 일반적으로 Type의 약자인 T를 주로 사용한다.
ex) E (Element의 약자), K (Key의 약자), V (Value의 약자) 등
- 복수로 사용할 때는 “,” 찍어주고, 서로 다른 변수 이름으로 선언 해야 한다.일반적으로 사용되는 알파벳 대문자와 그에 대한 관례적인 의미(꼭 아래의 변수명을 사용하지 않아도 됨)
- T (Type): 제네릭 타입 매개변수의 일반적인 이름으로, 임의의 타입을 나타냅니다.
- E (Element): 주로 컬렉션의 요소 타입을 나타내는 데에 사용됩니다.
- K (Key): 주로 맵의 키 타입을 나타내는 데에 사용됩니다.
- V (Value): 주로 맵의 값 타입을 나타내는 데에 사용됩니다.
- S, U, V 등: T 이외의 추가적인 제네릭 타입 매개변수를 표현할 때 사용됩니다.
- class MyClass<T, S> { // T는 제네릭 타입 매개변수입니다. // 클래스 내부에서 T를 사용할 수 있습니다. }
- 제네릭에는 기본 데이터 타입 사용이 불가하다.
제네릭은 제네릭 타입 매개변수는 참조 타입이어야 하기 때문에 기본 데이터 타입 (int, boolean, double 등)의 사용이 불가하다. → wrapper 클래스 사용
// 잘못된 사용 방법
MyClass<T, int> myObj = new MyClass<>();
위의 코드는 잘못된 예시코드로 실제 사용에서는 래퍼 클래스를 사용해 데이터 타입을 감싸줘야 한다. 래퍼 클래스는 제네릭으로 선언된 타입인자를 받아들여 그 타입을 감싸고, 해당 타입과 관련된 동작을 수행하는 메서드를 제공한다. → 타입 안정성 보장
MyClass<T, Integer> myObj = new MyClass<>();
✨ wrapper 클래스
자바에서는 기본 데이터 타입인 int, boolean, double 등에 대한 래퍼 클래스를 제공한다. 래퍼 클래스는 약어가 아닌 기본 데이터 타입의 이름으로 표현해야한다. 그리고 기본데이터의 첫 글자는 대문자로 표기한다.
ex) int ⇒ Integer, double ⇒ Double, long ⇒ Long …
- 래퍼 클래스의 언박싱(Unboxing)래퍼 클래스를 사용하여 제네릭에서 기본 데이터를 사용한 경우에는 필요에 따라 언박싱(Unboxing)해서 감싼 것을 풀어줘야 한다.
Integer wrapperValue = 42; // Integer 래퍼 클래스로 값을 감싸기 int primitiveValue = (int) wrapperValue; // 언박싱: 래퍼 클래스를 통해 기본 데이터 타입으로 값을 가져오기 // int primitiveValue = wrapperValue.intValue(); 이렇게 표현할 수도 있다. System.out.println(primitiveValue); // 출력: 42
✨ 언박싱이 필요한 경우
- 기본 데이터로의 형변환 :
제네릭으로 처리한 데이터를 기본 데이터 타입으로 사용해야 할 때 언박싱이 필요
* 참고 - 자바 5부터 도입된 오토박싱(Autoboxing)과 오토언박싱(Auto-unboxing) 기능으로 자동으로 처리 - 기본 데이터 타입을 요구하는 메서드에 인자로 전달:
메서드가 기본 데이터 타입을 요구할 경우, 제네릭으로 처리한 데이터를 언박싱하여 해당 메서드에 전달 - 수식 계산 등의 연산:
제네릭으로 처리한 데이터를 수식 계산 등의 연산에 사용하려면 언박싱이 필요
제네릭(Generic)의 제한
제네릭은 다양한 타입을 매개변수로 하는 인스턴스를 생성하도록 해준다. 경우에 따라 타입을 제한할 수도 있다. 제네릭의 제한은 extends 키워드를 사용하여 특정 클래스나 인터페이스를 제한한다.
제한 된 제네릭은 두 가지 형태로 사용될 수 있다.
1. 특정 클래스나 인터페이스를 상속받은 타입들만 제한하는 경우(클래스 제한)
public class MyClass<T extends Comparable<T>> {
...
}
2, 상속 계층 구조에서 특정 클래스의 상위 클래스나 인터페이스만을 제한하는 경우(상속 계층 구조를 기반으로 한 제한)
public class MyClass<T extends Number> {
...
}
이렇게 제한된 제네릭을 사용함으로 특정한 타입들로 제네릭 타입을 제한 시킴으로서 타입 안정성을 높일 수 있고 잘못된 타입 사용을 컴파일 시점에 방지할 수 있다.‘super’키워드로의 제한 : 하위 클래스를 제한하는 제한된 제네릭(super bounded generics)도 사용할 수 있다. → 특정 하위클래스만 인자로 사용
➕더 알아 놓으면 좋은 것
‘super’키워드로의 제한 : 하위 클래스를 제한하는 제한된 제네릭(super bounded generics)도 사용할 수 있다. → 특정 하위클래스만 인자로 사용
이렇게 한 번 더 정리를 안 하니 까먹는 것도 금방이고 깊이 알지 않게 되어 뒤에 문제들도 잘 해결하지 못하는 나를 발견했다. 늦었지만 컬렉션 정리를 꼼꼼하게 하고자 한다.
제네릭은 <>을 사용하여 선언하고 다양한 타입을 받을 수 있다. << 정도만 알았는데 작성하면서 오늘도 궁금한 점이 많이 생겼다. 왜 사용하는지, 그리고 래퍼 클래스에 대한 것들이라던가, 어떨 때는 T, S, U 등 의 타입을 받고 String, Integer 등과 같은 타입을 받기도 하고 class 를 받기도 하는 제네릭이 이해가 쉽게 되지 않은 채 넘어갔는데 정리를 통해 알게 되었다.
앞으로 컬렉션은 더 많이 사용 할텐데 너무 컬렉션이 익숙하지도 않고 사용법을 모르니 언제 사용해야 하는지도 모르고 어떻게 사용하는지도 몰라서 최대한 빨리 정리하는 것이 좋을 듯 하다.
제네릭은 간단하다고 생각했는데 결코 간단하지 않았다. ㅎㅎ는 후기
'LANGUAGE > JAVA' 카테고리의 다른 글
[JAVA] 자바 컨테이너(컬렉션 프레임워크 / Collection Framework)란? (0) | 2023.06.25 |
---|---|
[JAVA] 데이터 타입 / String 기본 메서드 및 데이터 타입 알아보기 (0) | 2023.06.18 |
[자료구조] 그래프(Graph)란? / 인접 행렬(Adjacency Matrix)과 인접 리스트(Adjacency List) (1) | 2023.05.17 |
[자료구조] 트리 구조 (Tree) / 이진 트리 (Binary Tree) (0) | 2023.05.16 |
[JAVA] 프로세스(Process)와 스레드(Thread) / 싱글 스레드와 멀티 스레드, 스레드 동기화, 상태 및 메서드 알아보기 (0) | 2023.05.08 |