[백기선님께서 주최하시는 Java 기초 스터디]
github.com/whiteship/live-study
이번주 과제는 위에 나와있듯이 제네릭이다
수행 해야하는 과제는 다음과 같다
타입스크립트의 문법과 자바의 문법이 유사한 점이 많고 제네릭에 대한 개념을 어렴풋이만 알고 있는데, 개인적으로 이번 주 과제를 수행함으로 인해서 자바 뿐 아니라 타입스크립트의 제네릭에 대한 개념도 같이 이해하는 데에 도움이 될 수 있음 좋겠다
제네릭(Generic)?
제네릭은 다양한 종류의 데이터를 처리할 수 있는 클래스와 메소드를 작성하는 기법이며, Java1.5 부터 추가되었다
클래스나 메소드를 정의할 때 내부에서 사용할 변수나 메소드 등의 타입을 구체적으로 명시하는 대신 T 등의 기호로 적어놓는 것이다
위의 그림에서 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 클래스를 확장한 클래스만 지정하여 객체화 될 수 있으며, 이 외의 타입을 사용해서 객체화 하면 빨간줄이 나타나는 것을 볼 수 있다
상속 받은 클래스의 타입 뿐 아니라, 특정 인터페이스를 구현한 클래스로 타입 매개변수를 제한할 수 있다
이제 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 타입을 매개변수로 받아서 사용할 수 있다
그러면 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 |