[JAVA] 지네릭스 (Generics) ②

DongGyu Jung·2022년 3월 9일
0

자바(JAVA)

목록 보기
40/60
post-thumbnail

🏃‍♂️ 들어가기 앞서..

본 게시물은 스터디 활동 중에 작성한 게시물로 자바의 정석-기초편 교재를 학습하여 정리하는 글입니다.
※ 스터디 Page : 〔투 비 마스터 : 자바〕

*해당 교재의 목차 순서와 구성을 참고하여 작성하며
각 내용마다 부족할 수 있는 내용이나 개인적으로 궁금한 점은
추가적인 검색을 통해 채워나갈 예정입니다.

"지네릭스""제네릭스" 발음 혼동주의....쓰다보니 계속 번갈아가면서 쓰게 되서..


🧶 제네릭 클래스

🚧 제한된 제네릭(Generic) 클래스

이전 글을 보게되면

타입 문자
즉, 타입 변수로 사용할 타입을 대입 하면
한 종류의 타입만 저장할 수 있게끔 타입 체크를 해준다는 것을 알 수 있었다.

하지만 대입을 하는데에 있어서는
모든 종류의 타입을 지정할 수 있다는 것도 알 수 있다.
( 타입이 뭐든 상관없이 입력된 타입에 맞춰 체크하는 것 )

이번에는
이 " 대입할 수 있는 타입을 제한 "하는 방법에 대해 알아보자.

대입 타입의 범위를 제한하는 방법으로는
이전 조상 클래스-자식 클래스간 상속에서 사용했었던
extends를 사용하는 방법이 있다.

class FruitBox<T extends Fruit> { // Fruit의 """자손"""만 타입으로 지정하도록 제한
	ArrayList<T> list = new ArrayList<T>() ;
    ...
}

/*
Apple : Fruit 상속 자손 클래스
Toy : 연관없는 다른 타 클래스
*/
FruitBox<Apple> appleBox = new FruitBox<Apple>() ; // Apple은 Fruit의 자손이기 때문에 대입 성공

// extends로 제한을 걸지 않았다면 <Toy> 타입에 맞게끔 제네릭 타입 변수 지정이 가능했지만 
// Fruit 클래스를 extends했기 때문에 불가능
FruitBox<Toy> toyBox = new FruitBox<Toy>() ; 

하지만 주의할 점은,
여기서 extends를 썼다고해서

인터페이스를 구현해야 하는 상황에서
인터페이스도 똑같이 implements를 사용하게 되면 안된다.

만약 인터페이스를 구현해야 한다는 제약이 필요하면
이 상황 또한
extends를 사용해야 한다.

또한 " 다중 extends "가 가능하다는 특징이 있기 때문에
어떤 클래스를 상속받아야하며 어떤 인터페이스를 구현해야한다는 조건이 붙게되면
& 기호로 연결해서 해결하면 된다.
( , 아님 주의)

/*
- 조건 : Eatable 인터페이스를 구현한 객체만 들어와야 한다.
*/
interface Eatable {}
// class FruitBox<T implements Eatable> {...} - XXXXXXX 절대 주의
class FruitBox<T extends Eatable> {...}

/*
+ 조건 : Fruit의 자손클래스 타입의 객체만 들어와야 한다
*/
class FruitBox<T extends Fruit & Eatable> {...}

🚧 제약

이렇게
어떤 지네릭 클래스 객체를 생성할 때,
" 타입 변수 대입 "을 객체별로 다르게 대입해서
구성할 객체 타입을 단일화 시키고
타입별로 그 타입과 일치하는 타입의 객체 생성이 이루어진다.

이 말은 즉슨, 지네릭스는 " 인스턴스별로 다르게 동작한게끔 만든다는 말 "인데
만약 모든 객체에 동일하게 사용되어야 하는 static 멤버의 경우는
지네릭스가 적용되게 되면 곤란하다.

각 인스턴스별로 동작하게 된다면 static 멤버로서의 의미가 사라지는 것이기 때문이다.
또한 타입변수 T는 " 인스턴스 변수 "로 간주된다.

그렇기 때문에
static멤버는 타입변수 T를 사용할 수 없다.
( 지금까지 배워온걸 되짚어보면 자연스레 static멤버는 인스턴스 변수를 참조할 수 없다는 것도 알 수 있다. )

class Box<T> {
	static T item ; // 불가능 _ static은 모든 객체에 동일하게 작동해야함.
    static int compare(T t1, T t2) {...} // 불가능

또한
''' 지네릭 타입배열 "생성"도 허용하지 않는다. '''

new 연산자는" 컴파일 할 때, 이 타입 T가 무엇인지 정확히 알아야 한다 "
하지만
이 T는 사용하는 상황에 따라 유동적으로 바뀔 수 있는 타입 변수이기 때문에
정확히 아는 것이 불가능하기에 자연스레 사용이 불가능한 것이다.

instanceof 연산자도 위와 같은 이유로 똑같이 사용 불가능하다.

주의해야할 점은
말그대로 "생성"이 불가능한 것이고
" T타입의 배열 "참조변수 선언"은 가능하다는 것 "을 주의해야한다.
( ex. new T[10] 과 같이 배열 생성 불가 )




🎴 와일드 카드

문자열 탐색 때나 보던 와일드 카드가 여기서도 나왔다.

기능은 그 때와 다를 것 없이
조건에 부합하는 경우의 수를 포함해야 할 때 사용한다.

즉, 참조 변수로 대입된 타입이 다른 객체를 참조할 수 있도록 해주는데
앞서 알아봤던 내용 중,
참조변수의 타입 변수와 생성 객체의 타입 변수가 일치해야한다는 내용이 있었는데

지금까지는 하나의 타입 변수로만 다뤘지만
이 와일드 카드를 사용하면
참조 변수의 가능 타입 범위를 조절할 수 있다.

/*
조상 : Product
자손 : Tv, Audio
*/
List<Product> list = new ArrayList<Product>();
List<Tv> list = new ArrayList<Tv>();

ArrayList<Product> list = new ArrayList<Product>();
ArrayList<Product> list = new ArrayList<Tv>(); //불가능
ArrayList<Product> list = new ArrayList<Audio>(); //불가능

/* 와일드 카드 사용 */
ArrayList<? extends Product> list = new ArrayList<Tv>(); 
ArrayList<? extends Product> list = new ArrayList<Audio>();
  • <? extends T> : 상한 제한 _ T와 그 자손만 가능

  • <? super T> : 하한 제한 _ T와 그 조상만 가능

  • <?> : 제한 X _ 모든 타입 가능 ( == <? extends Object> )

와일드 카드는
" 메서드매개변수 "에도 사용할 수 있다!

/*
와일드카드를 안쓴다면
매개변수로 Fruit 타입의 객체만 들어올 수 있다.
*/
// Juicer 라는 클래스의 메서드 makeJuice 선언
static Juice makeJuice( FruitBox<Fruit> box ) {
	String tmp = "" ;
    for( Fruit f : box.getlist() ) {
    	tmp += f + " " ;
    }
    return new Juice(tmp)
}


System.out.println(Juicer.makeJuice( new FruitBox<Fruit>() ) ) ; // 이건 가능
System.out.println(Juicer.makeJuice( new FruitBox<Apple>() ) ) ; // Apple 타입변수 _ 불가능

/* 매개변수에 와일드 카드 사용 */
static Juice makeJuice( FruitBox<? extends Fruit> box ) {
	String tmp = "" ;
    for( Fruit f : box.getlist() ) {
    	tmp += f + " " ;
    }
    return new Juice(tmp)
}

System.out.println(Juicer.makeJuice( new FruitBox<Apple>() ) ) ; // 가능


🧶 제네릭 메서드

메서드의 " 선언부 "에 제네릭 타입이 선언된 메서드 [ 선언 위치 : 반환 타입 바로 앞 ]
( 메서드 내에서만 유효 )

  • 제네릭 : 지금은 이 타입을 모르지만, 이 타입이 정해지면 그 타입 특성에 맞게 사용하겠다!
  • 와일드 카드 : 지금도 이 타입을 모르고, 앞으로도 모를 것이다!

제네릭 클래스 내부에 제네릭 메서드가 정의될 수 있는데
둘 다 타입 변수가 정의되겠지만

< 제네릭 클래스에 정의된 타입 변수 T >와
< 제네릭 메서드에 정의된 타입 변수 T >는
전혀 별개이다.

메서드에 선언된 제네릭 타입은
메서드 내에서만 지역적으로만 사용되고

클래스의 제네릭 타입보다
메서드의 정의된 타입이 우선순위가 높다.
( 타입 변수는 해당 메서드 내에서만 쓰이기 때문에 static 메서드이건 아니건 상관없다 )

본래
제네릭 메서드를 " 호출할 때마다 "
타입을 대입해야하지만

대부분의 경우 컴파일러가 선언부를 통해 대입된 타입을 추정할 수 있기에 생략한다.

// Juicer 라는 클래스의 메서드 makeJuice 를 제네릭 메서드로 변환

...
/*
서로 다른 타입이 대입된 여러 제네릭 객체를 다루고자 할 땐
static Juice makeJuice( FruitBox<? extends Fruit> box ) { 로 가능한데

위와 같이 와일드 카드 사용이 불가능할 때
아래의 """ 제네릭 메서드 """를 사용한다.
*/
static <T extends Fruit> Juice makeJuice( FruitBox<Fruit> box ) {
	String tmp = "" ;
    for( Fruit f : box.getlist() ) {
    	tmp += f + " " ;
    }
    return new Juice(tmp)
}
...

/*
fruitBox와 appleBox 의 선언부에 타입이 정의 되어있는 것을 통해
컴파일러가 대입된 타입을 추정할 수 있음.
*/
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>() ;
FruitBox<Apple> fruitBox = 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)) ; // 클래스 이름 생략 - 불가XXX
System.out.println(this.<Fruit>makeJuice(fruitBox)) ;
System.out.println(Juicer.<Fruit>makeJuice(fruitBox)) ;


🚩 제네릭 타입

🌀 형변환

※ 《 제네릭 ↔ 넌제네릭(Non-Generic) 》

제네릭 타입과 원시타입(primitive type)간의 형변환은
가능하긴하다
다만.. 경고가 발생할 뿐...

하지만 앞서 알아봤듯
가능은 하겠지만
JDK 1.5 이후로는 제네릭 타입 입력을 해야한다는 것을 명심하자.

//비권장
Box<Object> objBox = null;
Box box = (Box)objBox ; // Box<Object> 지네릭 타입 → Box 원시타입
objBox = (Box<Object>)box ; // Box 원시타입 box → Box<Object> 지네릭 타입 objBox 

※ 《 서로 다른 제네릭 ↔ 제네릭 》

위 제네릭과 넌 제네릭 간의 형변환과 달리
서로 다른 제네릭 간의 형변환은 불가능하다.

//불가능
Box<Object> objBox = null;
objBox = (Box<Object>)strbox ; // Box<String> 지네릭 타입 → Box<Object> 지네릭 타입 
strbox = (Box<String>)objbox ; // Box<Object> 지네릭 타입 → Box<String> 지네릭 타입

하지만 이번에도 와일드 카드라는 변수가 존재한다.
" 와일드 카드가 사용된 제네릭 타입 "으로는 형변환가능하다.

Box<Object> objBox = (Box<Object>)new Box<String>(); // 형변환 불가능

Box<? extends Object> wBox = (Box<? extends Object>)new Box<String>(); // 형변환 가능!!!!
Box<? extends Object> wBox = new Box<String>(); // 형변환 가능!!!!

위와 같이 와일드 카드를 사용하게 되면
형변환이 가능하기 때문에

이전에 봤었던 예제를 참고해 활용해 보면

// 매개변수 ← FruitBox<Fruit> / FruitBox<Apple> / FruitBox<Grape> 등등 Fruit를 상속받은 클래스 타입 모두 가능
static Juice makeJuice(FruitBox<? extends Fruit> box) {...}

FruitBox<? extends Fruit> box = new FruitBox<Fruit>() ; //가능
FruitBox<? extends Fruit> box = new FruitBox<Apple>() ; //가능
FruitBox<? extends Fruit> box = (FruitBox<? extends Fruit>)new FruitBox<Apple>(); // 가능

위와 같이 형변환을 수행할 수 있다.

❌ 제거

자바의 컴파일러는 ' 제너릭 타입 '을 이용해서 소스파일을 체크 하고 필요한 곳에 형변환을 넣어준다.
그리고 제너릭 타입을 제거한다.


▶ " 컴파일된 파일(*.class) 에는 사실 제네릭 타입에 대한 정보가 없는 것 이다. "
( JDK 1.5 이전 버전 소스코드와의 "" 하위 호환성 유지 ""를 위해 )
안정성에 치중한 언어.... 그의 이름 자바...

① 제네릭 타입 " 경계(bound) " 제거

예를 들어

// 컴파일 전
class Box<T extends Fruit> {
	void add (T t) {
    	...
    }
}

// 경계 제거 & 치환
class Box {
	void add (Fruit t) {
    	...
 	}       
}

제네릭 타입이 <T extends Fruit>라면 TFruit으로 치환되게 된다.

② 타입 제거 후, 타입 불일치 시 " 형변환 추가 "

또한
List 클래스에 정의되어 있는 get()메서드를 예를 들어보면
Object 타입반환하기 때문에 타입의 형변환이 필요하게 된다.

// 기존 
T get (int i) { // 타입 변수 T로 정의되어 있음
	return list.get(i) ; // get메서드는 Object 타입 반환
}

// 타입 불일치한 부분인 곳에 형변환을 추가함
Fruit get (int i) {
	return (Fruit)list.get(i) ;
}



/* 3. 와일드 카드의 경우 */
static Juice makeJuice( FruitBox<? extends Fruit> box ) { // Fruit과 그의 자손들 타입
	String tmp = "" ;
    for( Fruit f : box.getlist() ) {
    	tmp += f + " " ;
    }
    return new Juice(tmp)
}

// 적절한 타입으로의 형변환 추가됨.
// 와일드 카드 부분이 지워지기 때문에 for문의 Fruit f 대상 작업이 Iterator를 통한 hasNext()를 이용한 작업으로
static Juice makeJuice( FruitBox<Fruit> box ) {
	String tmp = "" ;
    Iterator it = box.getList().iterator() ; // Iterator로 표준화된 코드 Case
    while( it.hasNext() ) {
    	tmp += (Fruit)it.next() + " " ; // it.next()반환 타입 : Object → Fruit 형변환
    }
    return new Juice(tmp)
}

0개의 댓글