Java/live-study

백기선 라이브스터디 14주차 : 제네릭

jay Joon 2021. 2. 26. 02:41

학습할 것 (필수)

  • 제네릭 사용법
  • 제네릭 주요 개념 (바운디 드 타입, 와일드카드)
  • 제네릭 메서드 만들기
  • Erasure

제네릭 사용법

제네릭 사용법은 간단하다.

다이아몬드 연산자(<>)에 타입을 정의해주면 끝이다.

하지만 코드를 보다보면 ... 등 여러가지가 존재하는 것을 보았다.

해당 E,T,K ... 등은 무슨 기능이 있는건 아니다.

따라서 원하는 값을 넣어줘도 되긴 하지만

하지만 자바에는 제네릭 명명 규칙이 있는데 다음과 같다.

  • E : 요소(element)
  • K : 키(key)
  • N : 숫자(Number)
  • T : 타입(type)
  • V : 값(value)
  • S, U, V : 2,3,4번째 선언된 타입

해당 규칙은 코드를 이해하는데에 도움을 준다. 따라서 무조건은 아니지만 규칙이 있으니 따라주자.

제네릭 장점

제네릭으로 사용함으로써 개발자가 얻는 이점은 매우 많다.

  1. 컴파일타임때 타입추론을 통한 타입에러를 사전에 잡을 수 있다.
  2. 컴파일러가 타입추론을 통한 타입 캐스팅을 자동으로 해준다.
  3. 코드의 재사용성이 매우 높아진다.

위 3가지의 내용을 하나씩 살펴보자.

컴파일타임때 타입추론을 통한 타입에러를 사전에 잡을 수 있다.

해당 장점은 우리가 자주쓰는 List<E>를 통해 알아 볼 수 있다.

public interface List<E> extends Collection<E> {...}

List를 사용할때 우리는 다이아몬드 연산자(<>) 안에 어떠한 타입을 지정하고 사용한다.

List<String> stringList = new ArrayList<>();// 다이아몬드 연산자에 타입을 지정.

타입을 지정함으로써 컴파일러는 타입을 추론할수 있게된다.

따라서 List 요소는 String 인 객체만 들어올 수 있다.

image

해당 예제 처럼 String 타입 외 다른 타입이 들어온다면 개발자는 사전에 오류를 잡아낼 수 있다.

 

만약 타입을 지정하지 않고 로 타입(raw type)으로 사용시 컴파일러는 타입추론을 하지 못한다

 

따라서 리스트 내부에 어떠한 객체를 넣어도 개발자는 오류가 발생한걸 인지하지 못한다.

public static void main(String[] args) {
  List rawTypeList = new ArrayList(); //로 타입 리스트
  rawTypeList.add("String");
  rawTypeList.add(10);
}

이처럼 컴파일러는 제네릭을 통한 타입추론을 할 수 없기 때문에 해당 코드는 컴파일오류가 나지않는다.

 

그럼 뭐가 문제야?? 일단은 컴파일은된다는 거자나?? 라고 생각이 들 수 있다.

 

제네릭을 통한 타입추론이 가능해짐으로써 우리는 리스트를 믿고 사용할 수 있게된다.

 

즉 안정성을 보장할 수 있게된다는 뜻인데

 

만약 로 타입 리스트를 사용하게된다면 리스트 내부에 어떠한 객체가 있는지 알 수 없게된다.

이러한 문제점은 안정성이 없고 얼마든지 개발자의 실수로 인해 리스트내부에 다른 객체가 혼합하여 들어갈 수 있다.

컴파일러가 타입추론을 통한 타입 캐스팅을 자동으로 해준다.

해당 장점도 위와 연결되는 내용인데 바로 예제부터 살펴보자.

public static void main(String[] args) {
  List rawTypeList = new ArrayList();
  rawTypeList.add("String");
  rawTypeList.add(10);

  for (Object o : rawTypeList) {
    System.out.println(o);
  }
}

해당 예제는 매우 간단하다. 앞에서 말한것처럼

컴파일러는 제네릭을 보고 타입추론을 한다고 말하였다.

하지만 현재 예제의 리스트는 로 타입 리스트이기 때문에 컴파일러는 타입추론을하지 못한다.

따라서 for-each 문을 사용할때 Object 의 객체를 내보내게 된다.(어떠한 객체가 있는지 모르기 때문에)

이러한 문제점은 만약 리스트 내부에 서로다른 타입의 객체가 들어가 있으면

for (Object o : rawTypeList) {
  if(o instanceof String){
    System.out.println((String) o);
  }
  if(o instanceof Integer){
    System.out.println(o);
  }
}

일일이 비교를통한 타입캐스팅을 해줘야한다.

하지만 제네릭을 사용하게 된다면?

List<String> stringList = new ArrayList<>();
for (String s : stringList) {
  System.out.println(s);
}

다음과 같이 타입추론을 통한 자동 타입캐스팅을 컴파일러가 해준다.

앞서 살펴본 장점은 모두 제네릭이 제공해주는 안정성이 얼마나 편한지 알 수 있다.

코드의 재사용성이 매우 높아진다.

해당 내용은 먼저 예제부터 살펴보자.

public static void main(String[] args) {
  List<Integer> intList = new ArrayList<>(List.of(1,2,3,4,5));
  List<String> stringList = new ArrayList<>(List.of("a","b","c","d","f"));

  for (String s : stringList) {
    System.out.println(s);
  }

  for (Integer integer : intList) {
    System.out.println(integer);
  }
}

해당코드는 단순히 리스트를 순회하면서 콘솔에 찍는 행위만한다.

하지만 타입이 서로 다르기 때문에 어쩔 수 없이 반복하는 코드가 보인다.

해당 문제점은 제네릭 메소드를 만들어서 사용하면 해결 할 수 있다.

public class MainRunner {
  static <E> void ListForEach(List<E> list){
    for (E e : list) {
      System.out.println(e);
    }
  }
  public static void main(String[] args) {
    List<Integer> intList = new ArrayList<>(List.of(1,2,3,4,5));
    List<String> stringList = new ArrayList<>(List.of("a","b","c","d","f"));
    ListForEach(intList);
    ListForEach(stringList);
  }
}

다음과 같이 타입에 의존하지 않는 메서드를 만들어서 코드를 재활용 할 수 있다.

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

바운디드 타입(Bounded Generics)

간단하게 타입을 제한할 수 있는 제네릭이다.

만약 리스트 안에 Number 의 하위클래스만(자기포함)을 받고 싶다면 다음과 같이 해주면된다.

class List<E extends Number>{
 ...생략
}

이와 반대로 super 는 하위클래스가 아닌 상위클래스만(자기포함) 받고 싶을때 사용한다

와일드 카드(WildCard)

타입을 신경쓰지 않겠다. 라고 이해하면 될꺼 같다.

void Foo(List<?>list list){} // List 내부의 요소들을 Object 타입으로 취급하게됨

즉 타입을 신경쓰지 않으니 타입캐스팅을 해주지 않는다 (Object 타입으로 취급)

하지만 바운디드 타입과 같이 사용하게된다면?

void Foo(List<? extends Number > list){} // 내부의 요소를 Number 타입으로 취급하게됨 

Number 로 취급하게 된다.

와일드카드는 구체적인 데이터 타입이 필요하지 않을때 사용한다.

즉 구체적인 타입보단 각각의 데이터가 가지고 공통적인 부분 즉 추상적인 데이터를 필요할때 사용하는거 같다.

Erasure(소거)

Erasure 란 원소 타입을 컴파일 타임에만 검사하고 런타임에는 해당 정보를 알 수 없는 것이다.

즉 컴파일 타임에만 타입에 대한 제약조건을 적용하고 런타임에는 타입에 대한정보를 소거하는 행위다.

소거는 타입 파라미터가 unbounded 이면 타입파라미터를 object 타입으로 변경한다.

  1. Example<T> ->unbounded 임으로
  2. Example<Object>으로 변경
  1. Example<? extends Number> -> bounded임으로
  2. Example<Number> 으로 변경

소거는 제네릭을 적용할 수 있는 일반클래스, 인터페이스, 메서드에서만 발생하며.