자바의 정석 12장 (30일차) - 지네릭스(Generics) & 타입 변수 & 제약

728x90

지네릭스 <>

Collection 클래스의 타입체크를 해주는 기능을 가지고 있다.

 

아래의 예시처럼 지네릭스를 써주지 않으면 컴파일러가 arr.get(0)의 인덱스 자리의 값을 확인할 수 없기 때문에 일일이 형변환을 해주어야 컴파일오류가 나지 않는다.

		ArrayList arr = new ArrayList();
		arr.add(1);
		arr.add(1);
		arr.add(1);
		int i = (Integer) arr.get(0);

 

 

지네릭스를 아래와 같이 지정해주면 컴파일러에게 새로운 기준을 넣어준 것이기 때문에 형변환을 하지 않아도 컴파일오류가 발생되지 않는다.

단 지정타입을 제외한 타입을 저장하게 될 경우 컴파일 오류 발생

		ArrayList<Integer> arr = new ArrayList<Integer>();
		arr.add(1);
		arr.add(1);
		arr.add(1);
		int i = arr.get(0);
//		Generic을 쓰지않으면 형변환을 해줘야 하지만
//		Generic으로 타입을 지정하면 형변환을 하지 않아도 컴파일러가 자동으로 오토박싱을 해줌

 

 

이와 같이 지네릭스를 사용하여 타입을 지정해주면 아래와 같은 장점이 있다.

  • 타입을 미리 지정함으로써 타입의 안정성을 준다.
  • 형변환을 할 필요가 없으므로 코드를 간결화 시켜준다.

 


타입 변수

타입 변수는 'E'로 선언한다 - Element의 약자

Object를 선언하는대신 E로 선언하여 Object를 대체한다.

		ArrayList<E> arr = new ArrayList<E>();

*Good to Know*

E 와 T의 차이

지네릭스를 선언할 때 Object로 선언하는 대신 'E' 혹은 'T'로 대신 선언

 

E는 element, 즉 요소라는 의미인데 배열 클래스(ArrayList, LinkedList 등)들이 요소를 배열로 저장하기 때문에 배열 기반의 클래스들에는 'E'로 선언하는 것이 좋고, 그 외에는 'T'로 선언하는 것이 좋다.

둘다 혼용하여 사용해도 컴파일에는 문제가 없음.


 

타입 변수에 클래스타입을 대입할 때 지네릭스에 들어가는 타입이 동일해야한다.

		ArrayList<TV> arr = new ArrayList<TV>();
//		TV 클래스를 지네릭스에 넣어줌 = 일치

*Good to Know*

지네릭스 용어

Parameterized Type (매개변수화된 타입 = 대입된 타입)

		ArrayList<TV> arr = new ArrayList<TV>();
//		지네릭스를 선언한 Collection 클래스를 부를 때 '대입된 타입'이라 부른다


지네릭타입의 다형성

지네릭안의 객체 타입 변수는 무조건 같아야 하지만, 객체클래스의 다형성은 가능하다.

		List<Test> list = new ArrayList<Test>();
//		가능!

 

또한 대입된 타입의 자손객체가 존재하면, 지네릭스 타입과 일치하지 않더라도 다형성에 의해 add(), 즉 대입이 가능하다

  • TV클래스를 상속하는 sound, display의 객체 데이터를 저장 가능

주의할 점: 처음 선언하는 지네릭스의 대입 타입 변수는 무조건 같아야한다!!

		List<Tv> list = new ArrayList<Tv>();
		list.add(new sound());
		list.add(new display());
//		대입된 타입은 동일해야 하지만, 추가될 객체 데이터는 다형성이 가능하다
	}
}
class Tv{
}
class sound extends Tv{
}
class display extends Tv{
}

iterator의 지네릭타입

iterator의 지네릭안에 타입변수를 대입하여 형변환을 생략할 수 있다.

  • 만약 값만 출력할 목적이라면 평범하게 출력하면 되지만, 만약 값을 저장하여 재 사용 할 목적이라면 새로운 Tv변수에 저장을 하여 사용하기 때문에 형변환이 필요했다
  • 하지만 지네릭스에 미리 대입 타입을 지정해주면 형변환을 생략하여도 컴파일러가 오토박싱 기능을 수행하여 형변환을 생략할 수 있도록 해준다.
		Iterator<Tv> it = list.iterator();
//		지네릭스에 Tv추가!
		while(it.hasNext()) {
			Tv tv = it.next();
//			Tv tv = (Tv)it.next();
//			지네릭스를 사용하면 Tv로 형변환을 하지 않아도 컴파일 오류가 발생하지 않는다
			System.out.println(tv);
			
//			System.out.println(it.next());
//			평범한 출력이 목적이라면 그냥 출력하면 되지만,
//			출력값을 저장하여 재 사용 하려면 위와 같이 실행한다.

HashMap의 지네릭 타입

iterator의 지네릭안에 Key(키)와 Value(값)을 지정하여 선언할 수 있다. 

각각 다른 객체를 선언하여 키와 값을 넣어줄 수 있으며 키에 해당되는 값을 불러올 시 HashMap.get('값')을 선언하여 값에 해당되는 값을 불러온다.

 

아래의 코드의 출력문은 '중요도90'이다

		Map<lecture, importance> map= new HashMap<lecture, importance>();
		lecture l = new lecture("자바");

		map.put(l, new importance(90));
		map.put(new lecture("spring"), new importance(100));
		map.put(new lecture("html"), new importance(98));
		
		importance i = map.get(l);
		System.out.println(i);
	}
}
class lecture{
	String name;
	public lecture(String name) {
		this.name = name;
	}
	public String toString() {
		return "수업 이름" + name;
	}
}
class importance{
	int n;
	public importance(int n) {
		this.n = n;
	}
	public String toString() {
		return "중요도" + n;
	}
}

지네릭스의 제한

클래스를 선언하고 상속을 받을 때 지네릭스를 사용하여 상속받을 타입을 제한할 수 있다.

지네릭스에 <E extends Obect>의 형식으로 상속받을 클래스를 제한한다.

 

아래의 예시는 basil, rosemari의 클래스에 plant클래스를 부모클래스로 상속받고 그 plant를 상속받은 클래스에 한해서 plantbox 클래스에 상속할 수 있게 지네릭스로 제한을 두었다.

 

여기서 주목할점!

  • 지네릭스 format 중, 여러개의 클래스&인터페이스를 제한을 둘 때 '&'을 하나만 사용하여 선언
  • eatable이라는 인터페이스의 상속을 기본 제한으로 두었는데 이 조건을 충족시키기위해 아래 클래스 전부다 implements로 상속을 시켜버리면 basil클래스에 rosemari객체 데이터를 저장할 수 있게 되어버린다. 
    • 따라서 plant라는 큰 틀의 클래스에 interface를 상속시켜 조건부 제한을 두어야 한다.
		plantbox<basil> s = new plantbox<basil>();
		s.add(new basil());
		s.check();
		
//		s.add(new rose());
//		basil의 클래스만 저장하도록 제네릭스를 넣었기 때문에 다른 클래스를 넣으면 컴파일 오류가 발생한다.
//		save<rose> s = new save<rose>();
//		여기서 eatable과 plant를 상속받지 않은 rose를 입력하게 되면 컴파일 오류가 발생한다.
	}
}

class plantbox<E extends plant & eatable> extends control{
	public String toString() {
		return "save []";
	}
}

class control{
	ArrayList arr = new ArrayList();
	void add(Object obj) {
		arr.add(obj);
	}
	void check() {
		System.out.println(arr);
	}
}

class plant implements eatable{}
interface eatable{};
class basil extends plant{
	public String toString() {return "basil";}}
class pensil {
public String toString() {return "pensil";}}
class rosemari extends plant{
public String toString() {return "rosemari";}}

지네릭스의 제약

지네릭스를 사용할 때 제약이 두가지 있다

 

1. 지네릭스의 타입 변수에 static은 사용 불가능하다.

  • 지네릭스를 선언 시 지정한 타입 변수, 인스턴스 별로 다르게 대입하여야 하는데 static을 사용하면 인스턴스를 공통의 객체로 만들어버리기 때문에 컴파일 오류가 실행된다.

 

2. 배열을 생성할 때 타입 변수는 사용이 불가능하다

  • new 다음에 T를 선언하지 못하는데, 그 이유는 T의 타입 변수는 어떤 변수가 대입될지 확정이 되어야하는데 어떤 객체가 들어올지 모르니 컴파일 오류가 실행된다. 
  • 단, 타입 변수 배열 선언은 가능하다. 
class Gclass<T>{
	T[] Tarr;
//	배열 선언은 가능
	
	public Gclass(T t) {
//		Tarr = new T[initial];
//		배열 생성 시 대입 변수 사용 불가
//		T를 new 다음에 선언 불가능하다 - 어떤 객체가 들어올 지 모르기 때문
	}
}

 

728x90