본문 바로가기
JAVA

제네릭

by 요리하다그만둠 2022. 8. 23.

제네릭은 java 5부터 타입이 추가 되었습니다.

제네릭 타입을 이용함으로써 잘못된 타입이 사용될 수 있는 문제를 컴파일 과정에서 제거할 수 있게 됩니다.

제네릭은 컬렉션, 람다식, 스트림, NIO에서 널리 사용됩니다.

API 도큐먼트를 보면 제네릭 표현이 많기 때문에 제네릭을 이해 못하면 API 도큐먼트를 이해하기 어렵다고 합니다.

제네릭은 클래스와 인터페이스, 그리고 메서드를 정의할 때 타입(type)을 파라미터(parameter)로 사용할 수 있도록

합니다.

 

제네릭 타입(class<T>, interface<T>)

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

인터페이스 이름 뒤에 "<>" 부호가 붙고, 사이에 타입 파라미터가 위치합니다. 아래 코드에서 타입 파라미터의 이름은 T입니다.

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

타입 파라미터는 변수명과 동일한 규칙에 따라 작성할 수 있지만 일반적으로 대문자 알파벳 한 글자로 표현합니다.

제네릭 타입을 실제 코드에서 사용하려면 타입 파라미터에 구체적인 타입을 지정해야 합니다.

 

위의 타입의 파라미터 사용 이유를 보자면

public class Box{
	pirvate Object object;
    public void set(Object object) {this.object = object;}
    public Object get() { return object; }
    }

Box 클래스의 필드타입이 Object 인데, Object 타입으로 선언한 이유는 필드에 모든 종류의 객체를 저장하고 싶어서 입니다. Object 클래스는 모든 자바 클래스의 최상위 조상(부모) 클래스입니다. 따라서 자식 객체는 부모 타입에

대입할 수 있다는 성질 때문에 모든 자바 객체는 Object 타입으로 자동 타입 변환되어 저장됩니다.

Object object = 자바의 모든 객체;

set() 메서드는 매게 변수 타입으로 Object를 상용함으로써 매개값으로 자바의 모든 객체를 받을수 있게 했고,

받은 매개값을 Object 필드에 저장시킵니다. 반대로 get() 메서드는 Object 필드에 저장된 객체를 Objevt 타입으로 리턴합니다. 만약 필드에 저장된 원래 타입의 객체를 얻으려면 다음과 같이 강제 타입 변한을 해야합니다.

 

Box box = new Box();
box.set("hello"); // String 타입을 Object 타입으로 자동 타입 변환해서 저장
String str = (String) box.get(); // Object 타입을 String 타입으로 강제 타입 변환해서 얻음

 

package ex;

public class Box {
	private Object object;
	
	public void set(Object object) {
		this.object = object;
	}
	public Object get() {
		return object;
	}
}

 

package ex;

public class Apple {

}

비제네릭 타입 이용

package ex;

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 타입을 사용하면 모든 종류의 자바 객체를 저장할 수 있다는 장점이 있지만, 저장 할 때 타입

변환이 발생하고, 읽어올 때에도 타입 변환이 발생합니다. 이러한 타입 변환이 빈번해지면 전체 프로그램 성능에 좋지 못한 결과를 가져올 수 있습니다. 그렇다면 모든 종류의 객체를 저장하면서 타입 변환이 발생하지 않도록 하는 방법이 제네릭에 있습니다.

 

제네릭을 이용하여 Box 클래스를 수정

public class Box<T> {
	private T t;
    public T get() { return t; }
    public void set(T t) { this.t = t; }
    }

 

타입 파라미터 T를 사용해서 Object 타입을 모두 T로 대체했습니다. T는 Box 클래스로 객체를 생성할 때 구체적인

타입으로 변경됩니다. 예를 들어 다음과 같이 Box 객체를 생성했다고 가정해보면

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

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

public class Box<String> {
	private String t;
    public void set(Stirng t) { this.t = t; }
    public String get() { return t; }
    }

필드 타입이 String 으로 변경되었고, set() 메서드도 String 타입만 매개값으로 받을 수 있게 변경 되었습니다.

그리고 get() 메서드 역시 String 타입으로 리턴하도록 변경되었습니다. 그래서 다음 코드를 보면 저장할 때와 읽어올 때 전혀 타입 변환이 발생하지 않습니다.

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

이번에는 다음과 같이 Box 객체를 생성했다고 가정하고 Integer는 int 값에 대한 객체 타입으로 자바에서 제공하는

표준 API 입니다.

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

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

- 제네릭 타입 이용
public class Box<Integer> {
	private Integer t;
    public void set(Integer t) { this.t =t; }
    pbulic Integer get() { return t; }
    }

필드 타입이 Integer로 변경되었고, set() 메서드도 Integer 타입만 매개값으로 받을 수 있게 변경 되었습니다.

그리고 get() 메서드 역시 Integer 타입으로 리턴하도록 변경되었습니다. 그래서 다음 코드를 보면 저장할 때와 읽어올 때 전혀 타입 변환이 발생하지 않습니다.

Box<Integer> box = new Box<Integer>();
box.set(6); // 자동 Boxing
int value = box.get(); // 자동 UnBoxing

이와 같이 제네릭은 클래스를 설계할 때  구체적인 타입을 명시하지 않고, 타입 파라미터로 대체했다가 실제 클래스가 사용될 때 구체적인 타입을 지정함으로써 타입 변환을 최소화 시킵니다.

- 제네릭 타입 이용
package ex;

public class BoxExample {
	public static void main(String[] args) {
		Box<String> box1 = new Box<String>();
		box1.set("hello");
		String str = box1.get();
		
		Box<Integer> box2 = new Box<Integer>();
		box2.set(6);
		int value = box2.get();
		
	}
}

 

 

멀티 타입 파라미터(class<K,V,...>, interface<K,V,...>)

 

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

다음 예제는 Product<T, M> 제네릭 타입을 정의하고 ProductExample 클래스에서 Product<Tv, String> 객체와 

Product<Car, String> 객체를 생성합니다. 그리고 Getter와 Setter를 호출하는 방법을 보여줍니다.

package ex;

public class Product<T, M> {
	
	private T kind;
	private M model;
	
	public T getKint() { 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; }
	
}

 

package ex;

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();
	}
}

제네릭 타입 변수 선언과 객체 생성을 동시에 할 때 타입 파라미터 자리에 구체적인 타입을 지정하는 코드가 중복해서 나와 다소 복잡해질 수 있습니다. 자바 7 부터 제네릭 타입 파라미터의 중복 기술을 줄이기 위해 다이아몬드 연산자<> 를 제공합니다. 자바 컴파일러는 타입 파라미터 부분에 <> 연산자를 사용하면 타입 파라미터를 유추해서 자동으로 설정해줍니다. 예를 들어 다음은 자바 6 이전 버전에서 사용한 제네릭 타입 변수 선언과 객체 생성 코드입니다.

Product<Tv, String> product = new Product<Tv, String>();

자바 7부터는 다이아몬드 연산자를 사용해서 다음과 같이 간단하게 작성할 수 있습니다.

Product<Tv, String> product = new Product<>();

 

 

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

 

제네릭 메서드는 매개 타입과 리턴 타입으로 타입 파라미터를 갖는 메서드를 말한다. 제네릭 메서드를 선언하는 방법은 

리턴 타입 앞에<> 기호를 추가하고 타입 파라미터를 기술한 다음. 리턴 타입과 매개 타입으로 타입 파라미터를 사용하면 됩니다.

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

 

제네릭 메서드는 두 가지 방식으로 호출할 수 있습니다. 코드에서 타입 파라미터의 구체적인 타입을 명시적으로

지정해도 되고, 컴파일러가 매개값의 타입을 보고 구체적인 타입을 추정하도록 할 수도 있습니다.

리턴타입 변수  <구체적타입> 메서드명(매게값); // 명시적으로 구체적 타입을 지정
리턴타입 변수 = 메서드명(매개값); // 매개값을 보고 구체적 타입을 추정

다음 코드는 boxing() 메서드를 호출하는 코드입니다.

Box<Integer> box = <Integer>boxing(100); // 타입 파라미터를 명시적으로 Integer로 지정
Box<Integer> box = boxing(100); // 타입 파라미터를 Integer로 추정

Util 클래스에 정적 제네릭 메서드로 boxing()을 정의하고 BoxingMethodExample 클래스에 호출했습니다.

package ex;
= 제네릭 메서드
public class Util {
	public static <T> Box<T> boxing(T t) {
		Box<T> box = new Box<T>();
		box.set(t);
		return box;
	}
}
package ex;
= 제네릭 호출 메서드
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();
	}
}

Util 클래스에 정적 제네릭 메서드로 compare()를 정의하고 CompareMethodExample 클래스에 호출했습니다. 타입 파라미터는 K와 V로 선언되었는데, 제네릭 타입 Pair 가 K와 V를 가지고 있기 때문입니다. compare() 메서드는 두 개의 Pair를 매개값으로 받아 K와 V 값이 동일한지 검사하고 boolean으로 리턴해줍니다.

 

제네릭 메서드 
package ex;

public class Util {
	public static <K, V> boolean compare(Pair<K, V>p1, Pair<K, V>p2){
		boolean keyCompare = p1.getKey().equlas(p2.getKet());
		boolean valueCompare = p1.getValue().equlas(p2.getValue());
		return keyCompare && valueCompare;
	}
}
제네릭 타입
package ex;

public class Pair<K, V> {
	private 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 setValue(V value) {this.value=value;}
	public K getKey() {return key;}
	public V getValue() {return value;}
	
}
제네릭 메서드 호출
package ex;

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 = Util.<Integer, String>compare(p1, p2);
		
		if(result2) {
			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 = Util.compare(p3, p4);
		
		if(result1) {
			System.out.println("논리적으로 동등한 객체입니다."); 
		}
		else {
			System.out.println("논리적으로 동등하지 않는 객체이비다."); 
		}
	}
}

 

 

제한된 타입 파라미터(<T extends 최상위타입>)

타입 파라미터에 지정되는 구체적인 타입을 제한할 필요가 종종 있습니다.

예를 들어 숫자를 연산하는 제네릭 메서드는 매개값으로

Number 타입 or 하위 클래스 타입(Byte, s-Short, Integer< Long, Double)의 인스턴스만 가져야 합니다.

이것이 제한된 타입 파라미터(bounded type parameter)가 필요한 이유입니다.

제한된 파라미터를 선언하려면 타입 파라미터 뒤에 extends 키워드를 붙이고 사위 타입을 명시하면 됩니다.

(상위 타입은 클래스뿐만 아니라 인터페이스도 가능한데 인터페이스라 해서 implements 를 사용하지는 않음)

 

public <T extends 상위타입> 리턴타입 매서드(매개변수, ....) { ... }

타입 파라미터에 지정되는 구체적인 타입은 상위 타입이거나 상위 타입의 하위 또는 구현 클래스만 가능하다.

주의할 점은 메서드의 중괄호 {} 안에서 타입 파라미터 변수로 사용 가능한 것은 사위 타입의 맴버(필드, 메서드)로 제한됩니다. 하위 타입에만 있는 필드와 메서드는 사용할 수 없습니다. 

 

-숫자 타입만 구체적인 타입으로 갖는 제네릭 메서드 compare()이다. 두 개의 숫자 타입을 매개값으로 받아 차이를 리턴합니다.

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);
    }

doubleValue() 메서드는 Number 클래스에 정의되어 있는 메서드로 숫자를 double 타입으로 변환시킵니다.

Double.compare() 메서드는 첫 번째 매개값이 작으면 -1을, 같으면0을, 크면1을 리턴합니다.

package ex;

public class Util {
	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);
	    }
}

 

package ex;

public class BoundedTypeParameterExample {

	public static void main(String[] args) {
		//String str = Utril.compare("a", "b"); (x)
		//	("a", "b") 는 String은 Number 타입이 아님
		int result1 = Util.compare(10, 20);
		//	20은 int -> Integer (자동 Boxing)
		System.out.println(result1);
		
		int result2 = Util.compare(4.5, 3);
		// 	4.5은 double -> Double (자동 Boxing)
		System.out.println(result2);

	}

}

 

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

코드에서 ?를 일반적으로 와일드카드(wildcard)라고 부릅니다. 제네릭 타입을 매개값이나 리턴 타입으로 사용할 때 구체적인 타입 대신에 와일드카드를 다음과 같이 세가지 형태로 사용 가능합니다.

  • 제네릭타입<?> : Unbounded Wildcards(제한없음)
    타입 파라미터를 대치하는 구체적인 타입으로 모든 클래스나 인터페이스 타입이 올 수 있습니다.
  • 제네릭타입<? extends 상위타입> : Upper Bounded Wildcards(상위 클래스 제한)
    타입 파라미터를 대치하는 구체적인 타입으로 상위 타입이나 하위 타입만 올 수 있다.
  • 제네릭타입<? super 하위타입> : Lower Bounded Wildcards(하위 클래스 제한)
    타입 파라미터를 대치하는 구체적인 타입으로 하위 타입이나 상위 타입이 올 수 있습니다.
package ex;

이코드를 보면 제네릭 타입 Course는 과장 클래스로 과정 이름과 수강생을 저장할 수 있는
배열을 가지고 있습니다. 타입 파라미터 T가 적용된 곳은 수강생 타입 부분입니다.

public class Course<T> {
	
	private String name;
	private T[] students;
	
	public Course(String name, int capacity) {
		this.name = name;
		students = (T[]) (new Object[capacity]);
		// (T[]) (new Object[capacity]) 
		// = 타입 파라미터로 배열을 생성하려면 new T[n] 형태로 배열을 생성할 수 없고
		// (T[]) (new Object[n])으로 생성해야합니다.
	}
		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;
				}
			}
		
	}
}

다음 수강생이 될 수 있는 타입은 4자리 클래스라고 가정하고 Person의 하위클래스로 Worker와 Student가 있고, Student의 하위 클래스로 HighStudent가 있습니다.

  • Course<?>
    수강생은 모든 타입(Person, Worker, Student, HighStudent) 가 될 수 있씁니다.
  • Course<? extends Student>
    수강생은 Student와 HighStudent 만 될 수 있습니다.
  • Course<? super Worker>
    수강생은 Worker와 Person만 될 수 있습니다.

 

'JAVA' 카테고리의 다른 글

int와 Integer 차이  (0) 2022.10.31
JAVA 메서드  (0) 2022.07.21
JAVA 생성자 오버로딩  (0) 2022.07.20
JAVA 필드 초기화  (0) 2022.07.20
JAVA 생성자  (0) 2022.07.19