ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [이펙티브 자바 스터디] 제네릭
    언어/JAVA 2024. 5. 30. 19:18

    제네릭~

    제네릭(Generic)은 자바 5부터 도입된 기능으로, 클래스나 메서드를 선언할 때 타입을 파라미터로 받을 수 있게 해준다. 이는 코드의 재사용성을 높이고, 타입 안전성을 보장하며, 캐스팅(casting)을 줄여준다.
     
    그니까 컬렉션이 담을 수 있는 타입을 컴파일러에게 알려줘서 엉뚱한 타입의 객체를 넣으려는 시도를 컴파일 과정에서
    차단하여 런타임 시의 오류를 피할 수 있게 하는 것이다.

    Raw 타입은 사용하지 말라

    인텔리에서도 싫어한다.

    먼저 Raw 타입이란 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않은 것을 말하고 List<Integer> 의 
    Raw 타입은 List 인 것이다.
    이는 제네릭이 생기기 전 코드와 호환되도록 하기 위해 만들어둔 것으로 사용하지 않는 것이 좋다.
     
    제네릭의 장점은 다음과 같다.
     
    1. 클래스나 메소드 내부에서 사용되는 객체의 타입 안정성을 높일 수 있다.

        // no generic
        List stringList = new ArrayList<>();
        stringList.add("ryeoryeo");
        stringList.add(1);
        String result = (String) stringList.get(0) + (String) stringList.get(1); // runtime error!
        
        // generic
        List<String> stringList = new ArrayList<>();
        stringList.add("ryeoryeo");
        stringList.add(1);  // compile error!!

     
    코드로 보여주자면 이런식의 상황이다.
    generic 을 쓰지 않으니 객체를 담을 때는 문제가 발생하진 않지만
    get 으로 사용할 때 런타임 시의 에러가 발생한다.
     
    책에서 말하는 것은 내가 이렇게 단순하게 작성한 상황이니 바로 "ryeoryeo" 와 1 이라는 
    문제 시점을 파악하기 쉽지만 
    복잡한 비즈니스 로직을 가지고 있는 로직에서는 
    저 잘못들어간 시점을 찾기 어려울 것이라는 메세지이다.
     
    그래서 아래의 generic 이 사용된 코드를 보면 compile error 로 
    값이 잘못들어가는 시점을 잡을 수 있게 된다.
     
    2. 반환값에 대한 타입 변환 및 타입 검사에 들어가는 노력을 줄일 수 있다.

        // no generic
        List stringList = new ArrayList<>();
        stringList.add("ryeoryeo");
        String result = (String) stringList.get(0);  // 반환 타입 지정해줘야 함!!
    
        // generic
        List<String> stringList = new ArrayList<>();
        stringList.add("ryeoryeo");
        String result = stringList.get(0);

     
    제네릭을 사용하면 컴파일러는 저 List 에 String 만을 담아야 한다는 것을 알게 된다.
    그로 인해 컴파일러는 이 List 에서 꺼내는 모든 곳에서 보이지 않는 
    형변환을 추가하여 절대 실패하지 않음을 보장하게 된다.
     
    제네릭을 사용하지 않는다면, -> Raw 타입을 사용한다면 매번 반환 타입을 지정해줘야 한다.
     

    결과적으로 Raw 타입을 쓰게 된다면 제네릭이 안겨주는 안정성과 표현력을 모두 잃게 된다!

     
    근데 여러 타입을 받고 싶다면 어떻게 하는게 좋을까?

     

    와일드카드

    import java.util.ArrayList;
    import java.util.List;
    
    public class WildcardExample {
        public static void main(String[] args) {
            // Integer 타입의 리스트 생성
            List<Integer> intList = new ArrayList<>();
            intList.add(1);
            intList.add(2);
            intList.add(3);
    
            // Double 타입의 리스트 생성
            List<Double> doubleList = new ArrayList<>();
            doubleList.add(1.1);
            doubleList.add(2.2);
            doubleList.add(3.3);
    
            // printList 메서드 호출
            printList(intList);
            printList(doubleList);
        }
    
        // 어떤 타입의 리스트든 받아서 출력할 수 있는 메서드
        public static void printList(List<?> list) {
            // 리스트의 각 요소를 출력
            for (Object elem : list) {
                System.out.println(elem);
            }
        }
    }
    

    코드를 보면 printList라는 메서드는
    타입에 관계없이 Object로 받아서 리스트 컬렉션의 아이템들을 출력하고 있다.

    근데 와일드 카드를 쓰면서 특정 타입만 받고싶다면 어떻게 해야할까?

    extends와 super를 사용하면 된다.

    public static void printNumbers(List<? extends Number> list) {
        for (Number number : list) {
            System.out.println(number);
        }
    }
    

    ? extends T
    위의 코드는 T 자리의 Number를 작성하면
    이 Number의 하위 타입들인 Integer와 Double 등의 원소만 가능하다.

    public static void addIntegers(List<? super Integer> list) {
        list.add(1);
        list.add(2);
        list.add(3);
    }
    

    ? super T
    이는 extends와 반대로 상위타입만 받는다.
    Number나 Object를 받을 수 있는거다.

    여기까지 하고 이제 타입의 변성이라는 것에 대해 공부해보자.

    변성

    공변과 반공변 무변성 이라는 세 유형으로 나눌 수 있다.

    T가 T`의 상위 자료형이라 할 때

    무변성(Invariance)공변성(Covariance)반공변성(Contravariance)

    C는 C<T`>와 아무 관련이 없다. C는 C<T`>의 상위 자료형이다.
    out으로 지정
    C는 C<T`>의 하위 자료형이다.
    in으로 지정

    자바는 무변성을 디폴드로 한다.

    공변성(Covariance)

    공변성은 제네릭 타입이 다른 타입과 같은 방향으로 변할 때를 의미한다.

    즉, 만약 타입 A가 타입 B의 서브타입(subtype)이라면, List<A>도 List<B>의 서브타입이라는 것을 의미한다.

    이는 주로 반환 타입에서 사용된다.

    import java.util.ArrayList;
    import java.util.List;
    
    class Animal {
        void sound() {
            System.out.println("Some sound...");
        }
    }
    
    class Dog extends Animal {
        @Override
        void sound() {
            System.out.println("Bark!");
        }
    }
    
    public class CovarianceExample {
        public static void main(String[] args) {
            List<Dog> dogs = new ArrayList<>();
            dogs.add(new Dog());
    
            // 공변성을 통해 상위 타입으로 리스트를 받을 수 있다.
            List<? extends Animal> animals = dogs;
    
            // 리스트에서 항목을 꺼낼 수 있다.
            for (Animal animal : animals) {
                animal.sound(); // Dog의 sound() 메서드가 호출됨
            }
        }
    }

    이를 보면 Dog 라는 제네릭을 가진 List에 상위 타입으로 리스트를 받고

    또 Animal로 꺼낼 수 있다.

    List<? extends Animal> 는 List<Dog> 과 같은 하위 타입을 받을 수 있는 것이다.

     

    반공변성(Contravariance)

    반공변성은 제네릭 타입이 다른 타입과 반대 방향으로 변할 때를 의미한다.

    즉, 만약 타입 A가 타입 B의 서브타입이라면, List<B>는 List<A>의 서브타입이라는 것을 의미한다.

    이는 주로 인수 타입에서 사용된다.

    import java.util.ArrayList;
    import java.util.List;
    
    class Animal {
        void sound() {
            System.out.println("Some sound...");
        }
    }
    
    class Dog extends Animal {
        @Override
        void sound() {
            System.out.println("Bark!");
        }
    }
    
    public class ContravarianceExample {
        public static void main(String[] args) {
            List<Animal> animals = new ArrayList<>();
            animals.add(new Dog());
    
            // 반공변성을 통해 상위 타입으로 리스트를 받을 수 있다.
            List<? super Dog> dogs = animals;
    
            // 리스트에 항목을 추가할 수 있다.
            dogs.add(new Dog());
    
            // 하지만 리스트에서 항목을 꺼낼 때는 Object 타입으로 나온다.
            for (Object obj : dogs) {
                ((Animal) obj).sound(); // Downcasting 필요
            }
        }
    }

     

    여기서 List<? super Dog> 는 List<Animal>같은 상위 타입들을 받아들일 수 있다.

    반공변성은 주로 데이터를 쓸 때 유용하다.

    정리

    • 공변성 (Covariance): List<? extends T> 형태로 사용되며, 주로 데이터를 읽을 때 사용된다.
    • 반공변성 (Contravariance): List<? super T> 형태로 사용되며, 주로 데이터를 쓸 때 사용된다.


    공변과 반공변을 나타내는 그림이다.

     

     

     

    사실, 실무를 하면서 직접 변성을 지정하며 클래스를 설계하고

    그것을 사용하는 비즈니스 로직을 작성하는 것은 거의 드물 것이라 단언할 수 있다고 한다.

    하지만 언제 어느 상황에서 비즈니스 요구사항을 마주하며 변성을 사용하게 될지 모르니 무엇인지 이해하고 있도록 하자!

     
    비검사 경고를 제거하라

     

    컴파일러가 던지는 비검사경고에 대해서 무시하지 말하는 것이다.

    이 경고들은 최대한 해결하라는 것인데

    타입이 안정하다고 판단이 될 때만 

    @SuppressWarnings 를 사용해서 제거해주라고 한다.

     

    이렇게 제거를 해줘야 하는 이유는 이렇게 무시하다보면

    진짜 문제를 알리는 새로운 경고가 나와도 눈치채 지 못할 수 있다는 것이다.

    제거하지 않은 수많은 거짓 경고 속에 새로운 경고가 파묻힐 것이기 때문이다.

     

    애너테이션은 선언에만 달 수 있기 때문에 return 문에는 @SuppressWarnings를 다는 게 불가능하다.

    그렇기에 그 대신 반환값을 담을 지역변수를 하 나 선언하고 그 변수에 애너테이션을 달아주라고 한다.

     

    @SuppressWarning("unchecked") T[] result = (T[]) Arrays.copyof(elements, a.getClass());
    return result;

     

    이런 식으로 사용하라는 것이다.

     

     

     

    참고

     

Designed by Tistory.