-
JAVA - 제네릭(Generic)궁금했던 것들/java 2021. 10. 30. 17:11
자바 언어를 사용하면서 제네릭(Generic)이라는 말을 자주 듣고 사용도 했었다. 특히 리스트나 맵을 사용할때, 라이브러리를 받았을때 클래스에서 자주 봤었다.
그러나 제네릭(Generic)을 정확히 어떤 경우에 사용을 하면 좋은지, 왜 사용하는지, 생겨나게된 배경이라든지 이러한 지식들이 없어서 제데로 활용을 못하고 있었다. 그래서 이번 포스팅을 통해서 제네릭(Generic)에 대해 이해 해보려고 한다.
제네릭(Generic)을 직역하면 일반적인 이라는 뜻이다. 그런데 이것만 봐서는 와닿지가 않아서 찾아보니
'데이터 형식에 의존하지 않고, 하나의 값이 여러 다른 데이터 타입들을 가질 수 있도록 하는 방법' 이라고 한다.
아래와 같은 클래스가 있다고 하자.
public class ClassName{ private String str; void set(String s){ this.str = s; } String get(){ return str; } }
그런데 만약, 내가 위의 클래스를 String 뿐만 아니라 Integer, Double 등 여러 타입을 지원하는 클래스를 만들고 싶다고하자.
제일 먼저 생각 할 수 있는 방법은 각각의 타입별로 다 클래스를 만들어서 사용하는 것이다.
그러나 이 방법은 너무 비효율 적이라는 것을 여러분들도 알고 나도 안다.
그래서 사용 할 수 있는 방법이 바로 제네릭(Generic)이다.
제네릭(Generic)은 클래스 내부에서 타입을 지정하는 것이 아니라 외부에서 사용하는 사람에 의해서 지정되는 것을 의미한다.
검색 해본 제네릭(Generic) 의 장점입니다.
1. 제네릭을 사용하면 잘못된 타입이 들어올 수 있는 것을 컴파일 단계에서 방지할 수 있다.
=> 제네릭을 사용해서 타입의 범위를 지정(아래에서 설명) 해서 잘못된 타입일 경우 에러를 발생한다는 이야기 같습니다.
2. 클래스 외부에서 타입을 지정해주기 때문에 따로 타입을 체크하고 변환해줄 필요가 없다. 즉, 관리하기가 편하다.
=> 타입이 정해져있는 클래스인 경우 코딩을하면서 해당 클래스가 타입을 지원하는지 체크해야하는데 제네릭(Generic)을 사용하면 그럴 필요가 없다는 이야기 같습니다.
3. 비슷한 기능을 지원하는 경우 코드의 재사용성이 높아진다.
=> 타입만 다르고 같은 기능을 사용한다고 하면 굳이 두개의 클래스를 만들 필요가 없다는 이야기 같습니다.
• 제네릭(Generic) 사용 방법
타입 설명 <T> Type <E> Elemnet <K> Key <V> Value <N> Number 위 표처럼 일반적으로 약속을 하고 사용한다고 합니다.
반드시 맞춰야 할 필요는 없지만 맞춘다면 다른 사람이 이해하기 좋은 코드가 될것입니다.
1. 클래스 및 인터페이스
public class ClassName <T> { ... } public interface InterfaceName <T> { ... }
위처럼 선언하는 것이 가장 기본적인 방법이라고 한다.
T 라는 타입이 유효한 범위는 클래스와 인터페이스의 { ... } 안에서만 유효한다고 한다.
더 나아가 제네릭 타입을 2개 사용 할 수도 있다. 가장 대표적인것이 바로 HashMap 클래스이다.
HashMap의 클래스 선언 부분을 보면 아래와 같다.
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { ... }
이해 해보면 Key와 Value의 제네릭 타입을 가지는 클래스라고 이해 할수 있을 것이다.
그럼 이제 선언한 클래스를 사용 해보자.
public class ClassName <T, K> { ... } public class Main { public static void main(String[] args) { ClassName<String, Integer> a = new ClassName<String, Integer>(); } }
이렇게 사용 하면 T는 String, K는 Interger 타입이 될 것이다.
2. 제네릭 클래스
// 제네릭 클래스 class ClassName<E> { private E element; // 제네릭 타입 변수 void set(E element) { // 제네릭 파라미터 메소드 this.element = element; } E get() { // 제네릭 타입 반환 메소드 return element; } } class Main { public static void main(String[] args) { ClassName<String> a = new ClassName<String>(); ClassName<Integer> b = new ClassName<Integer>(); a.set("10"); b.set(10); System.out.println("a data : " + a.get()); // 반환된 변수의 타입 출력 System.out.println("a E Type : " + a.get().getClass().getName()); System.out.println("b data : " + b.get()); // 반환된 변수의 타입 출력 System.out.println("b E Type : " + b.get().getClass().getName()); } }
ClassName 객체를 생성할때 <>안에서 타입을 지정한다.
그러면 a객체의 ClassName의 E 제네릭 타입은 String 으로 모두 변환된다.
반대로 b객체의 ClassName의 E 제네릭 타입은 Integer 로 모두 변환된다.
실행결과는 아래와 같다.
2. 제네릭 메소드
선언 방식은 아래와 같다.
public <T> T genericMethod(T o) { // 제네릭 메소드 ... } [접근 제어자] <제네릭타입> [반환타입] [메소드명]([제네릭타입] [파라미터]) { // 텍스트 }
클래스에서는 클래스 이름 옆에 <E>라는 제네릭 타입을 붙여서 선언했다면 메소드에서는 반환타입 앞에 선언을 하고 사용 하면 된다.
위에서 만든 제네릭 클래스에 제네릭 메소드를 추가 해보자.
// 제네릭 클래스 class ClassName<E> { E notGenericMethod(E o) { // 제네릭 클래스와 같은 타입을 가지는 메소드 return o; } <T> T genericMethod(T o) { // 제네릭 메소드 return o; } } public class Main { public static void main(String[] args) { ClassName<String> a = new ClassName<String>(); // 일반 메소드 System.out.println("<E> returnType : " + a.notGenericMethod("str").getClass().getName()); // 제네릭 메소드 Integer System.out.println("<T> returnType : " + a.genericMethod(10).getClass().getName()); } }
ClassName 객체를 생성하면서 ClassName객체의 E 타입이 결정된다.
반면 T타입은 결정되지 않는다.
얼핏 보면 notGenericMethod도 제네릭 메소드 처럼 보이지만 그렇게 동작하지 않는다.
E 타입이 String으로 결정된 ClassName 객체에서 notGenericMethod에 String 이 아닌 다른 타입을 넣으면 에러가 발생한다.
그래서 genericMethod 처럼 선언을 해서 사용하면 다른 타입들도 사용이 가능한 제네릭 메소드를 만들수 있다.
즉, 클래스에서 지정한 제네릭유형과 별도로 메소드에서 독립적인 제네릭 유형을 선언해서 사용 할 수 있다.
왜 독립적으로 사용되는 방법이 필요한가 ? 에 대해 생각해보면 독립적으로 여러 유형을 사용 할 수 있다는 이유도 있지만 정적 메소드를 선언해보면 그 이유를 알 수 있다.
아래의 코드를 한번 보자.
class ClassName<E> { static E genericMethod(E o) { return o; } } class Main { public static void main(String[] args) { ClassName.getnerMethod(3); } }
문제가 없어 보일 수도 있다. 하지만 실행을 시키면 아래와 같이 에러가 발생한다.
이유가 무엇일까?
static 타입은 객체가 생성되기 이전에 메모리에 올라간다. 그래서 객체를 선언하지 않아도 바로 사용 할 수 있다.
그런데 ClassName의 E타입을 입력값과 리턴값으로 갖는 genericMethod는 어디서 E타입을 참조해서 메모리에 올라갈지 알 수 없다. 그래서 에러를 보면 int타입의 메소드가 정의 되어 있지 않다고 에러가 나오는 것이다.
그래서 어떤 방법으로 사용 해야할까??
// 제네릭 클래스 class ClassName<E> { static <E> E genericMethod(E o) { // 제네릭 메소드 return o; } } public class Main { public static void main(String[] args) { ClassName.genericMethod(3); } }
이렇게 사용하게 된다면 E 라는 타입은 메소드를 사용할때 정해지기 때문에 사용 할 수 있다.
( ※ E 라는 타입명이 같다고 해서 genericMethod의 E 타입이 ClassName의 E 타입과 같지 않다. 독립적이다.)
• 제네릭 범위 제한, 와일드 카드
위에서 본 예제들은 가장 일반적인 예시들이 였다.
그런데 사용 하다보면 특정 메소드가 정의 되어 있는 타입들만 사용하고 싶다거나 타입의 범위를 지정하고 싶다면 어떻게 해야할까 ?
이럴때 사용하는 것이 extends와 supper 그리고 ? 라고 한다. ?는 와일드 카드라고 해서 검색조건에 *을 쓰는 것과 비슷하다고 생각하면 될거같다.
<K extends T> // T와 T의 자손 타입만 가능 (K는 들어오는 타입으로 지정 됨) <K super T> // T와 T의 부모(조상) 타입만 가능 (K는 들어오는 타입으로 지정 됨) <? extends T> // T와 T의 자손 타입만 가능 <? super T> // T와 T의 부모(조상) 타입만 가능 <?> // 모든 타입 가능. <? extends Object>랑 같은 의미
extends T : 상한 경계
? super T : 하한 경계
<?>: 와일드 카드
위 처럼 이해하면 된다.
이때 주의해야 할 게 있다. K extends T와 ? extends T는 비슷한 구조지만 차이점이 있다.
'유형 경계를 지정'하는 것은 같으나 경계가 지정되고 K는 특정 타입으로 지정이 되지만, ?는 타입이 지정되지 않는다는 의미다.
1. <K extends T>, <? extends T>
가장 쉬운 예시로 다음과 같은 예시가 있다.
/* * Number와 이를 상속하는 Integer, Short, Double, Long 등의 * 타입이 지정될 수 있으며, 객체 혹은 메소드를 호출 할 경우 K는 * 지정된 타입으로 변환이 된다. */ <K extends Number> /* * T의 자손 타입만 지정될수 있으며 * 객체 혹은 메소드를 호출 할 경우 지정 되는 타입이 없어 * 타입 참조를 할 수는 없다. */ <? extends T> // T와 T의 자손 타입만 가능
이해 했다면 아래의 코드를 보자.
public class ClassName <K extends Number> { ... } public class Main { public static void main(String[] args) { ClassName<Double> a1 = new ClassName<Double>(); // OK! ClassName<String> a2 = new ClassName<String>(); // error! } }
ClassName의 K 타입에 올수있는 타입들은 Number을 상속하고 있는 클래스들이다.
예를 들면 Integer, Long, Byte, Double, Float, Short들이 올수 있다.
그러나 a2 인 경우 String 은 Number클래스를 상속하고 있지 않아서 에러가 발생한다.
2. <K super T>, <? super T>
super일 경우에는 T 타입의 부모 타입만 가능하다는 의미다.
즉, super 뒤에 오는 타입이 최하위 타입으로 한계가 정해지는 것이다.
public class ClassName <E extends Comparable<? super E> { ... }
이런 코드를 본적이 있을 것이다. 자기 참초를 비교 하고 싶은 경우 공통적으로 위와 같은 형식을 취한다.
하나하나 분석해보자면
E extends Comparable 부터 보면 extends는 extends 뒤에 오는 타입이 최상위 타입이 되고, 해당 타입과 그에 대한 하위 타입이라고 했다.
그러면 역으로 생각 해보면 E 객체는 반드시 Comparable을 구현해야한다는 의미 일 것이다.
public class SoltClass <E extends Comparable<E>> { ... } public class Student implements Comparable<Student> { @Override public int compareTo(Student o) { ... }; } public class Main { public static void main(String[] args) { SoltClass<Student> a = new SoltClass<Student>(); } }
이렇게만 쓴다면 E extends Comparable<E> 까지만 써도 무방하다.
즉, SoltClass의 E 는 Student 이 되어야 하는데, Comparable<Student> 의 하위 타입이어야 하므로 거꾸로 말해 Comparable을 구현해야한다는 의미인 것이다.
그러면 왜 Comparable<E> 가 아닌 <? super E> 일까?
잠깐 설명했지만, super E는 E를 포함한 상위 타입 객체들이 올 수 있다고 했다.
만약에 위의 예제에서 학생보다 더 큰 범주의 클래스인 사람(Person)클래스를 둔다면 어떻게 될까? 한마디로 아래와 같다면?
public class SoltClass <E extends Comparable<E>> { ... } // Error가능성 있음 public class SoltClass <E extends Comparable<? super E> { ... } // 안전성이 높음 public class Person {...} public class Student extends Person implements Comparable<Person> { @Override public int compareTo(Person o) { ... }; } public class Main { public static void main(String[] args) { SoltClass<Student> a = new SoltClass<Student>(); } }
쉽게 말하면 Person을 상속받고 Comparable 구현부인 comparTo에서 Person 타입으로 업캐스팅(Up-Casting) 한다면 어떻게 될까?
만약 <E extends Comparable<E>>라면 SoltClass<Student> a 객체가 타입 파라미터로 Student를 주지만, Comparable에서는 그보다 상위 타입인 Person으로 비교하기 때문에 Comparable<E>의 E인 Student보다 상위 타입 객체이기 때문에 제대로 정렬이 안되거나 에러가 날 수 있다.
그렇기 때문에 E 객체의 상위 타입, 즉 <? super E> 을 해줌으로써 위와같은 불상사를 방지할 수가 있는 것이다.
이 부분은 중요한 것이 이후 필자가 PriorityQueue와 TreeSet 자료구조를 구현할 것인데, 이 부분을 이해하고 있어야 가능하기 때문에 조금은 어렵더라도 미리 언급하고 가려한다.
<E extends Comparable<? super E>> 에 대해 설명이 조금 길었다. 이 긴 내용을 한 마디로 정의하자면 이렇다.
"E 자기 자신 및 조상 타입과 비교할 수 있는 E"
3. <?> (와일드 카드 : Wild Card)
마지막으로 와일드 카드다.
이 와일드 카드 <?> 은 <? extends Object> 와 마찬가지라고 했다. Object는 자바에서의 모든 API 및 사용자 클래스의 최상위 타입이다. 한마디로 다음과 같은 의미나 마찬가지다.
public class ClassName { ... } public class ClassName extends Object { ... }
우리가 public class ClassName extends Object {} 를 묵시적으로 상속받는 것이나 다름이 없다.
한마디로 <?>은 무엇이냐. 어떤 타입이든 상관 없다는 의미다. 당신이 String을 받던 어떤 타입을 리턴 받던 알빠 아니라는 조금 과격한 얘기..
이는 보통 데이터가 아닌 '기능'의 사용에만 관심이 있는 경우에 <?>로 사용할 수 있다.
'궁금했던 것들 > java' 카테고리의 다른 글
JAVA - Enum (0) 2021.11.01 JAVA - Collection (0) 2021.10.31