java - 제네릭스(Generics)
Generics
제너릭 타입(Generic Types)은 주로 자바 컬렉션에서 많이 사용되고 있다. 컬렉션은 자료구조이다. 컬렉션에는 어떤 자료를 담을지 알 수 없으므로 최상위 객체인 Object형태로 저장되고 관리되도록 설계되어 있다. 하지만, 의도하지 않은 자료형이 담기는 경우도 발생하게 된다. 이 때의 오류는 컴파일시에는 알 수가 없고 실행을 시켜보아야만 알 수 있다는 것이 문제점이었다. 제너릭 타입을 사용하면 프로그래머가 원하는 객체의 타입을 명시해서 의도하지 않은 객체는 저장될 수 없도록 컴파일시에 오류를 확인할 수있게 된다.
제너릭클래스 정의하기
제네릭 클래스를 정의하는 방법은 일반적인 클래스를 정의하는 것과 동일하다. 다만, 클래스명 뒤에 <제너릭타입, ...>이라고 덧붙여 준다.
public class Box<T> {
private T t; // T stands for "Type"
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
}
제너릭 클래스 선언 / 생성
일반적인 클래스와 동일하게 선언하고 생성할 수 있다. 다만, 클래스명 뒤에 <제너릭타입>을 덧붙여주면 된다.
Box<Integer> integerBox;
integerBox = new Box<Integer>();
제너릭 타입에 사용되는 파라미터
타입 매개변수는 하나의 대문자를 사용한다. 이들은 파일시스템에 실재로 존재하는 것은 아니다. 즉, T.java 라던지 T.class라는 파일은 없다. 타입매개변수를 여러개 사용할 수도 있지만 하나의 선언문에서 두 번 사용될 수는 없다. 즉, Box<T, U>는 가능하지만 Box<T, T>는 안된다.
E - Element (자바의 컬렉션에서 널리 사용되고 있다.)
K - Key
N - Number
T - Type
V - Value
S,U,V etc. - 2nd, 3rd, 4th types
제너릭 메서드 / 제너릭 생성자
타입 매개변수가 메서드의 선언 등에 사용될 수도 있다. 단, 매개변수의 범위가 메서드의 블록 이내로 한정된다.
public class Box<T> {
private T t;
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
public <U> void inspect(U u){
System.out.println("T: " + t.getClass().getName());
System.out.println("U: " + u.getClass().getName());
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
integerBox.add(new Integer(10));
integerBox.inspect("some text");
}
}
타입 파라미터 제한하기
타입매개변수를 적당히 제한해야 할 경우에는 extends ...를 사용한다. extends 뒤에는 클래스명일 수도 있고 인터페이스명일 수도 있다. 아래의 예에서는 U가 Number의 하위 클래스이어야 한다.
public <U extends Number> void inspect(U u){
System.out.println("T: " + t.getClass().getName());
System.out.println("U: " + u.getClass().getName());
}
여러 조건을 만족시켜야 할 경우에는 &를 사용할 수도 있다.
<U extends Number & MyInterface>
하위타입
이제, 객체지향의 '이다 (is a)'관계를 생각해 볼 때다. Integer는 Object에 할당할 수 있다. '이다'관계에 있기 때문이다. 마찬가지로 Number에 Integer를 할당할 수도 있고, Double을 할당할 수도 있다. 이러한 관계는 제너릭에서도 마찬가지이다.
Box<Number> box = new Box<Number>();
box.add(new Integer(10)); // OK
box.add(new Double(10.1)); // OK
그러나,
public void boxTest(Box<Number> n){
// method body omitted
}
이 경우에 Box<Integer>와 Box<Double>는 매개변수로 전달되지 않는다. 이것들은 Box<Number>의 하위타입이 아니기 때문이다. 꽤나 논리적인 내용 전개가 필요하지만 뭔말인지 헷갈리므로 그냥 패스~
와일드카드
?는 알 수 없는 타입을 뜻한다.
<?> - 모든 객체 자료형, 내부적으로는 Object로 인식한다.
<? super 객체자료형> - 명시된 객체자료형과 그 상위 객체, 내부적으로는 Object로 인식한다.
<? extends 객체자료형> - 명시된 객체 자료형과 이를 상속한 하위객체, 내부적으로는 명시된 객체 자료형으로 인식한다.
타입제거
제너릭 타입이 인스턴스화 될 때, 컴파일러는 타입파라미터와 관련된 정보를 제거한다. 제너릭을 사용하기 이전의 라이브러리 등과의 호환성을 유지하기 위해서이다.
Java Generic Programming 관련 내용 :
WIKI 사이트 : http://en.wikipedia.org/wiki/Generics_in_Java
SUN 사이트 : http://java.sun.com/developer/technicalArticles/J2SE/generics/index.html
J2SE 5.0에서 가장 두드러진 특징 중의 하나는 제네릭(Generic) 프로그래밍을 지원한다는 것이다. 제네릭 프로그래밍이란 효율적인 알로그림의 추상적인 형태로 표현하기 위한 프로그래밍 기법이다(Generic Programming is a programming mehod that is based in finding the most abstract representations of efficient algorithms. - Alexander Stepanov 정의, WIKI 사이트 참조).
자바는 제네릭 프로그래밍을 위해서 제네릭 타입과 메소드를 제공한다. 자바 제네릭 프로그래밍은 기존 C++언어의 템플릿과 유사한 점도 있지만 차이점도 많이 갖고 있다. 이러한 차이점들은 C++ 템플릿에 비해 자바 제네릭 프로그래밍에 여러 가지 장점을 제공한다. 자바 제네릭 프로그래밍은 C++ 의 템플릿에 대해서 다음과 같은 장점을 갖는다.
- 컴파일 시 타입 체킹 가능 - 자바 제네릭 프로그래밍은 컴파일 시에 타입 체킹이 가능하기 때문에 실행 시에 형변환에서 발생할 수 있는 많은 에러를 방지할 수 있다.
- 하나의 컴파일 된 코드 생성 - C++의 템플릿은 실제로 사용되는 데이터에 따라 여러 개의 컴파일된 코드를 생성하는 데 비해서 자바는 하나의 컴파일된 코드를 생성한다.
- 소스 코드 불필요 - C++의 템플릿을 사용하는 경우에 템플릿을 사용하기 위해서는 템플릿 소스 코드가 필요하지만, 자바 제네릭 프로그래밍을 사용하는 경우에는 컴파일된 라이브러리만 존재하면 된다.
● 제네릭 클래스, 인터페이스
자바에서 제네릭 클래스, 인터페이스, 메소드는 '<' 과 '>' 문자를 이용해서 표현한다. 예를 들어, GList라는 제네릭 클래스는 다음과 같은 형태로 정의할 수 있다. 이 때 E는 타입을 표현하기 위해서 사용되며, 포멀 파라미터 타입(Formal parameter type)이라고 한다.
제네릭 Glist 클래스 정의
void add(E x) {
...
}
...
}
정의된 제네릭 클래스는 생성해서 사용할 수 있다. 이 때 제네릭 클래스를 생성할 때 사용되는 타입( 예 : Integer )을 Actual Type Argument라고 한다. 또한 제네릭 타입 선언을 위해 호출하는 것( 예 : GList<Integer> )을 파라미터화된 타입이라고 한다.
● List 인터페이스 사용
파라미터화된 타입(parameterized type)은 클래스 혹은 인터페이스 이름 C와 파라미터 섹션에 해당되는 <T1, ... , Tn>으로 구성된다. 즉, C<T1, ... , Tn>으로 표현된다. 파라미터화된 타입은 다음과 같은 형태로 선언될 수 있다.
형태 : 파라미터화된 타이(Parameterized type)
Class Or Interface < ReferenceType [, ReferenceType ] >
다음 예는 파라미터화된 타입을 선언하는 것을 보여준다.
파라미터화된 타입
Vector<String>
Seq<Seq<A>>
Seq<String>.Zipper<Integer>
Collection<Integer>
Paint<String, String>
J2SE 5.0 에서 작성된 제네릭 프로그램은 컴파일된 후에 J2SE 1.4의 JVM에서도 실행될 수 있다. 이것은 제네릭 특성을 기존 JVM에서도 호환성 있도록 변환하기 떄문에 가능하다. 이처럼 제네릭 프로그램을 제네릭을 사용하지 않는 형태로 변환하는 것을 타입 제거(Type erasure)라고 한다.
java.util 패키지의 자바 컬렉션(Collection) 클래스들은 기본적으로 제네릭 프로그래밍을 지원하도록 만들어졌다. 예를 들어, java.util 패키지의 Vector 클래스도 제네릭 클래스 형태로 정의되어 있다. 따라서 우리는 Vector 클래스를 제네릭 프로그래밍 방법으로 사용할 수 있다. 다음의 StrinVector.java 예제는 Vector 를 이용해서 제네릭 프로그래밍을 사용하는 방법을 보여준다. 제네릭 프로그래밍을 사용하는 경우에 보다 편리하게 프로그래밍을 작성할 수 있다.
ex) StringVector.java
import java.util.*;
public class StringVector {
public static void main( String args[] ) {
Vector<String> v = new Vector<String>(); // 문자열을 원소로 갖는 백터 객체 v를 생성한다.
v.addElement("Hello");
v.addElement("World!!");
//v.add(5); 컴파일시 에러 발생, 5는 String 타입이 아니다.
for ( String s : v ) { //for 문을 이용해서 백터에 포함된 원소들을 찾아서 출력한다.
System.out.println( s );
}
}
}
ex) NormalVector.java (제네릭 프로그래밍 방법을 사용하지 않았을 경우)
import java.util.*;
public class NormalVector {
public static void main( String args[] ) {
Vector v = new Vector(); // 문자열을 원소로 갖는 백터 객체 v를 생성한다.
v.addElement("Hello");
v.addElement("World!!");
//v.add(5); 컴파일시 에러 발생, 5는 String 타입이 아니다.
int n = v.size();
for ( int i = 0 ; i < n ; i++ ) { //for 문을 이용해서 백터에 포함된 원소들을 찾아서 출력한다.
String s = (String) v.elementAt( i );
System.out.println( s )
}
}
}
제네릭 클래스를 사용할 때 타입 파라메터를 기술하지 않는 경우에 컴파일 시에 경고 메시지가 출력된다. -Xlint 옵션을 이용해서 컴파일 하면, 이 경고 메시지를 볼 수 있다.
Note: VectorTest.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
ex) ValueWrapper.java ( 타입 파라메터 T를 갖고, T 타입의 멤버 필드 value와 value() 메소드를 갖는다.)
public class ValueWrapper<T> { // ValueWrapper 클래스는 타입 파라메터 T를 갖는 제네릭 클래스이다.
private T value; // value 멤버필드는 T타입이다.
public ValueWrapper(T value) { //ValueWrapper 의 생성자,
this.value = value;
}
public T value() {
return value; // value() 메소드는 T 타입의 값을 리턴한다.
}
public static void main(String[] args) {
ValueWrapper<String> sf = new ValueWrapper<String>("Hello"); //<String>타입의 ValueWrapper의 객체 sf 생성
System.out.println( sf.value() );
ValueWrapper<Integer> si = new ValueWrapper<Integer>(new Integer(10)); // <Integer>타입은 <String>타입이 아니기 떄문에, new로 객체선언 해줘야한다.
System.out.println( si.value() );
}
}
실행결과( 객체 생성시 타입 파라메터 T를 <String>이나 <Integer>로 설정할 때 다른 값을 출력하는 것을 확인할 수 있다)
10
자바의 제네릭 프로그래밍은 JVM은 변경하지 않으면서 새로운 기능을 제공한다. 따라서 J2SE 5.0 이전에 작성된 프로그램들과도 호환성이 유지된다. 예를 들어, Vector 클래스는 제네릭 클래스로 정의되어 있지만, 타입을 갖기 않는 다음과 같은 형태로 사용할 수도 있다.
ex) 타입이 없는 경우
Vector v = new Vector();
이처럼 제네릭 클래스에서 타입 파라미터를 사용하지 않는 것을 로타입(Law Type)이라고 한다. 앞의 예에서 v는 로타입이다. 로타입을 사용할 경우에는 Object 클래스가 타입 파라메터로 사용된다. 파라미터화된 클래스 타입에서 로타입으로 할당은 가능하지만, 안전하지 않기 때문에 컴파일 시에 경고 메시지를 출력한다.
ex) Cell.java
- class Cell<E> {
private E value;
public Cell(E v) {
value = v;
}
public E get() {
return value;
}
public void set(E v) {
value = v;
}
public static void main(String[] args) {
Cell<String> x = new Cell<String>( "abc" ); // <String>타입의 Cell 개체 x 생성
String value = x.get(); // 개체x가 생성되면서 데이터 "abc"를 반환하는 get() 이용하여 String value 에 넣는다.
System.out.println( value );
x.set( "def" ); // 개체 x에 "def"를 대입한다.
Cell y = x; // String 타입을 갖는 Cell 객체 x는 로타입 형태인 y에 값을 할당할 수 있다.
value = (String) y.get(); // Y 는 로타입이기 때문에 타입 파라미터로 Object가 사용된다. 따라서 형변환을 해야 value에 값을 할당할 수 있다.
System.out.println( value ) ;
y.set( "hello" );
}
}
로 타입을 사용하는 경우에는 컴파일 할 때 경고 메시지가 출력된다.
y.set( "hello" );
^
1 warning
제네릭 프로그래밍은 타입을 매개 변수로 사용함으로써 프로그램의 일반성을 높이지만, 때로는 타입 파라미터의 범위를 제한해야 하는 경우도 존재한다. 이러한 필요성 때문에 타입 파라미터는 다음과 같은 형태로 범위를 제한할 수 있다.
ex) 타입 파라미터의 범위 제한
- public class C1<T extends Number> { ... }
- public class C2<T extends Person & Comparable> { ... }
C1 클래스를 사용하는 경우에 파라미터로는 Number 클래스의 서브클래스만 가능하다. C2 클래스의 경우에는 Person 클래스로부터 상속 받으며, Comparable 인터페이스를 구현한 클래스만 타입 파라미터로 사용될 수 있다. 타입 파라미터의 상위 타입을 지정하는 경우에는 부모 클래스를 처음에 오도록 하고, 인터페이스들은 & 를 이용해서 여러개 존재할 수 있다.
출처 - http://java.ihoney.pe.kr/16
Raw Types
A raw type is the name of a generic class or interface without any type arguments. For example, given the generic Box class:
public class Box<T> { public void set(T t) { /* ... */ } // ... }
To create a parameterized type of Box<T>, you supply an actual type argument for the formal type parameter T:
Box<Integer> intBox = new Box<>();
If the actual type argument is omitted, you create a raw type of Box<T>:
Box rawBox = new Box();
Therefore, Box is the raw type of the generic type Box<T>. However, a non-generic class or interface type is not a raw type.
Raw types show up in legacy code because lots of API classes (such as the Collections classes) were not generic prior to JDK 5.0. When using raw types, you essentially get pre-generics behavior — a Box gives you Objects. For backward compatibility, assigning a parameterized type to its raw type is allowed:
Box<String> stringBox = new Box<>(); Box rawBox = stringBox; // OK
But if you assign a raw type to a parameterized type, you get a warning:
Box rawBox = new Box(); // rawBox is a raw type of Box<T> Box<Integer> intBox = rawBox; // warning: unchecked conversion
You also get a warning if you use a raw type to invoke generic methods defined in the corresponding generic type:
Box<String> stringBox = new Box<>(); Box rawBox = stringBox; rawBox.set(8); // warning: unchecked invocation to set(T)
The warning shows that raw types bypass generic type checks, deferring the catch of unsafe code to runtime. Therefore, you should avoid using raw types.
The Type Erasure section has more information on how the Java compiler uses raw types.
Unchecked Error Messages
As mentioned previously, when mixing legacy code with generic code, you may encounter warning messages similar to the following:
Note: Example.java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details.
This can happen when using an older API that operates on raw types, as shown in the following example:
public class WarningDemo { public static void main(String[] args){ Box<Integer> bi; bi = createBox(); } static Box createBox(){ return new Box(); } }
The term "unchecked" means that the compiler does not have enough type information to perform all type checks necessary to ensure type safety. The "unchecked" warning is disabled, by default, though the compiler gives a hint. To see all "unchecked" warnings, recompile with -Xlint:unchecked.
Recompiling the previous example with -Xlint:unchecked reveals the following additional information:
WarningDemo.java:4: warning: [unchecked] unchecked conversion found : Box required: Box<java.lang.Integer> bi = createBox(); ^ 1 warning
To completely disable unchecked warnings, use the -Xlint:-unchecked flag. The @SuppressWarnings("unchecked") annotation suppresses unchecked warnings. If you are unfamiliar with the @SuppressWarnings
syntax, see Annotations.
source - https://docs.oracle.com/javase/tutorial/java/generics/rawTypes.html
제네릭 (23~29)
- 자바 1.5 부터 가능
- 그전에는 컬랙션에서 객체를 읽어낼때마다 형변환을 해야했다.
- 컬랙션에 이상한거 넣어서 에러나고 그랬음(컴파일 타임에서 에러를 검출 못했다)
ITEM23 : 새코드에는 무인지 제네릭 자료형을 사용하지 마라
- 용어
- 형인자(type parameter) :
- 형인자 자료형(parameterized type) : List <String>
- 실형인자(actual type parameter) : String
- 제네릭 자료형(generic type) : List<E>
- 형식 형인자(Formal Type Parameter) : E
- 무인자 자료형(raw type) : List
- 한정적 형인자(bunded type parameter) : <E extends Number>
- 재귀적 형 한정(recursive type bound) : <T extends Comparable<T>>
- 비한정적 와일드카드 자료형(unbounded wildcard type) : List<?>
- 한정적 와일드카드 자료형(bounded wildcard type) : List<? extends Number>
- 제네릭 메서드(generic method) : static
List asList(E[] a) - 자료형 토큰(type token) : String.class -
- 제네릭(Generic) : 형인자가 포함된 클래스나 인터페이스
- List 인터페이스는 List<E> 이렇게 부르는게 맞다
- List<String> : 스트링 인터페이스
- 무인자 자료형은 사용하면 안된다. 컴파일 타임에서 타입 안정성을 보장할수 없다. (ClassCastException 발생 함)
- size 무인자 자료형은 아직도 지원하긴한다. 무인자 자료형을 인자로 받는 메서드에 형인자 자료형 객체를 전달할수 있어야 하고 반대도 가능해야 한다. 이진 호환성을 지원하기 위해 어쩔수 없이 존재한다.
형인자 자료형을 사용하면 엉뚱한 자료형의 객체를 넣는 코드를 컴파일 할때 무엇이 잘못인지 알수 있다. 컬랙션에서 원소를 꺼낼떄 형변환을 하지않아도 된다. (컴파일러가 알아서 해줌)
- List vs List<Object>
- List 는 완전히 형검사 절차를 생략한것이고 List<Object> 는 형검사를 진행한다
- List 에는 String 을 넣을수 있지만 List<Object> 에는 넣을수 없다.
- List 에는 메서드에 List<String> 을 전달 가능하지만 List<Object> 는 불가능하다
- List<String> 은 List 의 하위자료형(subtype) 이지만 List<Object>의 하위 자료형은 아니다
- List 와 같은 무인자 자료형을 사용하면 형 안전성을 잃게 되지만 List<Object> 와 같은 형인자 자료형은 형안전성이 있다.
- 실행 도중 오류를 일으키는 무인자 자료형(List)
1 public static void main(String[] args){
2 List<String strings = new ArrayList<Object>();
3 unsafeAdd(strings, new Ineger(42));
4 String s = strings.get(0); // ClassCastException 발생!!!!
5 }
6 // 무인자 자료형에 인자로 보낼수는 있음
7 private static void unsafeAdd(List list, Object o){
8 list.add(0); // 경고 발생 unchecked call to add(E) in raw type List
9 }
10 // unsafeAdd(List<Object>... 로 바꾸어야 한다)
- 비한정적 와일드 카드 자료형
- 제네릭 자료형을 쓰고 싶으나 실제형인자가 무엇인지는 모르거나 신경쓰고 싶지 않을때는 형인자로 ?를 쓰면된다.
- Set<?> : 가장 일반적인 형인자 Set 자료형
1 //static int numElementsInCommon(Set s1, Set s2) { // 무인자 사용하면안된다
2 static int numElementsInCommon(Set<?> s1, Set<?> s2) { // 비한정적 와일드 카드 자료형
3 int result = 0;
4 for(Object o1 : s1){
5 if(s2.contains(01)){
6 result++;
7 }
8 }
9 return resultt;
10 }
- 와일드 카드 자료형은 안전, 무인자자료형은 안전하지 않다.
무인자 자료형에는 아무거나 넣을수 있어서 자료형 불변식이 쉽게 깨진다. Collection<?> 에는 null 이외에 어떤원소도 넣을수가 없다. 무언가 넣을려고 하면 컴파일 오류가 난다. 어떤 자료형 객체를 꺼낼수 있는지도 알수없다.
- 무인자 자료형을 그래도 써도 되는경우
- 클래스 리터럴(class literal)
- 자바 표준에서 클래스 리터럴에는 형인자 자료형을 쓸수 없다.(배열 자료형이나 기본 자료형은 쓸수있다)
- List.class, String[].class int.class 는 가능하다
- List<String>.class 나 List<?>.class 는 사용할수가 없다.
- instanceOf 연산자 사용규칙
- 제네릭 자료형 정보는 프로그램이 실행될때는 지워지므로 instanceOf 연산자는 형인자 자료형에 적용할수가없다. 비한정적 와일드 카드 자료형은 가능하다. 하지만 코드만 지저분해질뿐 굳이 쓸이유가없다.
- 클래스 리터럴(class literal)
1 if (o instanceof Set){ // 무인자 자료형
2 Set<?> m = (Set<?>) 0; // 와일드 카드 자료형
3 }
ITEM24 : 무검검 경고(unchecked warning)를 제거하라
- 제네릭을 사용해서 프로그램을 작성하면 컴파일 경고 메세지를 많이 보게 됨
- unchecked 캐스트 경고
- unchecked 메소드 호출 경고
- unchecked 제네릭 배열 생성 경고
- unchecked 변환 경고
- unchecked 예외를 무시하면 ClassCastException 가 생길수 있으므로 조심한다.
- @SuppressWarnings(“unchecked”) 주석을 사용해서 경고 메세지를 안나타나게 억제할수 있다.
- 다양한 범위로 사용 가능하다. 가급적 제일 작은 범위로 사용하도록 해야한다.
- 가능한 주석으로 SuppressWarnings을 사용하는 이유를 남겨라!
1 // ArrayList 의 toArray 함수
2 public <T> T[] toArray(T[] a){
3 if (a.length < size){
4 // unchecked cast (Object[] , required: T[])
5 @SuppressWarnings("unchecked") T[] result = (T[])Arrays.CopyOf(elements, size, a.getClass());
6 return result;
7 // return 문제 @SuppresseWarnings 를 사용할수 없다.
8 //return (T[])Arrays.CopyOf(elements, size, a.getClass());
9 }
10 System.arraycopy(elements, 0, a, 0, size);
11 if (a.length > size)
12 a[size] = null;
13 return a;
14 }
ITEM25 : 배열 대신 리스트를 써라
- 배열(Array) vs 제네릭 타입
- 배열 공변(covariant)이다. Sub 이 Super 의 서브타입이라면 Sub[] 은 Super[]의 서브 타입이다.
- 제네릭은 불변(invariant)이다. Type1 Type2 List<Type1> List<Type2> 의 서브타입도 슈퍼타입도 아니다.
1 // 런타임에서 에러 발생
2 Object[] objectArray = new Long[1];
3 objectArray[0] = "I don't fit in"; // ArrayStoreException 예외 발생
4 // 컴파일 에러! 컴파일 에러가 더 안전하고 좋은것!
5 List<Object> ol = new ArrayList<Long>(); // 호환이 안되는 타입이다!
6 ol.add("I don't fit in");
- 배열은 구체적(reified) : 자신의 요소타입을 런타임시에 알고 지키게 한다. String 객체를 Long 배열에 저장 하려고 하면 런타임에서 ArrayStoreException 예외 발생
제네릭은 소거자(Erasure) 에 의해 구현됨. 컴파일 시에만 자신의 타입 제약을 지키게 하고 런타임 시에는 자신의 요소타입 정보를 무시(소거) 한다.
- new List<E>[], new List<Sting>[] , new E[] 와 같은 배열 생성식은 불가
- 제네릭 배열 생성은 불가
- E, List<E>, List<String> 과 같은 타입들을 비구체화(nonreifiable) 타입 이라고한다.
- 비구체화 타입이란 컴파일 시보다 런타임 시에 더 적은 정보를 갖는
- 비구체화 타입은 배열 생성이 불가능
- List<?>, Map<?,?> 와 같은 언바운드 와일드 카드 타입은 구체화 타입이다. 따라서 배열 생성 하는건 적법함
- 따라서 제네릭 타입을 가변인자를 갖는 메소드와 함께 사용 불가능,가변인자는 내부적으로 배열이 생성되는 구조이기 때문
- 제네릭 배열 생성 에러가 발생하면 E[] 보다는 List
를 사용하는것이 좋다 - 다중 스레드간에 동기화에 제네릭 배열이 좋다.
1 // 제네릭을 사용하지 않으면서 동시성에도 결함이 없다.
2 // synchronized(list) 로 전체를 묶는 방법이 있지만 동기화된 코드에서는 외계인(alien) 메소드(apply) 를 호출 하면안된다.
3 // 따라서 lock 이걸로는 toArray() 를 사용해서 문제를 해결했다.
4 static Object reduce(List list, Functon f, Object initVal){
5 Object[] snapshot = list.toArray(); // 내부적으로 List 에 lock 이 걸림
6 Object result = initVal;
7 for(Object o : snapshot)
8 result = f.apply(result, o);
9 return result;
10 }
11
12 static <E> E reduce(List<E> list, Functon<E> f, E initVal){
13 // 타입 안정성이 보장되지 않는다. 런타임시에 E 가 무슨 타입이 될지 컴파일러가 알지 못한다.
14 // ClassCastException 예외가 발생할수 있다.
15 // 컴파일 시에는 String[] Integer[] 등 아무거나 될수있지만 런타임에서는 Object[] 이므로 위험
16 // E[] 로 캐스팅 하는건 특별한 상황에서만 고려 되야함
17 E[] snapshot = (E[]) list.toArray();
18 E result = initVal;
19 for(E o : snapshot)
20 result = f.apply(result, o);
21 return result;
22 }
23
24
25 static <E> E reduce(List<E> list, Functon<E> f, E initVal){
26 E[] snapshot;
27 synchronized(list) {
28 // toArray 함수와는 다르게 락이 걸리지 않으므로 synchronized 함수로 변경해야한다.
29 snapshot = new ArrayList<E>(list); // 배열보다는 ArrayList<E>
30 }
31 E result = initVal;
32 for(E o : snapshot)
33 result = f.apply(result, o);
34 return result;
35 }
ITEM26 : 가능하면 제네릭 자료형으로 만들 것
1 // Object 객체 기반의 컬랙션
2 public class Stack{
3 private Object[] elements;
4 private int size = 0;
5 private static final int DEFAULT_INITIAL_CAPACITY = 16;
6 public Stack(){
7 elements = new Object[DEFAULT_INITIAL_CAPACITY];
8 }
9 public void push(Object e){...}
10 public Object pop(){
11 if(size==0)
12 throw new EmptyStackException();
13 Object result = elements[--size];
14 elements[size] = null; // 쓸모없는 참조 제거
15 return result;
16 }
17 }
18
19 // 제네릭 적용1 - 캐스팅 이용
20 public class Stack<E>{
21 private E[] elements;
22 private int size = 0;
23 private static final int DEFAULT_INITIAL_CAPACITY = 16;
24
25 // E[] 로 캐스팅 하므로 warning 이 발생한다.
26 // elements 는 private 로써 밖에서는 사용이 안되므로 해당 캐스팅만 문제없으면 외부적으로도 문제 없으므로, 아래처럼 캐스팅 하는것은 문제가 없다.
27 @SuppressWarnings("unchecked")
28 public Stack(){
29 // 제네릭 배열 생성, 컴파일 에러 발생! 비구체화 타입을 저장하는 배열은 생성할수 없다.
30 //elements = new E[DEFAULT_INITIAL_CAPACITY];
31 elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
32 }
33 public void push(Object e){...}
34 public Object pop(){
35 if(size==0)
36 throw new EmptyStackException();
37 Object result = elements[--size];
38 elements[size] = null; // 쓸모없는 참조 제거
39 return result;
40 }
41 }
42
43 // 제네릭 적용2 - elements 타입자체를 변경
44 public class Stack<E>{
45 private Object[] elements;
46 private int size = 0;
47 private static final int DEFAULT_INITIAL_CAPACITY = 16;
48
49 public Stack(){
50 elements = new Object[DEFAULT_INITIAL_CAPACITY];
51 }
52 public void push(E e){...}
53 public E pop(){
54 if(size==0)
55 throw new EmptyStackException();
56
57 @SuppressWarnings("unchecked")
58 E result = (E)elements[--size];
59 elements[size] = null; // 쓸모없는 참조 제거
60 return result;
61 }
62 }
- 배열 타입에 대한 unchecked 캐스트 경고를 억제하는게 더 위험하므로 적용2 가 더 좋은 방법 일 수 있다.
- 하지만 적용2는 elements 를 사용하는 모든 부분에 캐스팅을 해야하고 @SupressWarnings 를 적용해야 되므로 더 많은일을 해줘야한다.
- Stack 클래스에서는 내부적으로 배열을 사용했다. 배열보다는 List 를 사용하라고 강조했지만 자바 언어 자체는 List 를 지원하지 않으므로 ArrayList 와 같은 일부 제네릭 타입은 내부적으로 배열을 사용한다. HashMap 은 성능 향상을 목적으로 배열을 사용하기도 한다.
- 제네릭 타입은 매개변수가 갖는 제약이 전혀없다. Stack<Object> Stack<int[]> Stack<List<String>> 등 여러 형태가 가능하다.
- <E extends Delayed> 같은 바운드 타입 배개변수(bounded type parameter)는 허용가능한 값을 제한할수 있다.
- 하지만 Stack<int> Stack<double> 같은 기본형은 불가능하다. 박스형 기본형 Integer Double 클래스를 사용하는것이 좋다.
ITEM27 : 가능하면 제네릭 메서드로 만들 것
- Collections 클래스의 모든 알고리즘 메소드들(binarySearch sort)는 제네릭화 되어있다.
1 // 제네릭이 적용안된 메소드
2 public static Set union(Set s1, Set s2){
3 // Warning! HashSet(Collection<? extends E>)
4 Set result = new HashSet(s1);
5 // Warning! result.addAll(Collection<? extends E>)
6 result.addAll(s2);
7 return result;
8 }
9 // 제네릭 적용 메소드
10 public static <E> Set<E> union(Set<E> s1, Set<E> s2){
11 Set result = new HashSet(s1);
12 result.addAll(s2);
13 return result;
14 }
- 타입매개변수를 선언하는 타입 매개변수 목록을 메소드의 접근 지시자와 반환 타입 사이에 둔다. <E>
반환 타입 Set<E>
- 제네릭 메소드로 중복 제거
1 Map<String, List<String>> anagram = new HashMap<String, List<String>>(); // 중복된 타입들
2
3 // 제네릭 static 팩토리 메소드
4 public static <K, V> HashMap<K,V> newHashMap{
5 return new HashMap<K,V>();
6 }
7 Map<String, List<String>> anagrams = newHashMap();
이런 제네릭 메소드가 기본으로 JDK 있으면 좋지만 없음.
제네릭 싱글톤 팩토리
1 public interface UnaryFunction<T>{
2 T apply(T arg);
3 }
4 // 불변적이지만 여러타입에대한 적합한 객체 생성
5 private static UnaryFunction<Object> IDENTIFY_FUNCTION = new UnaryFunction<Object>(){
6 public Object apply(Object arg) { return arg;}
7 }
8
9 // 상태값이 없고 언바운드 타입 매개변수를 갖는다.
10 // 따라서 모든 타입에서 하나의 인스턴스를 공유해도 안전
11 @SuppressWarnings("unchecked")
12 public static <T> UnaryFunction<T> identityFunction(){
13 return (UnaryFunction<T>)IDENTIFY_FUNCTION;
14 }
15 // 사용
16 UnaryFunction<String> sameString = identityFunction();
17 UnaryFunction<Number> sameNumber = identifyFunction();
- 재귀적 타입 바운드
- Comparable 인터페이스와 가장 많이 사용
1 public static <T extends Comparable<T>>
자신과 비교될수 있는 모든 타입 T
캐스팅 없이 메소드를 사용할수 있다는것은 메소드를 제네릭 하게 만들었다는 의미
ITEM28 : 한정적 와일드카드를 써서 API 유연성을 높여라
- 매개변수화 타입은 불변 타입
- 불변(invariant)!! Type1 Type2 에 대해서 List<Tyep1> 은 List<Type2> 의 서브타입도 아니고 슈퍼 타입도 아님
List<Object> 에는 아무거나 저장 가능하지만 List<String> 에는 스트링만 저장 가능
- 스택 pushAll pop 메소드
1 // 와일드 카드 타입을 사용하지 않는 pushAll - 불충분함!
2 public void pushAll(Iterable<E> src){
3 for(E e: src)
4 push(e);
5 }
6 Stack<Number> numberStack = new Stack<Number>();
7 Iterable<Integer> integers = ..;
8 // 에러 메세지 pushAll(Iterable<Number>) in Stack<Number>
9 // 불변형(상속관계아님) 이기 때문에 Integer iterable 은들어갈수없다.
10 numberStack.pushAll(integers);
11
12 // 와일드 카드 사용하지 않은 popAll 메소드 - 불충분함!
13 public void popAll(Collection<E> dst){
14 while(!isEmpty())
15 dst.add(pop());
16 }
17 Stack<Number> numberStack = new ..
18 Collection<Object> objects = ...;
19 // 컴파일 에러! Collection<Object> 는 Collection<Number> 의 서브 타입이 아니다!
20 numberStack.popAll(objects);
- 바운드 와일드 카드 타입(bounded wildcard type)
- pushAll 에는 E의 Iterable 이 아닌 E의 어떤 서브타입의 Iterable 이 되어야 한다.
1 // 와일드 카드 타입 pushAll 은 E 타입을 생산하므로 extends
2 public void pushAll(Iterable<? extends E> src){...}
- popAll 메소드의 인자 타입은 E타입을 저장하는 Collection 이 아닌 E의 어떤 수퍼 타입을 저장하는 Collection이 되어야 한다.
1 // 와일드 카드타입 popAll 은 E 타입을 소비하므로 super
2 public void popAll(Collection<? super E> dst){...}
- 유연성을 극대화 하려면 메소드 인자에 와일드 카드 타입을 사용하자.
- PECS : Producer->Extends, Consumer->Super
- T가 생산자를 나타내면 <? extedns T>
- T가 소비자를 나타내면 <? super T>
1 static <E> E reduce(List<E> list, Function<E> f, E initVal)
2 // E가 생산자 역활을 하는 와일드 카드 타입 변수
3 statiac <E> E reduce(List<? extends E> list, Function<E> f, E initVal);
와일드 카드를 쓰므로 인해 List
와 Function 를 사용해서 reduce 호출 가능하다! - 반환 타입에는 와일드 카드 타입을 사용하지 말자
- 유연성 보다는 클라이언트 코드에서 와일드 카드 타입을 사용해야 하는 문제가 생긴다.
- 클라이언트 코드에서 와일드 카드 타입 때문에 고민해야 된다면 그 클래스의 API 가 잘못된거다.
- 명시적 타입 매개변수
1 public static <E> Set<E> union(Set<? extends E> s1, SET<? extends E> s2){...}
2 Set<Integer> integers = ..
3 Set<Dobule> doubles = ...
4 // 컴파일 에러! 타입 추론을 할수가없다.
5 Set<Number> numbers = union(integers, doubles);
6 Set<Number> numbers = Union.<Number>union(integers, doubles);
- Comparable<T> 는 T 인스턴스를 소비한다. Comparable<? super T> 로 교체
- Comparator<T> 도 마찬가지
1 public static <T extends Comparable<? super T>> T max(List<? extends T> list){
2 // 컴파일 에러! Iterator<? extends T> 리턴한다.
3 Iterable<T> i = list.iterator();
4 Iterable<? extends T> i = list.iterator();
5 T result = i.next();
6 while(i.hasNext()){
7 T t = i.next();
8 if(t.compareTo(result) >) result = t;
9 }
10 return result;
11 }
- 언바운드 타입 매개변수 vs 언바운드 와일드 카드
1 // 언바운드 타입 매개변수
2 public static <E> void swap(List<E> list, int i, intj);
3 // 언바운드 와일드 카드
4 public static void swap(List<?> list, int i, int j);
- public API 라면 언바운드 타입 매개변수를 사용하는것이 좋다.
- 메소드 선언부 타입 매개변수가 한번만 나타나면 그것을 와일드 카드로 바꾸면 된다
1 public static void swap(List<?> list, int i, int j){
2 // 컴파일 에러! List<?> 이므로 list 에는 null 제외한 어떤값도 추가할수 없다.
3 list.set(i, list.set(j, list.get(i)));
4 }
5
6 public static void swap(List<?> list, int i, int j){
7 swapHelper(list, i, j);
8 }
9 // private 지원 메소드
10 private static <E> void swapHelper(List<E> list, int i, int j){
11 list.set(i, list.set(j, list.get(i))));
12 }
ITEM29 : 형 안전 다형성 컨테이너를 쓰면 어떨지 따져보라
- 컨테이너에 제네릭을 사용하면 컨테이너 당 사용 가능한 타입 매개변수의 숫자가 제한된다. 컨테이너에 들어가는 타입이 결정되어있기 때문에.
- 컨테이너 자체보다는 요소의 키에 타입매개변수를 두면 그런 제약을 극복할수 있고 서로 다른 타입의 요소가 저장 될수 있다. 이런 컨테이너를 혼성 컨테이너라고 부른다.
- 제네릭은 Set Map 그리고 ThreadLocal AtomicReference 같은 단일요소 저장 컨테이너에도 쓰임
- 제네틱 타입 시스템을 키(Class<T>)로 사용해서 Map Set을 만듬, 그것을 혼성 컨테이너라고 부름!
- 클래스 리터럴 타입 : Class<T>
- 컴파일과 런타입 두 시점 모두의 타입정보를 전달될때 그것을 타입토큰(type token)
1 // 타입 안전이 보장되는 혼성 컨테이너 패턴!
2 public class Favorite{
3 private Map<Class<?>, Object> favorites = new HashMap<Class<?>, Object>();
4 public <T> void putFavorite(Class<T> type, T instance){
5 if(type == null)
6 throw
7 favorites.put(type, instance);
8 }
9 public <T> T getFavorite(Class<T> type){
10 // Map 에는 Object 가 들어있지만 T 타입으로 리턴해야한다.
11 // 런타임시에 동적으로 cast 하는 cast 함수 Class<T> 정보가 있으므로 가능!
12 return type.cast(favorites.get(type));
13 }
14 }
Map<Class<?>, Object> 에서 언바운드 와일드 카드 타입때문에 아무것도 Map 에 넣을수 없다고 생각할수 있지만 키값에 와일드 카드가 붙어 있다. 따라서 모든 키가 서로 다른 매개변수화 타입을 가질수 있다! 예를 들어 Class<String>, Class<Integer>
혼성 컨테이너 문제
- Class 객체를 원천 타입의 형태로 사용하면 타입 안전이 보장되지 않을수 있다. 해결책은아래!
1 // put 메소드, 동적 캐스트를 사용해서 런타임 시의 타입 안전을 획득!
2 public <T> void putFavorite(Class<T> type, T instance){
3 favorites.put(type, type,cast(instance);
4 }
- 비구체화 타입에 사용 될 수 없다. Favorite 객체를 String 이나 String[] 에는 저장할수 있지만 List<String> 은 저장할수 없다.
- List<String> 에 대한 Class 객체를 얻을수 없다. List<String>.class 구문 에러
- 바운드 타입 토큰을 사용하면 해결이 가능하긴하다.
- 바운드 타입 토큰
1 public <T extends Annotation> T getAnnotation(Class <T> annotationType);
- annotationType 은 Annotation 타입을 나타내는 바운드 타입 토큰이다.
- 키가 Annotation타입 이고 타입 안전이 보장되는 혼성 컨테이너
1 static Annotation getAnnotation(AnnotationElement element, String annotationTypeName) {
2 Class<?> annitationType = null; // 언바운드 타입 토큰
3 try{
4 annotationType = Class.forName(annotationTypeName);
5 }catch{
6 ...
7 }
8 // asSubClass 메소드를 사용해서 언바운드 타입토큰을 바운드 타입 토큰으로
9 return element.getAnnotation(annotationType.asSubClass(Annotation.class));
10 }
- Class.forName 은 Class<?> 를 리턴함
- Class<?> 아입을 Class<? extends Annotation> 으로 캐스팅 하기위해 asSubClass 라는 메소드를 사용
출처 - http://rangken.github.io/blog/2015/effective-java-4/
제네릭이란?
제네릭(Generic)은 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미한다. 말이 어렵다. 아래 그림을 보자.
위의 그림은 아래의 코드를 간략화한 것이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | package org.opentutorials.javatutorials.generic; class Person<T>{ public T info; } public class GenericDemo { public static void main(String[] args) { Person<String> p1 = new Person<String>(); Person<StringBuilder> p2 = new Person<StringBuilder>(); } } |
그림을 보자. p1.info와 p2.info의 데이터 타입은 결과적으로 아래와 같다.
- p1.info : String
- p2.info : StringBuilder
그것은 각각의 인스턴스를 생성할 때 사용한 <> 사이에 어떤 데이터 타입을 사용했느냐에 달려있다.
클래스 선언부를 보자.
1 | public T info; |
클래스 Person의 필드 info의 데이터 타입은 T로 되어 있다. 그런데 T라는 데이터 타입은 존재하지 않는다. 이 값은 아래 코드의 T에서 정해진다.
1 | class Person<T>{ |
위 코드의 T는 아래 코드의 <> 안에 지정된 데이터 타입에 의해서 결정된다.
1 | Person<String> p1 = new Person<String>(); |
위의 코드를 나눠보자. 아래 코드는 변수 p1의 데이터 타입을 정의하고 있다.
1 | Person<String> p1 |
아래 코드는 인스턴스를 생성하고 있다.
1 | new Person<String>(); |
즉 클래스를 정의 할 때는 info의 데이터 타입을 확정하지 않고 인스턴스를 생성할 때 데이터 타입을 지정하는 기능의 제네릭이다.
제네릭이 무엇인가를 알았으니까 이제 제네릭을 사용하는 이유를 알아보자.
제네릭을 사용하는 이유
타입 안전성
아래 코드를 보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | package org.opentutorials.javatutorials.generic; class StudentInfo{ public int grade; StudentInfo( int grade){ this .grade = grade; } } class StudentPerson{ public StudentInfo info; StudentPerson(StudentInfo info){ this .info = info; } } class EmployeeInfo{ public int rank; EmployeeInfo( int rank){ this .rank = rank; } } class EmployeePerson{ public EmployeeInfo info; EmployeePerson(EmployeeInfo info){ this .info = info; } } public class GenericDemo { public static void main(String[] args) { StudentInfo si = new StudentInfo( 2 ); StudentPerson sp = new StudentPerson(si); System.out.println(sp.info.grade); // 2 EmployeeInfo ei = new EmployeeInfo( 1 ); EmployeePerson ep = new EmployeePerson(ei); System.out.println(ep.info.rank); // 1 } } |
그리고 아래 코드를 보자. 위의 코드는 StudentPerson과 EmployeeInfo가 사실상 같은 구조를 가지고 있다. 중복이 발생하고 있는 것이다. 중복을 제거해보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | package org.opentutorials.javatutorials.generic; class StudentInfo{ public int grade; StudentInfo( int grade){ this .grade = grade; } } class EmployeeInfo{ public int rank; EmployeeInfo( int rank){ this .rank = rank; } } class Person{ public Object info; Person(Object info){ this .info = info; } } public class GenericDemo { public static void main(String[] args) { Person p1 = new Person( "부장" ); EmployeeInfo ei = (EmployeeInfo)p1.info; System.out.println(ei.rank); } } |
위의 코드는 성공적으로 컴파일된다. 하지만 실행을 하면 아래와 같은 오류가 발생한다.
1 2 | Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to org.opentutorials.javatutorials.generic.EmployeeInfo at org.opentutorials.javatutorials.generic.GenericDemo.main(GenericDemo.java: 17 ) |
아래 코드를 보자.
1 | Person p1 = new Person( "부장" ); |
클래스 Person의 생성자는 매개변수 info의 데이터 타입이 Object이다. 따라서 모든 객체가 될 수 있다. 그렇기 때문에 위와 EmployeeInfo의 객체가 아니라 String이 와도 컴파일 에러가 발생하지 않는다. 대신 런타임 에러가 발생한다. 컴파일 언어의 기본은 모든 에러는 컴파일에 발생할 수 있도록 유도해야 한다는 것이다. 런타임은 실제로 애플리케이션이 동작하고 있는 상황이기 때문에 런타임에 발생하는 에러는 항상 심각한 문제를 초래할 수 있기 때문이다.
위와 같은 에러를 타입에 안전하지 않다고 한다. 즉 모든 타입이 올 수 있기 때문에 타입을 엄격하게 제한 할 수 없게 되는 것이다.
제네릭화
이것을 제네릭으로 바꿔보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | package org.opentutorials.javatutorials.generic; class StudentInfo{ public int grade; StudentInfo( int grade){ this .grade = grade; } } class EmployeeInfo{ public int rank; EmployeeInfo( int rank){ this .rank = rank; } } class Person<T>{ public T info; Person(T info){ this .info = info; } } public class GenericDemo { public static void main(String[] args) { Person<EmployeeInfo> p1 = new Person<EmployeeInfo>( new EmployeeInfo( 1 )); EmployeeInfo ei1 = p1.info; System.out.println(ei1.rank); // 성공 Person<String> p2 = new Person<String>( "부장" ); String ei2 = p2.info; System.out.println(ei2.rank); // 컴파일 실패 } } |
p1은 잘 동작할 것이다. 중요한 것은 p2다. p2는 컴파일 오류가 발생하는데 p2.info가 String이고 String은 rank 필드가 없는데 이것을 호출하고 있기 때문이다. 여기서 중요한 것은 아래와 같이 정리할 수 있다.
- 컴파일 단계에서 오류가 검출된다.
- 중복의 제거와 타입 안전성을 동시에 추구할 수 있게 되었다.
제네릭의 특성
복수의 제네릭
클래스 내에서 여러개의 제네릭을 필요로 하는 경우가 있을 것이다. 예제를 보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | package org.opentutorials.javatutorials.generic; class EmployeeInfo{ public int rank; EmployeeInfo( int rank){ this .rank = rank; } } class Person<T, S>{ public T info; public S id; Person(T info, S id){ this .info = info; this .id = id; } } public class GenericDemo { public static void main(String[] args) { Person<EmployeeInfo, int > p1 = new Person<EmployeeInfo, int >( new EmployeeInfo( 1 ), 1 ); } } |
위의 코드는 예외를 발생시키지만 문제는 다음 예제에서 처리하고 형식만 보자.
즉, 복수의 제네릭을 사용할 때는 <T, S>와 같은 형식을 사용한다. 여기서 T와 S 대신 어떠한 문자를 사용해도 된다. 하지만 묵시적인 약속(convention)이 있기는 하다. 그럼 예제의 오류를 해결하자.
기본 데이터 타입과 제네릭
제네릭은 참조 데이터 타입에 대해서만 사용할 수 있다. 기본 데이터 타입에서는 사용할 수 없다. 따라서 아래와 같이 코드를 변경한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | package org.opentutorials.javatutorials.generic; class EmployeeInfo{ public int rank; EmployeeInfo(int rank){ this.rank = rank; } } class Person<T, S>{ public T info; public S id ; Person(T info, S id ){ this.info = info; this. id = id ; } } public class GenericDemo { public static void main(String[] args) { EmployeeInfo e = new EmployeeInfo(1); Integer i = new Integer(10); Person<EmployeeInfo, Integer> p1 = new Person<EmployeeInfo, Integer>(e, i); System.out.println(p1. id .intValue()); } } |
new Integer는 기본 데이터 타입인 int를 참조 데이터 타입으로 변환해주는 역할을 한다. 이러한 클래스를 래퍼(wrapper) 클래스라고 한다. 덕분에 기본 데이터 타입을 사용할 수 없는 제네릭에서 int를 사용할 수 있다.
제네릭의 생략
제네릭은 생략 가능하다. 아래 두 개의 코드가 있다. 이 코드들은 정확히 동일하게 동작한다. e와 i의 데이터 타입을 알고 있기 때문이다.
1 2 3 4 | EmployeeInfo e = new EmployeeInfo( 1 ); Integer i = new Integer( 10 ); Person<EmployeeInfo, Integer> p1 = new Person<EmployeeInfo, Integer>(e, i); Person p2 = new Person(e, i); |
메소드에 적용
제네릭은 메소드에 적용할 수도 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | package org.opentutorials.javatutorials.generic; class EmployeeInfo{ public int rank; EmployeeInfo( int rank){ this .rank = rank; } } class Person<T, S>{ public T info; public S id; Person(T info, S id){ this .info = info; this .id = id; } public <U> void printInfo(U info){ System.out.println(info); } } public class GenericDemo { public static void main(String[] args) { EmployeeInfo e = new EmployeeInfo( 1 ); Integer i = new Integer( 10 ); Person<EmployeeInfo, Integer> p1 = new Person<EmployeeInfo, Integer>(e, i); p1.<EmployeeInfo>printInfo(e); p1.printInfo(e); } } |
제네릭의 제한
extends
제네릭으로 올 수 있는 데이터 타입을 특정 클래스의 자식으로 제한할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | package org.opentutorials.javatutorials.generic; abstract class Info{ public abstract int getLevel(); } class EmployeeInfo extends Info{ public int rank; EmployeeInfo(int rank){ this.rank = rank; } public int getLevel(){ return this.rank; } } class Person<T extends Info>{ public T info; Person(T info){ this.info = info; } } public class GenericDemo { public static void main(String[] args) { Person p1 = new Person(new EmployeeInfo(1)); Person<String> p2 = new Person<String>( "부장" ); } } |
위의 코드에서 중요한 부분은 다음과 같다.
1 | class Person<T extends Info>{ |
즉 Person의 T는 Info 클래스나 그 자식 외에는 올 수 없다.
extends는 상속(extends)뿐 아니라 구현(implements)의 관계에서도 사용할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | package org.opentutorials.javatutorials.generic; interface Info{ int getLevel(); } class EmployeeInfo implements Info{ public int rank; EmployeeInfo( int rank){ this .rank = rank; } public int getLevel(){ return this .rank; } } class Person<T extends Info>{ public T info; Person(T info){ this .info = info; } } public class GenericDemo { public static void main(String[] args) { Person p1 = new Person( new EmployeeInfo( 1 )); Person<String> p2 = new Person<String>( "부장" ); } } |
이상으로 제네릭의 기본적인 사용법을 알아봤다.
출처 - https://opentutorials.org/module/516/6237
4. 제네릭스(Generics)
제네릭스는, JDK1.5에서의 가장 큰 변화 중의 하나로, 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크(compile-time type check)를 해주는 기능이다. 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.
ArrayList와 같은 컬렉션 클래스는 다양한 종류의 객체를 담을 수 있긴 하지만 보통 한 종류의 객체를 담는 경우가 더 많다. 그런데도 꺼낼 때 마다 타입체크를 하고 형변환을 하는 것은 아무래도 불편할 수밖에 없다.
▶ 제네릭스의 장점
<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
1. 타입 안정성을 제공한다.
2. 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해 진다.
[참고] 타입 안정성을 높인다는 것은 의도하지 않은 타입의 객체를 저장하는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 형변환되어 발생할 수 있는 오류를 줄여준 다는 뜻이다.
간단히 얘기하면 다룰 객체의 타입을 미리 명시해줌으로써 형변환을 하지 않아도 되게 하는 것이다. 그게 전부이다. 너무 어렵게 생각하지 않길 바란다.
제네릭스에서는 참조형 타입(reference type), 간단히 말해서 ‘타입(type)’을 의미하는 기호로 ‘T'를 사용한다. ’T‘는 영단어 'type’의 첫 글자로, 어떠한 참조형 타입도 가능하다는 것을 의미한다.
'T'뿐 만아니라 때때로 요소(element)를 의미하는 ‘E', 키(key)를 의미하는 'K', 값(value)을 의미하는 ’V'도 사용된다. 이들은 기호의 종류만 다를 뿐 ‘임의의 참조형 타입’을 의미한다는 것은 모두 같다. 마치 수학식 ‘f(x, y) = x + y’가 ‘f(k, v) = k + v'와 다르지 않은 것처럼 말이다.
기존에는 다양한 종류의 타입을 다루는 메서드의 매개변수나 리턴타입으로 Object타입의 참조변수를 많이 사용했고, 그로 인해 형변환이 불가피했지만, 이젠 Object타입 대신 원하는 타입을 지정하기만 하면 되는 것이다.
[참고] 타입을 지정하지 않으면 Object타입으로 간주된다.
제네릭스는 자바의 부가적인 기능 중에 하나일 뿐이다. 사용하면 편리하긴 하지만 사용하지 않아도 그만이다. 저자의 개인적인 생각으로는 프로그래밍을 처음 배우는 사람에게는 학습부담이 될 까 다소 우려스럽다. 다형성이나 형변환, 타입체크 등의 중요한 기본원칙에 대해서 완전히 이해하지 못한 상태에서 원칙에서 벗어나는 기능인 제네릭을 배운다면 혼란스러울 수 있기 때문이다.
일단 편안하게 읽어보고 이해가 잘 안 간다면, 부담없이 건너뛰고 다음 진도로 넘어가길 바란다. 자바가 좀 더 익숙해 진 후에 다시 보면, 보다 쉽게 이해가 갈 수 있는 부분이기 때문이다.
4.1 ArrayList<E>
대표적인 컬렉션 클래스인 ArrayList를 통해 제네릭스를 사용하는 방법을 설명해나갈 것이다. 대부분의 컬렉션 클래스들에 대해서도 동일한 방식으로 사용하면 된다.
public class ArrayList extends AbstractList
implements List, RandomAccess, Cloneable, java.io.Serializable
{
private transient Object[] elementData;
public boolean add(Object o) { /* 내용생략 */ }
public Object get(int index) { /* 내용생략 */ }
...
}
위의 코드는 제네릭스가 도입되기 이전의 ArrayList의 소스이고 아래의 소스는 제네릭스가 도입된 이후의 소스이다. Object타입 대신 임의의 타입 ‘E'가 사용된 것을 알 수 있다.
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private transient E[] elementData;
public boolean add(E o) { /* 내용생략 */ }
public E get(int index) { /* 내용생략 */ }
...
}
위의 코드에 사용된 'E'는 요소를 뜻하는 'Element'의 약자로, ‘E' 대신 어떤 다른 문자를 사용해도 상관없지만 소스코드 내에서 같은 문자를 일관되게 사용해야한다.
이는 마치 메서드의 매개변수이름을 다르게 해도 메서드 내에서만 같은 이름을 사용하면 문제없는 것과 같다. 만일 타입을 지정하지 않으면 ‘E’는 Object타입으로 간주된다.
임의의 타입 ‘E'는 ArrayList타입의 참조변수를 선언하거나 ArrayList를 생성할 때 지정할 수 있으며, 만일 ArrayList에 Tv타입의 객체만을 저장하기 위해 ‘ArrayList<Tv> tvList = new ArrayList<Tv>();’와 같이한다면 ’E'는 ‘Tv'가 되는 것이다.
public class ArrayList<Tv> extends AbstractList<Tv>
implements List<Tv>, RandomAccess, Cloneable,
java.io.Serializable
{
private transient Tv[] elementData;
public boolean add(Tv o) { /* 내용생략 */ }
public Tv get(int index) { /* 내용생략 */ }
...
}
아래와 같이 컬렉션 클래스 이름 바로 뒤에 저장할 객체의 타입을 적어주면, 컬렉션에 저장할 수 있는 객체는 지정한 타입의 객체뿐이다.
컬렉션클래스<저장할 객체의 타입> 변수명 = new 컬렉션클래스<저장할 객체의 타입>();
ArrayList<Tv> tvList = new ArrayList<Tv>();
아래의 코드는 ArrayList에 Tv객체만 저장할 수 있도록 작성한 것이다. Tv타입이 아닌 객체를 저장하려하면 컴파일 시에 에러가 발생한다.
// Tv객체만 저장할 수 있는 ArrayList를 생성
ArrayList<Tv> tvList = new ArrayList<Tv>();
tvList.add(new Tv());
tvList.add(new Audio()); // 컴파일 에러 발생!!!
저장된 객체를 꺼낼 때는 형변환할 필요가 없다. 이미 어떤 타입의 객체들이 저장되어 있는지 알고 있기 때문이다. 제네릭스를 적용한 코드(오른쪽)와 그렇지 않은 코드(왼쪽)를 잘 비교해보자.
ArrayList tvList = new ArrayList(); tvList.add(new Tv()); Tv t = (Tv)tvList.get(0);
| ArrayList<Tv> tvList = new ArrayList<Tv>(); tvList.add(new Tv()); Tv t = tvList.get(0);
|
만일 다형성을 사용해야하는 경우에는 조상타입을 지정함으로써 여러 종류의 객체를 저장할 수 있다.
class Product{}
class Tv extends Product{}
class Audio extends Product{}
// Product클래스의 자손객체들을 저장할 수 있는 ArrayList를 생성
ArrayList<Product> list = new ArrayList<Product>();
list.add(new Product());
list.add(new Tv()); // 컴파일 에러가 발생하지 않는다.
list.add(new Audio()); // 컴파일 에러가 발생하지 않는다.
Product p = list.get(0); // 형변환이 필요없다.
Tv t = (Tv)list.get(1); // 형변환을 필요로 한다.
ArrayList가 Product타입의 객체를 저장하도록 지정하면, 이들의 자손인 Tv와 Audio타입의 객체도 저장할 수 있다. 다만 꺼내올 때 원래의 타입으로 형변환해야 한다.
제네릭스에서도 다형성을 적용해서 아래와 같이 할 수 있다.
List<Tv> tvList = new ArrayList<Tv>(); // 허용
그러나 Product클래스가 Tv클래스의 조상이라 할지라도 아래와 같이 할 수는 없다.
ArrayList<Product> list = new ArrayList<Tv>(); // 허용 안함~!!!
그래서 아래와 같이 메서드의 매개변수 타입이 ArrayList<Product>로 선언된 경우, 이 메서드의 매개변수로는 ArrayList<Product>타입의 객체만 사용할 수 있다. 그렇지 않으면 컴파일 에러가 발생한다.
public static void printAll(ArrayList<Product> list) {
for(Unit u : list) { // 향상된 for문, 부록 참고
System.out.println(u);
}
}
public static void main(String[] args) {
ArrayList<Product> productList = new ArrayList<Product>();
ArrayList<Tv> tvList = new ArrayList<Tv>();
printAll(productList);
printAll(tvList); // 컴파일 에러 발생~!!!
}
컬렉션에 저장될 객체에도 다형성이 필요할 때도 있을 것 같은데 다형성을 사용할 수는 없을까?
물론 방법이 있다. 와일드 카드‘?’를 사용하면 된다. 보통 제네릭에서는 단 하나의 타입을 지정하지만, 와일드 카드는 하나 이상의 타입을 지정하는 것을 가능하게 해준다. 아래와 같이 어떤 타입('?')이 있고 그 타입이 Product의 자손이라고 선언하면, Tv객체를 저장하는 ‘ArrayList<Tv>’ 또는 Audio객체를 저장하는 ‘ArrayList<Audio>’를 매개변수로 넘겨줄 수 있다. Tv와 Audio 모두 Product의 자손이기 때문이다.
// Product 또는 그 자손들이 담긴 ArrayList를 매개변수로 받는 메서드
public static void printAll(ArrayList<? extends Product> list) {
for(Unit u : list) {
System.out.println(u);
}
}
만일 아래와 같은 코드가 있다면 타입을 별도로 선언함으로써 코드를 간략히 할 수 있다.
public static void printAll(ArrayList<? extends Product> list, ArrayList<? extends Product> list2) {
for(Unit u : list) { System.out.println(u); } }
|
public static <T extends Product> void printAll(ArrayList<T> list, ArrayList<T> list2) {
for(Unit u : list) { System.out.println(u); } }
|
두 번째 코드는 ‘T’라는 타입이 Product의 자손타입이라는 것을 미리 정의해 놓고 사용한 것이다. 위의 두 코드는 서로 같은 의미의 코드이므로 잘 비교해보자.
[주의] 여기서 만일 Product가 클래스가 아닌 인터페이스라 할지라도 키워드로 'implements'를 사용하지 않고 클래스와 동일하게 'extends'를 사용한다는 것에 주의하자.
[예제11-80]/ch11/GenericsEx1.java |
import java.util.*;
class Product {} class Tv extends Product{} class Audio extends Product{}
class GenericsEx1 { public static void main(String[] args) { ArrayList<Product> productList = new ArrayList<Product>(); ArrayList<Tv> tvList = new ArrayList<Tv>();
productList.add(new Tv()); productList.add(new Audio());
tvList.add(new Tv()); tvList.add(new Tv());
printAll(productList); // printAll(tvList); // 컴파일 에러가 발생한다.
printAll2(productList); // ArrayList<Product> printAll2(tvList); // ArrayList<Tv> }
public static void printAll(ArrayList<Product> list) { for(Product p : list) { System.out.println(p); } }
// public static void printAll2(ArrayList<? extends Product> list) { public static <T extends Product> void printAll2(ArrayList<T> list) { for(Product p : list) { System.out.println(p); } } } |
|
[실행결과] |
Tv@9cab16 Audio@1a46e30 Tv@9cab16 Audio@1a46e30 Tv@3e25a5 Tv@19821f |
|
이 예제의 결과는 별 의미가 없다. 예제를 변경해가면서 전에 설명한 내용을 직접 테스트 해보자.
한 가지 설명하고 넘어갈 것은 예제에 사용된 for문의 형태인데, 이 새로운 구문의 for문은 ‘향상된 for문’이라고 한다. list에 담긴 모든 요소를 반복할 때마다 하나씩 가져다 Product타입의 참조변수 p에 저장한다. 아래의 두 코드는 같은 것이니 잘 비교해 보자.
[참고] 향상된 for문에 대한 보다 자세한 내용은 책의 마지막에 있는 ‘부록’을 참고하라.
Iterator it = list.iterator();
for(;it.hasNext();) { Product p = (Product)it.next(); System.out.println(p); }
| for(Product p : list) { System.out.println(p); } |
4.2 Iterator<E>
다음은 Iterator의 실제 소스인데, 컬렉션 클래스 뿐 만아니라 Iterator에도 제네릭스가 적용되어 있는 것을 알 수 있다.
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
아래의 예제는 Iterator에 제네릭스를 적용한 예이다.
[예제11-81]/ch11/GenericsEx2.java |
import java.util.*;
class GenericsEx2 { public static void main(String[] args) { ArrayList<Student> list = new ArrayList<Student>(); list.add(new Student("자바왕",1,1)); list.add(new Student("자바짱",1,2)); list.add(new Student("홍길동",2,1)); list.add(new Student("전우치",2,2));
Iterator<Student> it = list.iterator();
while(it.hasNext()) { Student s = it.next(); System.out.println(s.name); } } // main }
class Student { String name = ""; int ban; int no;
Student(String name, int ban, int no) { this.name = name; this.ban = ban; this.no = no; } } |
|
[실행결과] |
자바왕 자바짱 홍길동 전우치 |
|
ArrayList에 Student객체를 저장할 것이라고 지정을 했어도 Iterator타입의 참조변수를 선언할 때 저장된 객체의 타입을 지정해주지 않으면, Iterator의 next()를 호출할 때 형변환을 해야 한다.
Iterator it = list.iterator();
while(it.hasNext()) { Student s = (Student)it.next(); System.out.println(s.name); }
| Iterator<Student> it = list.iterator();
while(it.hasNext()) { Student s = it.next(); System.out.println(s.name); }
|
4.3 Comparable<T>과 Collections.sort()
클래스의 기본 정렬기준을 구현하는 Comparable인터페이스에도 제네릭스가 적용된다. 먼저 예제를 살펴보자. 이 예제는 Comparable를 사용해서 학생들의 총점을 기준으로 내림차순 정렬하여 출력한다.
[예제11-82]/ch11/GenericsEx3.java |
import java.util.*;
class GenericsEx3 { public static void main(String[] args) { ArrayList<Student> list = new ArrayList<Student>(); list.add(new Student("자바왕",1,1,100,100,100)); list.add(new Student("자바짱",1,2,90,80,70)); list.add(new Student("홍길동",2,1,70,70,70)); list.add(new Student("전우치",2,2,90,90,90));
Collections.sort(list); // list를 정렬한다.
Iterator<Student> it = list.iterator();
while(it.hasNext()) { Student s = it.next(); System.out.println(s); } } }
class Student implements Comparable<Student> { String name = ""; int ban = 0; int no = 0; int koreanScore = 0; int mathScore = 0; int englishScore = 0;
int total = 0;
Student(String name, int ban, int no, int koreanScore, int mathScore, int englishScore) { this.name = name; this.ban = ban; this.no = no; this.koreanScore = koreanScore; this.mathScore = mathScore; this.englishScore = englishScore;
total = koreanScore + mathScore + englishScore; }
public String toString() { return name + "\t" + ban + "\t" + no + "\t" + koreanScore + "\t" + mathScore + "\t" + englishScore + "\t" + total + "\t"; }
public int compareTo(Student o) { return o.total - this.total; } } // end of class Student |
|
[실행결과] |
자바왕 1 1 100 100 100 300 전우치 2 2 90 90 90 270 자바짱 1 2 90 80 70 240 홍길동 2 1 70 70 70 210 |
|
학생들의 정보를 담은 Student인스턴스를 ArrayList에 담은 다음, Collection.sort()를 이용해서 Student클래스에 정의된 기본정렬(총점별로 내림차순)로 정렬해서 출력하는 간단한 예제이다.
먼저 제네릭스가 적용된 Comparable의 실제 소스를 보면 아래와 같다.
public interface Comparable<T> {
public int compareTo(T o); // 지정한 타입 T를 매개변수로 한다.
}
여기서 타입 ‘T’대신 타입 ‘Student’를 지정했기 때문에 아래와 같이 된다.
public interface Comparable<Student> {
public int compareTo(Student o); // 지정한 타입 T를 매개변수로 한다.
}
만일 이 예제에서 제네릭스를 사용하지 않았다면, 왼쪽의 코드 대신 오른쪽과 같은 코드를 사용해야 했을 것이다.
public int compareTo(Student o) { return o.total - this.total; }
| public int compareTo(Object o) { int result = -1;
if(o instanceof Student) { Student tmp = (Student)o; result = tmp.total-this.total; } return result; }
|
한 눈에 봐도 왼쪽의 코드가 더 간결하다는 것을 알 수 있다. 타입이 미리 체크되어 있기 때문에 instanceof로 타입을 체크하거나 형변환할 필요가 없기 때문이다.
여기서 Collections.sort()에 적용된 제네릭스에 대해서 좀 더 자세히 살펴볼 필요가 있다.
다음은 Collections.sort()의 선언부인데 지금까지 보던 것과는 달리 좀 복잡하다.
public static <T extends Comparable<? super T>> void sort(List<T> list)
② ①
① ArrayList와 같이 List인터페이스를 구현한 컬렉션을 매개변수의 타입으로 정의하고 있다. 그리고 그 컬렉션에는 'T'라는 타입의 객체를 저장하도록 선언되어 있다.
② 'T'는 Comparable인터페이스를 구현한 클래스의 타입이어야 하며(<T extends Comparable>), 'T'또는 그 조상의 타입을 비교하는 Comparable이어야한다는 것(Comparable<? super T>)을 의미한다.
만일 Student클래스가 Person클래스의 자손이라면, <? super T>는 Student타입이나 Person타입이 가능하다.(물론 Object타입도 가능)
<? extends T> - T 또는 T의 자손 타입을 의미한다.
<? super T> - T 또는 T의 조상 타입을 의미한다.
앞서 배운 <? extends T>와 반대로 <? super T>는 T 또는 T의 조상 타입을 의미한다.
[예제11-83]/ch11/GenericsEx4.java |
import java.util.*;
class GenericsEx4 { public static void main(String[] args) { ArrayList<Student> list = new ArrayList<Student>(); list.add(new Student("자바왕",1,1,100,100,100)); list.add(new Student("자바짱",1,2,90,80,70)); list.add(new Student("홍길동",2,1,70,70,70)); list.add(new Student("전우치",2,2,90,90,90));
Collections.sort(list); // list를 정렬한다.
Iterator<Student> it = list.iterator();
while(it.hasNext()) { Student s = it.next(); System.out.println(s); } } // main }
// <T extends Comparable<? super T>>에서 'T'가 Student타입이므로 // <Student extends Comparable<Student>>와 // <Student extends Comparable<Person>>이 가능하다. class Student extends Person implements Comparable<Person> { String name = ""; int ban = 0; int no = 0; int koreanScore = 0; int mathScore = 0; int englishScore = 0;
int total = 0;
Student(String name, int ban, int no, int koreanScore, int mathScore, int englishScore) { super(ban+"-"+no, name); this.name = name; this.ban = ban; this.no = no; this.koreanScore = koreanScore; this.mathScore = mathScore; this.englishScore = englishScore;
total = koreanScore + mathScore + englishScore; }
public String toString() { return name + "\t" + ban + "\t" + no + "\t" + koreanScore + "\t" + mathScore + "\t" + englishScore + "\t" + total + "\t"; }
// Comparable<Person>이므로 Person타입의 매개변수를 선언. public int compareTo(Person o) { return id.compareTo(o.id); // String클래스의 compareTo()를 호출 }
} // end of class Student
class Person { String id; String name;
Person(String id, String name) { this.id = id; this.name = name; } } |
|
[실행결과] |
자바왕 1 1 100 100 100 300 자바짱 1 2 90 80 70 240 홍길동 2 1 70 70 70 210 전우치 2 2 90 90 90 270 |
|
이전 예제의 Student클래스를 변경하여 Person클래스의 자손이 되도록 하고, Student의 조상인 Person을 Comparable의 타입으로 지정하였다.
Collections.sort()의 매개변수가 ‘<T extends Comparable<? super T>>’이기 때문에 예제에서는 'T'가 Student타입이므로 ‘<Student extends Comparable<Person>>' 또는 ‘<Student extends Comparable<Student>>'가 가능하며, 그 중에서 ‘<Student extends Comparable<Person>>'의 경우를 보여주고 있다.
// <T extends Comparable<? super T>>에서 'T'가 Student타입이므로
// <Student extends Comparable<Student>>와
// <Student extends Comparable<Person>>이 가능하다.
class Student extends Person implements Comparable<Person> {
...
Person클래스에 정의된 멤버변수 id는 Student클래스의 멤버변수 ban과 no를 문자열로 붙여서 만들었기 때문에, 실행결과를 보면 전과 달리 반과 번호를 기준으로 오름차순 정렬되어 있는 것을 알 수 있다.
4.4 HashMap<K,V>
HashMap처럼 데이터를 키(key)와 값(value)의 형태로 저장하는 컬렉션 클래스는 지정해 줘야할 타입이 두 개이다. 그래서 ‘<K,V>’와 같이 두 개의 타입을 콤마‘,’로 구분해서 적어줘야 한다. 여기서 ’K'와 ‘V'는 각각 'Key'의 ’Value'의 첫 글자에서 따온 것일 뿐, 'T'나 ‘E'와 마찬가지로 임의의 참조형 타입(reference type)을 의미한다.
다음은 HashMap의 실제 소스이다.
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
{
...
public V get(Object key) { /* 내용 생략 */ }
public V put(K key, V value) { /* 내용 생략 */ }
public V remove(Object key) { /* 내용 생략 */ }
...
}
만일 키의 타입이 String이고 저장할 값의 타입이 Student인 HashMap을 생성하려면 다음과 같이 한다.
HashMap<String, Student> map = new HashMap<String, Student>(); // 생성
map.put("자바왕", new Student("자바왕",1,1,100,100,100)); // 데이터 저장
위와 같이 HashMap을 생성하였다면, HashMap의 실제 소스는 'K'대신 String이, 'V'대신 Student가 사용되어 아래와 같이 바뀌는 셈이 된다.
public class HashMap<String,Student>
extends AbstractMap<String,Student>
implements Map<String,Student>, Cloneable, Serializable
{
...
public Student get(Object key) { /* 내용 생략 */ }
public Student put(String key, Student value) { /* 내용 생략 */ }
public Student remove(Object key) { /* 내용 생략 */ }
...
}
여기서도 타입을 지정하지 않으면 Object타입으로 간주된다.
[예제11-84]/ch11/GenericsEx5.java |
import java.util.*;
class GenericsEx5 { public static void main(String[] args) { HashMap<String,Student> map = new HashMap<String,Student>(); map.put("1-1", new Student("자바왕",1,1,100,100,100)); map.put("1-2", new Student("자바짱",1,2,90,80,70)); map.put("2-1", new Student("홍길동",2,1,70,70,70)); map.put("2-2", new Student("전우치",2,2,90,90,90));
Student s1 = map.get("1-1"); System.out.println("1-1 :" + s1.name);
Iterator<String> itKey = map.keySet().iterator();
while(itKey.hasNext()) { System.out.println(itKey.next()); }
Iterator<Student> itValue = map.values().iterator(); int totalSum = 0;
while(itValue.hasNext()) { Student s = itValue.next(); totalSum += s.total; }
System.out.println("전체 총점:"+totalSum); } // main }
class Student extends Person implements Comparable<Person> { String name = ""; int ban = 0; int no = 0; int koreanScore = 0; int mathScore = 0; int englishScore = 0;
int total = 0;
Student(String name, int ban, int no, int koreanScore, int mathScore, int englishScore) { super(ban+"-"+no, name); this.name = name; this.ban = ban; this.no = no; this.koreanScore = koreanScore; this.mathScore = mathScore; this.englishScore = englishScore;
total = koreanScore + mathScore + englishScore; }
public String toString() { return name + "\t" + ban + "\t" + no + "\t" + koreanScore + "\t" + mathScore + "\t" + englishScore + "\t" + total + "\t"; }
// Comparable<Person>이므로 Person타입의 매개변수를 선언. public int compareTo(Person o) { return id.compareTo(o.id); // String클래스의 compareTo()를 호출 }
} // end of class Student
class Person { String id; String name;
Person(String id, String name) { this.id = id; this.name = name; } } |
|
[실행결과] |
1-1 :자바왕 2-2 1-2 1-1 2-1 전체 총점:1020 |
|
이전 예제를 ArrayList대신 HashMap을 사용하도록 변경한 것일 뿐 별로 특별한 것은 없다. HashMap에서 값을 꺼내오는 get(Object key)를 사용할 때, 그리고 저장된 키와 값들을 꺼내오는 ketSet()과 values()를 사용할 때 형변환을 하지 않아도 된다.
Student s1 = (Student)map.get("1-1");
| Student s1 = map.get("1-1");
|
출처 - http://cafe.naver.com/javachobostudy