공부했던 자료 정리하는 용도입니다.

재배포, 수정하지 마세요.

 

 

 

 

지네릭스(Generics)

  지네릭스는 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의  타입 체크(compile-time type check)를 해주는 기능이다. 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다. 의도하지 않은 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여줘 타입 안정성을 높인다.

 

 

지네릭스의 용어

class Box<T> { }
 Box<T>  : 지네릭 클래스. 'T의 Box' 또는 'T Box'라고 읽는다.
 T  : 타입 변수 또는 타입 매개변수(T는 타입문자, 다른 문자를 써도 된다)
 Box  : 원시 타입(raw type)
 < >  : 다이아몬드 연산자라고 부른다.

 

 

 

 

지네릭 클래스의 선언과 사용

class Box<T>{	// 지네릭 타입 T선언
    T item;
    
    void setItem(T item) { this.item = item; }
    T getItem() { return item; }
}

위의 코드에서  T (Type의 첫 글자를 따서)는 '타입 변수(type variable)'라고 하며 '임의의 참조형 타입'을 의미한다.( T 말고 다른 영문자를 사용해도 된다.) 타입 변수가 여러 개인 경우  Map<K, V> 와 같이 콤마( , )를 구분자로 쓴다.

 

 

Box<String> b = new Box<String>();	// 타입 T대신, 실제 타입을 지정
//b.setItem(new Object());	//위에서 String타입으로 지정해주었기 때문에 다른타입은 지정불가
b.setItem("ABC");	//String타입은 가능
String item = b.getItem();	//형변환할 필요 X

지네릭 클래스가 된 객체를 생성할 때는 위와 같이 참조 변수와 생성자에 타입  T 대신에 사용될 실제 타입을 지정해 주어야 한다. 지네릭이 도입되기 이전의 코드와 호환을 위해, 지네릭 클래스인데도 예전의 방식으로 객체를 생성하는 것이 허용은 된다. 그러나 지네릭 타입을 지정하지 않아서 안전하기 않다는 경고가 발생한다. 또한 타입 변수  T   Object 타입을 지정하면, 타입을 지정하지 않은 것이 아니라 알고 적은 것이므로 경고가 발생하지 않는다. 

 

 

 

 

지네릭스의 제한

Box<Apple> appleBox = new Box<Apple>();
Box<Grape> grapeBox = new Box<Grape>();

지네릭 클래스  Box 의 객체를 생성한다고 하면 위의 코드같이 객체별로 다른 타입을 지정하는 것은 당연히 가능하다. 그러나  T 는 인스턴스 변수로 간주되기 때문에, (모든 객체에 대해 동일하게 동작해야 하는) static 멤버에는 타입 변수  T 를 사용할 수 없다.  

 

지네릭 배열 타입의 참조 변수를 선언하는 것은 가능하지만 지네릭 타입의 배열을 생성하는 것은 허용되지 않는다. 이유는  new  연산자 때문인데,  new 연산자는 컴파일 시점에 타입  T 가 뭔지 정확히 알아야 하기 때문이다.  instanceof 연산자도 마찬가지로  T 를 피연산자로 사용할 수 없다. 꼭 지네릭 배열을 생성해야 할 필요가 있을 때는,  new 연산자 대신 'Reflection API'의  newInstance( ) 와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나,  Object 배열을 생성해서 복사한 다음에  T[ ] 로 형변환하는 방법 등을 사용한다.

 

 

 

 

지네릭 클래스의 객체 생성과 사용

// 지네릭 클래스의 객체를 생성할 때는 참조변수와 생성자에 대입된 타입(매개변수화된 타입)이 일치해야 한다.
Box<Apple> appleBox = new Box<Apple>();	//가능
//Box<Apple> appleBox = new Box<Grape>();	//에러, 참조변수와 생성자에 대입된 타입이 일치하지 X


//두 타입이 상속관계에 있어도 마찬가지이다
//Box<Fruit> appleBox = new Box<Apple>();	//에러, 대입된 타입이 다르다.


//단, 두 지네릭 클래스의 타입이 상속관계에 있고, 대입된 타입이 같은 것은 가능하다.
//FruitBox가 Box의 자손이라고 가정
Box<Apple> appleBox = new FruitBox<Apple>();	//가능. 다형성

지네릭 클래스의 객체를 생성할 때는 위의 예시의 경우에만 가능하다.

(JDK 1.7부터는 추정이 가능한 경우 타입을 생략할 수 있다)

 

 

//지네릭 클래스의 객체에 메서드'void add(T item)'로 객체를 추가할 때, 대입된 타입과 다른 타입의 객체는 추가할 수 없다.
Box<Apple> appleBox = new Box<Apple>();
appleBox.add(new Apple());	//가능
appleBox.add(new Grape());	//에러. Apple객체만 추가 가능하다


//그러나 타입변수가 조상타입인 경우 자손들은 메서드의 매개변수가 될 수 있다.
//(Apple이 Fruit의 자손이라고 가정)
Box<Fruit> fruitBox = new Box<Fruit>();
fruitBox.add(new Fruit());	//가능
fruitBox.add(new Apple());	//마찬가지로 자손도 가능

생성된 객체에  void add(T item) 메서드로 객체를 추가한다고 가정하면, 대입된 타입과 다른 타입의 객체는 추가할 수 없지만 타입 변수가 조상 타입일 경우 자손들은 메서드의 매개변수가 될 수 있다.

 

 

 

 

제한된 지네릭 클래스

//지네릭 타입에 extends를 사용하면 특정타입의 자손들만 대입할 수 있게 제한할 수 있다.
class FruitBox<T extends Fruit>	{	// Fruit의 자손만 타입으로 지정 가능
	ArrayList<T> list = new ArrayList<T>;
    ...
}

//인터페이스를 구현해야한다는 제약이 필요할 때도 extends를 사용한다(implements가 X)
intergace Eatable{}
class FruitBox<T extends Eatable> { ... }

//자손이면서 인터페이스도 구현해야 한다면 &로 연결한다.
class Fruit<T extends Fruit & Eatable> { ... }

지네릭 타입에  extends 를 사용하면, 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.

인터페이스를 구현해야 한다는 제약을 할 때도  extends 를 사용한다( implements 가 아님)

자손이면서 인터페이스도 구현해야 한다면  & 기호로 연결한다.

 

 

 

 

와일드카드

<? extends T> : 와일드 카드의 상한 제한. T와 그 자손들만 가능
<? super T> : 와일드 카드의 하한 제한. T와 그 조상들만 가능
<?> : 제한없음. 모든 타입이 가능. <? extends Object>와 동일

 static 메서드에는 지네릭 타입을 사용할 수 없다. 또한 지네릭 타입이 다른 것으로는 오버로딩도 성립하지 않는다. 지네릭 타입은 컴파일러가 컴파일할 때만 사용하고 제거해 버리기 때문이다. 이럴 때 사용하라고 만들어진 것이 와일드카드이다.

 

 Comparator 의 경우에는 거의 대부분  <? super T> 가 붙는다. 와일드카드를 사용하면 조상 클래스에만  comparator 를 구현해놓고 사용하면 되기 때문이다. (와일드카드를 사용하지 않으면 자손이 생길 때마다 비교하는 코드를 별도로 구현해야 한다)

 

 

 

 

 

지네릭 메서드

static <T> void sort(LIst<T> list, Comparator<? super T> c)

메서드의 선언부에 지네릭 타입이 선언된 메서드를 지네릭 메서드라고 한다. 지네릭 타입의 선언 위치는 반환 타입 바로 앞이다.

 

 

class FruitBox<T>{
	...
    static <T> void sort(List<T>, list, Copmparator<? super T> c){
    	...
    }
}

지네릭 클래스에 정의된 타입 매개변수와 지네릭 메서드에 정의된 타입 매개변수는 같은 타입 문자  T 를 사용하지만 서로 다른 것이다. 원래  static 멤버에는 타입 매개변수를 사용할 수 없지만, 메서드에 지네릭 타입을 선언하고 사용하는 것은 가능하다. 지역변수를 선언한 것과 비슷해서 메서드 내에서만 지역적으로 사용될 것이므로 메서드가  static 이건 아니건 상관이 없다.

 

 


//지네릭 메서드로 바꾸기 전
static Juice makeJuice(FruitBox<? extends Fruit> box){ ... }

//지네릭 메서드로 바꾼 후
static <T extends Fruit> Juice makeJuice(FruitBox<T> box){ ... }

만약 위의  makeJuice 라는 메서드를 지네릭 메서드로 바꾸면 위의 코드처럼 된다.

매개변수의 타입이 복잡할 때도 유용하게 사용된다. 타입을 별도로 선언함으로써 코드를 간략히 할 수 있다.


 

//지네릭 메서드를 호출할 때는 아래와 같이 타입 변수에 타입을 대입해야 한다.
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();
...
System.out.println(Juicer.<Fruit>makeJuice(fruitBox));
System.out.println(Juicer.<Apple>makeJuice(appleBox));


//하지만 대부분의 경우 컴파일러가 타입을 추정할 수 있기 때문에 생략가능하다.
System.out.println(Juicer.makeJuice(fruitBox));	//대입된 타입 생략가능
System.out.println(Juicer.makeJuice(appleBox));


//주의) 대입된 타입을 생략할 수 없는 경우에는 참조변수나 클래스 이름을 생략할 수 없다.
System.out.println(<Fruit>makeJuice(fruitBox));	//에러, 클래스 이름 생략 불가
System.out.println(this.<Fruit>makeJuice(fruitBox)); //가능
System.out.println(Juicer.<Fruit>makeJuice(fruitBox));	//가능

지네릭 메서드를 호출할 때는 타입 변수에 타입을 대입해야 한다. 하지만 대부분의 경우 컴파일러가 타입을 추정할 수 있기 때문에 생략 가능하다. 지네릭 메서드를 호출할 때, 대입된 타입을 생략할 수 없는 경우에는 참조 변수나 클래스 이름을 생략할 수 없다는 것을 주의해야 한다. 같은 클래스 내에 있는 멤버들끼리는  this 나 클래스 이름을 생략하고 메서드 이름만으로 호출이 가능하지만, 대입된 타입이 있을 때는 반드시 써줘야 한다.

 

 

 

 

지네릭 타입의 형변환

 Optional<Object>  →  Optional<T>  : 형변환 불가능
 Optional<Object>  →  Optional<?>  →  Optional<T>  : 형변환 가능하지만 경고발생

 Optional<Object>  Optional<String> 으로 직접 형변환 하는것은 불가능하지만, 와일드카드가 포함된 지네릭 타입으로 형변환하면 가능하다. 대신 확인되지 않는 타입으로의 형변환이라는 경고가 발생한다.

 

FruitBox<? extends Object> objBox = null;
FruitBox<? extends String> strBox = null;

strBox = (FruitBox<? extends String>)objBox;	//가능. 미확정 타입으로 형변환 경고
objBox = (FruitBox<? extends Object>)strBox;	//가능. 미확정 타입으로 형변환 경고

와일드카드가 사용된 지네릭 타입끼리도 형변환이 가능하지만,  와일드카드는 확정된 타입이 아니라서 컴파일러는 미확정 타입으로 형변환 하는 것이라고 경고한다.

 

 

 

 

지네릭 타입의 제거

  컴파일러는 지네릭 타입을 이용해서 소스파일을 체크하고, 필요한 곳에 형변환을 넣어준 뒤 지네릭 타입을 제거한다. 그래서 컴파일된 파일( *.class )에는 지네릭 타입에 대한 정보가 없다. 이는 지네릭이 도입되기 이전의 소스코드들과의 호환성을 유지하기 위해서이다. JDK 1.5부터 지네릭스가 도입되었지만, 아직도 원시 타입을 사용해서 코드를 작성하는 것을 허용한다. 하지만 가능한 원시 타입을 사용하지 않는 것이 좋다. 

 


    지네릭 타입의 제거 과정

  1. 지네릭 타입의 경계(bound)를 제거한다.
    지네릭 타입이  <T extends Fruit> 라면  T  Fruit 으로 치환된다.  <T> 인 경우에  T  Object 로 치환된다. 그리고 클래스 옆의 선언은 제거된다.

  2. 지네릭 타입을 제거한 후에 타입이 일치하지 않으면 형변환을 추가한다.
    와일드 카드가 포함되어 있는 경우에는 추가적인 형변환이 일어난다.

 

 

 

'Back end > Java' 카테고리의 다른 글

[Java] Properties, Collections  (0) 2019.07.26
[Java] 해싱(hashing)과 해시함수(hash function)  (0) 2019.07.25
[Java] HashMap과 Hashtable, TreeMap  (0) 2019.07.25
[Java] HashSet, TreeSet  (0) 2019.07.24
[Java] Iterator, Arrays, Comparator  (0) 2019.07.23

+ Recent posts