Java

[Java-36] 자바 제네릭, < >

lee-maru 2021. 2. 24. 14:35

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 타입의 하위클래스를 제한한다.

    다음과 같은 클래스 다이어그램이 있다고 하고, 이에 대해서 알아보도록 하자.

    image
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에 대한 정보를 담고있지 않는다는 것이다.