Generics는 클래스나 인터페이스, method에서 type을 parameter로 지정하는 기법입니다.
Method를 정의할 때 parameter를 넣는 경우가 있죠?
public int calculator(int number) { ... }
위의 method는 int 형태의 값인 number를 parameter로 받고 있습니다.
반면 generics의 경우는 parameter로 값을 받는 것이 아니라 type을 받도록 하는 것입니다. 그래서 type parameter (parameterized types)라고 부르기도 합니다.
지금까지 공부한 내용 중에도 generics를 사용하는 친구가 이미 있었습니다 😯. 바로 ArrayList 입니다. ArrayList의 선언 방식은 primitive type이나 일반 class type의 선언 방식과 모습이 조금 다른게 기억나시나요?
ArrayList<String> arrList;
이렇게 <> 다이아몬드 표시를 쓰고 그 안에 ArrayList 안에 들어갈 데이터의 type을 명시해주었습니다. 저 다이아몬드 안에는 어떤 타입의 데이터를 넣을 것이냐에 따라 String이나 Integer, Double 등의 wrapper class나 다른 class type을 넣어주게 됩니다. 바로 <> 이 표시가 바로 generics를 사용하는 방법입니다.
ArrayList의 모습을 좀 더 살펴보면
ArrayList<E> 라고 정의되어 있습니다.
바로 저 <E>가 있기 때문에 E 자리에 우리가 사용하기 원하는 type을 지정하여 ArrayList를 사용할 수 있게되는 것입니다.
E는 element의 약자로 generics에서 주로 사용되는 알파벳입니다. E 말고도 T(type)를 사용하기도 하고, Map과 같은 경우는 K(key), V(value)를 사용하기도 합니다. 사용되는 장소에 맞게 의미가 더 잘 어울리는 알파벳으로 적어주는 것입니다. 사실 어떠한 알파벳이나 단어를 넣어도 상관은 없습니다만 가독성을 위해 대부분 주로 사용되는 알파벳을 넣어줍니다. (method의 parameter 명을 자유롭게 지정할 수 있듯이 generics의 type parameter 명도 자유롭게 지정할 수 있는 것이라고 생각하면 될 것 같습니다.)
Generics를 사용하는 이유
메서드에 파라미터를 int, double, 등등의 값으로 넣어줄 수 있듯이, 파라미터를 타입으로 넣어주는 것을 generics라고 하는 것은 알겠는데, 왜 type을 parameter로 넘겨주어야 하는 일이 생긴 걸까요? 🤔
1) ArrayList에서도 알 수 있듯이 generics를 사용하면 필요에 따라 class 내부에서 다룰 수 있는 type을 지정할 수 있어 효율적이기 때문입니다. (element별 casting 생략 가능)
List list = new ArrayList(); //Generics 사용X
list.add("hello");
String s = (String) list.get(0); //list에 담긴 데이터의 타입을 모르기 때문에 사용 시에 casting 필요!
이렇게 generics를 사용하지 않는 경우에는 데이터의 타입이 불명확하기 때문에 list 안에 담긴 데이터를 사용할 때 type casting을 해서 type을 명시해주어야 합니다. 벌써 이것부터 좀 번거롭죠? List, Set, Map은 여러 데이터를 한번에 편하게 관리하려고 사용하는 건데, 매 데이터마다 casting을 해주려면 사용이 더욱 힘들 수 있을 것 같다는 생각이 듭니다.
하지만 generics를 사용하면 type을 지정해주기 때문에 casting을 해주지 않아도 됩니다.
List<String> list = new ArrayList<String>(); //generics 사용하여 String으로 사용할 타입 지정
list.add("hello");
String s = list.get(0); //casting 필요 없음
2) 또, 지정해 준 type에 맞지 않는 데이터가 들어오려고 할 때 compile-time에서 error가 발생되기 때문에 runtime에서 프로그램이 먹통이 되는 일을 방지할 수 있습니다. (stronger compile-time type check)
Generics를 사용하지 않으면 아래와 같은 문제도 발생할 수도 있습니다.
ArrayList arrlist = new ArrayList<>(); //generics를 사용하지 않은 경우 (raw type)
ArrayList<Double> doubleArrayList = new ArrayList<>();
doubleArrayList.add(0.23);
//generics로 타입을 지정하지 않았기 때문에 모든 타입의 데이터가 들어갈 수 있습니다.
arrlist.add(1);
arrlist.add(0.24);
arrlist.add(doubleArrayList);
arrlist.add("Hi");
double d = (Double) arrlist.get(2); //runtime error 발생
위의 예제처럼 generics를 사용하지 않은 경우에는 ArrayList에 모든 타입의 데이터가 추가될 수 있습니다.
그러면 편한게 아닌가 하고 생각하실 수도 있지만, ArrayList에 데이터를 담은 이후에 사용이 굉장히 까다로워질 수 있습니다.
몇번째 index에 어떤 타입의 데이터가 들어있는지를 알고있어야 하기 때문입니다(type casting이 필요하니까요!). 예시로는 int, double, ArrayList<Double>, String 타입의 데이터를 arrlist에 넣어보았는데요, 마지막 라인에 double type 변수에 두번째 인덱스에 위치한 데이터를 배정해보려고 시도했습니다. 두번째 인덱스에 위치한 데이터는 ArrayList<Double> 타입이죠? Double로 casting을 하려다보니 runtime error가 발생합니다.
조금 더 자세한 예시
임의로 Generics class를 만들어서 어떻게 작동하는지에 대해 좀 더 살펴보겠습니다.
GenericsEX라는 클래스를 하나 만들었습니다. 그리고 <T>로 parameter type을 받도록 설정해주었습니다.
class GenericsEx<T> {
String name;
T genericVar;
public GenericsEx(String name, T genericVar) {
this.name = name;
this.genericVar = genericVar;
}
public String getName() {
return this.name;
}
public T getGenericVar() {
System.out.println(this.genericVar);
return this.genericVar;
}
}
이렇게 만든 class를 이제 사용해보겠습니다.
public class Main {
public static void main(String[] args) {
//T자리에 String을 넣은 경우
GenericsEx<String> genericsEx1 = new GenericsEx<>("GG", "HH");
//T자리에 Integer를 넣은 경우
GenericsEx<Integer> genericsEx2 = new GenericsEx<>("GG", 1);
}
}
Class를 선언할 때 <T>에 해당 클래스에서 사용할 type을 넣어주면 됩니다. genericsEx1에서는 String을 사용했고, genericsEx2에서는 Integer를 사용하였습니다.
String으로 지정한 경우에는 위의 GenericsEx class에 T로 적힌 모든 부분이 String으로 바뀌게 되고, Integer인 경우는 Integer로 바뀌게 됩니다.
//...
GenericsEx<String> genericsEx1 = new GenericsEx<>("GG", "HH");
genericsEx1.getGenericVar(); //Prints HH
int test = genericsEx1.getGenericVar(); //Compiletime erro(type error) 발생
//...
int test 변수에 getGenericVar()의 return 값을 할당하려고 하니 바로 compile 에러가 발생합니다. 위에서 generics를 사용하지 않은 경우에는 runtime error가 발생한 것과 다르죠?
Generics 사용방법
그러면 generics는 어떻게 사용하면 될까요?
Class에 generics를 사용하려면 class 명 뒤에 <T>를 붙여주면 됩니다. (위에서 얘기한 것처럼 T이던 E이던 Thing이던 상관 없습니다.)
public class SingleGenericsClass<T> {}
public class DoubleGenericsClass<T, U> {} //generics를 두개 이상 사용도 가능합니다.
경우에 따라 2개 이상의 타입 파라미터를 사용할 수도 있습니다. 그럴 때는 parameter를 표시하는 알파벳만 다른 알파벳으로 설정해주면 됩니다.
Interface의 경우도 interface 명 뒤에 <T>를 붙여주면 됩니다.
public interface Comparable<T> {
public int compareTo(T o);
}
좋은 예시 중에 하나가 Comparable interface인데요, comparable을 사용할 때 어떤 것들끼리 비교할 것인지를 고려해서 T 자리에 type을 넣어주게 됩니다.
Method의 경우는 return type 앞에 <T>를 붙여주면 됩니다.
2. Type bounds
하지만 type parameter 자리에 아무 타입이나 들어올 수 있다는 것은 장점이지만 동시에 단점이 되기도 합니다. 원하지 않는 종류의 타입이 들어올 수도 있기 때문입니다. 그럴 때는 Type에 제한을 둘 수 있습니다.
class GenericsEx<T extends A> {
//...
}
제한을 두는 방법은 <T extends A> 와 같이 작성하여 A를 extends하는 T class type만 받도록 하면 됩니다.
특정한 class나 subclass만 받을 수 있도록 제한을 두는 것입니다. (A 자리에는 class 뿐만 아니라 interface도 올 수 있습니다! interface여도 implements가 아닌 extends라고 쓰시면 됩니다. )
조금 더 잘 와닿도록 더 구체적으로 바꾸어 보겠습니다.
class GenericsEx<T extends Number> {
//...
}
T 자리에 Number class를 extends하는 친구들만 올 수 있도록 하였습니다. Number class는 Integer, Double, Float 등 우리가 익숙하게 알고있는 primitive type의 wrapper class들이 extends하고 있습니다. 그 말은 곧 T 자리에는 숫자 type만 올 수 있게 되는 것입니다. String이나 Number class를 상속하지 않은 class는 올 수가 없게 되는 것이죠.
이렇게 type bound를 해주면
GenericsEx<String> genericsEx1 = new GenericsEx<>("GG", "HH"); //compile error 발생
아까전에는 가능했던 위의 예시에서 compile error가 발생하게 됩니다.
String은 Number를 extends하는 친구가 아니니까요!
위 예시에서는 extends를 하나만 해주었지만, 두개 이상도 가능합니다. <T extends A & S> 와 같이 & 표시 뒤에 추가로 extends할 클래스나 인터페이스를 적어주면 됩니다.
이렇게 Generics의 기본적인 내용에 대해 알아보았습니다. Generics는 쉽지 않은 개념이라고 하는데 자주 연습해보면서 익숙해져야겠습니다. wild card와 object와의 차이점에 대해서도 알아보려고 했는데 글이 너무 길어져서 다음번에 마저 올려보도록 하겠습니다.
감사합니다.☺️
*참고자료
Generics vs Object
https://stackoverflow.com/questions/18242007/difference-between-wildcard-and-type-parameter-in-java
https://docs.oracle.com/javase/tutorial/java/generics/why.html
https://www.oracle.com/technical-resources/articles/java/juneau-generics.html
<Type bound>
'Java' 카테고리의 다른 글
Stream 사용하기(2) - 중간 연산의 종류와 방법 (2) | 2022.05.20 |
---|---|
Stream 사용하기(1) - stream 생성 (0) | 2022.05.20 |
Final 키워드 알아보기 (0) | 2022.05.16 |
Static 키워드 알아보기 (0) | 2022.05.12 |
Inner Class - Anonymous class(익명 클래스) (0) | 2022.05.10 |