모던 php 를 사용할 때는 다양한 타입을 지원했고 따로 명시해 줄 필요도 없었다. Java를 다루면서 데이터 형식을 명확히 해야하고 이에 대한 규제가 정확했다. 이에 대해 정확히 알아보고자 이 글을 작성한다.
🟢 제네릭(Generic)
제네릭(Generic)는 '일반적인', ' 데이터 타입을 일반화함'이라는 뜻이다. 클래스나 메소드에서 사용할 내부 데이터 타입을 컴파일 시 미리 지정한다고 이해할 수 있다.
우리가 어떠한 자료구조를 사용한다고 가정해보자, List, Queue, LinkedList 와 같은 것들. String 타입 뿐만 아니라 Integer 타입도 지원하고 싶은 경우에 타입 별로 클래스나, 변수들을 생성하진 않을 것이고 이는 비효율적이다. 이러한 문제를 해결하기 위해 우리는 제네릭이라는 것을 사용한다.
이렇듯 제네릭(Generic) 은 클래스 내부에서 지정하는 것이 아닌 외부에서 사용자에 의해 지정되는 것을 의미한다. 한마디로 특정 타입을 미리 지정해주는 것이 아닌 필요에 의해 지정할 수 있도록 하는 일반(Generic) 타입이라는 것이다.
Generic의 장점
1. 제네릭을 사용하면 잘못된 타입이 들어올 수 있는 것을 컴파일 단계에서 방지할 수 있다.
2. 클래스 외부에서 타입을 지정해주기 때문에 따로 타입을 체크하고 변환해줄 피룡가 없다.
3. 비슷한 기능을 지원하는 경우, 코드의 재사용성이 높아진다.
즉, 미리 타입 검사(type check) 를 함으로써 클래스나 메소드 내부에서 사용되는 객체의 타입 안정성을 높일 수 있으며, 반환 값에 대한 타입 변환 및 타입 검사에 들어가는 노력을 줄일 수 있어서 사용한다.
💡타입 안정성이 프로그램을 잘못되게 하지 않을 때, 타이핑이 잘 됐다고 한다. 여기서 타입 안정성은 int 가 실제로 int 임을 보장하는 것을 의미한다. 아래의 코드는 잘 정의됐지만 잘 타이핑되지는 않은 코드다. (x는 int일수도 String일 수도 있음)
if(p) x = 5;
else x = "hello";
if(p) return x+5;
else return strlen(x);
💡JDK 1.5 이전에는 클래스나 메소드에서 인수나 반환값으로 Object 타입을 사용했는데 이 경우 반환된 Object 객체를 다시 원하는 타입으로 타입 변환해야하며 오류가 발생할 가능성도 존재한다. JDK 1.5 이후에는 제네릭 사용하면 컴파일 시 미리 타입 정해져서 타입 검사나 타입변환과 같은 작업 생략 가능하다.
✅사용 방법
선언 및 생성
class MyArray<T> {
T element;
void setElement(T element) { this.element = element; }
T getElement() { return element; }
}
⭐ T, 타입 변수 (type variable)
T 외의 다른 어떤 문자든 사용 가능하며, 여러 개의 타입 변수는 쉼표로 구분 가능하다. 또한 클래스 뿐만 아니라 메소드의 매개변수나 반환값으로도 사용할 수 있다. 타입 변수에도 컨벤션(규칙)이 존재한다.
제네릭 타입은 컴파일 시 컴파일러에 의해 자동으로 검사돼서 타입 변환되고, 컴파일 된 .class 파일에는 어떠한 제네릭 타입도 포함되지 않는다. 이는 제네릭을 사용하지 않는 코드와의 호환성을 유지하기 위해서이다.
🔻제네릭 사용 X
public static void main(String[] args) {
List numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
int sum = 0;
for (Object number : numbers) {
sum += (int) number;
}
}
제네릭을 사용하지 않을 시에는 직접 형변환이 필요하다. 이 경우에 리스트에 문자열을 넣어도 컴파일 에러가 발생하지 않고, 런타임 시 ClassCastException 이 발생하며, 컴파일 시 타입을 체크하고 에러를 찾아낼 수 있는 컴파일 언어의 장점을 발휘하지 못한다.
🔻제네릭 사용 O
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
int sum = 0;
for (Integer number : numbers) {
sum += number;
}
}
💡제네릭을 사용하면 원하는 타입이 있어도 모든 타입이 들어올 수 있다는 문제점이 있지만 이를 해결할 수 있는 방법이 있다. 바로 extends 키워드를 사용하는 것이다. 아래와 같이 타입 변수 뒤에 extends 키워드를 사용해 타입을 제한하는 방안이다.
public class Car<T extends CharSequence> {
private final T name;
public Car(T name) {
this.name = name;
}
...
}
🟢제네릭 메서드
제네릭 타입을 메서드 리턴 타입 앞에 선언한 메서드로, 다양한 타입의 객체들과 작동할 수 있도록 설계되어 있다. 이는 코드의 재사용성과 타입 안정성을 높여주는 방법이다. 제너릭 메서드는 메서드 선언에 타입 파라미터를 포함시키는 것을 말하며, 이 타입 파라미터는 메서드 내에서 변수 타입, 반환 타입, 파라미터 타입 등으로 사용될 수 있다.
public class GenericMethodTest {
// 제너릭 메서드
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
public static void main(String[] args) {
// 정수 배열
Integer[] intArray = { 1, 2, 3, 4, 5 };
// 문자열 배열
String[] stringArray = { "Hello", "World" };
// 제너릭 메서드 호출
printArray(intArray);
printArray(stringArray);
}
}
타입 파라미터 T 간에는 상/하위 관계가 없고 raw-type 간에만 상/하위 관계가 존재한다. 이를 해결하기 위해 아래와 같이 표기하면 된다. (? : wildcard)