
타입 안전성(Type Safety): 제네릭을 사용하면 컴파일 시점에 타입을 확인하여 잘못된 타입의 객체가 사용되는 것을 방지할 수 있습니다. 이는 런타임에 발생할 수 있는 ClassCastException과 같은 오류를 줄여줍니다.
코드 재사용성(Code Reusability): 제네릭은 다양한 타입을 처리할 수 있는 일반적인 클래스를 정의할 수 있게 해줍니다. 이를 통해 코드 중복을 줄이고, 여러 타입을 지원하는 코드 작성을 가능하게 합니다.
런타임 에러 방지: 제네릭을 사용하면 타입 캐스팅으로 인한 런타임 오류를 줄일 수 있습니다. 컴파일 시점에서 타입 검사를 하므로 런타임에 발생할 수 있는 예기치 않은 오류를 예방할 수 있습니다.
의미 있는 코드: 제네릭을 사용하면 코드의 가독성과 의미를 명확하게 할 수 있습니다. 타입 정보가 명시되어 있어 코드의 의도가 더 분명해지고, 유지보수도 용이해집니다.
<T>와 같이 타입 매개변수를 지정합니다. T는 타입 매개변수를 나타내며, 임의의 이름을 사용할 수 있지만 일반적으로 T, E, K, V, N 등의 관습적인 이름을 사용합니다.T(Type), N(Numbers), K(Key), V(Value), E(Element), 등 입니다.T1, T2와 같이 사용하는 것을 추천합니다.public class Box<T> {
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
Box 클래스는 T라는 제네릭 타입을 사용합니다. Box 클래스는 T 타입의 객체를 저장하고 반환할 수 있게 되며, T는 사용자가 클래스 인스턴스를 생성할 때 구체적인 타입으로 대체됩니다.static 키워드를 통한 정의는 할 수 없습니다.Box<String>으로 객체를 생성하면 T는 String으로 대체되고, Box<Integer>로 생성하면 T는 Integer로 대체됩니다.Box<String> stringBox = new Box<String>(); // 오른쪽 타입 생략 가능 (타입추론)
stringBox.setItem("Hello");
String item = stringBox.getItem(); // 타입 캐스팅 불필요
Box<Integer> intBox = new Box<>(); // 오른쪽 타입 생략 가능 (타입추론)
intBox.setItem(123);
Integer number = intBox.getItem(); // 타입 캐스팅 불필요
stringBox는 String 타입의 데이터를, intBox는 Integer 타입의 데이터를 안전하게 저장하고 반환할 수 있습니다. new Box<>()처럼 문장 안에서 타입을 추정 할 수 있는 경우 생략할 수 있고, 이를 타입 추론이라 부릅니다. (왼쪽의 변수 선언 부분에선 생략하면 안됩니다.)<T extends SomeClass>와 같은 형태로 사용됩니다. (SomeClass는 임의의 클래스)<T extends SomeClass>는 타입 매개변수 T가 SomeClass의 하위 타입이거나, SomeClass 자체여야 함을 의미합니다.
public class NumberBox<T extends Number> {
private T number;
public NumberBox(T number) {
this.number = number;
}
public double doubleValue() {
return number.doubleValue();
}
}
NumberBox 클래스에서 T는 Number 클래스 또는 그 하위 클래스(Integer, Double, Float, 등)여야 합니다. T 타입의 객체는 항상 Number 클래스의 메서드를 사용할 수 있게 됩니다.& 연산자를 사용하여 여러 클래스를 조합하여 타입 매개변수에 여러 제한을 동시에 걸 수도 있습니다. (예를 들어, T가 특정 클래스를 상속받고, 동시에 여러 인터페이스를 구현하도록 제한할 수 있습니다.)public class MultiBoundBox<T extends Number & Comparable<T>> {
private T number;
public MultiBoundBox(T number) {
this.number = number;
}
public boolean isGreaterThan(T other) {
return number.compareTo(other) > 0;
}
}
public <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
printArray 메서드는 타입 매개변수 T를 사용하여 어떤 타입의 배열이든 받아서 그 배열의 모든 요소를 출력할 수 있습니다. T는 메서드를 호출할 때 결정되며, 여러 타입에 대해 동일한 메서드를 재사용할 수 있게 합니다.public static <T> T getFirstElement(T[] array) {
if (array == null || array.length == 0) {
return null;
}
return array[0];
}
getFirstElement 메서드는 배열의 첫 번째 요소를 반환하는 제네릭 메서드입니다. public static <T extends Comparable<T>> T findMax(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
findMax 메서드는 두 개의 Comparable 타입 객체를 받아, 더 큰 값을 반환하는 제네릭 메서드입니다. Comparable 인터페이스를 구현한 모든 객체에 대해 사용할 수 있습니다.static 메서드에서도 사용할 수 있습니다. public class Utility {
public static <T> void printElement(T element) {
System.out.println(element);
}
}
printElement 메서드는 Utility 클래스의 static 메서드로, 어떤 타입의 객체도 받아서 출력할 수 있습니다. ? 기호로 표시되며, 이는 "어떤 타입이든 가능하다"는 의미를 가집니다. <?>는 어떤 타입이라도 받을 수 있는 제네릭 타입을 의미합니다. public void printList(List<?> list) {
for (Object element : list) {
System.out.println(element);
}
}
printList 메서드는 어떤 타입의 리스트라도 받아서 그 요소들을 출력할 수 있습니다.List<String>, List<Integer> 등 다양한 리스트 타입을 처리할 수 있습니다.<? extends T>는 와일드카드가 T 타입이나 T의 하위 타입을 나타낼 수 있음을 의미합니다. public void processNumbers(List<? extends Number> list) {
for (Number number : list) {
System.out.println(number.doubleValue());
}
}
processNumbers 메서드는 Number 타입이나 그 하위 타입(Integer, Double 등)의 리스트를 받아서 처리할 수 있습니다. <? super T>는 와일드카드가 T 타입이나 T의 상위 타입을 나타낼 수 있음을 의미합니다. public void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
}
addNumbers 메서드는 Integer 타입이나 그 상위 타입(Number, Object)의 리스트에 Integer 값을 추가할 수 있습니다. List<?>는 어떤 타입의 요소가 들어있는지 알 수 없기 때문에 리스트에 요소를 추가하거나 특정 타입의 메서드를 호출할 수 없습니다.public void processList(List<?> list) {
// list.add(new Object()); // 컴파일 에러: 특정 타입을 추가할 수 없음
Object item = list.get(0); // 요소를 가져오는 것은 가능
}
list의 타입이 불명확하기 때문에 요소를 추가하는 등의 작업이 제한됩니다. 이로 인해 코드의 기능이 제한적일 수 있습니다.<? extends T>)를 사용할 경우 리스트에 요소를 추가하는 작업이 제한됩니다. public void addNumber(List<? extends Number> list) {
// list.add(new Integer(10)); // 컴파일 에러: 추가할 수 없음
}
Number의 하위 타입을 추가할 수 없습니다. 이는 상한 경계 와일드카드를 사용하면 리스트의 요소가 특정 타입을 벗어날 수 있다는 위험 때문에 발생합니다.public <T> void processList(List<T> list) {
list.add(list.get(0)); // 동일한 타입의 요소를 추가할 수 있음
}
processList 메서드는 타입 매개변수 T를 사용하여 리스트의 타입을 명확하게 지정하고 있습니다. Object 타입 또는 지정된 상한 타입으로 대체됩니다.Box<T>에서 T가 컴파일 시점에 Object나 지정된 상한 타입으로 대체됩니다.public class Box<T> {
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
Box<String>으로 사용된 제네릭 클래스는 컴파일 후 다음과 같은 형태로 변환됩니다public class Box {
private Object item;
public void setItem(Object item) {
this.item = item;
}
public Object getItem() {
return item;
}
}
T가 Number로 제한된 경우, 제네릭 타입은 Number로 대체됩니다.public class NumberBox<T extends Number> {
private T number;
public void setNumber(T number) {
this.number = number;
}
public T getNumber() {
return number;
}
}
NumberBox<Integer>는 컴파일 후 다음과 같이 변환됩니다public class NumberBox {
private Number number;
public void setNumber(Number number) {
this.number = number;
}
public Number getNumber() {
return number;
}
}
ClassCastException이 발생할 가능성을 증가시킵니다.Box<String> stringBox = new Box<>();
stringBox.setItem("Hello");
// 컴파일 후:
Box objectBox = stringBox; // Box 타입으로 변환
String item = (String) objectBox.getItem(); // 명시적 캐스팅 필요
List<String>과 List<Integer>는 컴파일 후 동일한 List로 간주됩니다.instanceof의 제약: 타입 이레이저로 인해 런타임 시 제네릭 타입 정보가 사라지기 때문에, 제네릭 타입에 대해 instanceof를 사용할 수 없습니다. instanceof 연산자는 정확한 타입을 확인할 수 없습니다.List<String> stringList = new ArrayList<>();
if (stringList instanceof List<String>) { // 컴파일 에러 발생
// Do something
}public void method(List<String> list) { }
public void method(List<Integer> list) { } // 컴파일 에러 발생
new 키워드의 제약)new T()와 같은 구문은 사용할 수 없습니다. public class Box<T> {
private T item;
public Box() {
this.item = new T(); // 컴파일 에러 발생
}
}
위 코드에서 new T()는 컴파일 에러를 발생시킵니다. 이는 T가 컴파일 시점에 구체적인 타입이 아니기 때문에, 어떤 생성자를 호출해야 하는지 알 수 없기 때문입니다.
instanceof나 new와 같은 구문에 제약이 발생합니다.