-
[이펙티브 자바 스터디] 제네릭언어/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;
이런 식으로 사용하라는 것이다.
참고
'언어 > JAVA' 카테고리의 다른 글
[이펙티브 자바 스터디] 직렬화 - 2 (1) 2024.06.19 [이펙티브 자바 스터디] 직렬화 (0) 2024.06.13 [이펙티브 자바 스터디] 제네릭 - 2 (1) 2024.06.07