본문 바로가기
프로그래밍/Java

Java - 제네릭 (1)

by 왕거 2020. 9. 22.

스프링 예제 코드들 보면서 이게 뭐지 싶었던 문법인 제네릭에 대한 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