[Java-36] 자바 제네릭, < >
Java Generic
Generic, 제네릭 기본기
generic <> 을 알아보기전에, 만약 제네릭이 없었을 때를 생각해보자. 일단 ArrayList <>에서 또한 우리가 알고 있는 제네릭이다. 그렇다면 없었을 경우에는
ArrayList list = new ArrayList(); 와 같은 방법으로 리스트 선언이 가능했다. 마치 파이썬에서 리스트를 선언하듯이 일정의 타입없이 사용이 가능하다는 것이다. 코드를 보도록 해보자.
public static void main(String[] args){
ArrayList list = new ArrayList();
list.add("Hello");
String str = (String) list.get(0);
}
문제는 이와 같다. list 에서 꺼내야 하는 타입을 타입 캐스팅 해줘야 한다는 것이다. 이는 리스트가 모든 Object를 넣을 수 있기때문에 가능한일이다. 그럼 제네릭에서는 어떻게 편리하게 해결할까?
제네릭 사용법
제네릭은 강력한 타입의 규제라고 얘기할 수 있다. 예를들어보자 만약 *ArrayList 에 * String 타입의 문자열을 add(_) 할 수 있을까? 문제가 발생한다. 대신 List는 Integer 로만 한정되어있기 때문에, 불필요한 타입캐스팅을 하지 않아도 된다는 점이다.
public static void main(String[] args){
ArrayList<String> strList = new ArrayList<>();
strList.add("Hello");
System.out.println(strList.get(0));
}
class , interface
제네릭 타입이라 함은 타입을 매개변수로 받는 클래스 또는 인터페이스를 이뤄 말을 한다. 선언하는 방법은 다음과 같다.
- Generic Type class : public class 클래스
- Generic Typer interface : public class 인터페이스
public class Box <T> {
T t;
public T get() {return t;}
public void set(T t){
this.t = t;
}
}
public static void main(String[] args) {
Box<String> box = new Box<>();
box.set("Hello");
System.out.println(box.get());
Box<Integer> box2 = new Box<>();
box2.set(100_000);
System.out.println(box2.get());
}
다음과 같이 사용할 수 있는게 제네릭이다.
멀티 타입 파라미터 (class<K,V>, interface<K,V>)
제네릭 타입은 두 개 이상의 멀티 타입 파라미터를 사용할 수 있다. 우리가 자주 사용해봤던,
Map<K,V> 또한 멀티 타입 파라미터를 사용한 것이다. K는 key 값과 V 는 value 값이다.
public interface Map<K,V> {
// Query Operations
int size();
boolean isEmpty();
boolean containsKey(Object key);
//code..
}
public class Product<T, M> {
private T kind; // 상품 종류
private M model; // 모델 명
public T getKind(){return this.kind;}
public M getModel(){return this.model;}
public void setKind(T kind) { this.kind = kind;}
public void setModel(M model){this.model = model;}
}
public static void main(String[] args) {
Product<Tesla, String> product = new Product<>();
product.setKind(new Tesla());
product.setModel("모델 x");
System.out.println(product.getKind().toString());
System.out.println(product.getModel());
}
result :
Tesla@6ed3ef1
모델 x
제네릭 메소드 <T,R> R method(T t)
제네릭 메서드는 메개 타입, 리턴 타입으로 타입 파라미터를 가진다. 간단한 예제를 통해서 알아보도록 하자. 이런식으로 제네릭을 통한 메소드 구현도 가능하다.
public static void main(String[] args) {
Box<String> box = packaging("boxing Starting :)");
Box<Integer> box2 = Main.packaging(100);
System.out.println(box.get());
System.out.println(box2.get());
}
public static <T> Box<T> packaging(T t){
Box<T> box = new Box<T>();
box.set(t);
return box;
}
//result : boxing String :)
와일드 카드
우리가 코드에서 볼 수 있는 ? 는 와일드 카드 라고 부른다. 제네릭에서 구체적인 타입을 작성하는대신, 와일드 카드를 사용할 수 있다는 것이다. 총 사용할 수 있는 형태는 3가지가 있다.
-
<?> Unbounded WildCard : 클래스와 인터페이스 사용에 있어서, 제한 없이 사용할 수 있다.
-
<? extends a타입> Upper Bounded Wildcard : a타입의 상위클래스를 제한한다.
-
<? super b타입> Lower Bounded Wildcard : b 타입의 하위클래스를 제한한다.
다음과 같은 클래스 다이어그램이 있다고 하고, 이에 대해서 알아보도록 하자.
public class AnimalList<T> {
ArrayList<T> al = new ArrayList<T>();
public static void cryingAnimalList(AnimalList<? extends LandAnimal> al) {
LandAnimal la = al.get(0);
la.crying();
}
void add(T animal) { al.add(animal); }
T get(int index) { return al.get(index); }
boolean remove(T animal) { return al.remove(animal); }
int size() { return al.size(); }
}
- <? extends LandAnimal> 특성상, LandAnimal 의 하위 , 즉 자식 클래스만 사용할 수 있을 것이다.
public static void main( String[] args ) {
AnimalList<Cat> catList = new AnimalList<Cat>();
catList.add(new Cat());
AnimalList<Dog> dogList = new AnimalList<Dog>();
dogList.add(new Dog());
AnimalList<Sparrow> sparrows = new AnimalList<>();
sparrows.add(new Sparrow());
AnimalList.cryingAnimalList(catList); // 가능 냥냥
AnimalList.cryingAnimalList(dogList); // 가능 월월
AnimalList.cryingAnimalList(sparrows);
// error 발생
}
다음과 같은 상속 형태를 가진 클래스가 있다고 가정을 해보자.
Type Erase
Erasure 라 함은, 자바의 타입소거를 의미한다. 예를 들어보도록 하자. 만약 다음과 같은 코드가 있다고 하다.
String str = "Hello";
여기서 str 변수와 "Hello"의 리터럴은 Stiring 타입정보와 정보를 내제하고 있다. 런타임시 타입체크에서 다음과 같이 두개의 타입이 같은지 확인하고 매칭을 시키는데, 이 둘이 일치하지 않는다면, 매칭 에러가 발생한다는 것이다.
하지만 제네릭은 그렇지 않다는 것이다. generic을 선언한 뒤 제네릭의 타입토큰은, T 가 아닌 SignatureObject 인 Object 와 비슷 한 형태를 띈다는 것이다.
public class Test<T> {
public void foo(T t){
System.out.println(t.toString());
}
}
라는 java 파일을 타입파라미터에 Integer를 넣는다고 한들, 컴파일 시 Test 의 타입 파라미터가 Test 로 변경되지 않는 다는 것이다.
package org.example;
public class Test<T> {
public Test() {
}
public void foo(T t) {
System.out.println(t.toString());
}
}
public class org.example.Test<T> {
public org.example.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void foo(T);
Code:
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_1
4: invokevirtual #13 // Method java/lang/Object.toString:()Ljava/lang/String;
7: invokevirtual #17 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
10: return
}
디컴파일된 클래스에서도 또는 바이코드에서도 Integer에 대한 정보를 담고있지 않는다는 것이다.