effective java 5장 제네릭

·2024년 10월 24일

effectivejava

목록 보기
4/4

이번 장을 정리하기전에 제네릭이 무엇인지 정리해보기로했다.

제네릭이란?

타입을 일반화 하는것을 의미한다.
함수, 클래스를 만들 때 타입을 고정으로 선언하지 않고 사용시 명시해서 타입을 유연하게 해주는 것이다.

완전 타입, 로직적으로 서로 다른 타입을 다뤄야 되는데 공통화할 때 사용하면 된다.

public <T> void run(T t) {
    System.out.println(t);
}

Object를 사용 모든 타입을 받을 수있다 그렇게 하면 안될까??

사용하지 않아야한다라고 판단했다. why? 타입의 안정성을 보장할 수 없기때문에
이유는 Object를 사용하면 컴파일 시점에 모든 타입을 다 허용하여 유연할 수있지만 컴파일 타입에 타입 오류를 잡지못해서 런타임시 오류를 발생 할 수있다.
그래서 나의 생각은 Object를 사용하지 말아야한다고 생각한다.

Object를 사용하면 왜 런타임시 오류를 발생할까?
그건 바로 Object는 모든 타입을 받을 수 있으므로, 잘못된 형 변환으로 인해 런타임 오류가 발생할 위험있기 때문이다.

Object 사용 예시

ex1)
Object obj = "Hello";
Integer num = (Integer) obj; // ClassCastException 발생

ex2)
List list = new ArrayList();
list.add("Hello");
list.add(123); // 잘못된 타입 추가, 컴파일에서는 에러가 나지 않음

String s = (String) list.get(1); // 런타임에서 ClassCastException 발생

이런 부분들을 해결하기 위해 제네릭이 나온게 아닌가 싶다! +.+

제네릭을 사용하면 어떻게 해결된다는거지?

  • 제네릭을 사용하면 컴파일 시점에 타입이 고정되어 타입 안정성이 확보된다.
    제네릭의 타입 파마리터(T,E,K,V 등)로 선언하면 실제로 사용할 타입을 컴파일 시점에 지정한다.
    특정 타입으로 제한하는 효과를 가지고 모든 타입을 받지 못하게 된다.
  • 형변환이 필요가 없다.
    타입 캐스팅이 필요 없어 코드가 더 간결해진다.
제네릭 사용 예시

    ex1)
    List<String> list = new ArrayList<>();
    list.add("Hello");
    String s = list.get(0); // 형변환 불필요

    ex2)
    public class Box<T> {
    	
   	  private T content;

      public Box(T content) {
          this.content = content;
      }

      public T getContent() {
          return content;
      }
	}
    
    Box<String> stringBox = new Box<>("Hello");
	String content = stringBox.getContent(); // 안전하게 String으로 타입이 지정됨

여기까지 제네릭을 왜쓰고 어떻게쓰는지 정리했다.
하지만, 제네릭을 쓴다고 다 되는건 아니다.
마지막으로 단점을 알아보고 5장의 제네릭 정리 내용으로 넘어가야겠다.

제네릭의 단점

  • 타입 체크는 컴파일 시점에만 제공하고, 런타임 시점에는 안정성을 제공하지 못한다.
public abstract class Car {
    public abstract void go();
}

public class Niro extends Car {
    @Override
    public void go() {
        System.out.println("niro let`s go!! ");
    }

public class Avante extends Car {
    @Override
    public void go() {
        System.out.println("avante let`s go!! ");
    }
}

public void run() {
    List<Niro> niros = new ArrayList<>();
    niros.add(new Niro());
   // niros.add(new Niro()); -- 컴파일 에러발생!!!
    start(niros);
}

public <T extends Car> void start(List<T> cars) {
    cars.add((T) new Avante()); 
    for (T car : cars) {
        car.go();
    }
}

코드에서 보면 리스트에 niro 객체만 받을 수 있도록 선언하였다.
하지만 start메서드에서 제네릭 타입으로 받은 후 avante를 넣었을때 어떻게될까?
당연히 오류없이 리스트에 추가되고 아래와 같이 로그가 찍힌다.

niro let`s go!! 
avante let`s go!! 

단점까지 알아봤다.
아래는 이펙티브 자바5장 읽고 정리한 부분이다.

로타입을 사용하지 말자.

객체 타입의 안정성을 유지하며 개발해야한다.
제네릭 사용시 로타입을 사용하면 런타임시 오류가 발생 할 수있다.

비검사 경고를 제거하라

잠재적으로 오류가 발생하지 않도록 컴파일 타임에 타입 안정성을 보장해야한다.

배열보다는 리스트를 사용하라

배열은 런타임에 타입 오류가 발생 할 수있다.
리스트를 사용하면 컴파일 타임에 타입 안정성을 보장 할 수있기때문에 리스트를 사용하는 것이 더 안전한 선택이다.

배열은 공변이라 런타임 시점에 타입불일치로 오류가 발생할수있지만 리스트는 불공변(상속불가)이기때문에 컴파일시점에 타입 불일치가 발생하면 컴파일 오류로 잡아낼 수 있다.

즉 제네릭도 불공변이기때문에 리스트 사용을 권장하는것이다.

그렇다면 모든 열거형 유형에 있는 메서드가 배열을 반환하는 이유는 무엇일까요 ?
이 메서드의 가장 일반적인 용도는 반환 값의 요소를 반복하는 것이고, 어떤 구현 values보다 배열을 반복하는 것이 훨씬 저렴하기 때문입니다 .
이 구문을 사용하면 반복하는 코드는 어느 쪽이든 동일합니다.List for-each

이왕이면 제네릭 타입으로 만들라

타입 안정성과 재사용성을 함께 고려해서 설계해야한다.

코드에 컴파일러한테 더많은 정보를 알려주면 컴파일러가 실수할수있는걸 더 많이 잡아준다.

이왕이면 제네릭 메서드로 만들라

타입 안정성과 코드의 재사용성을 활용해라.
경고를 없애려면 메서드를 타입 안전하게 만들어야한다.

제네릭은 런타임에 타입 정보가 소거되므로 하나의 객체를 어떤 타입으로든 매개변수화 할 수있다.

한정적 와일드카드를 사용해 API 유연성을 높이라

제네릭을 사용하면 타입 안정성은 보장되지만 유연성이 떨어질 수 있으므로, 와일드 카드를 사용하여 코드의 유연성을 높여라

제네릭과 가변인수를 함께 쓸 때는 신중하라

제네릭과 가변인수를 함께 사용할거면 타입 안정성 문제를 유발할 수있으므로 방지할 수있도록 설계해야한다.

가변인수 메서드를 호출하게 되면 가변 인수를 담기 위한 배열이 자동으로 하나 만들어진다.

제네릭과 가변인수를 함께 사용할 때, 가변인수가 배열로 변환되기 때문에 공변성 문제로 인해 타입 안전성이 깨질 수 있습니다. 이는 배열의 공변성 특성과 제네릭의 불공변성 사이의 충돌 때문입니다.

배열은 공변적이어서 잘못된 타입의 값을 허용할 수있다.

타입 안전 이종 컨테이너를 고려하라

타입 안정성을 유지하면서도 다양한 타입을 다룰 수 있도록 유연하게 설계하라는 의미입니다.


타입 안전 이종 컨테이너란?
제네릭을 사용하여 여러 다른 타입의 객체를 안전하게 저장하고 관리할 수 있는 컨테이너를 의미합니다.

5장은 제네릭을 잘 활용하여 타입의 안정성과 재사용성 그리고 코드의 유연성에 대해 설명하고있다. 제네릭을 알고 사용하는것과 모르고 사용하는 것은 큰 차이가 있다고 한다.

0개의 댓글