ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [스터디 14주차] 제네릭
    프로그래밍 언어/JAVA 2021. 7. 24. 16:52
    더보기

    목표: 자바의 제네릭에 대해 학습

     

     

     

     

    1. 제네릭 사용법

    제네릭은 Java 5부터 추가된 타입으로 잘못된 타입이 사용될 수 있는 문제를 컴파일 과정에서 제거할 수 있게 해준다.

    제네릭은 클래스와 인터페이스, 메소드를 정의할 때 타입을 파라미터로 사용할 수 있도록 한다.

     

    그러면 제네릭을 사용하면 어떠한 장점이 있을까?

    - 컴파일 시 강한 타입 체크를 할 수 있다.

    컴파일 시에 미리 타입을 강하게 체크해 사전에 에러를 방지해준다.

     

    - 타입 변환을 제거한다.

    제네릭을 사용한 경우와 사용하지 않은 경우를 코드를 보며 이해해보자.

    List list = new ArrayList();
    list.add("hello");
    String str = (String) list.get(0);

    제네릭을 사용하지 않은 위 코드를 보면 리스트에 문자열을 저장했지만 꺼내 올때에 String으로 타입 변환을 해야하는 번거로움이 있다.

     

    하지만, 아래의 코드처럼 제네릭을 사용해 리스트에 저장되는 요소를 String 타입으로 정해주어 요소를 꺼낼 때 타입 변환을 해주지 않아도 된다.

    List<String> lsit = new ArrayList<String>();
    list.add("hello");
    String str = list.get(0);

     

    제네릭을 사용하는 방법은 클래스 또는 인터페이스 뒤에 "<>"부호가 붙고 < 와 > 사이에 타입 파라미터가 위치한다.

    public class 클래스명<type parameter> {
        ...
    }
    
    public interface 인터페이스명<type parameter> {
        ...
    }

    타입 파라미터는 변수명과 동일한 규칙에 따라 작성되지만 일반적으로 대문자 알파벳 한 글자로 표현하다.

    제네릭 타입을 실제 코드에서 사용하려면 타입 파라미터에 구체적인 타입을 지정해야 한다.

    즉, 아래와 같이 코드를 구성해야한다는 것이다.

    public class Box<T> {
        private T t;
        
        public void set(T t) {
            this.t = t;
        }
        
        public T get() {
            return t;
        }
    }

    먼저 제네릭 타입을 사용하여 간단하게 요소를 입력받고 출력해주는 Box클래스를 만든다.

    여기서, 받는 요소와 출력되는 요소의 타입은 제네릭 타입인 것을 볼 수 있다.

    그러면 직접 Box 클래스로 객체를 생성할 때 어떻게 하면 되는지 아래의 코드를 보면 알 수 있다.

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

    이렇게 객체를 생성할 때에는 타입 파라미터에 구체적으로 어떠한 타입을 쓸 것인지 입력해주어야한다.

    그리고 직접 입력한 타입으로 Box 클래스의 내부는 아래와 같이 자동으로 재구성 된다.

    public class Box<String> {
        private String t;
        
        public void set(String t) {
            this.t = t;
        }
        
        public String get() {
            return t;
        }
    }

    필드 타입이 String으로 변경되고 set() 메소드도 String타입만 매개값으로 받을 수 있게 변경되고 get() 메소드도 String 타입으로 리턴되도록 변경된다.

     

     

    멀티 타입 파라미터

    제네릭 타입은 위에서 본 예제 코드처럼 한 개의 타입 파라미터를 가지는 것이 아닌 두 개 이상의 멀티 타입 파라미터를 사용할 수 있는데 이 경우에는 각 타입 파라미터를 콤마로 구분한다.

    예제 코드를 보며 어떻게 사용되는지 알아보자.

    public class MulltiTypeParameterProduct<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;
        }
    }

    위 코드처럼 어떠한 클래스에 T, M이라는 타입 파라미터를 가진 제네릭 타입을 사용하여 선언한 것을 볼 수 있다.

    이 클래스를 사용하여 객체를 생성하는 코드는 아래와 같다.

    public class ProductExample {
        public static void main(String[] args) {
            MulltiTypeParameterProduct<Tv, String> product = new MulltiTypeParameterProduct<Tv, String>();
            product.setKind(new Tv());
            product.setModel("Samsung");
            Tv tv = product.getKind();
            String tvmodel = product.getModel();
    
        }
    }

     

    자바 7의 새로운 기능

    위에서 본 객체 생성 코드에서 간소화 됐으면 하는 부분이 보인다.

    MulltiTypeParameterProduct<Tv, String> product = new MulltiTypeParameterProduct<Tv, String>();

    저는 타입 파라미터로 이거와 이거 쓸거에요를 2번 얘기하는 느낌이 들기 때문이다.

    이러한 점은 간소화하기 위해서 자바 7에서는 다이아몬드 연산자를 제공한다.

    다이아몬드 연산자를 사용하면 아래의 코드처럼 작성할 수 있다.

    Product<Tv, String> product = new Product<>();

     

     

     

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

    바운디드 타입

    제네릭 타입을 사용할 경우에 종종 타입 파라미터에 지정되는 구체적인 타입을 제한할 필요가 있다.

    예를 들어 숫자를 연산하는 제네릭 메소드는 매개값으로 Number 타입 또는 Byte, Short, Integer, Long, Double 같은 타입만 가져야 할 것이다.

    이를 위해 사용하는 것이 제한된 타입 파라미터(bounded type parameter)이다.

    바운디드 타입을 사용하기 위해서는 타입 파라미터 위에 extends 키워드를 붙여 상위 타입을 명시하면 된다.

    public <T extends 상위타입> 리턴타입 메소드명(매개변수, ...) {
       ...
    }

    이렇게 바운디드 타입을 사용하여 메소드를 정의하면 타입 파라미터에 지정되는 구체적인 타입은 상위 타입이거나 상위 타입의 하위 또는 구현 클래스만 가능해진다.

    여기서 주의해야할 점은 메소드의 중괄호 안에서 타입 파라미터 변수로 사용가능한 것은 상위 타입의 멤버(필드, 메소드)로 제한되어서 하입 타입에만 있는 필드와 메소드는 사용 불가능하다는 것이다.

     

     

    와일드 카드

    와일드 카드는 제네릭 타입을 매개값이나 리턴 타입으로 사용할 때 구체적인 타입 대신에 사용하는 것으로 아래와 같이 세 가지 형태로 사용할 수 있다.

    1) 제네릭타입<?>

    Unbounded Wildcards로 타입 파라미터를 대치하는 구체적인 타입으로 모든 클래스나 인터페이스 타입이 올 수 있다.

     

    2) 제네릭타입<? extends 상위타입>

    Upper Bounded Wildcards로 타입 파라미터를 대치하는 구체적인 타입으로 상위 타입이나 하위타입만 올 수 있다.

     

    3) 제네릭타입<? super 하위타입>

    Lower Bounded Wildcards로 타입 파라미터를 대치하는 구체적인 타입으로 하위 타입이나 상위 타입이 올 수 있다.

     

     

     

    3. 제네릭 메소드 만들기

    제네릭 메소드는 매개 타입과 리턴 타입으로 타입 파라미터를 갖는 메소드를 말한다.

    제네릭 메소드를 만드는 방법은 리턴 타입 앞에 < >기호를 추가하고 타입 파라미터를 기술한 다음에 리턴 타입과 매개 타입으로 타입 파라미터를 사용하면 된다.

    public <타입파라미터, ...> 리턴타입 메소드명(매개변수, ...) {
        ....
    }

     

    제네릭 메소드는 두 가지 방식으로 호출 할 수 있다.

    리턴타입 변수 = <구체적타입> 메소드명(매개값);
    리턴타입 변수 = 메도르명(매개값);

    첫 번째 경우는 코드에서 타입 파라미터의 구체적인 타입을 명시적으로 지정하는 것이고

    두 번째 경우는 컴파일러가 매개값의 타입을 보고 구체적인 타입을 추정하도록 하는 것이다.

     

    그러면 직접 제네릭 메소드를 만들고 호출해보는 코드를 작성해보자.

    public class Util {
        public static <T> GenericBox<T> boxing(T t) {
            GenericBox<T> box = new GenericBox<>();
            box.set(t);
            return box;
        }
    }

    정적인 메소드로 boxing을 정의하고 리턴타입으로는 위에서 사용한 GenericBox<T>를 사용하고 리턴 타입 앞에 제네릭 타입을 추가한 것을 볼 수 있다.

    이렇게 제네릭 메소드를 만들면 아래처럼 호출해서 사용할 수 있다.

    public class BoxingMethodExmple {
        public static void main(String[] args) {
            // 타입 파라미터의 구체적인 타입을 명시
            GenericBox<Integer> box1 = Util.<Integer>boxing(100);
            int intValue = box1.get();
            System.out.println(intValue);
    
            // 타입 파라미터 명시 생략
            GenericBox<String> box2 = Util.boxing("Ratel");
            String strValue = box2.get();
            System.out.println(strValue);
        }
    }

     

     

    이번에는 타입 파라미터를 2개 사용해서 생성한 메소드를 코드로 봐보자.

    public class Pair<K, V> {
        private K key;
        private V value;
    
         public Pair(K key, V value) {
             this.key = key;
             this.value = value;
         }
    
         public void setKey(K key) {
             this.key = key;
         }
         public void setValue(V value) {
             this.value = value;
         }
         public K getKey() {
             return key;
         }
         public V getValue() {
             return value;
         }
    }
    public class Util {
        public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
            boolean keyCompare = p1.getKey().equals(p2.getKey());
            boolean valueCompare = p1.getValue().equals(p2.getValue());
            return keyCompare && valueCompare;
        }
    }

    위 코드처럼 타입 파라미터를 2개 사용해서 메소드를 생성할 수 있다.

    그리고 아래의 코드와 같이 생성된 제네릭 메소드를 호출하여 사용하는 것을 볼 수 있다.

    public class CompareMethodExample {
        public static void main(String[] args) {
            Pair<Integer, String> p1 = new Pair<>(1, "사과");
            Pair<Integer, String> p2 = new Pair<>(1, "사과");
            boolean result1 = Util.compare(p1, p2);
            if (result1) {
                System.out.println("논리적으로 동등한 객체입니다.");
            } else {
                System.out.print("논리적으로 동등하지 않은 객체입니다.");
            }
            
        }
    }

     

     

     

    4. Erasure

    위에서 잠깐 언급했는데 컴파일러는 컴파일 타임에 타입 파라미터를 사용하는 대상의 타입을 컴파일러가 정하는 구체적인 타입으로 대체하는 Erasure가 실행하게 된다.

     

    Erasure의 규칙은 아래와 같다.

    - 제네릭 타입의 타입 파라미터가 상하한이 있는 경우에는 타입 파라미터를 한계 타입으로, 없는 경우에는 모든 타입 파라미터를 object로 바꾼다.

    - type-safety를 유지하기 위해 필요한 경우 타입 캐스팅을 사용할 수 있다.

    - 제네릭 타입을 상속받은 클래스에서는 다형성을 유지하기 위해 브러지 메서드를 사용한다.

     

    직접 컴파일 후에 만들어진 바이트코드를 보며 이해해보자.

    public class WitchPot<T> {
        private T meterial;
    
        public WitchPot(T meterial) {
            this.meterial = meterial;
        }
    
        public T get() {
            return this.meterial;
        }
        public void set(T meterial) {
            this.meterial = meterial;
        }
    
    }

    이 클래스에는 바인드 되지 않은 상태이므로 바이트 코드로 보면 Object로 대체되는 것을 아래의 바이트 코드에서 볼 수 있다.

    > javap -c WitchPot 
    Warning: File ./WitchPot.class does not contain class WitchPot
    Compiled from "WitchPot.java"
    public class javageneric.WitchPot<T> {
      public javageneric.WitchPot(T);
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: aload_0
           5: aload_1
           6: putfield      #2                  // Field meterial:Ljava/lang/Object;
           9: return
    
      public T get();
        Code:
           0: aload_0
           1: getfield      #2                  // Field meterial:Ljava/lang/Object;
           4: areturn
    
      public void set(T);
        Code:
           0: aload_0
           1: aload_1
           2: putfield      #2                  // Field meterial:Ljava/lang/Object;
           5: return
    }

     

    댓글

Designed by Tistory.