[Java 24] 자바 - 제네릭

학습목표

  1. 콜렉션 프레임워크 학습 전, 제네릭을 정확히 이해한다.

제네릭 타입 (class, interface)

제네릭 타입은 타입을 파라미터로 가지는 클래스와 인터페이스를 말합니다.

public class 클래스명<T> {...}
public interface 인터페이스명<T> {...}

예제 1

[Box] 클래스

public class Box {
	private Object object;	//필드에 모든 종류의 객체를 저장하기 위해 Object타입 선언
	public void set(Object object) { this.obbject = object; }
	public Object get() { return object; }
}

[Apple] 클래스

public class Apple {
}

[BoxExample] 비제네릭 타입 이용

public class BoxExample {
	public static void main(String[] args]) {
		Box box = new Box();
		box.set("홍길동");	// String -> Object (자동 타입 변환)
		String name = (String) box.get(); // Object -> String (강제 타입 변환)

		box.set(new Apple());	// Apple -> Object (자동 타입 변환)
		Apple apple = (Apple) box.get(); // Object -> Apple (강제 타입 변환) 
	}
}

이처럼 Object 타입을 사용하면 모든 자바 객체를 저장할 수 있다는 장점이 있지만, 저장할 때마다 타입 변환이 발생하고, 읽어올 때도 타입변환이 생깁니다. 이처럼 타입변환이 잦아지게 되면 프로그램 성능에 좋지 않은 영향을 끼칩니다. 그렇다면 이 매번 발생하면 타입 변환을 해결해준다면 문제는 해결될 것입니다. 이 해결책이 바로 제네릭입니다.


예제 2

[Box] 수정된 Box 클래스

public class Box<T> {
	// Object 타입을 모두 T로 대체
	private T t;	
	public void set(T t) { this.t = t; }
	public T get() { return t; }
}

위의 수정된 Box클래스는 타입 파라미터 T를 사용해서 Object 객체를 모두 T로 대체했습니다. T는 Box 클래스로 객체를 생성할 때 구체적인 타입으로 변경됩니다.

다음과 같이 Box 객체를 생성했다고 가정해봅시다.

Box<String> box = new Box<String>();

타입 파라미터 T는 String으로 변경되어 Box 클래스는 내부적으로 다음과 같이 자동적으로 재구성됩니다.

public class Box<String> {
	// T는 String 타입으로 변경되어 다음과 같이 재구성
	private String t;	
	public void set(String t) { this.t = t; }
	public String get() { return t; }
}

필드 타입이 String으로 변경되었고, set() 메소드 역시 String 타입만 매개값으로 받게 되었습니다. 그래서 다음 코드를 보면 저장할 때와 읽어올 때 타입 변환 없이 사용할 수 있습니다.

Box<String> box = new Box<String>();
box.set("hello");
String str = box.get();

예제 3

이번에는 다음과 같이 Box 객체를 생성했다고 가정해봅시다.

Box<Integer> box = new Box<Integer>();

타입 파라미터 T는 Integer으로 변경되어 Box 클래스는 내부적으로 다음과 같이 자동적으로 재구성됩니다.

public class Box<Integer> {
	// T는 Integer 타입으로 변경되어 다음과 같이 재구성
	private Integer t;	
	public void set(Integer t) { this.t = t; }
	public Integer get() { return t; }
}

그러면 실제적인 코드에서 다음과 같이 타입변환이 최소화해 사용할 수 있습니다.

Box<Integer> box = new Box<Integer>();
box.set(6);
int value = box.get();

예제 4

예제 1의 예제를 제네릭을 사용하면 다음과 같이 작성할 수 있습니다.

public class Box<T> {
	private T t;	
	public void set(T t) { this.t = t; }
	public T get() { return t; }
}
public class BoxExample {
	public static void main(String[] args]) {
		Box<String> box1 = new Box<String>();
		box.set("hello");	
		String str = box.get(); 

		box<Integer> box2 = new Box<Integer>();
		box2.set(6);
		int value = box2.get(); 
	}
}


멀티 타입 파라미터

제네릭은 두 개 이상의 멀티 타입 파라미터를 사용할 수 있는데, 이 경우 각 타입 파라미터를 콤마로 구분합니다.

class Tv{ }
class Car{ }

public class Product<T, M> {
	private T kind;
	private M model;

	public T getKind() { return this.kind; }
	public M getModel() { return this.model; }
	public void setKind(T kind) { this.kind = kind;	}
	public void setModel(M model) {	this.model = model;	}
}
public class ProductExample {
	public static void main(String[] args) {
		Product<Tv, String> product1 = new Product<Tv, String>();
		product1.setKind(new Tv());
		product1.setModel("스마트 TV");
		Tv tv = product1.getKind();
		String tvModel = product1.getModel();

		Product<Car, String> product2 = new Product<Car, String>();
		product2.setKind(new Car());
		product2.setModel("디젤");
		Car car = product2.getKind();
		String carModel = product2.getModel();
	}
}

제네릭 메소드( <T, R> R method(T, t))

제네릭 메소드 선언방법

public <타입파라미터, ...> 리턴타입 메소드명(매개변수, ...) {...}
public <T> Box<T> boxing(T t) {...}

제네릭 메소드 호출방법

리턴타입 변수 = <구체적타입> 메소드명(매개값); // 명시적으로 구체적 타입을 지정
리턴타입 변수 = 메소드명(매개값); / /매개값을 보고 구체적 타입을 추정
Box<Integer> box = <Integer>boxing(100);	// 타입 파라미터를 명시적으로 Integer로 추정
Box<Integer> box = boxing(100);		// 타입 파라미터를 Integer로 추정

예제 1

// Util 클래스에서 boxing()을 정의
public class Util {
	public static <T> Box<T> boxing(T t) {
		Box<T> box = new Box<T>();
		box.set(t);
		return box;
	}
}
// boxing() 호출
public class BoxingMethodExample {
	public static void main(String[] args) {
		Box<Integer> box1 = Util.<Integer>boxing(100);
		int intValue = box1.get();
		
		Box<String> box2 = Util.boxing("홍길동");
		String strValue = box2.get();
	}
}

예제 2

public class Util2 {
	public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) { //타입 파라미터는  K와 V로 선언
		boolean keyCompare = p1.getKey().equals(p2.getKey());
		boolean valueCompare = p1.getValue().equals(p2.getValue());
		return keyCompare && valueCompare;
	}
}
public class Pair<K, V> {
	public K key;
	private V value;
	
	public Pair(K key, V value) {
		this.key = key;
		this.value = value;
	}
	
	public void setKey(K key) { this.key = key; }
	public void setVlaue(V value) { this.value = value; }
	public K getKey() { return key; }
	public V getValue() { return value; }
}
public class CompareMethodExample {
	public static void main(String[] args) {
		Pair<Integer, String> p1 = new Pair<Integer, String>(1, "사과");
		Pair<Integer, String> p2 = new Pair<Integer, String>(1, "사과");
		boolean result1 = Util2.<Integer, String>compare(p1, p2);
		if (result1) {
			System.out.println("논리적으로 동등한 객체입니다.");
		} else {
			System.out.println("논리적으로 동등하지 않은 객체입니다.");
		}

		Pair<String, String> p3 = new Pair<String, String>("user1", "홍길동");
		Pair<String, String> p4 = new Pair<String, String>("user2", "홍길동");
		boolean result2 = Util2.compare(p3, p4);
		if (result2) {
			System.out.println("논리적으로 동등한 객체입니다.");
		} else {
			System.out.println("논리적으로 동등하지 않은 객체입니다.");
		}
	}
}

실행결과

논리적으로 동등한 객체입니다.
논리적으로 동등하지 않은 객체입니다.

제한된 타입 파라미터()

타입 파라미터에 지정되는 구체적인 타입을 제한할 필요가 종종 있습니다. 예를 들어 숫자를 연산하는 제네릭 메소드는 매개값으로 Number 타입 또는 하위 클래스 타입(Byte, Short, Integer, Long, Double)의 인스턴스만 가져야 합니다.

// 상위타입은 클래스와 인터페이스 모두 가능, 인터페이스라고 implement 사용하지 않는다.
public <T extends 상위타입> 리턴타입 메소드(매개변수, ...) { ... }  

타입 파라미터에 지정되는 구체적인 타입은 상위 타입이거나 상위 타입의 하위 또는 구현 클래스만 가능합니다. 주의할 점은 중괄호 {} 아에서 타입 파라미터 변수로 사용 가능한 것은 상위 타입의 멤버(필드, 메소드)로 제한됩니다.

public < T extends Number> int compare(T t1, T t2) {
	double v1 = t1.doubleValue(); 	//Number의 doubleValue 메소드 사용
	double v2 = t2.doubleValue();	//Number의 doubleValue 메소드 사용
	return Double.compare(v1, v2);
}

예제 1

public class Util3 {
	public static <T extends Number> int compare(T t1, T t2) {
		double v1 = t1.doubleValue();
		double v2 = t2.doubleValue();
		return Double.compare(v1, v2);
	}
}
public class BoundedTypeParameterExample {
	public static void main(String[] args) {
		// String result1 = Util3.compare("a", "b"); String은 Number타입이 아님
		// 첫번째 매개값이 작으면 -1, 같으면 0, 크면 1을 리턴
		int result1 = Util3.compare(10,  20);  //20 : int -> Integer 자동박싱
		System.out.println(result1);
		
		int result2 = Util3.compare(4.5, 3);   //3.5 : double -> Double 자동박싱
		System.out.println(result2);
	}
}

와일드카드 타입(<?>, <? extends …>, <? super …>)

코드에서 ?은 보통 와일트카드라고 부릅니다. 제네릭에서 와일드 카드는 다음과 같이 세 가지 형태로 사용할 수 있습니다.

1. 제네릭타입<?> : Unbounded Wildcard (제한 없음)
타입 파라미터를 대치하는 구체적인 타입으로 모든 클래스나 인터페이스 타입에 올 수 있다.

2. 제네릭타입<? extends 상위타입> : Upper Bounded Wildcard(상위 클래스 제한)
타입 파라미터를 대치하는 구체적인 타입으로 상위 타입이나 하위 타입만 올 수 있다.

3. 제네릭타입<? super 하위타입> : Lower Bounded Wildcards(하위 클래스) 제한
타입 파라미터를 대치하는 구체적인 타입으로 하위 타입이나 상위 타입이 올 수 있다.

태그:

카테고리:

업데이트:

댓글남기기