자바 제네릭
자바에서 제네릭은 데이터의 타입을 일반화하는 것을 의미한다. 클래스나 메소드에서 사용할 데이터의 타입을 컴파일 시에 미리 지정하는 방법이다. 제네릭(Generic)이라는 단어의 의미에도 '일반적인'이라는 뜻이 있다.
우리가 자바로 프로그래밍을 할 때 많이 사용하는 컬렉션 타입들이 제네릭을 구현되어 있다. 예를들어
ArrayList<String> stringList = new ArrayList<>();
ArrayList<Integer> integerList = new ArrayList<>();
ArrayList 객체의 경우 객체에서 다룰 데이터의 타입을 제네릭으로 입력하도록 되어 있다.
ArrayList처럼 다양한 타입에 대해 동작하는 클래스를 구현하고 싶을 때 제네릭을 사용한다. 제네릭은 데이터의 타입을 클래스 내부에서 지정하는 것이 아닌 외부에서 사용자에 의해 지정되는 것을 의미한다. 특정 타입을 미리 지정해주는 것이 아니라 필요에 의해 지정할 수 있도록 하는 일반적인 타입이다.
예를 들어 다음 메소드를 생각해보자.
public void printValue(int value) {
System.out.println("value is '" + value + "'");
}
이 메소드는 int 형 데이터를 입력받아서 출력해준다. 만약 문자열 데이터를 입력받아서 출력하고 싶다면 새로 메소드를 구현해야한다.
public void printValue(String value) {
System.out.println("value is '" + value + "'");
}
혹은 MyValue라는 객체를 입력받아서 toString() 메소드로 문자열을 뽑아 출력하고 싶다면 또 다른 메소드를 구현해야한다.
public void printValue(MyValue value) {
System.out.println("value is '" + value + "'");
}
이렇게 지원하고 싶은 데이터 타입이 생길 때마다 메소드를 추가할 수는 없다. 추가되는 메소드를 살펴보면 입력받은 value 변수의 타입만 다를 뿐 코어 로직은 똑같다.
자바 제네릭은 이럴때 사용하면 좋다.
class MyClass <T> {
public void printValue(T value) {
System.out.println("value is '" + value + "'");
}
}
그리고 제네릭을 사용할 때 타입을 써주면 된다.
MyClass<Integer> intClass = new MyClass<>();
intClass.printValue(10);
MyClass<String> stringClass = new MyClass<>();
stringClass.printValue("my value");
MyClass<MyValue> myValueClass = new MyClass<>();
MyValue myValue = new MyValue("my value");
myValueClass.printValue(myValue);
3개의 메소드가 제네릭을 사용해서 하나의 메소드로 줄었다. 타입에 대한 상세 사항을 클래스, 메소드 구현에서 숨겨 코드가 깔끔해지는 효과를 얻었다.
자바 제네릭의 장점
위에서 본 것처럼 제네릭을 사용하면 타입에 대한 고려로 쓸데 없이 코드가 많아지지 않는다. 코드가 깔끔해지는 장점을 얻을 수 있다. 즉, 코드의 재사용성이 높아진다.
만약 제네릭을 사용하지 않는다면, 범용으로 사용할 수 있는 Object 클래스나 특정 인터페이스를 타입으로 명시하고 사용해야한다. 이 경우 잘못된 타입의 데이터가 들어와도 컴파일 타임에는 알 수가 없다. 런타임에 가서야 타입 캐스팅 에러 등이 발생하게 된다. 제네릭을 사용하면 잘못된 타입 사용을 컴파일 타임에 알 수 있다.
자바 제네릭 사용법
자바의 제네릭은 클래스나 인터페이스의 선언에 꺽쇠 문자로 적어넣어주면 된다. 예를들어
public class MyClass <T> { ... }
public interface MyInterface <T> { ... }
이런식으로 선언해주고, 클래스와 인터페이스의 바디 부분의 타입에 꺽쇠 사이에 쓴 문자를 쓰면된다. 여기서는 T를 쓰면된다. 위에서 봤던 MyClass 클래스 정의를 보면
class MyClass <T> {
public void printValue(T value) {
System.out.println("value is '" + value + "'");
}
}
제네릭으로 사용한 T라는 글자가 printValue() 메소드의 인자의 타입으로 사용하는 것을 볼 수 있다. 객체를 생성할 때, T 부분에 어떤 타입을 사용할 것인지 적어두면 컴파일러가 T 부분에 타입을 치환해서 코드를 만들게 된다.
MyClass<Integer> intClass = new MyClass<>();
MyClass<String> intClass = new MyClass<>();
MyClass<MyObject> intClass = new MyClass<>();
이 코드는 위에서부터 Integer, String, MyObject 타입이 T 문자에 대응되어 처리된다고 보면 된다.
제네릭으로 꼭 하나의 타입만 사용해야하는 것은 아니다. 두 개의 타입도 제네릭으로 처리할 수 있다.
class MyClass <T, U> {
public void printValue(T value1, U value2) {
System.out.println("value1 is '" + value1 + "'");
System.out.println("value2 is '" + value2 + "'");
}
}
제네릭으로 쓰는 문자는 아무거나 사용해도 좋다. 하지만 일반적으로 다음과 같은 의미로 사용한다.
타입 인자 | 의미 |
<T> | Type |
<E> | Element |
<K> | Key |
<N> | Number |
<V> | Value |
<R> | Result |
자바 제네릭 한정적 타입 매개변수
제네릭으로 사용할 수 있는 타입을 한정할 수도 있다. 예를 들어 특정 인터페이스를 구현한 클래스만 인자로 입력받을 수 있도록 클래스 선언시 제네릭 타입 타입 매개변수를 제한 할 수 있다.
public class MyClass <T extends MyClass>
클래스의 상속을 정의할 때 사용한 extends
키워드를 제네릭 정의에 사용하면, 제네릭 타입을 제한한다는 의미가 된다. 위 코드에서 제네릭 T로 사용할 수 있는 클래스는 반드시 MyClass 혹은 MyClass를 상속한 클래스로 제한된다. 인터페이스나 추상클래스 모두 extends 키워드를 사용한다.
public class MyClass <T super MyClass>
super 키워드를 이용해서 특정 클래스의 상위 클래스만 타입으로 가지도록 제한할 수도 있다.
제네릭 클래스를 정의할 때 이런식으로 타입 제한을 걸어둘 경우, 이 제한이 맞지 않는 클래스를 제네릭으로 사용하려한다면 컴파일 에러가 발생하게 된다.
제네릭 메소드
클래스의 정의와 마찬가지로 메소드 역시 제네릭으로 만들 수 있다. 참고로 제네릭 메소드를 사용하기 위해 클래스까지 제네릭일 필요는 없다.
제네릭 메소드는 다음과 같이 정의할 수 있다.
public <T> T genericMethod(T param) {
// Body
}
제네릭 메소드는 컴파일러에게 자신이 제네릭 메소드임을 알리기 위해 리턴 타입 바로 앞에 제네릭 메소드임을 명시해줘야 한다. 참고로 클래스에 지정된 타입 파라미터와 제네릭 메소드에 정의된 타입 파라미터는 상관이 없다. 즉, 제네릭 클래스에 를 이용하고 그 클래스에 포함된 제네릭 메소드에 를 사용한 경우, 클래스의 T와 메소드의 T는 전혀 관련이 없다. 각자 자기 범위에 적용된다.
제네릭 메소드가 필요한 이유는 static 메소드때문이다. 자바의 제네릭은 new 메소드를 이용해서 객체를 생성할 때 타입을 받아서 구체화된다. 하지만 static 클래스는 객체를 생성하지 않는다. 때문에 클래스에 정의된 제네릭을 사용할 수 없다.
제네릭의 편리함을 static 메소드에도 적용할 수 있도록 제네릭 메소드의 정의 방법이 필요하다.
제네릭을 사용할 수 없는 경우
반면 제네릭을 사용할 수 없는 경우도 존재한다.
- 제네릭으로 배열을 생성할 수 없다.
- static 변수를 제네릭으로 사용할 수 없다.
댓글