공부/whiteship-java

14주차: 제네릭

chulphan 2021. 2. 27. 13:48

[백기선님께서 주최하시는 Java 기초 스터디]

 

github.com/whiteship/live-study

 

14주차 과제: 제네릭 · Issue #14 · whiteship/live-study

목표 자바의 제네릭에 대해 학습하세요. 학습할 것 (필수) 제네릭 사용법 제네릭 주요 개념 (바운디드 타입, 와일드 카드) 제네릭 메소드 만들기 Erasure 마감일시 2021년 2월 27일 토요일 오후 1시까

github.com

 

이번주 과제는 위에 나와있듯이 제네릭이다

 

수행 해야하는 과제는 다음과 같다

 

 

타입스크립트의 문법과 자바의 문법이 유사한 점이 많고 제네릭에 대한 개념을 어렴풋이만 알고 있는데, 개인적으로 이번 주 과제를 수행함으로 인해서 자바 뿐 아니라 타입스크립트의 제네릭에 대한 개념도 같이 이해하는 데에 도움이 될 수 있음 좋겠다

 

제네릭(Generic)?

제네릭은 다양한 종류의 데이터를 처리할 수 있는 클래스와 메소드를 작성하는 기법이며, Java1.5 부터 추가되었다

클래스나 메소드를 정의할 때 내부에서 사용할 변수나 메소드 등의 타입을 구체적으로 명시하는 대신 T 등의 기호로 적어놓는 것이다

 

java 의 ArrayList 클래스의 선언부

위의 그림에서 ArrayList 클래스의 꺽쇠 안에 E 가 명시되어 있는데, 클래스를 정의할 때에는 구체적인 타입을 명시해놓지 않고

우리가 객체를 생성할 때에 구체적인 타입을 명시해서 사용하는 방법이다

 

▶ 제네릭 사용법

제네릭의 사용방법은 그리 어렵지 않다.

 

객체를 생성할 때에 해당 객체에서 사용할 타입을 지정해주면 된다

 

이제 변수 list 는 Integer 타입의 원소들을 가지는 ArrayList 임을 알 수 있다

 

※ 참고로, <> 내부에는 자바의 Primitive 타입 즉, int, double 등을 명시할 수 없다. Reference 타입만 명시해줄 수 있다

 

■ 제네릭이 없었을 때에는....

Java 1.5 전에는 제네릭이 도입되지 않았다고 한다. 제네릭이 없었을 때에는 어떻게 제네릭처럼 코드를 작성했을까??

 

하나의 데이터를 저장할 수 있는 Box 클래스를 아래와 같이 정의했다

 

item 이라는 필드 타입, setItem의 매개변수 타입, getItem 의 반환타입은 모두 Object 로 통일해주었다

 

이제 이 클래스에는 Object 타입을 비롯한 모든 자손 클래스의 타입으로 지정하여 사용할 수 있다

 

여기까지 box 객체는 모든 타입에 대해서 잘 동작하는 것 처럼 보이지만, 객체에 있는 필드와 어떠한 작업을 해줄 때에는 항상 형변환(Type casting) 을 해주어야 한다.

 

또, Object 로 되어있기 때문에, 현재 객체가 어떠한 값을 가지고 있는지 항상 신경 쓰고 있어야 하고 버그가 발생했을 때 찾기도 힘들 것이다

 

한 달 후의 나는 잘 작동할 것이라 생각하고 코드를 실행시켰지만...

 

위와 같은 에러를 발견하고 식은땀을 흘릴 것이다

 

이제 제네릭이 위와 같은 상황을 피하게 해줄 것이다. Box 클래스를 제네릭을 통해 정의해보자

 

여기서 Box<T> 에 있는 T 는 타입 매개변수(type parameter) 라고 하며 위에서 봤듯이 객체 생성 시에 타입을 넘겨주면 넘겨 받은 타입으로 이 클래스의 타입이 지정된다

 

이제 이 제네릭 클래스를 가지고 객체화를 하여 사용해보자

이젠 컴파일 하기 전에 뭔가 문제가 있음을 바로 알 수 있게 되었다!

 

이제 box1 변수와 box2 변수의 타입을 정확하게 알 수 있게 됐고 Object를 썼을 때 처럼 형변환에 신경 쓸 필요도 없게 되었으니 

위에 처럼 어처구니 없는 실수를 해서 식은땀 흘리는 일도 없을 것이다

 

그리고 한 가지 좋은 점은 해당 클래스의 유연성이 증가해서 재사용성이 좋아졌다고 생각한다.

만약에 Box 라는 클래스가 Integer 라는 클래스만 가질 수 있었다면, 같은 작업을 하지만 타입이 다른 String 에 대한 클래스도 만들어야 할 것이다

 

■ 타입 매개변수의 표기

제네릭 클래스는 여러 개의 타입 매개변수를 가질 수 있지만 타입의 이름은 클래스나 인터페이스 안에서 유일해야한다

편의에 의해 하나의 대문자로 표기하며 변수의 이름과 타입의 이름을 구분할 수 있게 하기 위함이다

일반적으로 사용하는 이름은 다음과 같다

 

  • E: Element (원소)
  • K: Key
  • N: Number
  • T: Type
  • V: Value
  • S, U, V, 등: 2번째 3번째, 4번째 타입 등등..

 

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

■ 바운디드 타입(Bounded Type)

바운디드 타입은 타입 매개변수로 전달되는 타입을 제한하고 싶을 때 사용하며 extends 키워드를 사용한다

 

예를 들어, 위에서 정의한 Box 클래스를 String 타입에만 제한되게 사용하고 싶다고 가정하면 다음과 같이 정의할 수 있다

이제 Box 클래스는 String 타입 또는 String 클래스를 확장한 클래스만 지정하여 객체화 될 수 있으며, 이 외의 타입을 사용해서 객체화 하면 빨간줄이 나타나는 것을 볼 수 있다

 

 

상속 받은 클래스의 타입 뿐 아니라, 특정 인터페이스를 구현한 클래스로 타입 매개변수를 제한할 수 있다

T 는 Comparable 을 구현한 클래스로 제한된다

이제 Box 클래스가 가질 수 있는 타입은 Comparable 인터페이스를 구현한 모든 클래스가 될 것이다

 

예를 들어 중간고사 중 꼴찌의 점수를 알아보기 위한 코드를 작성해보자

 

 

여기서 T 에는 Comparable 인터페이스를 구현하지 않은 클래스가 전달될 수도 있기 때문에 compareTo 메소드를 사용하지 못한다

만약에 우리가 Comparable 을 구현한 클래스만 타입 매개변수로 전달될 수 있도록 제한을 해준다면 위 메소드를 이용하면서 

우리가 구현하고자 하는 코드를 구현할 수 있을 것이다

 

 

※ Raw 타입

제네릭 클래스를 다음과 같이 아무 타입도 명시하지 않고 객체화를 하는 것을 Raw 타입이라고 한다

Box box = new Box(); // 여기서 Box 는 제네릭 클래스

이 Raw 타입은 JDK5 이전에 제네릭이 없었기 때문에 호환을 위해 사용했다

하지만 Raw 타입은 쓰지 않는게 좋다. 왜냐하면 사용하려면 항상 형변환을 해줘야 하기 때문에 제네릭의 장점을 잃어버린다

 

■ 와일드 카드(Wild Card)

와일드 카드는 어떠한 타입이든지 표시할 수 있음을 나타내며 ? 를 사용한다.

매개변수, 필드, 지역변수의 타입을 나타내는 데에 사용된다

 

이미 제네릭을 통해서 (T) 타입을 나타낼 수 있는데 이런게 왜 필요한지 알아보자

 

■ 제네릭과 상속

먼저 Integer 클래스와 Double 클래스는 Number 클래스를 슈퍼 클래스로 가지고 확장한다

그리고 다형성을 통해서 각각을 모두 사용할 수 있다

 

Number intNumber = new Integer(10);
Number doubleNumber = new Double(99.9);

Number, Integer, Double 의 상속관계

 

제네릭도 마찬가지로, Number 타입으로 객체를 생성했으면 Integer 타입과 Double 타입을 매개변수로 받아서 사용할 수 있다

 

Java9 부터 위와 같이 값을 Boxing 하는 것은 Deprecate 됐다

그러면 StudentScore<Number> 를 제네릭 클래스의 타입으로 명시해놓고 StudentScore<Integer> 와 StudentScore<Double> 을 사용할 수 있을까?? 즉, 아래와 같은 관계가 성립을 할까??

 

 

결론은 성립하지 않는다

 

 

왜냐하면 Integer 클래스와 Double 클래스는 Number 클래스의 서브 클래스인 반면에 StudentScore<Integer> 클래스는 StudentScore<Number> 의 서브 클래스가 아니기 때문이다 (Object 를 부모 클래스로 가진다)

 

그럼 위 그림과 같은 관계를 만들 수는 없을까??....

 

있다. 그러기 위해서 와일드 카드를 사용해야 한다

 

■ 다시 와일드 카드

와일드 카드에는 3가지 종류가 있다

  • 상한이 있는 와일드 카드
  • 하한이 있는 와일드 카드
  • 제한이 없는 와일드 카드

먼저 상한이 있는 와일드 카드 부터 알아보자

 

♣ 상한이 있는 와일드 카드(Upper Bounded Wild Card)

어떤 클래스의 자손 클래스들을 와일드 카드로 나타내는 방법은 <? extends A> 이다.

전체 타입을 나타내는 것이 아니라 상한이 있는 타입을 표시하는 데에 사용된다

 

위의 예제에서 StudentScore<Integer>, StudentScore<Double>, StudentScore<Number> (또는 Number 의 서브 클래스들 모두) 를 표시하는 와일드 카드는 다음과 같이 작성할 수 있다

 

이제 이 메소드의 매개변수는 StudentScore<Number> 에서 Number의 모든 서브 클래스를 받아낼 수 있게 됐다 (StudentScore<Integer>, StudentScore<Double>, StudentScore<Float> 등)

 

♣ 하한이 있는 와일드 카드(Lower Bounded Wild Card)

어떤 클래스의 조상 클래스들을 와일드 카드로 표현하는 방법은 <? super A> 이다

 

예를 들어 StudentCard<Integer> 로 어떤 작업을 하는 메소드를 정의 할 때에 유연성을 주기 위해서 StudentCard<Number> 와 StudentCard<Object> 타입의 매개변수도 받으려고 한다

 

이럴 때에는 StudentCard<? super Integer> 를 명시해주면 가능하다

 

이제 이 printScore 라는 메소드는 StudentScore<Integer>, StudentScore<Number>, StudentScore<Object> 타입의 매개변수만 받을 수 있다

 

♣ 제한이 없는 와일드 카드(Unbounded Wild Card)

모든 타입에 매치되게 하기 위해서는 ? 로 표시한다

 

예를 들어서 StudentScore<?> 는 모든 StudentScore<타입> 에 대해서 매치 됨을 의미한다

즉, StudentScore<?> 는 StudentScore<Integer>, StudentScore<String>, StudentScore<Double> 등등을 모두 받을 수 있다

다른 말로 하면, StudentScore<A> 는 항상 StudentScore<?> 의 서브 타입이 된다

 

제네릭 클래스의 상속관계

 

 

▶ 제네릭 메소드 만들기

제네릭 클래스에서 뿐 아니라 일반 클래스에서도 제네릭 메소드를 정의할 수 있다

 

이 때 타입 매개변수의 범위는 메소드 내부로 정의된다. 즉, 메소드 내부에서만 타입 매개변수를 사용할 수 있다

 

제네릭 메소드를 정의하는 방법은 다음과 같다

 

보시다시피 일반 클래스 내부에 제네릭 메소드를 정의할 수 있다

 

printMinScore 는 매개변수로 Comparable 을 구현한 클래스 타입만을 받는다는 것을 의미한다

 

 

만약에 원한다면 자료형을 명시해서 사용해줄 수도 있다

하지만 딱히 의미가 있어보이지는 않는다.

 

▶ Erasure

타입 Erasure 는 제네릭 매개변수를 실제 클래스나 bridge 메소드로 대체하는 처리를 말한다

또, 컴파일러가 추가적인 클래스를 생성하지 않고 런타임 오버헤드가 없음을 보장한다

 

규칙:

  • 만약에 경계 타입 매개변수가 사용됐다면 제네릭 타입의 타입 매개변수를 그 타입의 경계로 대체한다
  • 만약에 경계가 없는 타입 매개변수가 사용 됐다면 제네릭 타입의 타입 매개변수를 Object 로 대체한다
  • 타입에 대한 안전을 보존하기 위해 형변환을 삽입한다
  • 브릿지 메소드를 생성해서 확장된 제네릭 타입의 다형성을 보장한다

 

여기서 경계 타입과 경계가 없는 타입에 대한 설명은 과제 중 바운디드 타입에 연관되어 있다

 

 

 

 

[참고]

어서와 Java는 처음이지! JDK8로 배우는 자바 프로그래밍 천인국 지음 | 인피니티북스 

www.tutorialspoint.com/java_generics/java_generics_method_erasure.htm

'공부 > whiteship-java' 카테고리의 다른 글

15주차: 람다식  (0) 2021.03.06
13주차: I/O  (0) 2021.02.25
12주차: 애노테이션  (0) 2021.02.06
11주차: enum  (0) 2021.01.30
10주차: 멀티쓰레드 프로그래밍  (0) 2021.01.22