스프링 예제 코드들 보면서 이게 뭐지 싶었던 문법인 제네릭에 대한 21강이었다.
다음 강의에는 제네릭을 좀 더 심화해서 다룬다는데 중요한 개념인 것 같으니 확실하게 정리하고 넘어가자.
제네릭이란?
- 자료형을 정하지 않고 클래스의 형태만 갖추어 놓는 것.
- 필요할 때에 해당 클래스의 자료형을 결정하는 방식
- 클래스의 경우는 인스턴스 선언시, 자료형 결정
- 메소드의 경우는 메소드 호출시, 자료형 결정
왜 제네릭이 등장했는가?
- 제네릭 이전 여러 타입의 데이터를 다루기 위해 일반적으로 Object 클래스를 사용한 클래스를 만들고, 명시적 형변환을 이용했다.
- 명시적 형변환을 다수 사용하면서 코드 안정성이 저하되고, 코드 에러를 컴파일 레벨에서 잡아내기 어려웠다.
- 무엇보다도 일일이 명시적 형변환을 지정하는 것이 굉장히 번거로운 작업이었다.
// 제네릭 이전의 코드 예제
class Apple {
public String toString() {
return "I am an apple.";
}
}
class Orange {
public String toString() {
return "I am an orange.";
}
}
class Box {
private Object ob; // 제네릭 이전에는 최상위 클래스인 Object 클래스를 사용했다.
public void set(Object o) {
ob = o;
}
public Object get() {
return ob;
}
}
public class Main {
public static void main(String[] args) {
Box aBox = new Box();
Box oBox = new Box();
aBox.set(new Apple());
oBox.set(new Orange());
Apple a = (Apple)aBox.get(); // 인스턴스 내부의 값을 꺼낼 때 명시적 형변환이 필수였다.
Orange o = (Orange)oBox.get();
System.out.println(a);
System.out.println(o);
}
}
// 실행 결과
I am an apple.
I am an orange.
제네릭 이전 방식의 문제점
- 명시적 형변환 과정으로 인해서 컴파일러의 개입이 줄어들어 프로그래머의 실수가 컴파일 레벨에서 잡히지 않는다.
- 더 나아가서 어떤 실수의 경우는 실행과정에서도 잡히지 않을 수 있다.
// 제네릭 이전 방식의 문제점 1 - 실행과정에서 에러 발생 케이스
class Apple {
public String toString() {
return "I am an apple.";
}
}
class Orange {
public String toString() {
return "I am an orange.";
}
}
class Box {
private Object ob;
public void set(Object o) {
ob = o;
}
public Object get() {
return ob;
}
}
public class Main {
public static void main(String[] args) {
Box aBox = new Box();
Box oBox = new Box();
aBox.set("Apple"); //Apple 클래스의 인스턴스가 아닌 문자열을 입력했다.
oBox.set("Orange"); //Orange 클래스의 인스턴스가 아닌 문자열을 입력했다.
Apple a = (Apple)aBox.get(); //없는 인스턴스를 저장하려 한다.
Orange o = (Orange)oBox.get(); //없는 인스턴스를 저장하려 한다.
System.out.println(a);
System.out.println(o);
}
}
// 실행결과
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to com.company.Apple
at com.company.Main.main(Main.java:34)
// 제네릭 이전 방식의 문제점 2 - 실행과정에서도 에러가 발생하지 않는 경우
class Apple {
public String toString() {
return "I am an apple.";
}
}
class Orange {
public String toString() {
return "I am an orange.";
}
}
class Box {
private Object ob;
public void set(Object o) {
ob = o;
}
public Object get() {
return ob;
}
}
public class Main {
public static void main(String[] args) {
Box aBox = new Box();
Box oBox = new Box();
aBox.set("Apple"); //프로그래머의 실수 1
oBox.set("Orange"); //프로그래머의 실수 2
System.out.println(aBox.get());
System.out.println(oBox.get());
}
}
// 실행결과
Apple // 이 경우 실수가 없었다면 I am an apple가 표시되어야 옳다.
Orange // 이 경우 실수가 없었다면 I am an orange가 표시되어야 옳다.
제네릭 기반으로 클래스 정의
class Box {
private Object ob;
public void set(Object o) {
ob = o;
}
public Object get() {
return ob;
}
}
///////////////////////////////////
// 제네릭 적용
class Box<T> {
private T ob;
public void set(T o) {
ob = o;
}
public T get() {
return ob;
}
}
- 자료형의 정보를 <> 키워드로 감싼 T를 통해서 받는다.
- 제네릭 관련 용어
- 타입 매개변수 : Box<T> 에서 T, <>키워드로 감싸진 변수
- 타입 인자 : 인스턴스 선언 또는 메소드 호출의 Box<Apple> 에서 Apple, 즉 클래스 이름
- 매개변수화 타입 : Box<Apple> 전체
// 제네릭을 적용한 예제
class Apple {
public String toString() {
return "I am an apple.";
}
}
class Orange {
public String toString() {
return "I am an orange.";
}
}
class Box<T> {
private T ob;
public void set(T o) {
ob = o;
}
public T get() {
return ob;
}
}
public class Main {
public static void main(String[] args) {
Box<Apple> aBox = new Box<Apple>(); //기본적인 제네릭 클래스의 인스턴스 선언 방법
Box<Orange> oBox = new Box<>(); //다이아몬드 기호를 사용한 인스턴스 선언 방법
aBox.set(new Apple());
oBox.set(new Orange());
Apple a = aBox.get(); //형변환이 이루어지지 않아도 된다.
Orange o = oBox.get(); //형변환이 이루어지지 않아도 된다.
System.out.println(a);
System.out.println(o);
}
}
// 실행결과
I am an apple.
I am an orange.
제네릭의 기본 문법
- 2개 이상의 타입 매개변수를 사용할 수 있다.
- 타입 매개변수 네이밍 규칙
- 한개의 대문자로 구성하는 것이 규칙
- 예제
- E : Element
- K : Key
- N : Number
- T : Type
- V : Value
- 타입 인자에 기본 자료형은 사용할 수 없다.
- 제네릭 클래스를 예로 들어 Box <Apple> box = new Box<>(); 식으로 뒤의 타입 인자를 생략할 수 있다. 이를 반 공식적인 용어로 다이아몬드 기호라고 부름
// 다중 매개변수를 사용한 예제
class DBox <L, R> { // 다중 매개변수 기반 제네릭 클래스 정의 방법
private L left;
private R right;
public void set(L l, R r) {
left = l;
right = r;
}
@Override
public String toString() {
return left + "&" + right;
}
}
public class Main {
public static void main(String[] args) {
DBox<String, Integer> box = new DBox<String, Integer>(); //여러개의 타입 매개변수를 사용해도 다이아몬드 기호를 사용할 수 있다.
box.set("Apple", 125);
System.out.println(box);
}
}
// 실행결과
Apple&125
- 제네릭 클래스의 타입 인자 제한하기
- extends 키워드를 사용해서 해당 되는 클래스 또는 해당 클래스를 상속하는 클래스만 타입 인자로 사용할 수 있도록 제한할 수 있다.
- 의미 그대로 타입 인자를 제한하는 용도로 쓸 수 있지만, 제한을 걸어주면 제한 기준이 되는 클래스의 클래스 메소드를 제네릭 클래스 내부에서 사용할 수 있다.
- 제네릭 클래스 내부에서 사용할 수 있는 클래스 메소드의 기준은 교집합으로 잡히며, 제한이 없을 경우에는 최상위 클래스인 Object 클래스 자체의 클래스 메소드만 사용할 수 있다.
// 제네릭 클래스의 타입 인자 제한하기 예제
class Box<T extends Number> { //extends 키워드를 사용해서 Number 클래스 또는 해당 클래스를 상속하는 클래스들로 제한을 걸었다.
private T ob;
public void set(T o) {
ob = o;
}
public T get() {
return ob;
}
public int toIntValue() {
return ob.intValue(); //Number 클래스의 내부 클래스 메소드인 intValue()를 사용할 수 있다.
} //제한 안 걸어주면 에러 발생함
public double toDoubleValue() {
return ob.doubleValue(); //Number 클래스의 내부 클래스 메소드인 doubleValue()를 사용할 수 있다.
}
}
public class Main {
public static void main(String[] args) {
Box<Integer> iBox = new Box<>();
iBox.set(24);
Box<Double> dBox = new Box<>();
dBox.set(5.97);
System.out.println(iBox.toDoubleValue());
System.out.println(dBox.toIntValue());
}
// 실행결과
24.0
5
- 제네릭 클래스의 타입 인자를 인터페이스로 제한 할 수 있다.
- 동일하게 extends 키워드를 사용해서 제한을 걸어줄 수 있으며 인터페이스를 조건으로 주게 되면 해당 인터페이스를 구현한 클래스들만 해당 제네릭 클래스에 사용할 수 있다.
- 인터페이스의 메소드도 동일하게 제네릭 클래스 내부에서 사용할 수 있다.
- 하나의 클래스와 하나의 인터페이스에 대해서 동시에 제한을 걸 수 있다. 이 경우 & 키워드를 사용해서 클래스 이름과 인터페이스 이름을 묶어준다.
- ex> class Box<T extends Number & Eatable) {...}
제네릭 메소드
- 제네릭 클래스와 대부분의 경우, 동일하다. 클래스가 아닌 제네릭으로 정의 된 메소드를 말함
- 제네릭 클래스와 달리 메소드의 경우는 메소드 호출 시점에 타입 인자를 사용해서 타입을 결정함
- 메소드 인자를 통해서 타입을 판단할 수 있기 때문에 타입 인자를 생략해도 된다.
- 제네릭 클래스와 동일한 방법으로 타입 인자를 제한할 수 있으며, 이로 인한 효과도 동일함
// 제네릭 메소드 예제
class BoxFactory {
public static <T> Box<T> makeBox(T o) { //가장 왼쪽에서부터 첫번째의 <T>
Box<T> box = new Box<T>;
box.set(o);
return box;
}
}
Box<String> sBox = BoxFactory.<String>makeBox("Sweet"); //기본적인 제네릭 메소드 호출 방법
Box<String> sBox = BoxFactory.makeBox("Sweet"); //타입 인자를 생략한 제네릭 메소드 호출 방법
'프로그래밍 > Java' 카테고리의 다른 글
Java - Collections (1) (0) | 2020.10.09 |
---|---|
Java - 제네릭 (2) (0) | 2020.09.27 |
Java - Java의 기본 클래스 (2) (0) | 2020.09.21 |
Ubuntu에서 Java 개발환경 세팅 (0) | 2020.09.14 |
Java - Java의 기본 클래스 (1) (0) | 2020.07.25 |