[Java] Generic
Generic의 기본 이해.
Generic은 컴파일 시점에 데이터 타입을 지정하지 않아도 유연하게 코드를 작성할 수 있게 하는 기능이다.
즉, 코드 작성 시점에 데이터 형식을 하나로 정해두지 않고, 여러 데이터 타입에 대응할 수 있도록 하는 기능이다.
Java 기반의 프로그래밍을 하면서 자주 사용하게 되는 ArrayList나 LinkedList의 인스턴스로 생성할 때 < >
안에 자료형을 제공한 적이 있을 것이다.
ArrayList<Integer> integerList = new ArryaList<>();
ArrayList<String> stringList = new ArryaList<>();
LinkedList<Double> doubleList = new LinkedList<>();
위와 같이 말이다.
ArrayList처럼 주어진 타입에 맞는 ArrayList를 생성할 수 있듯이 타입을 외부에서 정하고 싶을 때 타입에 대응하는 클래스를 직접 하나하나 만들지 않게 도와주는 역할이 제네릭 타입이다.
장점과 단점
장점
- 재사용성 향상 : 동일한 코드로 다양한 데이터 타입에 대응할 수 있다.
- 안정성 향상 : 컴파일 시점에 타입 검사를 수행하기에 잘못된 타입이 들어올 때의 런타임 오류를 줄일 수 있다.
- 성능 향상 : Object 배열 객체로 생성한 후 특정 타입의 기능을 이용하기 위해선 Down Casting이 필수지만 미리 타입을 지정하고 제한하면서 형 변환의 번거로움과 메모리를 아낄 수 있다.
단점
- 이해의 어려움: 제네릭의 사용법을 모른다면 이해하고, 코드를 작성하는데 어려움이 있다.
사용
위에서 보았던 <>
를 사용하는데 이를 다이아몬드 연산자라고 한다.
이를 식별자 기호로 지정하면서 파라미터화 시킬 수 있다.
다이아몬드 연산자 내부에는 명명 규칙만 만족한다면 어떠한 글자가 들어오는 것도 관계없지만 보통은
타입 | 설명 |
---|---|
<T> |
Type |
<E> |
Element |
<K> |
Key |
<V> |
Value |
<N> |
Number |
<T, U> |
중복 제네릭 선언 |
와 같이 암묵적인 규칙으로 사용하는 플레이스 홀더명을 사용한다.
Generic을 Parameter로 가질 수 있는건
- class :
class Box<T> {}
- interface :
interface DataProcessor<T> {}
- method :
public <T> void add(T element);
가 있다.
추가로 다이아몬드 연산자 내부에는 여러개의 Generic Type을 제공할 수 있다.
흔히 Key : Value로 사용하는 HashMap에서 이를 확인할 수 있다.
HashMap<String, Integer> hashMap = new HashMap<>();
에서 명시된 String, Integer 타입은 K에 String, V에 Integer 타입이 generic type으로 사용된다.
주의사항
1. 기본 자료형은 사용할 수 없다.
Integer, Double, String 등과 같은 Reffrence Type, Wrapper Class만 사용 가능하기에 int, double, char과 같은 Primitive Type은 generic type으로 사용할 수 없다.
이유는
- 기본 자료형은 값으로만 다뤄지기에 컴파일 시점에 제네릭 타입 파라미터와 관계를 검사하기 어렵다.
- 기본 자료형을 제네릭 타입으로 사용 시 자동 박싱/언박싱 과정이 발생하면서 성능 저하를 일으킬 수 있다.
반대로 이 뜻은 사용자가 정의한 클래스를 제네릭 타입으로 사용할 수 있다는 뜻이다.
2. 받아오는 제네릭 타입을 인스턴스화할 수 없다.
class Box<T> {
public void makeInstance() {
T t = new T();
}
}
위의 코드와 같이 제네릭으로 참조된 타입으로 새로운 인스턴스를 생성할 수 없다.
3. static 메소드 사용 시 별도의 제네릭 타입을 제공해야한다.
제네릭이라는 개념 자체가 컴파일 이후에 외부에서 타입을 정하는 것인데, static으로 지정해버리면 객체를 생성하기 전에 프로그램이 실행 될 때 메모리에 상주 시켜버리면서 외부에서 타입을 정하는 것에 의미가 사라지게 된다.
static을 사용하기 위해서는 static 메소드 선언 시 타입에 또 다른 제네릭 타입을 지정해줘야한다.
class Box<T> {
// X
// generic으로 받아오는 타입을 사용 정적으로 활용할 수 없다.
// 'Box.this' cannot be referenced from a static context
public static T get() {
return value;
}
// O
// generic 클래스와 별도로 취급되는 독립적인 제네릭 타입을 제공 받아야한다.
public static <T> T get() {
return value;
}
}
4. 제네릭을 배열로 선언할 때 클래스 자체를 배열로 선언할 수 없다.
Box<String>[] box_str1 = new Box<>[5];
와
Box<String>[] box_str1 = new Box[5];
의 차이점은 무엇일까?
위의 코드는 제네릭 클래스 자체를 배열로 만든다는 뜻으로 Cannot create array with '<>'
와 같은 에러가 뜬다.
즉, new Box<>[5]
와 같이 제네릭 클래스 자체로 배열을 선언할 수 없다.
아래의 코드는 Box<String>()
를 타입으로 가지는 배열을 사용한다는 뜻으로
Box<String>[] box_str1 = new Box[5];
box_str1[0] = new Box<String>();
box_str1[1] = new Box<>(); // 타입 추론
// Generic으로 String을 지정했기에 Integer 타입은 저장할 수 없다.
box_str1[2] = new Box<Integer>();
와 같은 상황을 허용하며 거부한다.
자 이제 사용법과 주의 사항을 어느정도 숙지했으니 실제로 Generic을 사용해보자.
먼저 Class로 Generic을 사용해보자.
Generic Class
//Generic Class
class Box<E> {
private E value;
public void put(E value) {
this.value = value;
}
public E get() {
return value;
}
}
public class Generic {
public static void main(String[] args){
//Generic class 생성 시 Type을 String으로 줌.
Box<String> box_str = new Box<>();
box_str.put("String Data");
System.out.println("box_str's Value : " + box_str.get());
System.out.println("Instance of : " + box_str.get().getClass().getName());
System.out.println();
//Generic class 생성 시 Type을 Integer로 줌.
Box<Integer> box_int = new Box<>();
box_int.put(10);
System.out.println("box_int's Value : " + box_int.get());
System.out.println("Instance of : " + box_int.get().getClass().getName());
}
}
[output]
box_str's Value : String Data
Instance of : java.lang.String
box_int's Value : 10
Instance of : java.lang.Integer
클래스에 <>
를 사용하여 generic class라는 것을 명시 해주고 해당 클래스를 인스턴스화 할 때 각각 String과 Integer로 Reference type을 generic type 으로 명시하였다.
위에서 설명한 것과 같이 Generic type은 여러개 지정이 가능하다.
//Generic Class but Multiple Generic type
class BoxBox<K, V> {
private K key;
private V value;
public void put(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
}
public class Generic {
public static void main(String[] args){
// 첫 번째 K에 String Generic type을
// 두 번째 V에 Integer Generic type을
BoxBox<String, Integer> box_KV = new BoxBox<>();
box_KV.put("String", 10);
System.out.println("box_KV's Key : " + box_KV.getKey());
System.out.println("Instance of : " + box_KV.getKey().getClass().getName());
System.out.println();
System.out.println("box_KV's Value : " + box_KV.getValue());
System.out.println("Instance of : " + box_KV.getValue().getClass().getName());
}
}
[output]
box_KV's Key : String
Instance of : java.lang.String
box_KV's Value : 10
Instance of : java.lang.Integer
Generic Interface
Interface명 뒤에 Generic을 선언하고 이를 implements 하는 class 또한 Generic을 선언해준다.
이 때 Interface에서 선언한 Generic 명을 implements 하는 class의 generic과 매치 시켜야한다.
이 예제에서는 T를 사용할 것이다.
interface IBox<T>{
public void put(T t);
public T get();
}
class BoxImpl<T> implements IBox<T>{
private T value;
@Override
public void put(T t) {
this.value = t;
}
@Override
public T get() {
return value;
}
}
위와 같이 인터페이스와 클래스를 generic으로 사용할 수 있게 된다.
Generic Method
기본 method 선언 방식은
[접근 제어자] [return 타압] [메소드명]([parameter type] [parameter 명])
와 같다면 Generic Method는
[접근 제어자] <Generic type> [return 타압] [메소드명]([Generic type] [parameter 명])
과 같이 접근 제어자와 return 타입 사이에 generic을 명시 해준다.
Generic Method를 만들기 위해서는 Method 선언부에 <>
가 필수로 들어가야한다.
이말은
public <E> E genericMethod(E out) {
return out;
}
와
public E genericMethod(E out) {
return out;
}
는 다른 의미를 가진다는 것이다.
메서드에 직접 <E>
를 선언한 제네릭 메서드는 해당 메서드에 들어오는 타입을 독립적으로 사용할 수 있는 메서드가되고,
아래의 메서드는 인스턴스 생성 시 선언된 클래스의 제네릭 타입을 단순히 반환하는 메서드가 된다.
Generic Class에서 사용한 코드를 수정하여 아래와 같이 적용해봤다.
//Generic Class
class Box<T> {
private T value;
public void put(T value) {
this.value = value;
}
public T get() {
return value;
}
// class의 generic에 종속되지 않고 독립적으로 generic 활용이 가능한 generic method // 선언 없이 사용하기 위해 static으로(<E> 선언으로 독립적인 메소드가 되었기에
public static <E> E genericMethod(E out) {
return out;
}
}
public class Generic {
public static void main(String[] args){
//Generic class 생성 시 Type을 String으로 줌.
Box<String> box_str = new Box<>();
box_str.put("String Data");
System.out.println("box_str's Value : " + box_str.get());
System.out.println("Instance of : " + box_str.get().getClass().getName());
System.out.println();
//Generic class 생성 시 Type을 Integer로 줌.
Box<Integer> box_int = new Box<>();
box_int.put(10);
System.out.println("box_int's Value : " + box_int.get());
System.out.println("Instance of : " + box_int.get().getClass().getName());
System.out.println();
//Generic Method에 String Type
System.out.println("Generic Method's Instance of : " +
Box.<String>genericMethod("String").getClass().getName());
//Generic Method에 Integer Type
System.out.println("Generic Method's Instance of : " +
Box.genericMethod(10).getClass().getName());
//Generic Method에 사용자 정의 Class
//Reference Type을 Generic으로 줄 수 있으니 사용자 정의 class도 제공 가능하다.
System.out.println("Generic Method's Instance of : " +
Box.genericMethod(box_str).getClass().getName());
}
}
[output]
box_str's Value : String Data
Instance of : java.lang.String
box_int's Value : 10
Instance of : java.lang.Integer
Generic Method's Instance of : java.lang.String
Generic Method's Instance of : java.lang.Integer
Generic Method's Instance of : Box
여기선 Generic Method에 static을 주었는데, new 생성자를 사용하지 않고 클래스와 독립적인 제네릭을 사용하기 위함이다.
또한
System.out.println("Generic Method's Instance of : " +
Box.<String>genericMethod("String").getClass().getName());
의 코드에서는 genericMethod앞에 <String>
으로 Generic type을 지정하였는데
아래
//Generic Method에 Integer
TypeSystem.out.println("Generic Method's Instance of : " + Box.genericMethod(10).getClass().getName());
에서는 선언하지 않았는데 어떻게 동작이 수행 될까?
이는 제네릭 타입 추론이라는 개념 때문에 가능했다.
제네릭 타입 추론
먼저 타입 추론이란 명시적으로 타입을 지정하지 않아도 컴파일러가 자동으로 추론할 수 있다.
List<String> names = new ArrayList<>(); // 명시적으로 제네릭 타입 지정
List names2 = new ArrayList<>(); // 컴파일러가 String 타입으로 추론
names2.add("String");
와 같이 매개 변수로 주어진 값을 보고 추정하는 개념을 얘기한다.
( 위의 예시는 말 그대로 예시를 위한 것이고 ArryaList의 add 메서드는 독립적으로 운용되는 Generic Method가 아니기 때문에 실행은 된다만 Unchecked call to ‘add(E)’ as a member of raw type ‘java.util.ArrayList와 같은 경고문이 뜬다. )
제네릭 타입 또한 타입 추론이 적용되어
System.out.println("Generic Method's Instance of : " +
Box.<String>genericMethod("String").getClass().getName());
//Generic Method에 Integer
TypeSystem.out.println("Generic Method's Instance of : " + Box.genericMethod(10).getClass().getName());
와 같이 사용할 수 있게된 것이다.
제네릭 타입 한정
제네릭 타입은 Integer, String, Double 등과 같이 유연하게 외부에서 들어오는 타입에 맞는 클래스, 인터페이스, 메소드를 제공할 수 있다.
하지만 반대로 자율성이 증대 되기에 안정성을 보장된다고 보기엔 어렵다.
예시로 계산기 클래스에서는 Integer, Double 등과 같은 Number만 다뤄야하는데 String이나 다른 형이 들어오면 목적과 다른 동작과 output이 나오게된다.
이런 불상사를 방지하기 위해
extends
: 상한 제한super
: 하한 제한.?
: 특정 데이터 타입을 지정하지 않음. 가 사용된다.
extends
extends로 제공되는 클래스와 이를 구현한 하위 클래스만 제네릭 타입으로 제한된다.
public class Box<T extends Number> { ... }
Box의 인스턴스를 생성할 때 사용할 수 있는 Generic Type T는 Number와 이를 상속한 타입만을 사용가능하다.
Number는 Integer, Double, Long 등의 슈퍼 클래스이기에 이를 구현한 타입만 사용 가능하다.
https://techvidvan.com/tutorials/java-number/
즉
Box<Number> box_str = new Box<>();
Box<Byte> box_str = new Box<>();
Box<Integer> box_str = new Box<>();
Box<Long> box_str = new Box<>();
는 가능하지만
Box<String> box_str = new Box<>();
Box<Character> box_str = new Box<>();
Box<Object> box_str = new Box<>();
는 불가능하다.
extends
뒤에 제한되는 타입은 클래스, 추상 클래스, 인터페이스 모두 올 수 있다.
클래스 같은 경우에는 Number 클래스로 예시를 들었기에 설명이 됐다지만 인터페이스의 경우에는 따로 설명해야겠다.
extends - Interface
interface Rideable{
...
}
// 인터페이스를 직접 구현한 클래스
public class Children implements Rideable{
...
}
// EVA를 생성하기 위해서는 Rideable을 구현한 클래스만 제네릭에 사용할 수 있다.
public class EVA <T extends Rideable> {
...
}
...
EVA<Children> EVA_01 = new EVA<Children>();
최종적으로 생성하여 인스턴스로 활용할 클래스에 제네릭 타입을 제공하고, 이 제네릭 타입은 Rideable을 구현한 클래스만 받는다.
초호기를 생성(new EVA<Children>()
)하기 위해서는 에바에 탈 수 있는 Children(Children implements Rideable
)이 필요하다.
여기서 &
을 이용하여 여러개의 인터페이스를 구현한 클래스만 제네릭으로 받는 다중 타입 한정을 할 수 있다.
interface Rideable {...}
interface synchroable {...}
public class Children implements Rideable, synchroable {...}
public class EVA <T extends Rideable & synchroable> {...}
super
extends를 상한 제한, super를 하한 제한으로 설명했다.
extends는 Number 클래스로 쉽게 이해가 가능했을 텐데 super는 다르게 설명해야겠다.
https://st-lab.tistory.com/153
일단 위와 같은 상속 관계에서 extends는 해당 클래스와 하위 클래스만 제네릭으로 올 수 있다.
<T extends B> // 자신 : B, 자식 : C
<T extends E> // 자신 : E
<T extends A> // 자신 : A, 자식 : B, C, D, E
반대로 super는 해당 클래스와 해당 클래스의 슈퍼 클래스만 허용된다.
<K super B> // 자신 : B, 부모 : A
<K super E> // 자신 : E, 부모 : D, A
<K super A> // 자신 : A
이는 부모 클래스에 해당되는 자식 클래스를 Up Casting할 때 활용된다.
에바 0호기, 초호기, 2호기를 에바로써 묶기 위함이랄까…
자기 자신이 들어간 표현식으로 타입 매개변수의 범위를 한정 시키는 것을 말하며 보통 Comparable 인터페이스와 사용된다.
<E extends Comparable<? super E>>
와 같이 표현하는데 이는
“E 타입은 본인을 서브타입으로 구현한 Comparable 구현체(super) 만 받는다(extends) 라는 의미를 가진다.”
여기서 Comparable<? super E>
을 굳이 ? super E
로 주는 이유는 무엇일까?
결론만 먼저 말하자면 Up Casting 시 에러를 방지할 수 있기 때문이다.
먼저 단순히 E extends Comparable<E>>
를 한 예시를 보자
public class EVA <E extends Comparable<E>> {
...
}
//Comparable을 구현
public class EVA_01 implements Comparable<EVA_01> {
@Override
public int compareTo(EVA o) {...};
}
...
EVA<EVA_01> eva_01 = new EVA<EVA_01>();
본인만을 구현하도록 강제되어 있는 제네릭 제한 방법이라 할 수 있다.
하지만 본인만을 Comparable로 구현했기에 EVA를 감싸는 더 큰 클래스를 핸들링하기 위해 Up Casting 할 때는 에러가 생길 수 있다.
public class EVA <E extends Comparable<E>> {
...
}
public class EVA <E extends Comparable<? super E>> {
...
}
//EVA의 상위 클래스
public class Nerv {...}
//Comparable을 구현할 때 상위 클래스를 구현한다.
public class EVA_01 implements Comparable<Nerv> {
@Override
public int compareTo(EVA o) {...};
}
...
EVA<EVA_01> eva_01 = new EVA<EVA_01>();
<? super E>
로 인해서 E뿐만이 아니라 그 상위 클래스도 Comparable을 구현해야하기 때문에 Up Casting 시에도 본인보다 상위에 있는 부모 요소도 Comparable을 구현하기에 에러가 나지 않는다.
?(Wildcards)
상한이나 하한의 제한 없이 타입을 지정할 때 사용하며, <?>
은 <? extends Object>
와 같은 의미를 가지게된다.
Object는 클래스의 최상위 타입인만큼 어느 값이든지 받아들인다는 의미다.
List<?> numbers = new ArrayList<>(); // 모든 타입 허용
List<? extends Number> numbers = new ArrayList<>(); // Number의 하위 클래스만 허용
List<? super Comparable> numbers = new ArrayList<>(); // Comparable 인터페이스를 구현하는 모든 타입 허용
위와 같이 사용할 수 있긴한데 자세한 부분은 스킵하겠다 하하..