제네릭(Generic)은 자바에서 데이터 타입을 일반화하여 다양한 데이터 타입을 하나의 클래스나 메서드에서 사용할 수 있도록 하는 기능입니다. 제네릭을 사용하면 타입 안정성을 보장하고, 코드 재사용성을 높일 수 있어 자바 컬렉션 프레임워크와 같은 라이브러리에서 특히 유용하게 사용됩니다.
제네릭이 도입되기 전에는, 다양한 타입의 데이터를 하나의 컬렉션에 저장할 때 Object
타입을 사용했습니다. 이 방식은 데이터를 꺼낼 때 다운캐스팅(downcasting)이 필요했고, 잘못된 타입이 들어가도 컴파일러가 잡아주지 못하는 문제가 있었습니다.
List list = new ArrayList();
list.add("Hello");
String s = (String) list.get(0); // 다운캐스팅 필요
위 예시에서 다운캐스팅을 통해 데이터를 꺼내야 하는데, 이 경우 타입 오류가 발생할 수 있습니다.
제네릭을 사용하면 컴파일 시점에 타입이 고정되어, 잘못된 타입이 들어가는 것을 방지할 수 있습니다. 다음은 제네릭을 사용하여 타입 안정성을 높이는 예시입니다:
List<String> list = new ArrayList<>();
list.add("Hello");
String s = list.get(0); // 다운캐스팅 불필요
위 코드에서 List<String>
은 String
타입의 요소만 담을 수 있는 리스트를 의미하며, 다른 타입을 넣으려고 하면 컴파일 오류가 발생합니다.
제네릭 클래스는 클래스 선언에 타입 매개변수(type parameter)를 추가하여 정의합니다. 제네릭 클래스의 타입 매개변수는 보통 <T>
와 같은 형식으로 표기하며, T
는 타입 파라미터를 의미합니다. (T는 Type
의 약자이며, E(Element), K(Key), V(Value) 등으로도 사용됩니다.)
public class Box<T> {
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
Box<String> stringBox = new Box<>();
stringBox.setItem("Hello");
System.out.println(stringBox.getItem());
Box<Integer> intBox = new Box<>();
intBox.setItem(123);
System.out.println(intBox.getItem());
위 예제에서 Box<T>
는 제네릭 클래스로, String
과 Integer
타입의 Box
객체를 각각 생성할 수 있습니다.
package com.exam;
public class Box1 {
// Object 타입 필드 - 모든 타입의 객체 저장 가능
Object object;
// 저장된 Object 타입 객체를 반환
public Object getObject() {
return object;
}
// Object 타입 객체를 필드에 저장
public void setObject(Object object) {
this.object = object;
}
}
public class BoxEx01 {
public static void main(String[] args) {
// Box1 객체 생성 (Object 타입 사용)
Box1 box1 = new Box1();
// String 객체를 Object 타입 필드에 저장
box1.setObject(new String("홍길동"));
// 저장된 Object를 String으로 형변환하여 가져옴
String name = (String) box1.getObject();
System.out.println(name); // "홍길동" 출력
// Integer 객체를 Object 타입 필드에 저장
box1.setObject(Integer.valueOf(10));
// 저장된 Object를 Integer로 형변환하여 가져옴
int data = (int) box1.getObject();
System.out.println(data); // 10 출력
// 주의: 비제네릭 코드에서 형변환이 필요하며, 타입이 맞지 않으면 런타임 오류 발생 가능
}
}
package com.exam;
// 제네릭 클래스 Box2 정의 - 타입 매개변수 <T>를 사용하여 다양한 타입 지원
public class Box2<T> {
// 필드 T 타입 - T는 나중에 지정될 타입을 의미
T t;
// T 타입의 객체 반환
public T getT() {
return t;
}
// T 타입의 객체를 필드에 저장
public void setT(T t) {
this.t = t;
}
}
package com.exam;
public class BoxEx02 {
public static void main(String[] args) {
// Box2 클래스의 String 타입 객체 생성
Box2<String> box1 = new Box2<>();
box1.setT("홍길동"); // String 타입 값 설정
String name = box1.getT(); // 형변환 없이 값 반환
System.out.println(name); // "홍길동" 출력
// Box2 클래스의 Integer 타입 객체 생성
Box2<Integer> box2 = new Box2<>();
box2.setT(10); // Integer 타입 값 설정
int data = box2.getT(); // 형변환 없이 값 반환
System.out.println(data); // 10 출력
// 제네릭을 사용하여 타입 안전성이 보장되고 형변환이 필요하지 않음
}
}
package com.exam;
// 제네릭 클래스 Product 정의 - 두 개의 타입 매개변수 <K, M> 사용
public class Product<K, M> {
private K kind; // K 타입 필드 - 제품 종류
private M model; // M 타입 필드 - 모델 정보
// K 타입의 kind 반환
public K getKind() {
return kind;
}
// K 타입의 kind 설정
public void setKind(K kind) {
this.kind = kind;
}
// M 타입의 model 반환
public M getModel() {
return model;
}
// M 타입의 model 설정
public void setModel(M model) {
this.model = model;
}
}
package com.exam;
// Car 클래스 정의 - Product에서 사용하기 위한 타입
public class Car {
// Car 클래스의 내용은 생략
}
package com.exam;
// Tv 클래스 정의 - Product에서 사용하기 위한 타입
public class Tv {
// Tv 클래스의 내용은 생략
}
package com.exam;
public class ProductEx01 {
public static void main(String[] args) {
// Product 인스턴스 생성 (K 타입은 Tv, M 타입은 String)
Product<Tv, String> product1 = new Product<>();
product1.setKind(new Tv()); // kind 필드에 Tv 객체 설정
product1.setModel("스마트 TV"); // model 필드에 모델명 설정
System.out.println("Product1 - Kind: " + product1.getKind() + ", Model: " + product1.getModel());
// Product 인스턴스 생성 (K 타입은 Car, M 타입은 String)
Product<Car, String> product2 = new Product<>();
product2.setKind(new Car()); // kind 필드에 Car 객체 설정
product2.setModel("전기차"); // model 필드에 모델명 설정
System.out.println("Product2 - Kind: " + product2.getKind() + ", Model: " + product2.getModel());
}
}