▸JAVA/기본 문법

제네릭(generic)

코데방 2019. 12. 23.
728x90

[ 제네릭(generic) ]

  • 데이터 타입이 런타임 시점에 동적으로 결정되는 방식
  • 가상의 데이터 타입을 사용하여 코드 작성 후 사용 시 타입 지정

컬렉션 프레임워크 글에서 간략이 언급했던 제네릭입니다. 리스트를 다룰 때 뿐만 아니라 매우 많은 부분에서 유용하게 쓸 수 있는 문법입니다. 매개변수 등의 전달 타입을 Object로 하는 것과 비슷하지만 더 활용성을 높이고 오류 가능성을 줄이는 방향으로 발전된 기능입니다.

 

 


 

[ 제네릭 클래스 생성 ]

  • class 클래스명<타입 매개변수> { }
  • 꺽쇠기호 안에 가상 타입의 매개변수 이름(보통 T, V 등) 삽입

클래스 생성 시 가상의 타입을 사용해서 필드와 메소드를 만들어 둔 뒤, 실제 클래스를 이용할 때 타입을 지정해줍니다. 대표적인 예로 리스트를 생성할 때 어떤 데이터 타입의 리스트를 생성해줄 지 결정해주는 방법이 있습니다. 배열을 조작하는 것은 타입에 관계 없이 동일하기 때문에 가상 타입을 사용해서 타입에 상관 없이 한 번만 코드를 작성해주면 됩니다. C언어에서는 같은 로직이라도 타입이 다르면 별도로 함수를 만들어줘야 하지만 자바에서는 이런 비효율성을 해결해줄 수 있습니다.

 

제네릭 기능이 없더라도 모든 타입을 담을 수 있는 Object 타입을 사용해 매개변수를 받고 다시 필요한 타입으로 형변환을 해주며 사용할 수도 있습니다. 하지만 제네릭은 타입 사용을 보다 엄격하게 검사해서 코드 사용 시 발생할 수 있는 타입 mismatch를 피할 수 있도록 해줍니다. 즉, 타입 지정 및 변환의 안정성을 높여줍니다.  아래 코드에서는 T, V라는 가상의 타입을 사용해 클래스와 메소드를 작성한 뒤 실제 클래스 호출 시에 타입을 결정해주는 클래스 예시입니다.

package study.first;

public class Test<T, V> {
	
	T thing;
	V value;
	
	// 생성자 선언
	public Test(T thing, V value) {
		
		this.thing = thing;
		this.value = value;
	}
	
	// 가상 타입을 이용한 메소드 작성
	public void print() {
		
		System.out.println("물건 : " + thing);
		System.out.println("가치 : "+ value);
	}
}

 


 

[ 제네릭 클래스 사용 ]

  • 클래스<제네릭> a = new 클래스<>(생성자);
  • 일반적인 인스턴스 생성과 동일하며, 꺽쇠기호 안쪽에 타입지정만 해주면 됨

사용법은 간단합니다. 제네릭 기호 안에 타입만 지정해주면 됩니다. 지정 타입은 클래스에서 허용하는 타입만 가능합니다. 그리고 원시타입의 경우 래퍼 클래스로 지정해줘야 합니다. 타입 지정만 해주면 오토래핑을 통해 자동 변환됩니다.

 

package study.first;

public class Main {
	public static void main(String[] args) {
		
		
		Test<String, Integer> a = new Test<>("컴퓨터", 100);
		
		a.print();		
	}
}

 


 

[ 제네릭 타입 제한 ]

  • 가상 타입 변수가 특정 클래스를 상속할 경우에만 사용 가능하도록 제한 가능
  • <T extends superclass> 형태로 지정

제네릭을 사용하더라도 모든 타입을 담을 수 있도록 하면 본래 의도와 다르게 잘 못 사용될 수 있습니다. 따라서 특정 클래스를 상속받는 클래스 타입만 매개변수로 사용할 수 있도록 제한할 수 있습니다. 원시 타입의 경우 래퍼 클래스로 대체해 사용하기 때문에 역시 특정 클래스를 상속받는다 할 수 있습니다. 예를 들어 숫자 타입만 매개변수로 받아 연산을 수행하는 클래스를 만든다면 아래와 같이 'Number' 클래스를 상속받는 클래스로 매개변수를 제한할 수 있습니다. 매개변수에 원시타입을 넣으면 래퍼 클래스로 오토래핑됩니다.

package study.first;

public class Main {
	public static void main(String[] args) {
		
		Test<Integer> a = new Test<>(100, 200);
		
		a.plus(); // 300.0
	}
}
package study.first;

public class Test<T extends Number> {
	
	T num1;
	T num2;
	
	// 생성자 선언
	public Test(T num1, T num2) {
		
		this.num1 = num1;
		this.num2 = num2;
	}
	
	// 가상 타입을 이용한 메소드 작성
	public void plus() {
		
		System.out.println(num1.doubleValue() + num2.doubleValue());
	}
}

 


 

[ 제네릭 와일드카드 ]

  • 제네릭이 선언된 클래스의 인스턴스를 메소드의 매개변수로 받을 때, 타입을 무시하고 받음
  • <?> 형태로 사용

본래 동일 클래스의 인스턴스는 같은 타입으로 간주됩니다. Test 클래스의 인스턴스 t1과 t2는 같은 타입이기 때문에 Test 타입 변수를 매개변수(parameter)로 받는 메소드에서는 t1과 t2를 모두 인자값으로 사용할 수 있습니다. 하지만 만약 Test 클래스에서 제네릭을 통해 타입을 지정받도록 하였고, t1과 t2에서 지정한 타입이 다르다면 둘은 동일 클래스의 인스턴스지만 다른 타입으로 간주됩니다. 타입 검사를 엄격히 하는 제네릭의 특성이자 장점 때문입니다.

 

하지만 클래스 내에는 제네릭으로 지정한 가상 타입의 필드 외에도 고정된 타입의 필드 또한 존재할 수 있습니다. 아래와 같은 클래스가 있다고 가정해봅니다. String 타입 필드를 하나 추가했습니다. 

package study.first;

public class Test<T extends Number> {

	T num1;
	T num2;
	String name; // String 타입 필드 생성

	// 생성자 선언
	public Test(T num1, T num2, String name) {

		this.num1 = num1;
		this.num2 = num2;
		this.name = name;
	}

	// 가상 타입을 이용한 메소드 작성
	public void plus() {

		System.out.println(num1.doubleValue() + num2.doubleValue());
	}
}

 

 

제네릭 타입 T는 Integer가 될 수도 있고 Double이 될 수도 있습니다. 서로 다른 타입의 인스턴스 2개를 아래와 같이 생성합니다.

package study.first;

public class Main {
	public static void main(String[] args) {
		
		Test<Integer> a = new Test<>(1, 2, "합계");
		Test<Double> b = new Test<>(200.0,300.0, "합계");
		
		a.plus(); // 3.0
		b.plus(); // 500.0
	}
}

 

 

a와 b는 동일 클래스의 인스턴스지만 제네릭 타입을 다르게 줬기 때문에 서로 다른 타입으로 간주됩니다. 따라서 만약 이 클래스에서 a,b의 name을 비교해야 한다면 타입 변환을 해줘야 합니다. 제네릭의 와일드카드를 쓰지 않고 equals 메소드를 오버라이딩하면 아래와 같이 작성 가능합니다.

package study.first;

public class Main {
	public static void main(String[] args) {
		
		Test<Integer> a = new Test<>(1, 2, "합계");
		Test<Double> b = new Test<>(200.0,300.0, "합계");
		
		a.plus(); // 3.0
		b.plus(); // 500.0
		
		System.out.println(a.equals(b)); // true
	}
}
package study.first;

public class Test<T extends Number> {

	T num1;
	T num2;
	String name; // String 타입 필드 생성

	// 생성자 선언
	public Test(T num1, T num2, String name) {

		this.num1 = num1;
		this.num2 = num2;
		this.name = name;
	}

	// 가상 타입을 이용한 메소드 작성
	public void plus() {

		System.out.println(num1.doubleValue() + num2.doubleValue());
	}
	
	// equals 메소드 오버라이딩 (name 문자열 비교)
	public boolean equals(Object t) {
		
		Test temp = (Test)t;
		
		if (temp.name.equals(this.name))
			return true;
		
		return false;
	}
}

 

 

같은 결과를 내지만 좀 더 간단한 제네릭 와일드카드를 사용하면 아래와 같습니다. <?>는 제네릭으로 지정한 타입을 무시하고 같은 타입의 인스턴스로 인식하여 매개변수로 받겠다는 의미입니다.

	// equals 메소드 오버라이딩 (name 문자열 비교)
	public boolean equals(Test<?> t) {
		
		if (t.name.equals(this.name))
			return true;
		
		return false;
	}

 

 


 

[ 와일드카드 제한 ]

  • 클래스의 제네릭 타입 제한과 같이 메소드에서도 제네릭 와일드카드의 타입 제한이 가능
  • 상위제한 : 자기 자신을 포함하여 super클래스를 상속받고 있는 자식 클래스로 제한 (super)
  • 하위 제한 : 자기 자신을 포함하여 자식 클래스의 상위 클래스로 제한 (extends)

메소드에서 <?> 형태의 와일드카드를 사용할 때도 상위제한과 하위제한을 이용하여 타입 제한이 가능합니다. 아래 코드는 배열을 모두 담을 수 있는 List 인터페이스 타입의 인스턴스를 메소드의 매개변수로 받을 때, 숫자 타입의 배열만 받을 수 있도록 하는 예시 코드입니다. 만약 상위 제한을 사용한다고 하면 extends 대신 super 명령어를 사용하면 됩니다.

package study.first;

import java.util.Arrays;
import java.util.List;

public class Main {
	public static void main(String[] args) {

		
		List<Integer> a2 = Arrays.asList(new Integer[] {1,2,3,4,5});
		System.out.println(sum(a2));
		
	}
	
	// 리스트 값의 합계를 구하는 메소드 (숫자 타입의 List만 받음)
	static double sum(List<? extends Number> list) {
		
		double value = 0;
		for (Number v : list)
			value += v.doubleValue();
		
		return value;
	}
}

 


 

[ 제네릭 메소드 ]

  • 메소드의 매개변수를 제네릭 가상 타입으로 받을 수 있음]
  • <제네릭> int test(제네릭 타입 매개변수) 형태로 메소드 작성
  • 소속.<제네릭>test(매개변수) 형태로 메소드 사용 (소속을 명확히 해줘야 함)

위의 예시들에서는 제네릭 클래스에서 사용된 가상 타입의 변수를 메소드의 매개변수로 사용했습니다. 하지만 제네릭 선언이 되지 않은 일반 클래스에서도 제네릭을 사용하여 동적인 매개변수 타입을 사용할 수 있습니다. 간단한 사용 예시는 아래 코드와 같습니다. 일반적인 경우 클래스 내에 있는 static 메소드는 메소드명만 호출해서 쓸 수 있지만 제네릭 메소드의 경우 자신의 클래스 내에서도 소속된 클래스를 명시해줘야 컴파일 에러가 발생하지 않습니다.

 

package study.first;

public class Main {
	public static void main(String[] args) {

		Main.<String, Integer>test("세 개는", 3);
		
		Main.<String, Double>test("네 개는", 4.0);
		
		Main.<Integer, Double>test(3, 4.5);
		
	}
	
	// 제네릭 메소드
	static <T, V extends Number> void test(T a, V b) {
		
		System.out.println(a + " : " + b);
		
	}
}

 


 

[ 제네릭 생성자 ]

  • 제네릭 메소드와 같이 일반 클래스의 생성자 또한 제네릭 타입으로 받을 수 있음
  • 가상타입으로 생성자를 받을 후 필드 타입에 맞게 타입 변환

일반 클래스에서도 생성자를 제네릭 타입으로 지정할 수 있습니다. 필요에 따라 아무 타입으로 받은 뒤 필드값에 맞게 적절히 타입 변환을 해서 값을 넣어주면 됩니다. 

 

package study.first;

public class Main {
	public static void main(String[] args) {

		Test t1 = new Test(123, 456);
		t1.test();
		
		Test t2 = new Test("ABC", 123);
		t2.test();

	}
}


// 제네릭 생성자 사용
class Test {
	
	String str;
	
	// 제네릭 생성자 선언
	<T, V>Test(T t, V v) {
		
		this.str = t.toString() + v.toString();

	}
	
	
	void test() {
		
		System.out.println(str);
	}
	
}

 

728x90

댓글

💲 추천 글