Generic

김성혁·2021년 2월 19일
0

컴파일 타임 버그와 같은 경우, 몇몇 버그들보다 초기에 발견하기가 쉽다. 컴파일러의 오류 메세지를 사용하여 문제가 무엇인지를 찾고 즉시 수정할 수 있다. 하지만 런타임에 발생한 버그들은 문제가 즉시 드러나지 않기 때문에 훨씬 더 큰 문제가 될 수 있다.

제네릭은 컴파일 타임에, 즉 초기에, 버그를 감지할 수 있도록 코드에 안정성을 추가해준다.

왜 제네릭을 사용하는가?


클래스, 인터페이스와 메서드를 정의할 때 제네릭은 type (classe and interface)이 prarameter가 되도록 할 수 있다.

타입 매개변수는 다른 입력으로 동일한 코드를 재사용하는 방법을 제공한다.

형식 매개변수(formal parameter)에 대한 입력은 값(value)이고, 타입 매개변수(type parameter)에 의한 입력은 타입(type)이다.

  • 컴파일 시간에 강한 타입 체크를 제공한다.

    • 자바 컴파일러는 코드가 타입 안정성을 침해할 때 generic code와 issue error에 강한 타입 체크를 제공
    • 컴파일 타입 에러는 발견하기 어려운 런타입 에러를 고치는 것보다 고치기 쉬움
  • 타입 캐스팅이 필요없다.

    • 다음과 같이 제네릭 없이 코드를 작성할 경우 타입 캐스팅이 필요

      List list = new ArrayList();
      list.add("hello");
      String s = (String) list.get(0);
    • 제네릭을 이용할 경우 타입 캐스팅이 불필요

      List<String> list = new ArrayList<String>();
      list.add("hello");
      String s = list.get(0);   // no cast
  • 제네릭을 사용함으로써 다른 타입의 컬렉션에서 작동하고, 커스터마이즈될 수 있으며 타입이 안전하고 읽기 쉬운 제네릭 알고리즘을 구현할 수 있다.

Generic Types


제네릭 타입은 타입을 매개변수화시켜 만든 제네릭 클래스 또는 인터페이스를 말한다.

  • non-generic Box class
public class Box {
    private Object object;

    public void set(Object object) { this.object = object; }
    public Object get() { return object; }
}

Box 클래스의 메서드는 Object 타입을 받아들이거나 반환하기 때문에 primitive 타입이 아니라면 원하는대로 자유롭게 전달이 가능하다. 하지만 컴파일 타임에 해당 클래스가 어떻게 사용되는지 검증할 방법이 없다. 코드의 한 부분은 Integer를 box에 넣고 Integer를 가져올 것으로 예상 할 수 있지만 코드의 다른 부분은 실수로 String을 전달하여 런타임 오류가 발생할 수 있다.

public class Test {
    public static void main(String[] args) {
        Box box = new Box();
				
        box.set("50"); //코드 중 잘못된 부분

        Integer value1 = (Integer) list.get(0)
   }
}
  • Generic Version of the Box Class

  • Generic class 정의

class name<T1, T2, ..., Tn> { /* ... */ }
/**
 * Generic version of the Box class.
 * @param <T> the type of the value being boxed
 */
public class Box<T> {
    // T stands for "Type"
    private T t;

    public void set(T t) { this.t = t; }
    public T get() { return t; }
}

Type Parameter Naming Conventions


타입 파라미터 이름은 단일 대문자로 작성하는 것을 규칙으로 한다.

  • E - Element (used extensively by the Java Collections Framework)
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S,U,V etc. - 2nd, 3rd, 4th types

"Type Parameter"와 "Type Argument"의 차이

Type Argument는 매개변수화된 타입을 생성하기 위해 제공된다.

Foo의 T는 "Type Parameter" 이고 Foo의 String은 "Type Argument"

Raw Types


Raw Type은 type argument가 없는 제네릭 클래스 또는 인터페이스의 이름을 말한다.

public class Box<T> {
    public void set(T t) { /* ... */ }
    // ...
}
//실제 type argument를 제공한 Box<T>의 매개변수화된 타입
Box<Integer> box = new Box<>();

//Box<T>의 raw type
Box rawBox = new Box();

* Box is the raw type of the generic type Box<T>. 
* non-generic class or interface type is not a raw type
Box<String> stringBox = new Box<>();
Box rawBox = stringBox;               // OK

Box rawBox = new Box();           // rawBox is a raw type of Box<T>
Box<Integer> intBox = rawBox;     // warning: unchecked conversion

Box<String> stringBox = new Box<>();
Box rawBox = stringBox;
rawBox.set(8);  // warning: unchecked invocation to set(T)

JDK 5.0 이전에는 많은 API 클래스들이 generic type으로 선언되지 않았기 때문에 이전 버전과의 호환성을 위해

매개변수화된 타입을 raw type에 할당이 가능하다. 반대로 raw type을 매개변수화된 type에 할당 시 warning 발생. 또한 raw type을 사용하여 해당 generic type에 정의된 메서드 호출 시 warning 발생. warning은 안전하지 않은 코드의 포착을 런타임으로 연기함으로써 raw type이 제네릭 타입 체크를 우회하면서 발생한다.(?)

제네릭 타입을 사용할 때 생성되는 오브젝트의 Type Argument는 Raw Type으로 사용되는데 컴파일러에 의해 필요한 곳에 형변환 코드가 추가된다. 해당 내용은 .java 확장자를 가진 파일을 컴파일 후 디컴파일을 했을 때 형변환 코드를 확인할 수 있다.

Unchecked Error Messages

제네릭 코드와 레거시 코드를 혼합 시 다음과 같은 경고 메세지를 만날 수 있다

Note: Example.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

해당 경고 메세지는 raw type에서 작동하는 오래된 API를 사용할 때 발생할 수 있다.

public class WarningDemo {
    public static void main(String[] args){
        Box<Integer> bi;
        bi = createBox();
    }

    static Box createBox(){
        return new Box();
    }
}

"unchecked" : 컴파일러가 타입 안정성을 보장하는데 필요한 충분한 type 정보를 가지고 있지 않다

컴파일러가 힌트를 제공하지만 "unchecked"경고는 기본적으로 비활성화됨. "unchecked"경고를 모두 보려면

-Xlint : unchecked로 다시 컴파일

WarningDemo.java:4: warning: [unchecked] unchecked conversion
found : Box
required: Box<java.lang.Integer>
bi = createBox();
^
1 warning

확인되지 않은 경고를 완전히 사용하지 않으려면 -Xlint : -unchecked 플래그를 사용

@SuppressWarnings ( "unchecked") 어노테이션은 unchecked warning을 억제한다. @SuppressWarnings 구문에 익숙하지 않은 경우 주석을 참조

Bounced Type Parameter (한정적 타입 매개변수)


한정적 타입 매개변수는 매개변수화된 타입안에 type argument로써 사용되는 타입을 제한하기 위해 사용된다. 예를 들어 type argument로 Number 또는 Number의 하위 클래스의 인스턴스들만 사용하도록 제한할 수 있다.

public class Box<T> {

    private T t;          

    public void set(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }

    public <U extends Number> void inspect(U u){
        System.out.println("T: " + t.getClass().getName());
        System.out.println("U: " + u.getClass().getName());
    }

    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<Integer>();
        integerBox.set(new Integer(10));
        integerBox.inspect("some text"); // error: this is still String!
    }
}
  • extends 키워드는 클래스 상속의 extends 키워드, 인터페이스 구현의 implements 키워드와 같이 일반적인 의미로써 사용된다.

  • 또한 한정적 타입 매개변수는 bound에 정의된 메서드들을 호출할 수 있다.

    public class NaturalNumber<T extends Integer> {
    
        private T n;
    
        public NaturalNumber(T n)  { this.n = n; }
    
        public boolean isEven() {
            return n.intValue() % 2 == 0;
        }
    
        // ...
    }
  • type parameter can have multiple bounds

    <T extends B1 & B2 & B3>
    Class A { /* ... */ }
    interface B { /* ... */ }
    interface C { /* ... */ }
    
    class D <T extends A & B & C> { /* ... */ } //type parameter 정의는 클래스 다음 인터페이스 순서로 온다.
  • 한정적 타입 매개변수에서 객체 간의 비교를 수행할 때 비교연산자는 객체간의 비교에 사용할 수 없으므로 Comparable 인터페이스를 extends하여 수행한다.

  • Bounded Type을 사용할 경우 어느 타입의 서브 클래스여야 한다는 제약 조건은 컴파일 후에도 남는다.

    public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
        int count = 0;
        for (T e : anArray)
            if (e.compareTo(elem) > 0)
                ++count;
        return count;
    }
  • extends 키워드 사용 시 hierarchy를 고려할 것!

Generic Method


제네릭 메서드란 메서드의 선언부에 타입 변수를 사용한 메서드

접근제어자 static <T> 반환타입 함수이름( ... ) { ... }
  • 제네릭 클래스에서 정의된 타입 변수와 제네릭 메서드에서 사용된 타입 변수는 상관이 없다.

Type Inference


타입 추론은 각 메서드 호출과 상응하는 선언을 살펴보고 해당 호출에 적용할 수 있는 type argument를 결정하는 자바 컴바일러의 기능이다.

static <T> T pick(T a1, T a2) { return a2; }
Serializable s = pick("d", new ArrayList<String>());

/*
pick 메서드에 전달되는 두 번째 인수가 Serializable 타입인지 확인한다
*/
  • angle bracket으로 감싼 타입 없이도(일반 메서드처럼) 제네릭 메서드를 호출할 수 있다.

    public class BoxDemo {
    
      public static <U> void addBox(U u, 
          java.util.List<Box<U>> boxes) {
        Box<U> box = new Box<>();
        box.set(u);
        boxes.add(box);
      }
    
      public static <U> void outputBoxes(java.util.List<Box<U>> boxes) {
        int counter = 0;
        for (Box<U> box: boxes) {
          U boxContents = box.get();
          System.out.println("Box #" + counter + " contains [" +
                 boxContents.toString() + "]");
          counter++;
        }
      }
    
      public static void main(String[] args) {
        java.util.ArrayList<Box<Integer>> listOfIntegerBoxes =
          new java.util.ArrayList<>();
        BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
        BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes); //다음과 같이 사용
        BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes); //다음과 같이 사용
        BoxDemo.outputBoxes(listOfIntegerBoxes);
      }
    }
  • 제네릭 클래스를 인스턴스화 했을 때도 마찬가지로 타입 추론이 가능하다.

    Map<String, List<String>> myMap = new HashMap<String, List<String>>();
    Map<String, List<String>> myMap = new HashMap<>(); //다음과 같이 사용
  • 제네릭 클래스의 생성자는 다음과 같이 사용

    //정의
    class MyClass<X> {
      <T> MyClass(T t) {
        // ...
      }
    }
    //선언
    MyClass<Integer> myObject = new MyClass<>("");

Generic Type erasure란?

소거란 원소 타입을 컴파일 타입에만 검사하고 런타임에는 해당 타입 정보를 알 수 없는 것입니다.
한마디로, 컴파일 타임에만 타입 제약 조건을 정의하고, 런타임에는 타입을 제거한다는 뜻입니다.
..추가 예정

코딩교육 티씨피스쿨

자바 제네릭 이해하기 Part 1

Lesson: Generics (Updated)

0개의 댓글