제네릭은 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은 안전하지 못하다.



중첩 클래스는 다른 클래스 안에 정의된 클래스다. 중첩 클래스는 해당 클래스가 속한 클래스 안에서만 사용된다. 그렇지 않으면 중첩클래스를 만들면 안된다. 


정적 멤버 클래스

정적 멤버 클래스는 바깥 클래스의 모든 멤버(private 포함)에 접근할 수 있다. 정적 멤버 클래스는 바깥 클래스의 정적 멤버이며, 다른 정적 멤버와 동일한 접근 권한 규칙을 따른다. 바깥 클래스와 함께 사용할 때만 유용한 public 도움 클래스를 정의할 때 유용하다. 예를 들어 계산을 책임지는 클래스안의 사칙 연산을 나타내는 enum클래스같은 경우가 있다.

비-정적 멤버 클래스

비-정적 멤버 클래스 객체는 바깥 클래스 객체와 자동적으로 연결된다. 바깥 클래스 객체와 독립적으로 존재할 수 있도록 하려면 정적 멤버 클래스로 선언해야 한다. 비-정적 멤버 클래스는 업댑터를 정의할 때 많이 사용하는 클래스이다. 바깥 클래스 객체를 다른 클래스 객체인 것처럼 보이게 하는 용도로 사용한다.



private 정적 멤버 클래스

바깥 클래스 객체의 컴포넌트를 표현하는 데 주로 쓰인다. 에를 들어, 키와 값을 연관 짓는 Map 객체를 보자. Map을 구현하는 많은 클래스는 내부적으로 키-값 쌍을 보관하는 Entry 객체를 사용한다. 각 Entry 객체는 특정 맵에 속할 것이지만, Entry 객체의 메서드(getKey,getValue ...) 는 맵 객체에 접근할 필요가 없다. 따라서 Entry를 비-정적 메서드로 표현하는 것은 낭비이며, private 정적 멤버 클래스로 정의하는 것이 최선이다.
why? Map객체를 여러 개 생성했을 때 비-정적 메서드로 정의되어 있지 않다면 모든 Map객체들이 Entry객체를 공유하게 될 것인데, Entry객체안의 key-value가 겹치게 되면 큰일이 나지 않나..? 이해가안된다..ㅠㅠ 나중에 다시 보도록하자!

익명 클래스

익명 클래스는 이름이 없다. 익명 클래스를 사용하는 데는 많은 제약이 있다. 선언하는 바로 그 순간에만 객체를 만들 수 있으며, instanceof를 비롯, 클래스 이름이 필요한 곳에는 사용할 수 없다. 또한 클래스가 길게 작성되면 가독성이 떨어지기도 한다.

 이러한 익명 클래스는 함수 객체를 정의할 때 널리 쓰인다. 또한 Runnable, Thread 객체 같은 프로세스 객체를 만드는데도 널리 쓰인다.

지역 클래스

지역 클래스는 지역 변수가 선언될 수 있는 곳이라면 어디서든 선언할 수 있다. 

요약

중첩 클래스에는 네 가지 종류가 있다. 중첩 클래스를 메서드 밖에서 사용할 수 있어야 하거나, 메서드 안에 놓기에 너무 길 경우에는 멤버 클래스로 정의하라. 멤버 클래스의 객체 각각이 바깥 객체에 대한 참조를 가져야 하는 경우에는 비-정적 멤버 클래스로 만들고, 그렇지 않은 경우에는 정적 멤버 클래스로 만들자. 중첩 클래스가 특정한 메서드에 속해야 하고, 오직 한곳에서만 객체를 생성하며, 해당 중첩 클래스의 특성을 규정하는 자료형이 이미 있다면 익명 클래스로 만들고 그렇지 않은 경우에는 지역클래스로 만들면 된다.


+ Recent posts