Java Generic Type

Java Generic 활용

Featured image

왜 제네릭 인가?

java 5부터 제네릭 타입이 추가 되었는데 제네릭을 이용함으로써 잘못된 타입이 사용될 수 있는
문제를 컴파이 ㄹ과정에서 제거할 수 있게 되었다.
제네릭은 컬렉션, 람다식, 스트림, NIO에서 널리 사용되므로 확실히 이해해 두어야 한다.

아래는 제네릭 타입의 기초 문법 이다.

// Box Class
public class Box<T> { // Box라는 클래스에 <T> 라는 문법으로 제네릭 타입을 표시해 준다.
	private T t; // 제네릭 형태의 변수를 선언한다.

	// 변수 t 에 대한 Getter과 Setter 을 선언한다.
	public T get() { return t; } 
	public void set(T t) { this.t = t; }
}

// Apple Class
public class Apple {
	// 제네릭 테스트를 위해 빈 Class를 만든다.
}

// Main Class
public class GenericExam {
	
	public static void main(String[] args) {
		Box<String> stringBox = new Box<String>(); // String 형 객체를 담을 수 있는 제네릭 Class
		stringBox.set("안녕?"); // String 객체를 t 변수에 대입
		System.out.println(stringBox.get()); // 출력
		
		Box<Apple> appleBox = new Box<Apple>(); // 아까 만들어 놓은 Apple Class형태의 제네릭 box
		appleBox.set(new Apple()); // Apple 인스턴스를 생성해 제네릭 박스에 Set
		System.out.println(appleBox.get()); // Get 해본다.
	}
}

위 제네릭 타입의 문법으로 제네릭한 소스코드를 작성 할 수 있다. 다음은 많이 봤을 법한 제네릭 문법이다.

// Pair Class 생성
public class Pair<K, V> {
	// 코딩하다 보면 무의식 적으로 많이 쓰는 Map이 바로 이런 key value 제네릭 형태이다.
	private K key; // Key값을 제네릭 형태로.
	private V value; // Value도 마찬가지
	
	// Getter Setter 셋팅
	public K getKey() {
		return key;
	}
	public void setKey(K key) {
		this.key = key;
	}
	public V getValue() {
		return value;
	}
	public void setValue(V value) {
		this.value = value;
	}	
}

// 제네릭 객체를 서로 비교하기 위한 Static Compare Method 작성
public class Util { // 객체 비교를 위한 Util 클래스 작성
	// Static 으로 작성 한다.
	// 타입 파라미터는 <K, V> 로 작성 되었다. 이는 compare 메소드가 
	// 제네릭 타입 Pair<K, V> 를 가지고 있기 때문이다. 공식이다.
	public static <K, V> boolean compare(Pair<K, V> o1, Pair<K, V> o2) {
		// equals로 서로의 Key 값을 동등 비교
		boolean keyCompare = o1.getKey().equals(o2.getKey());
		// value의 값을 동등 비교
		boolean valueCompare = o1.getValue().equals(o2.getValue());
		// 경과를 AND 연산으로 연산후 return
		return keyCompare && valueCompare;
	}
}


// Main Class
public class CompareMethodExam {

	public static void main(String[] args) {
		// 우선 비교할 객체를 만든다.
		Pair<String, Object> o1 = new Pair<String, Object>();
		Pair<String, Object> o2 = new Pair<String, Object>();
		
		// o1 의 키와 값을 세팅
		o1.setKey("1");
		o1.setValue(1);
		
		// 위와 같음
		o2.setKey("2");
		o2.setValue(2);
		
		// Static 메소드를 호출하여 위에서 만든 객체를 서로 비교후
		// return 되는 boolean 값을 compare 변수에 저장
		boolean compare = Util.compare(o1, o2);
		if (compare) { // 동등 객체 라면 동등함 출력
			System.out.println("동등함");
		} else { // 아니라면 동등하지 않다 출력
			System.out.println("동등하지 않음");
		}
	}
}

타입 파라미터에 지정되는 구체적인 타입을 제한할 필요가 종종 있다.
예를 들어 숫자를 연산하는 제네릭 메소드는 매개값으로number 타입 또는 하위 클래스 타입의
인스턴스만 수용 하여야 한다. 이것이 제한된 파라미터가 필요한 이유이다.

이런 제네릭 문법을 활용하면 좀더 범용성 있게 활용이 가능하다. 와일드 카드 타입이 그 예 이다.

아래는 와일드 카드 타입 문법이다.

// 가장 상위 타입 Person Class
public class Person {

	private String name;
	
	public Person(String name) {
		super();
		this.name = name;
	}
	public String getName() {
		return name;
	}
	
	@Override
	public String toString() {
		// TODO Auto-generated method stub
		return name;
	}
}

// 아래는 person을 상속 받고 있는 자식 클래스
public class Student extends Person{

	public Student(String name) {
		super(name);
	}
}

public class Worker extends Person{

	public Worker(String name) {
		super(name);
	}
}

// 위 객체들을 배열로 담을 Class 구현
public class Course<T> { // 제한 없는 T형태
	private String name;
	private T[] students; // 제네릭 타입의 배열
	
	// 생성자
	public Course(String name, int capacity) {
		this.name = name;
		
		/*
		 * 타입 파라미터로 배열 생성시엔
		 * new T[n]형태로 생성이 불가하다.
		 * (T[])(new Object[n]) 형태로 생성 해야 한다.
		 */
		students = (T[]) (new Object[capacity]);
	}
	
	public String getName() {
		return name;
	}
	public T[] getStudents() {
		return students;
	}
	public void add(T t) {
		
		// 빈 배열을 찾아 요소를 삽입 해준다.
		for(int i=0; i<students.length; i++) {
			if (students[i] == null) {
				students[i] = t;
				break;
			}
		}
	}
}

// 위 소스를 실행할 Main 구현
public class WildCardExample {
	
	// 제한되지 않은 모든 과정이 담길 수 있다.
	public static void registerCourse( Course<?> course ) {
		System.out.println(course.getName() + " 수강생: " + 
			Arrays.toString(course.getStudents()));
	}

	// 상위 타입이 Student인 객체만 담길 수 있다.
	public static void registerCourseStudent( Course<? extends Student> course ) {
		System.out.println(course.getName() + " 수강생: " + 
			Arrays.toString(course.getStudents()));
	}
	
	// 하위 타입이 Worker인 객체만 담길 수 있다.
	public static void registerCourseWorker( Course<? super Worker> course ) {
		System.out.println(course.getName() + " 수강생: " + 
			Arrays.toString(course.getStudents()));
	}
	
	public static void main(String[] args) {
		// WorKer 과 Student Class는 모두 Person을 상속 받고 있기 때문에 전부 Person 형태로 
		// 삽입이 가능하다.
		Course<Person> personCourse = new Course<Person>("일반인 과정", 5);
		personCourse.add(new Person("일반인"));
		personCourse.add(new Worker("직장인"));
		personCourse.add(new Student("학생"));
		
		// Worker 타입의 Course 객체
		Course<Worker> workerCourse = new Course<Worker>("직장인 과정", 5);
		workerCourse.add(new Worker("직장인"));
		
		// Student 타입의 Course 객체
		Course<Student> studentCourse = new Course<Student>("학생과정", 5);
		studentCourse.add(new Student("학생"));
		studentCourse.add(new Student("고등학생"));
		
		registerCourse(personCourse); // 제한되지 않은 파라미터 타입 이기 때문에 모두 가능
		registerCourseStudent(studentCourse); // 부모 클래스가 student인 객체만 등록 가능
		registerCourseWorker(workerCourse); // 부모 클래스가 worker인 객체는 등록 불가능
		System.out.println();
		
		// student형 객체를 담고 있는 studentCourse는 담길 수 없다.
		// 하위 파라메터 제한은 제한 객체가 상속받고 있는 부모들만 올 수 있고 
		// 동등한 자식 객체들은 제한된다.
		// registerCourseWorker(studentCourse);
		// registerCourseStudent(workerCourse);
		System.out.println();
		
		registerCourseWorker(personCourse); // person는 최상위 객체 이기 때문에 하위클래스 제한이 걸리지 않음.
		registerCourseWorker(workerCourse); // worker도 당연히 가능.
	}
}