ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 14주차 과제: 제네릭
    Programming/Java live study 2021. 2. 24. 15:03
     

    14주차 과제: 제네릭 · Issue #14 · whiteship/live-study

    목표 자바의 제네릭에 대해 학습하세요. 학습할 것 (필수) 제네릭 사용법 제네릭 주요 개념 (바운디드 타입, 와일드 카드) 제네릭 메소드 만들기 Erasure 마감일시 2021년 2월 27일 토요일 오후 1시까

    github.com

    목표

    자바의 제네릭에 대해 학습하세요.

    학습할 것

     - 제네릭 사용법

     - 제네릭 주요 개념 (바운디드 타입, 와일드 카드)

     - 제네릭 메소드 만들기

     - Erasure


    1. 제네릭

    참고 : docs.oracle.com/javase/tutorial/java/generics/why.html

     

    제네릭은 클래스, 인터페이스 및 메서드를 정의 할 때 유형 (클래스 및 인터페이스)이 매개 변수가되도록한다. 메서드 선언에 사용되는 보다 친숙한 형식 매개 변수 와 마찬가지로 형식 매개 변수는 다른 입력으로 동일한 코드를 재사용 할 수있는 방법을 제공한다. 차이점은 형식 매개 변수에 대한 입력은 값이고 유형 매개 변수에 대한 입력은 유형이라는 것이다.

     

    제네릭을 사용하는 코드는 제네릭을 사용하지 않는 코드에 비해 많은 이점을 가지고 있다.

     

     - 컴파일 타임에 더 강력한 타입 검사가 가능하다. Java 컴파일러는 제네릭을 사용하지 않은 일반 코드에 강력한 타입 검사를 적용하고 코드가 안정성을 위반할 경우 오류를 발생시킨다. 컴파일 오류는 런타임 오류를 수정하는 것 보다 쉽기 때문에 좋은 이점을 가지고 있다.

     

     - 제네릭을 사용하면 타입 캐스팅을 제거할 수 있다.

    밑의 코드는 제네릭을 사용하지 않은 코드이다.

     

    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

     

    타입 캐스팅을 하지 않아도 정상적으로 사용이 가능하다.

     

     - 프로그래머가 제네릭을 사용하여 다양한 유형의 컬렉션에서 작동하고 사용자 정의가 가능하며, 유형에 안전하고 읽기 쉬운 제네릭 알고리즘을 구현할 수 있다.


    2. 제네릭 사용법

    참고 : docs.oracle.com/javase/tutorial/java/generics/types.html

     

    제네릭 클래스를 사용하기 위해 Oracle에서 제공하는 예시를 사용해보았다.

     

    제네릭을 사용하지 않았을 때 Box 클래스이다.

     

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

     

    Box의 필드 object는 타입이 모든 클래스의 상위 타입인 Object 이기 때문에 모든 클래스의 정보를 가질 수 있다. 하지만 해당 필드를 get/set으로 잘못 꺼내서 쓰거나 저장할 경우 런타임 시에 타입이 맞지 않는 오류가 발생할 수 있다.

     

    public class Exam01 {
    
        public static void main(String[] args) {
    
            Box box = new Box();
            box.set(10); // 정수
            String s = (String) box.get(); // 실수로 String 을 get
        }
    }

     

    ClassCastException이 발생한 것을 알 수 있다.

    이제 위의 Box 클래스를 제네릭 클래스로 정의하였다.

     

    /**
     * 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; }
    }

     

    제네릭 클래스는 다음과 같은 형식으로 정의할 수 있다.

     

    클래스 이름 <T1, T2, ..., Tn> {/ * ... * /}

     

    <>로 구분된 매개 변수는 클래스 이름 뒤에 온다. 해당 괄호에는 지정할 타입 파라미터를 넣을 수 있다. 또한 인터페이스에서도 동일하게 적용이 가능하다. 이제 위 Box 제네릭 클래스를 사용해보았다.

     

    컴파일 오류가 발생한다.

    그렇기 때문에 타입이 Integer로 받아야만 사용이 가능하다.

     

    public class Exam02 {
    
        public static void main(String[] args) {
    
            Box<Integer> box = new Box<>();
    
            box.set(10);
            Integer integer = box.get();
        }
    }

    2.1 타입 매개변수 명명 규칙

    단일 대문자로 작성한다. 해당 규칙을 적용하면 일반 클래스  혹은 인터페이스의 이름의 차이를 구분하여 직관적으로 사용할 수 있다.

     

     - E element 

     - K key

     - T type

     - V value

     - S, U, V ..

    2.2 제네릭 타입 선언 및 인스턴스화

    제네릭 클래스인 Box에 구체적인 타입을 선언하기 위해서는 <>를 활용해야 한다.

     

    Box<Integer> box;

     

    다른 변수선언과 비슷한 모양을 가지고 있고, 단순히 Integer 타입을 가지는 Box에 대한 참조 변수를 선언한 것이다. 이 클래스를 인스턴스화 하기 위해서는 동일하기 new 키워드를 사용한다.

     

    Box<Integer> box = new Box<Integer>();

     

    Java 7 부터는 클래스의 생성자를 호출하는데 필요한 타입 인수를 빈 유형으로 바꿀 수 있다. 해당 <>을 다이아몬드라고 부른다. 

     

    Box<Integer> box = new Box<>();

     

    생략하여도 적절히 타입 추론되어 인스턴스화 되는 것을 알 수 있다.


    3. 제네릭 주요 개념 (바운디드 타입, 와일드 카드)

    참고 :

    medium.com/%EC%8A%AC%EA%B8%B0%EB%A1%9C%EC%9A%B4-%EA%B0%9C%EB%B0%9C%EC%83%9D%ED%99%9C/java-generic-%EC%9E%90%EB%B0%94-%EC%A0%9C%EB%84%A4%EB%A6%AD-f4343fa222df

    docs.oracle.com/javase/tutorial/java/generics/wildcards.html

    3.1 바운디드 타입

    만약 type 매개변수에 제한을 두고 싶다면 다음과 같이 사용할 수 있다.

     

    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 print(U u) {
            System.out.println(t.getClass());
            System.out.println(u.getClass());
        }
    }

     

    print 메소드의 type 매개변수를 extends 키워드를 사용하여 Number로 제한하였다. 해당 U에는 Integer, Float 등등의 클래스만 허용된다.

     

    매개변수로 String type을 넘길 경우 컴파일 오류를 확인할 수 있다.

    3.2 와일드 카드

    와일드 카드를 표현하는 ?는 알 수 없는 타입을 나타낸다. 와일드 카드는 다양한 상황에서 사용할 수 있다. 매개 변수, 필드 혹은 지역 변수, 때때로 반환 타입에도 사용된다.

     

    하지만 와일드 카드는 제네릭 메소드 호출, 제네릭 클래스 인스턴스 생성 또는 슈퍼 타입의 타입 인수로는 사용되지 않는다.

     

    Upper Bounded Wildcards

    변수에 대한 제한을 완화시키고 싶을 때 사용한다. extends 키워드 앞에 wildcard를 붙여 사용한다.

     

    public static double sumOfList(List<? extends Number> list) {
        double s = 0.0;
        for (Number n : list)
            s += n.doubleValue();
        return s;
    }

     

    List<Integer> li = Arrays.asList(1, 2, 3);
    System.out.println("sum = " + sumOfList(li));

     

    List<Double> ld = Arrays.asList(1.2, 2.3, 3.5);
    System.out.println("sum = " + sumOfList(ld));

     

    import java.util.Arrays;
    import java.util.List;
    
    public class Exam04 {
    
        public static double sumOfList(List<? extends Number> list) {
            double s = 0.0;
            for (Number n : list)
                s += n.doubleValue();
            return s;
        }
    
        public static void main(String[] args) {
    
            List<Integer> li = Arrays.asList(1, 2, 3);
            System.out.println("sum = " + sumOfList(li));
    
            List<Double> ld = Arrays.asList(1.2, 2.3, 3.5);
            System.out.println("sum = " + sumOfList(ld));
        }
    }
    

     

    sumOfList는 Number type 뿐만아니라 Number의 sub type이 모두 매치된다.

     

    Unbounded Wildcards

     

    매개변수로 전달받은 list의 모든 항목을 출력하는 printList 메소드를 만들었다.

     

    public static void printList(List<Object> list) {
        for (Object elem : list)
            System.out.println(elem + " ");
        System.out.println();
    }

     

    하지만 printList는 Object List의 목록한 매개변수 목록으로 받을 수 있기 때문에, List<Double>, List<Integer> 등 타입의 list를 전달하면 컴파일 오류를 발생시킨다. 

     

     

    import java.util.ArrayList;
    import java.util.List;
    
    public class Exam06 {
    
        public static void main(String[] args) {
    
            List<Double> list = new ArrayList<>();
            list.add(10.0);
            list.add(11.0);
            list.add(12.0);
    
            printList(list);
        }
    
        public static void printList (List<?> list) {
            for (Object elem : list)
                System.out.print(elem + " ");
            System.out.println ();
        }
    }

     

    printList를 위와 같이 수정하여 작성하면 모든 List에 대응이 가능하다.

     

    Lower Bounded Wildcards

    Upper Bounded Wildcards와 반대로, 특정 type에서 해당 type의 super type까지 허용하는 제네릭을 만드는 경우 사용된다. wildcard 뒤에 super 키워드를 붙여서 사용한다.

     

    import java.util.ArrayList;
    import java.util.List;
    
    public class Exam07 {
    
        public static void main(String[] args) {
    
            List<Integer> integers = new ArrayList<>();
            List<Number> numbers = new ArrayList<>();
    
            integers.add(10);
            numbers.add(10.0);
    
            printList(integers);
            printList(numbers);
        }
    
        public static void printList(List<? super Integer> list) {
            for (Object o : list)
                System.out.println(o.getClass());
        }
    }
    

     

    Integer 타입의 super tpye들이 허용되는 것을 알 수 있다.


    4. 제네릭 메소드 만들기

    메소드의 선언부에 제네릭 타입이 선언되 메소드를 제네릭 메소드라고 부른다. 위에 만들었던 printList를 제네릭 메소드로 변경하였다.

     

    import java.util.ArrayList;
    import java.util.List;
    
    public class Exam08 {
    
        public static void main(String[] args) {
    
            List<Double> list = new ArrayList<>();
            list.add(10.0);
            list.add(11.0);
            list.add(12.0);
    
            printList(list);
        }
    
        public static <T extends Number> void printList(List<T> list) {
            for (Object elem : list)
                System.out.println(elem + " ");
            System.out.println();
        }
    }

    기존의 printList와 동일하게 적용된다. 매개변수의 타입이 복잡할 때 직관적으로 확인이 가능하기 때문에 유용하게 사용이 가능하다.


    5. Erasure

    참고 : 

    docs.oracle.com/javase/tutorial/java/generics/erasure.html

     

    Java에 제네릭이 도입되어 컴파일 타임에 엄격한 타입 검사를 지원한다. 제네릭을 구현하기 위해 Java 컴파일러는 다음과 같은 type erasure를 제공한다.

     

     - 제네릭 타입의 모든 타입 매개변수를 해당 범위 또는 타입 매개변수가 제한되지 않은 경우 Object로 바꾼다. 따라서 생성된 바이트코드에는 일반 클래스, 인터페이스 및 메소드만 포함된다.

     - 타입 안정성을 유지하기 위해 필요한 경우 타입 캐스트를 삽입한다.

     - 확장된 제네릭 유형에서 다형성을 유지하는 브리지 메소드를 생성한다.

     

    type eraure는 매개 변수화된 타입에 대해 새 클래스가 생성되지 않도록 한다. 결과적으로 제네릭은 런타임 오버 헤드를 발생 시키지 않는다.

     

    public class Node<T> {
    
        private T data;
        private Node<T> next;
    
        public Node(T data, Node<T> next) {
            this.data = data;
            this.next = next;
        }
    
        public T getData() { return data; }
        // ...
    }

     

    위 Node 클래스의 바이트코드를 확인해보았다.

     

    // class version 55.0 (55)
    // access flags 0x21
    // signature <T:Ljava/lang/Object;>Ljava/lang/Object;
    // declaration: me/hyeonic/week14/Node<T>
    public class me/hyeonic/week14/Node {
    
      // compiled from: Node.java
    
      // access flags 0x2
      // signature TT;
      // declaration: data extends T
      private Ljava/lang/Object; data
    
      // access flags 0x2
      // signature Lme/hyeonic/week14/Node<TT;>;
      // declaration: next extends me.hyeonic.week14.Node<T>
      private Lme/hyeonic/week14/Node; next
    
      // access flags 0x1
      // signature (TT;Lme/hyeonic/week14/Node<TT;>;)V
      // declaration: void <init>(T, me.hyeonic.week14.Node<T>)
      public <init>(Ljava/lang/Object;Lme/hyeonic/week14/Node;)V
       L0
        LINENUMBER 8 L0
        ALOAD 0
        INVOKESPECIAL java/lang/Object.<init> ()V
       L1
        LINENUMBER 9 L1
        ALOAD 0
        ALOAD 1
        PUTFIELD me/hyeonic/week14/Node.data : Ljava/lang/Object;
       L2
        LINENUMBER 10 L2
        ALOAD 0
        ALOAD 2
        PUTFIELD me/hyeonic/week14/Node.next : Lme/hyeonic/week14/Node;
       L3
        LINENUMBER 11 L3
        RETURN
       L4
        LOCALVARIABLE this Lme/hyeonic/week14/Node; L0 L4 0
        // signature Lme/hyeonic/week14/Node<TT;>;
        // declaration: this extends me.hyeonic.week14.Node<T>
        LOCALVARIABLE data Ljava/lang/Object; L0 L4 1
        // signature TT;
        // declaration: data extends T
        LOCALVARIABLE next Lme/hyeonic/week14/Node; L0 L4 2
        // signature Lme/hyeonic/week14/Node<TT;>;
        // declaration: next extends me.hyeonic.week14.Node<T>
        MAXSTACK = 2
        MAXLOCALS = 3
    
      // access flags 0x1
      // signature ()TT;
      // declaration: T getData()
      public getData()Ljava/lang/Object;
       L0
        LINENUMBER 13 L0
        ALOAD 0
        GETFIELD me/hyeonic/week14/Node.data : Ljava/lang/Object;
        ARETURN
       L1
        LOCALVARIABLE this Lme/hyeonic/week14/Node; L0 L1 0
        // signature Lme/hyeonic/week14/Node<TT;>;
        // declaration: this extends me.hyeonic.week14.Node<T>
        MAXSTACK = 1
        MAXLOCALS = 1
    }
    

     

    제네릭 타입과 관련된 것들이 모두 Object로 되어 있는 것을 알 수 있다. 결국 위의 Node 클래스는 밑의 클래스와 거의 유사하게 해석되는 것을 알 수 있다.

     

    public class Node {
    
        private Object data;
        private Node next;
    
        public Node(Object data, Node next) {
            this.data = data;
            this.next = next;
        }
    
        public Object getData() { return data; }
        // ...
    }

    'Programming > Java live study' 카테고리의 다른 글

    13주차 과제: I/O  (0) 2021.02.14
    12주차 과제: 애노테이션  (0) 2021.02.03
    11주차 과제: Enum  (0) 2021.01.27
    10주차 과제: 멀티쓰레드 프로그래밍  (0) 2021.01.21
    9주차 과제: 예외 처리  (0) 2021.01.13

    댓글

Designed by Tistory.