제네릭(Generics)
데이터의 타입(data type)을 일반화한다(generalize)
개념
클래스나 메서드에서 사용할 내부 데이터의 타입을 미리 정하는 것
- 내부 데이터 타입을 컴파일 시에 미리 지정하여,
- 잘못된 타입이 사용될 수 있는 문제를 컴파일 과정에서 제거가 가능해짐
장점
- 컴파일 시에 미리 타입 검사(type check)를 수행하기 때문에, 실행 중에 일어나는 에러 방지
- 제네릭 사용 시에, 불필요한 타입 변환을 하지 않아도 됨
컬랙션(Collection) 클래스에서 불필요한 타입 변환 발생
- 기존의 컬렉션 클래스에는 모든 타입의 객체(Object형태로 저장)를 저장할 수 있음
- 따라서, 다른 타입의 객체들이 저장될 수 있음
- 반대로 컬렉션에 저장된 객체를 읽어올 때에 타입 변환이 필요함
- 즉, 원래의 클래스 타입으로 다운캐스팅 해줘야 하는데, 다운캐스팅이 잘못 되더라도 컴파일 시점에서는 알 수 없고, 실행된 후에야 알 수 있음
용어 정리하고 가기
1. Type Parameter (타입 파라미터)
- generic 타입을 명시하기 위한 곳
- 정의된 타입 파라미터가 내부적으로 실제 타입으로 변환
- 예제 : T
2. Type Argument (타입 인수)
- 제네릭 클래스 생성 시에, 사용할 타입을 작성하는 곳
- 예제 : Integer
3. Parameterized Type
- Type Argument에 의해서 Type Parameter(T)가 치환된 전체 데이터 타입
- 예제 : Age<Integer>
제네릭의 선언과 생성
클래스, 인터페이스와 메서드에서 다음과 같은 방법으로 제네릭을 선언할 수 있음
1. 제네릭 선언하기
class GenericClass <T> { // T: 타입 파라미터
T element;
void setElement(T element) {
this.element = element;
}
T getElement() {
return element;
}
}
2. 제네릭 생성하기
클래스명<타입> 참조변수명 = new 클래스명<타입>();
클래스명<타입> 참조변수명 = new 클래스명<>(); // 생성자 다음의 타입은 생략 가능
- 제네릭 클래스를 생성할 때에 사용할 타입을 명시하면, 정의된 타입 파라미터가 내부적으로 실제 타입으로 변환되어 처리가 됨
- 클래스 내부에서 사용될 데이터의 타입을 외부에서 지정해 주는 것
- 타입이 매개변수화 된 것
[ 예제 ]
ArrayList<Integer> list = new ArrayList<>();
- 클래스를 생성할 때 사용할 타입(Integer)을 명시하면, 내부적으로는 정의된 타입 타라미터가 실제 타입으로 변환이 되어
- 해당 ArrayList 클래스 내에서 사용하는 데이터는 Ingeter 타입만 저장할 수 있음
✔ <T> 자리에는 참조형 타입만 사용할 수 있음
- 타입 파라미터 자리에 사용할 타입을 작성할 때, 기본형 타입은 사용할 수 없음
- 기본형 타입은 래퍼(Wrapper) 클래스를 사용해야 함
제네릭 타입 파라미터
정의
- 클래스나 메서드를 설계할 때에 제네릭 타입 파라미터를 사용
제네릭 타입 파라미터에 사용 가능한 타입
- 제네릭 타입 파라미터로 사용할 수 있는 타입은 참조(reference) 타입
- 기본형 타입(primitive type)은 제네릭의 타입 파라미터로 사용할 수 없음
- 따라서, 기본형 타입을 사용하고 싶다면, 기본 Wrapper 클래스를 사용해야 함(Integer형, Double형)
// 기본 타입은 제네릭의 타입 파라미터에 사용할 수 없음
List<int> aList = new ArrayList<>(); -- (X)
// Wrapper 클래스 사용
List<Integer> aList2 = new ArrayList<>(); -- (O)
예제
// 제네릭을 이용하여 클래스 설계
class Box<T> {
List<T> fruits = new ArrayList<>();
public List<T> getFruits() {
return fruits;
}
public void add(T fruit) {
fruits.add(fruit);
}
}
// 클래스
class Apple { }
public class Main {
public static void main(String[] args) {
// 1. 제네릭 타입 매개변수에 integer를 할당
Box<Integer> box = new Box<>();
box.add(10);
box.add(20);
for (Object obj : box.getFruits()) {
System.out.println(obj);
}
// 2. 제네릭 타입 매개변수에 String 할당
Box<String> box2 = new Box<>();
box2.add("String");
box2.add("타입만 저장");
for (Object obj : box2.getFruits()) {
System.out.println(obj);
}
// 3. 제네릭 타입 매개변수에 class도 할당 가능
Box<Apple> box3 = new Box<>();
box3.add(new Apple());
box3.add(new Apple());
for (Object obj : box3.getFruits()) {
System.out.println(obj);
}
}
}
제네릭을 사용하는 이유
1. 컴파일 시점에 타입 검사를 하여 에러 방지
- 여러 타입을 다루기 위해서 return 타입(리턴 타입)이나 파라미터(매개변수)에 Object 타입을 사용함
- Object으로 타입을 선언할 경우, 반환된 Object 객체는 다시 원래의 타입으로 형 변환 해야 하는데
- 컴파일 시에는 형변환 오류가 확인 되지 않고, 런타임(실행 시)에 형 변환의 오류가 발견되어 에러가 발생함
[ 예제 ]
1-1. 제네릭을 사용하지 않았을 때
[실행 결과]
- Cat 타입의 배열(Catarr)에 Cat객체를 생성하여 저장했고
- 배열의 요소를 가지고 오는 메서드 home.getAnimalHome(1)를 호출하여, Cat객체 타입의 값을 가지고 왔고
- Cat 객체 타입의 값을 Dog 타입으로 변경하려는 코드를 작성하여, 형 변환 오류가 있음에도 불구하고
- 컴파일 시에는 에러가 확인되지 않음
- 런타임 시(실행 시)에 ClassCastExcepton (형 변환 오류) 에러를 확인할 수 있음
1-2. 제네릭을 사용했을 때
- <T>는 매개변수로 전달 되는 객체(참조 타입)가, 곧 클래스 타입이 된다는 것
- 클래스를 정의할 때, 제네릭 타입 파라미터를 <Cat>으로 지정하여, 해당 클래스에 저장될 수 있는 데이터 타입을 Cat타입으로 정함
- 따라서, 해당 배열은 Cat 객체 저장이 가능하므로 Cat만 저장되어 있음
- 따라서, 해당 데이터 값을 Dog 타입으로 형 변환 하려고 하면, 컴파일 에러가 발생
- 잘못된 타입이 사용될 경우에, 컴파일 과정에서 문제를 확인하여 제거할 수 있음
2. 불필요한 형 변환(캐스팅)을 없앰
- Object 배열에 하위 타입의 데이터를 저장할 수 있음
- Object 클래스는 가장 상위 클래스이므로
- 하위 클래스는 Object 클래스로 업캐스팅이 가능
- Object 타입의 요소를 가지고 올 때, 반드시 다운 캐스팅을 하여 원래 가지고 있는 타입으로 변환시켜 줘야 함
- 이는 곧 추가적인 오버헤드가 발생하는 것
[ 예제 ]
2-1. Object 타입의 요소를 가지고 올 때, 다운캐스팅 하지 않으면 컴파일 오류 발생
- 배열(catArr)에 Cat 객체가 저장되어 있고
- AnimalHome 클래스 인스턴스를 생성하며, 배열(catArr)를 넣어서 값 초기화를 함
- AnimalHome 생성자 메서드가 실행되면서
- this.animal = catArr
- Object [ ] animal = catArr로 멤버변수의 값이 초기화 됨
- animal 배열은 Object 타입이고, animal 참조변수에 catArr 객체의 주소값이 저장됨
- 따라서, home.getAnimalHome(0)의 리턴 값은 Object 타입이고
- Object 타입을 Cat 타입에 넣으려고 해서 컴파일 오류가 발생함
2-2. 제네릭은 미리 타입을 지정해놓기 때문에, 형 변환(캐스팅)을 하지 않아도 됨
- T는 매개변수로 전달되는 객체가 곧 클래스 타입이 됨
- (예) class AnimalHome<Cat>
- Cat 객체가 클래스의 타입이 됨
- Cat 객체만 저장이 됨
- 배열(catArr)에 Cat 객체가 저장되어 있고
- AnimalHome 클래스 타입 파라미터 < >에 Cat을 할당하고, 클래스의 인스턴스를 생성하기 위해 배열(catArr)를 넣어서 값 초기화를 함
- AnimalHome<Cat> home = new AnimalHome<>(catArr)
- class AnimalHome<T>의 T가 Cat이 되고
- class AnimalHome<Cat> { }
- Cat 객체가 AnimalHome 클래스의 타입이 됨
- 즉, 해당 클래스에 Cat 객체만 저장이 됨
- AnimalHome 생성자 메서드가 실행되면서
- 생성자 AnimalHome(T [ ] animal )의 T가 Cat이 되고
- this.animal = catArr로 멤버변수(animal)의 값이 초기화 됨
- Cat [ ] animal = catArr
- animal 배열은 Cat 타입이고, 참조변수(animal )에 catArr 객체의 주소값이 저장됨
- 따라서, home.getAnimalHome(0)의 리턴 값은 Cat타입
- public Cat getAnimalHome(int index)의 리턴 값은 Cat타입이고
- Cat 타입의 데이터를 Cat 타입 참조변수에 넣기 때문에, 형 변환이 필요 없음
클래스에 제네릭을 사용한다고 해서 클래스가 여러개 만들어지는 것이 아님
클래스 선언문 옆에 제네릭 타입 파라미터를 주어 제네릭을 선언하고 생성한다고 해서 클래스가 여러 개 만들어지는 게 아니라, 해당 클래스에서 사용하는 데이터 타입이 정해지는 것
class Box<T> { T value; } public class Test { public static void main(String[] args) { Box<String> box1 = new Box<String>(); Box<String> box2 = new Box<>(); } }
// 제네릭 타입 매개변수를 사용하여 객체를 생성했다고 해서, 새로운 클래스가 생성되는 것이 아님 class Box <String> { //(x) String Value; } class Box <String> { //(x) String Value; }
출처
'JAVA' 카테고리의 다른 글
[JAVA] 공변성이 없는 제네릭, 제네릭 와일드 카드 (0) | 2024.04.26 |
---|---|
[JAVA] 제네릭 객체 생성 및 사용하기 (0) | 2024.04.26 |
[JAVA] Set 인터페이스(HashSet, LinkedHashSet, TreeSet) (0) | 2024.04.25 |
[JAVA] List 인터페이스(ArrayList, Vecror, LinkedList, Stack) (0) | 2024.04.25 |
[JAVA] 컬렉션 프레임워크와 종류 (0) | 2024.04.25 |