개요
다양한 타입의 객체를 다루는 메서드로, 컬렉션 클래스에서 컴파일 시에 타입을 체크한다.
미리 사용할 타입을 명시해서 형 변환을 하지 않아도 되게 한다.
이로 인해 객체의 타입에 대한 안전성 향상 및 형 변환의 번거로움을 감소할 수 있다.
✅ Generics의 주요 특징
- 컴파일 시 타입 체크 : 잘못된 타입 사용 시 컴파일 단계에서 오류를 잡을 수 있다.
- 형 변환 불필요 : 제네릭을 사용하면 객체를 꺼낼 때 명시적 형 변환(casting)이 필요 없다.
- 유지보수 용이 : 타입 안정성이 보장되어 코드 가독성 및 유지보수가 쉽다.
제네릭 클래스 정의
클래스 또는 인터페이스 선언 시 <>에 타입 파라미터를 표시한다.
public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
- T는 타입 매개변수로, Box 클래스 내부에서 실제 타입으로 대체된다.
- T는 보통 Type의 약자로 사용하지만, E(Element), K(Key), V(Value) 등 자유롭게 사용 가능하다.
public class Main {
public static void main(String[] args) {
Box<String> box1 = new Box<>(); // String 타입
box1.set("Hello");
System.out.println(box1.get()); // Hello
Box<Integer> box2 = new Box<>(); // Integer 타입
box2.set(123);
System.out.println(box2.get()); // 123
}
}
- Box 객체 생성 시 타입을 지정하여, 다양한 타입의 객체를 저장할 수 있다.
- Box<String> → String 타입 저장, Box<Integer> → Integer 타입 저장
객체 생성 시 변수 쪽과 생성 쪽의 타입은 반드시 같아야 한다.
제네릭 메서드 정의
class Util {
public static <T> void printAndReturn(T value) {
System.out.println(value);
}
}
public class Main {
public static void main(String[] args) {
Util.printAndReturn("Hello"); // String
Util.printAndReturn(123); // Integer
}
}
호출 시 타입이 자동 추론되어 T에 적절한 타입이 대입되는 것을 볼 수 있다.
상속 관계
interface Printer<T> {
void print(T value);
}
class StringPrinter implements Printer<String> {
public void print(String value) {
System.out.println(value);
}
}
class IntegerPrinter implements Printer<Integer> {
public void print(Integer value) {
System.out.println(value);
}
}
public class Main {
public static void main(String[] args) {
Printer<String> printer = new StringPrinter();
printer.print("Hello");
Printer<Integer> printerInteger = new IntegerPrinter();
printerInteger.print(123);
}
}
제네릭 인터페이스를 정의한 후 두 클래스 StringPrinter와 IntegerPrinter를 정의하였다.
두 클래스는 해당 인터페이스를 상속받아 각 타입에 맞는 print함수를 구현하였다.
두 클래스 Person과 Student는 상속 관계이지만 두 박스 pbox와 sbox는 상속 관계가 없다.
전달 시점
타입 파라미터는 객체를 생성하면서 전달된다.
타입의 결정 시점은 객체 생성 시점이므로 static 멤버에서는 사용이 불가하다.
제네릭은 컴파일 타임에 지정한 타입으로 존재한다.
런타임에는 타입 정보가 삭제되고 단순 Object로 관리가 된다.
컴파일러가 이미 타입을 체크했기 때문에 런타임에는 자유롭게 사용할 수 있다.
하지만 런타임에 동작하는 new나 instanceof키워드는 사용이 불가하다.
배열은 런타임에 객체의 정보를 유지하고 동일한 타입의 객체만 처리한다.
따라서 T[]타입의 배열은 컴파일 후 런타임 시에는 Object[]로 변경된다.
이로 인해 제네릭 타입으로는 배열 생성이 불가능하다, 컴파일 단계에서 에러를 발생시킨다.
이는 동일한 타입의 객체를 처리하는 배열 특성상 타입 안정성을 확보할 수 없기 때문이다.
와일드 카드
import java.util.ArrayList;
import java.util.List;
class Util {
public static void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
}
public class Main {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Util.printList(list);
List<String> list1 = new ArrayList<>();
list1.add("Hello");
list1.add("World");
Util.printList(list1);
}
}
<?>는 모든 타입의 리스트를 받을 수 있는 와일드카드이다.
제네릭 제한
1. 상한 경계 (extends)
필요에 따라 구체적인 타입 제한이 필요하다.
만약 정수 타입의 인자만 받고 싶다면 T를 Number클래스를 상속받은 타입만 사용 가능하게 명시해야 한다.
public <T extends Number> void print(T value) {
System.out.println(value);
}
T는 Number 클래스를 상속받은 타입만 사용 가능.
2. 하한 경계 (super)
public <T super Integer> void print(T value) {
System.out.println(value);
}
T는 Integer의 상위 클래스만 사용 가능.
클래스와 함께 인터페이스 제약 조건을 이용할 경우 &로 연결한다.
따라서 extends는 조건 클래스의 하위 클래스만, super는 조건 클래스의 상위 클래스만 받을 수 있다.
'웹(WEB) > 자바(Java)' 카테고리의 다른 글
[Java] 자바 annotation (0) | 2024.12.31 |
---|---|
[Java] 자바 Enum (0) | 2024.12.31 |
[Java] 자바 인터페이스 (0) | 2024.12.30 |
[Java] 자바 Object 클래스 (0) | 2024.12.30 |
[Java] 자바 제한자 (0) | 2024.12.30 |