제네릭이란(Generic)
데이터형식에 의존하지 않고, 하나의 값이 여러 다른 데이터 타입들을 가질 수 있도록 하는 방법이다.
객체<타입> 객체명 = new 객체<타입>();: <> 괄호 안에 들어가는 타입을 지정해준다
우리가 어떤 자료구조를 만들어서 이용할때 String, Int타입등 다양하게 많은 타입을 지원하고 싶으면?
타입에 맞춰 하나하나 선언하기엔 너무 비효율적이다. 그래서 우리가 제네릭이라는 것을 사용한다.
이렇듯 제네릭(Generic)은 클래스 내부에서 지정하는 것이 아닌 외부에서 사용자에 의해 지정되는 것을 의미한다. 한마디로 특정(Specific) 타입을 미리 지정해주는 것이 아닌 필요에 의해 지정할 수 있도록 하는 일반(Generic) 타입이라는 것이다.
제네릭의 장점
- 제네릭을 사용하면 잘못된 타입이 들어올 수 있는 것을 컴파일 단계에서 방지할 수 있다.
- 클래스 외부에서 타입을 지정해주기 때문에 따로 타입을 체크하고 변환해줄 필요가 없다. 즉, 관리하기가 편하다.
- 비슷한 기능을 지원하는 경우 코드의 재사용성이 높아진다
제네릭 필요성제네릭이란 JDK 1.5부터 도입한 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법이다.
// List interface
public interface List<E> extends Collection<E> {
...
}
public static void main(String[] args) {
List<String> words = new ArrayList<>();
}
우리는 이미 제네릭을 자주 사용하고 있다. 그렇다면 제네릭을 왜 사용할까? 간단한 예제를 통해 알아보자.
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;
}
}
List에 있는 모든 숫자를 더하는 로직이다. List에 타입 지정을 안 했기 때문에 Object로 타입이 지정되고 더하는 부분에서 형 변환을 직접 해줘야 하는 번거로움이 있다. 위 예제에서는 형 변환을 한 번밖에 안 했지만 만약 타입 지정을 안 한 List가 사용되는 곳이 1000군데가 넘는다면 1000군데서 전부 예제처럼 직접 형변환을 해줘야 하는 번거로움이 있다.
아래와 같이 제네릭을 사용한다면
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;
}
}
불필요한 형 변환을 안 해도 되고 코드도 더 깔끔해진다.
또한 타입 안전성이 있다.
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;
}
}
int형으로 형 변환을 해주며 더해주지만 List의 요소가 int형이라는 보장이 없다. 위 예제와 같이 List에 문자열을 넣어주어도 컴파일 에러가 발생하지 않고 런타임에 ClassCastException 이 터지게 된다. 컴파일 시 타입을 체크하고 에러를 찾아낼 수 있는 컴파일 언어의 장점을 발휘하지 못하는 셈이다.
제네릭을 사용했다면 아래와 같이 컴파일 시에 의도하지 않은 타입이 들어오는 걸 막을 수 있다.
타입 파라미터 컨벤션
제네릭에서 사용하는 타입 파라미터에 자주 봤던 T 같은 문자가 아니고 아무런 문자나 넣어도 코드가 동작하는 데는 문제가 없다.
public class Car<DunDung> {
private final DunDung name;
...
}
하지만 타입 파라미터에도 컨벤션이 존재한다. 컨벤션을 왜 지켜야 하는지는 다들 잘 아실 것이다. 기억이 안 난다면 Code Conventions for the Java Programming Language 글의 Why Have Code Conventions 부분을 보자.
그래서 타입 파라미터 컨벤션은 아래와 같다.
제네릭 클래스나 메서드를 구현할 일이 있다면 컨벤션에 맞춰서 구현하자!
제네릭 메서드제네릭 타입을 메서드 리턴 타입 앞에 선언한 메서드를 제네릭 메서드라 한다.
public class Car<T> {
private final T name;
private Car(T name) {
this.name = name;
}
public static <M> Car<M> from(M name) {
return new Car<>(name);
}
public <M> void printEqualsCar(Car<M> car) {
System.out.println(this.equals(car));
}
}
위와 같은 Car 클래스에서 from 메서드와 printEqualsCar 메서드가 제네릭 메서드이다. 이해를 돕기 위해 제네릭 메서드 타입을 컨벤션에 맞지 않게 으로 정한 점을 이해해주시길 바란다.
제네릭 메서드 타입 를 리턴 타입 앞에 붙여서 표시하므로 헷갈릴 수 있는데 제네릭 타입을 메서드 리턴 타입 앞에 붙여주는 것이 문법일 뿐이다. 메서드 리턴 타입과는 다른 영역이다.
또한 클래스에 선언한 와 제네릭 메서드에 붙인 과는 관련이 없다. 제네릭 메서드에 붙인 과 같은 타입은 해당 메서드에서만 적용되는 제네릭 타입이다.
제네릭 메서드를 사용하려면 반드시 리턴 타입 앞에 해당 메서드에서 어떤 제네릭 타입을 사용할 것인지를 붙여줘야 한다는 것을 기억하자. 아래와 같은 형태는 컴파일 에러가 난다.
public static Car<M> from(M name) { // 컴파일 에러
return new Car<>(name);
}
마지막으로 제네릭 클래스가 아닌 일반 클래스에서도 제네릭 메서드를 사용할 수 있다는 점을 기억하자.
제한된 제네릭제네릭은 원하는 타입이 있을 때도 모든 타입이 들어올 수 있는 문제가 있다.
public class Car<T> {
private final T name;
public Car(T name) {
this.name = name;
}
...
}
위와 같은 Car 클래스 인스턴스 변수 name은 문자 관련 타입 지정되기를 원해도 아래와 같이 아무 타입이나 들어올 수 있다.
public static void main(String[] args) {
Car<Integer> car = new Car<>(1);
}
이럴 때 타입 뒤에 extends 키워드를 사용해 타입을 제한시킬 수 있다.
public class Car<T extends CharSequence> {
private final T name;
public Car(T name) {
this.name = name;
}
...
}
CharSequence 인터페이스 하위 객체들만 데이터 타입으로 지정할 수 있게 함으로써 인스턴스 변수 name에 문자열 관련 타입만 지정할 수 있게 되었다.
public static void main(String[] args) {
Car<String> car = new Car<>("sports");
}
아래와 같이 Collections.sort에서도 제한된 제네릭을 사용하고 있다.
public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null);
}
정렬하기 위해서 List에 지정될 타입 T를 Comparable 인터페이스 하위 타입으로 제한시킨 것이다.
**참고**
https://st-lab.tistory.com/153
'IT' 카테고리의 다른 글
트랜잭션(Transaction) (0) | 2022.12.31 |
---|---|
로드 밸런서 (0) | 2022.12.31 |
데이터베이스 정규화(Normalization), 역정규화(DeNormalization) (0) | 2022.12.18 |
자바 Static이란? 정적 멤버란? (0) | 2022.12.15 |
싱글톤 패턴(Singleton Pattern) (0) | 2022.12.15 |