열거 자료형은 고정 개수의 상수들로 값이 구성되는 자료형이다. 자바에는 enum이라는 자료형이 도입되기 전에는 아래와 같이 통상 int 형의 상수를 정의해서 열거 자료형을 흉내냈다.

 

 이 기법은 INT ENUM 패턴으로 알려져 있는데, 단점이 많다. int 형의 값으로 상수를 비교하기 때문에 int형 값이 중복되는 경우 제대로 된 비교가 되지 않는다. 또한 이러한 상수는 출력하거나 디버거로 봤을 경우 의미없는 숫자로만 보인다. 또한 int enum 깨지기 쉬운데 그 이유는 상수는 컴파일 시점 상수이기 떄문에 상수를 사용하는 클라이언트 코드와 함께 컴파일 된다. 상수의 int 값이 변경되면 클라이언트도 다시 컴파일해야 한다.


 자바의 enum 자료형 이면에 감춰진 기본적 아이디어는 단순하다. 열거 상수별로 하나의 객체를 public static final 필드 형태로 제공하는 것이다. enum 자료형으로 새로운 객체를 생성하거나 계승을 통해 확장할 수 없기 때문에, 이미 선언된 enum 상수 이외의 객체는 사용할 수 없다. 즉 enum 자료형의 개체수는 엄격이 통제된다는 것이다. 

 

 또한 enum 자료형은 컴파일 시점 형 안전성을 제공한다. Apple형의 인자를 받는다고 선언한 메서드는 반드시 Apple 값 중 하나의 값만 받을 수 있다. 엉뚱한 자료형의 값을 인자로 전달하려 하면, 자료형 불일치 때문에 컴파일 할 떄 오류가 발생한ㄷ.ㅏ 

 

 enum 자료형은 같은 이름으 ㅣ상수가 평화롭게 공존할 수 있도록 한다. 이름 공간이 분리되기 떄문이다. 상수를 추가하거나 순서를 변경해도 클라이언트는 다시 컴파일할 필요가 없다. 상수를 제공하는 필드가 enum 자료형과 클라이언트 사이에서 격리 계층 구실을 하기 때문이다. 또한 enum 자료형은 toString 메서드를 통해 인쇄 가능 문자열로 쉽게 변환할 수 있다. 


 enum 자료형은 임의의 메서드나 필드도 추가할 수 있도록 한다. 임의의 인터페이스를 구현할 수도 있다. 그렇다면 enum 자료형에 메서드나 필드를 추가하는 이유는 무엇일까? 상수에 데이터를 연관시키면 좋겠다는 생각이 들어서인데, enum 자료형은 상수 묶음에서 출발해서 점차로 완전한 기능을 갖춘 추상화 단위로 진화해 나갈 수 있다. 


 일반적으로 유용하게 쓰일 enum이라면, 최상위 클래스로 선언해야 한다. 특정한 최상위 클래스에서만 쓰이는 enum이라면 해당 클래스의 멤버 클래스로 선언해야 한다. 


요약

enum 자료형은 int 상수에 비해 많은 중요한 장점을 가지고 있다. enum을 사용한 코드는 가독성도 높고, 안전하며, 더 강력하다. 상당수의 enum은 생성자나 멤버가 필요없으나, 데이터 또는 그 데이터에 관계된 메서드를 추가해서 기능을 향상 시킬수도 있다. 또한 동일한 메서드가 상수별로 다르게 동작하도록 만들어야 하는 enum도 구현 가능하다. 


제네릭은 Set이나 Map 같은 컬렉션등과 같이 하나의 원소만을 담는 컨테이너에 가장 많이 쓰인다. 따라서 컨테이너별로 형인자의 수는 고정된다.  하지만 좀 더 유연한 방법이 필요하다면 어떻게 해야할까? 컨테이너 대신 키에 형인자를 지정하여 사용하는 방법이 있다. 컨테이너에 값을 넣거나 뺄 때마다 키를 제공하는 것이다. 예를 들어 임의 클래스 객체 가운데 맘에 드는 것을 골라 저장하고 꺼낼 수 있는 Favorites 클래스를 만든다고 하자. 인자화 된 키 구실은 Class 객체가 할 것이다. 컴파일 시간 자료형이나 실행시간 자료형 정보들 메서드들에 전달할 목적으로 class 리터럴을 이용하는 경우, 그런 class 리터럴을 자료형 토큰이라 부른다. 아래는 Favorites 클래스의 API이다.

 위의 Favoites 클래스는 형인자가 지정되는 곳이 키라는 것만 제외하면 간단한 맵같아 보인다. 하지만 일반적인 맵과 달리 모든 키의 자료형이 서로 다르다. 따라서 Favorites와 같은 클래스를 형 안전 다형성 컨테이너라 부른다. 

 Favorites 맵의 값 자료형이 Object다. 다시 말해, 이 맵은 키와 값 사이의 자료형 관계가 일치되도록, 그러니까 값의 자료형이 키가 나타내는 자료형이 되도록 보장하지 않는다. 하지만 자바에서는 자료형 시스템은 그런 관계까지 표현할 정도로 강력하지 않다. 이러한 구현은 putFavorite 메소드에서 담당하며 case메소드를 이용하여 구현한다. 


요약

컬렉션 API를 통해 확인할 수 있는 제네릭의 일반적인 용법에 따르면, 컨테이너별로 형인자 개수는 고정되어 있다. 그런데 컨테이너 대신 키를 제네릭으로 만들면 그런 제약이 없는 형 안전 다형성 컨테이너를 만들 수 있다. 그런 컨테이너는 Class 객체를 ㄱ키로 쓰는데, 그런 Class 객체를 자료형 토큰이라 부른다. 


형인자 자료형은 불변 자료형이다. 불변 자료형이란 Type1과 Type2가 있을 때, List<Type1>과 List<Type2> 사이에는 어떤 상위-하위 자료형 관계도 성립할 수 없다는 것이다. 그러나 때로는 불변 자료형보다 높은 유연성이 필요할 때가 있다.


 Stack이란 클래스안에 pushAll이란 메소드를 추가한다고 생각해보자. 얼핏 보기에는 문제가 없어보이고, 스택의 자료형과 Iteable src가 가리키는 자료형이 일치하면 정상적으로 동작한다. 하지만 아래의 코드를 보면 오류를 발생시킨다.

제네릭이 불변자료형이란걸 생각해보면 위의 코드가 왜 오류를 발생시키는 지 알것이다. 이러한 문제를 해결하기위해 자바에서는 와일드카드 자자료형이라는 특별한 형인자 자료형을 제공한다. 위 코드의 문제점을 해결하려면 pushAll의 인자 자료형을 "E의 Iterable"이 아니라 "E의 하위 자료형의 Iterable" 이라고 명시할 방법이 필요하다. 즉 Iterable<? extends E>라고 인자 자료형을 바꾸면 된다. 그렇다면 이 상황과 반대의 상황을 보자.


위의 예제역시 앞선 예제와 비슷한 오류가 발생한다. 이와 같은 문제를 해결하기 위해서는 popAll의 인자 자료형을 "E의 컬렉션"이 아니라 "E의 상위 자료형의 컬렉션"이라고 명시해야 한다. 즉 Collection<? super E>를 인자 자료형으로 주면된다.


 위의 두 상황을 토대로 생각해보면, 유연성을 최대화하려면, 객체 생산자나 소비자 구실을 하는 메서드 인자의 자료형은 와일드 카드 자료형으로 하라는 것이다. PECS(Produce - Extends , Consumer - Super) 약어를 기억하자. 즉 인자가 T 생상자라면 <? extends T>라고 하고, T 소비자라면 <? super T>라고 하라는 것이다. 하지만 반환값에는 와일드카드 자료형을 쓰면 안된다.

와일드 카드는 왜 쓰는 것이지? 와일드 카드 자료형은 받아야 할 자료형은 받을 수 있도록 해주고, 거부해야 할 자료형의 인자는 거부할 수 있도록 해주기 때문에 사용한다.



메서드 역시 제네릭화로 많은 혜택을 받을 수 있다. 특히 static 유틸리티 메서드는 제네릭화하기 좋은 메서드이다.  두 집합의 합집합을 반환하는 메서드를 제네릭화 하면 만들어보자. 

위와 같이 형인자를 선언하는 형인자 목록은 메서드의 수정자와 반환값 자료형 사이에 둔다. 위의 메서드를 자세히 보면 입력과 반환값 자료형이 전부 같다는 것이다. 정적 와일드카드 자료형을 사용하면 좀 더 유연한 메서드를 구현할 수 있다.

그렇다면 제네릭 메서드의 이점은 무엇일까? 제네릭 메서드는 제네릭 생성자를 호출할 때는 명시적으로 주어야 했던 형인자를 전달할 필요가 없다는 것이다. 컴파일러는 메서드에 전해진 인자의 자료형을 보고 형인자의 값을 유추해낸다. 메서드를 이용한 객체생성을 하려면 아래와 같이 제네릭 정적 팩터리 메서드를 만들면 된다.


요약

제네릭 자료형이나 마찬가지로 제네릭 메서드는 클라이언트가 직접 입력 값과 반환값의 자료형을 형변환해야 하는 메서드보다 사용하기 쉽고 형 안전성도 높다. 


클래스를 제네릭화하는 첫 번째 단계는 선언부에 형인자를 추가하는 것이다.



 위와 같은 클래스를 만들면 아마 elements = new E[DEFAULT_INITIAL_CAPACITY]; 구분에서 오류가 발생할 것이다. 이는 e 같은 실체화 불가능 자료형으로는 배열을 생성할 수 없기 때문이다. 해결책은 두가지다.
 1. 제네릭 배열을 만들 수 없기 떄문에 우회하는 방법. oBJECT의 배열을 만들어서 제네릭 배열 자료형으로 변환하는 것이다. 이 또한 오류는 없지만 경고메세지가 출력되게 된다. 프로그래머가 형 안전성이 보장된다는 확신하에 @SuppressWarnings("unchecked") 어노테이션을 사용한다.
 2. elements의 자료형을 E[]에서 Object[]로 바꾸는 것이다.  E result = elements[--size] 를 E result = (E) elements(--size)로 바꾸면 오류메세지는 사라지지만 경고메세지가 나타난다. 이 또한 프로그래머가 형변환 안전성이 보장된다는 판단하에 @SuppressWarning("unchecked") 어노테이션으로 처리한다.

 제네릭 배열 생성 오류를 피하는 두 방법 가운데 어떤 것을 쓸지는 대체로 취향 문제다. 그리고 형진자를 제한하는 제네릭 자료형도 있다.
 인자 목록(<E extends Delayed>)을 보면, 실 형인자 E는 반드시 Delayed의 하위 자료형이어야 한다. 따라서 DelayQueue를 구현 할 때나 사용할 때, DelayQueue에 저장되는 원소들을 명시적으로 Delayed 자형으로 변환하거나 ClassCaseException이 발생할 위험을 무릎쓰지 않아도 된다.


요약

제네릭 자료형은 클라이언트가 형변환을 해야만 사용할 수 있는 자료형보다 안전할 뿐 아니라 사용하기도 쉽다. 새롱누 자료형을 설계할 때는 형번환 없이도 사용할 수 있도록 하라. 


배열은 제네릭 자료형과 두 가지 중요한 차이점을 갖고 있다. 

 첫 째는 배열은 공변 자료형이라는 것이다. 공변자료형이란 Sub가 Super의 하위자료형이랑 Sub[]도 Super[]의 하위자료형 이라는 것이다. 반면 제네릭은 불변 자료형이다. Type1과 Type2가 있을 때 List<Type1>과 List<Type2>는 서로 상위 자료형이나 하위자료형이 될 수 없다.

 먼저 위의 코드는 배열은 공변자료형이기 때문에 Long이 Object의 하위자료형이기 때문에 Long[]도 Object[]의 하위자료형이다. 따라서 컴파일단계에서 데이터를 저장하는데는 문제가 없지만, 실행단계에서 오류가 발생하게 된다. 아래의 코드는 제네릭은 불변자료형이라서 Long이 Object의 하위자료형 이더라도, List<Long>은 List<Object>의 하위자료형이 아니기때문에 컴파일단계에서 오류를 잡아내게 된다.

 두 번째 차이는 배열은 실체화되는 자료형이다. 즉 배열의 각 원소의 자료형은 실행할 때 결정된다는 것이다. 반면 제네릭은 컴파일 시점에만 자료형에 대한 조건들이 적용되고, 실제 실행할 때는 자료형에 대한 정보가 사라진다. 자료형 삭제덕에, 제네릭 자료형은 제네릭을 사용하지 않고 작성된 오래된 코드와도 문제없이 연동된다.


 이러한 기본적인 차이점 때문에 배열과 제네릭은 섞어 쓰기 어렵다. 예를 들어, 제네릭 자료형(List<E>)나 형인자 자료형(List<String>), 또는 형인자의 배열(E[])을 생성하는 것은 문법적으로 허용되지 않는다. 왜 허용되지 않을까? 형 안전성이 보장되지 않기 떄문이다. 문법이 허용된다면 컴파일러가 자동으로 만드는 형변환 구문(cast)들은 실행 도중에 ClassCastException 예외를 만들고 말았을 것이다.  


(1) 은 원래 오류구문이지만, 정상적으로 컴파일 된다고 가정하자. 
(3) 은 List<String>배열을 Object[]에 대입한다. 배열은 공변자료형이므로 가능하다. 
(4) 에서는 List<Integer>를 Object 배열에 있는 유일한 원소에 대입한다. List<String>[] 에다가 List<Integer>를 대입하는 형태인데, 제네릭이 실행 시 형 삭제가 되기 떄문에 List[] = List 와 같은 형태가 되기때문에 가능하다.
(5) stringLists안에 있는 데이터를 꺼내는 작업을 하고있는데, 컴파일러는 자동적으로 String으로 형변환을 하려고 할 것이다. 사실은 Integer데이터인데 !! 그렇기 때문에 ClassCastException이 발생하고 말것이다.

 이러한 문제를 방지하기 위해서 처음부터 문법적으로 제네릭배열을 막는것이다.


요약

제네릭과 배열이 따르는 자료형 규칙은 서로 다르다. 배열은 공변 자료형이자 실체화 가능 자료형이다. 제네릭은 불변 자료형이며, 실행 시간에 형인자의 정보는 삭제된다. 따라서 배열은 컴파일 시간에 형 안전성을 보장하지 못하며, 제네릭은 그 반대다. 일반적으로 배열과 제네릭은 쉽게 혼용할 수 없다. 만일 배열과 제네릭을 뒤섞어 쓰다가 컴파일 오류나 경고 메세지를 만나게 되면, 배열을 리스트로 바꿔야겠다는 생각이 들어야 한다.


제네릭으로 프로그램하다 보면 많은 컴파일러 경고 메시지를 보게 된다. 모든 무점검 경고는 가능하다면 없애야 한다. 그런 경고를 모두 없애고 나면 코드의 형 안전성이 보장된다. 제거할 수 없는 경고 메시지는 형 안전성이 확실할 때만 @SupressWarnings("unchecked") 어노테이션을 사용해 억제해야 한다. 

SupressWarning 어노테이션은 지역 변수 선언부터 클래스 전체에까지, 어떤 크기으 ㅣ단위에도 적용할 수 있따. 하지만 가능한 한 작은 범위에 적용해야 한다. @SubpressWarnings("unchecked") 어노테이션을 사용할 때마다, 왜 형 안전성을 위반하지 않는지 밝히는 주석을 붙이자.

요약

무점검 경고는 중요하고, 무시해서는 안된다. 모든 무점검 경고는 프로그램 실행 도중에 ClassCastException이 발생할 가능성을 나타낸다. 이러한 경고는 제거하고, 제거할 수 없는 경우에는 형 안전성을 보장하는 사실을 입증하여서 @SuppressWarnings("unchecked") 어노테이션을 사용해서 경고를 제거하라.


 제네릭을 도입하기 전, 프로그래머는 컬렉션에서 객체를 읽어낼 때마다 형변환을 해야 했다. 하지만 제네릭을 사용하면 컬렉션에 넣는 객체의 자료형이 무엇인지 컴파일러에게 알릴 수 있으므로, 형변환 코드는 컴파일러가 알아서 넣어준다. 잘못된 자료형의 객체를 컬렉션에 넣으려는 시도도 컴파일단계에서 발견하게 된다.


선언부에 형인자(<String>)가 포함된 클래스나 인터페이스는 제네릭 클래스나 인터페이스라고 부른다. 그러나 제네릭 자료형을 형인자 없이 사용할 수도 있다. 이러한 무인자 자료형을 쓰면 형 안전성이 사라지고(아무 자료형을 다 넣을 수 있음) , 제네릭의 장점중 하나인 표현력 측면에서 손해를 보게된다.


 무인자 자료형을 사용하면 안되지만 List<Object>와 같은 자료형은 써도 좋다. List 와 List<Object>의 차이점은 무엇일까? List는 형 검사 절차를 완전히 생략한 것이고, List<Object>는 아무 객체나 넣을 수 있다는 것을 컴파일러에게 알리는 것이다. 코드로 살펴보자.

 
 위의 코드는 컴파일 단계에서 오류는 발생하지 않으나, 실행시켜보면 ClassCastingException이 발생할 것이다. 왜 일까? 이유는 strings에는 String객체만 담을 수 있도록 되어있는데 unsafeAdd메서드에는 무인자 자료형인 List로 인자를 받는다. 인자로 받은 리스트에 Integer객체를 담았다. 그러나 String s = strings.get(0) 을 실행할 때 컴파일러는 String으로 자료형을 변환하려 할 것인데, 실제 인자로 저장되어있는 값은 Integer이기 때문에 에러가 발생한다. 
 이러한 문제점때문에 무인자 자료형을 사용하면 안된다. 그래서 비한정적 와일드카드 자료형이라는 좀 더 안전한 대안을 제공한다. 제네릭 자료형을 쓰고 싶으나 실제 형 인자가 무엇인지 모르거나 신경 쓰고 싶지 않을 때는 형 인자로 "?"를 사용하면 된다.

 이러한 비한정적 와일드카드 자료형에는 null이외의 어떠한 원소도 넣을 수 없으므로, 불변식을 지켜준다. 


 새로 만드는 코드에는 무인자 자료형을 쓰면 안된다고 하였지만, 예외 2가지가 있다. 첫 번째는 클래스 리터럴에는 반드시 무인자 자료형을 사용해야 한다. 자바 표준에 의하면 클래스 리터럴에는 형인자 자료형을 쓸수 없다.(배열 자료형이나 기본 자료형은 쓸수 있다.) 

 두 번째 예외는 instanceof 연산자 사용 규칙에 관한 것이다. 제네릭 자료형 정보는 프로그램이 실행될 때는 지워지기 때문에 instanceof 연산자는 비한정적 와일드카드 자료형 이외의 형인자 자료형에 적용할 수 없다. 그래서 다음과 같이 하는 것이 좋다.

위와 같이 o가 Set객체라는 것이 확실해진 다음에는 와일드카드 자료형 Set<?>로 형변환 해야하는 것에 주의하자.

요약

무인자 자료형을 쓰면 프로그램 실행 도중에 얘외가 발생할 수 있으므로 새로 만드는 코드에는 무인자 자료형을 쓰지 말아야 한다. Set<Object>는 어떤 자료형의 객체도 담을 수 있는 집합의 형인자 자료형이며, Set<?>는 모종의 자료형 객체만을 담을 수 있는 집합을 표현하는 와일드카드 자료형이다 ( add가 안되는? 모종의 자료형 객체가 무슨말이야..). Set은 무인자 자료형으로, 제네릭 자료형 시스템의 일부가 아니다. Set<Object>와 Set<?>은 안전하지만, Set은 안전하지 못하다.



+ Recent posts