뭐라도 끄적이는 BLOG

Java Generic 본문

Java/Java 기본

Java Generic

Drawhale 2023. 7. 2. 11:01

제네릭

제네릭은 JDK 1.5부터 등장하였다. 제네릭은 데이터 타입을 일반화 하는 것을 의미한다. 클래스나 메소드에서 사용할 내부 데이터 타입을 컴파일 시에 미리 지정하는 방법이다. 이렇게 컴파일시 type check를 하면 클래스나 메소드 내부에서 사용되는 객체의 타입의 안정성을 높일 수 있으며 반환값에 대한 타입 변환 및 타입 검사에 들어가는 노력을 줄일 수 있다.

제네릭 이전에는 클래스나 메소드의 매개변수나 반환값으로 Object타입을 사용다. 하지만 반환된 Object객체를 다시 원하는 타입으로 변환하는 과정에서 프로그래머의 실수가 잦았다. 이젠 제네릭을 사용하여 컴파일 타임에 미리 타입이 정해져 타입 검사나 타입 변환과 같은 번거로운 작업을 생략할 수 있게 되었다.

제네릭 사용법

class Box<T>{
	private T ob;
	public void set(T o){
		ob = 0;
	}
	public T get(){
		return ob;
	}
}

해당 클래스는 제네릭을 적용한 클래스다. 인스턴스를 생성할 때 T를 결정하여 T가 어떤것인지 컴파일러에게 알려 줄 수 있다.

Box<Apple> aBox = new Box<Apple>();

T를 Apple로 결정하여 인스턴스를 생성하였다. Apple과 Apple을 상속하는 하위클래스의 인스턴스를 저장할 수 있다. T는 타입 매개변수, 타입인자, 매개변수화 타입 등으로 불린다. 위 코드에서 Box<Apple>은 컴파일러에서 별도의 자료형으로 인식한다.

Box<Apple> aBox = new Box<>();

인스턴스 생성시 <>안을 생략할 수 있다. 흔히들 다이아몬드 기호라고 표현을 많이 하지만 이는 비공식적인 이름이다.

타입 매개변수의 이름 규칙 (관례)

대문자 한문자로 이름을 짓습니다.

  • E → Element
  • K → Key
  • N → Number
  • T → Type
  • V → Value

바운디드 타입

제네릭 클래스는 타입 인자를 제한할 수 있다.

class Box<T extends Number>{...}

해당 코드는 인스턴스 생성 시 타입 인자로 Number또는 이를 상속하는 클래스만 올 수 있다.

class Box<T>{
	private T ob;
	....
	public int toIntValue(){
		return ob.intValue(); //ERROR
	}
}
class Box <T extends Number>{
	private T ob;
	....
	public int toIntValue(){
		return ob.intValue();
	}
}

단순히 제네릭만 사용한다면 위와 같이 Number의 intValue()메소드를 직접적으로 사용할 수 없는 것을 확인할 수 있다. 바운디드 타입을 사용하면 Number 클래스의 intValue()를 사용할 수 있게 된다. 이렇듯 단순히 제한에서 끝나는 것이 아니라 제한을 했더니 제한된 클래스들의 메소드를 호출 할 수 있는 부수적인 효과도 노릴 수 있다.

interface Eatable{ public String eat();}
class Box <T extends Eatable>{
	...
}

클래스 뿐만 아니라 인터페이스로 제한할 수 있다.

class Box <T extends Number & Eatable> {...}

하나의 클래스와 여러 인터페이스에 대해서 동시 제한을 할 수도 있다.

제네릭 메소드 만들기

클래스가 아닌 메소드 하나에 대해 제네릭으로 정의 할 수 있다.

class BoxFactory{
	public static <T> Box<T> makeBox(T o){
		Box<T> box = new Box<T>();
		box.set(o);
		return box;
	}
}

static 이후의 <T>를 선언하게 되면 해당 메소드만 제네릭을 표현한다.

Box<String> sBox = BoxFactory.<String>makeBox("sweet");

메소드를 사용할 때 다음과 같이 사용할 수 있으며 타입은 메소드가 호출될 때 결정된다.

Box<String> sBox = BoxFactory.makeBox("sweet");
Box<String> sBox = BoxFactory.makeBox(7.12);

오토박싱또한 컴파일러가 알아서 해준다.

와일드 카드

제네릭으로 구현된 메소드의 경우 선언된 타입으로만 매개변수를 입력해야 한다. 이를 상속받은 클래스 혹은 부모 클래스를 사용하고 싶어도 불가능하고 어떤 타입이 와도 상관없는 경우에 대응하기 좋지 않다. 이를 위한 해법으로 WildCard가 있다.

public static <T> void peekBox(Box<T> box){
    ...
}
public static void peekBox(Box<?> box){
    ...
}

와일드카드 기반 메소드 정의가 더 간결하므로 우선시 되어야 한다고 권고하고 있다.

Upper-Bounded Wildcards

public static void peekBox(Box<? extends Number> box){
    ...
}

Box형 인스턴스를 전달 받는데 Number 이거나 Number를 상속하는 하위 클래스 여야한다.

Lower-Bounded Wildcards

public static void peekBox(Box<? super Integer> box){
    ...
}

전달되는 인스턴스의 T는 Integer 또는 Integer가 상속하는 클래스 여야한다. 즉, Integer, Number, Object로 제한된다.

Erasure

Erasure란 원소 타입을 컴파일 타임에서만 검사를하고 런타임에는 해당 타입 정보를 알수 없는것을 말한다. 즉 컴파일 타임에만 제약 조건을 적용하고 런타임에는 타입에 대한 정보를 소거하는 프로세스이다. 실제 컴파일 한 후 바이트 코드를 살펴보면 타입 정보를 찾아볼 수 없다.

이렇게 만들게 된 이유는 하위 호환성을 지키기 위해서 이다. 제네릭을 사용하더라도 제네릭이 없는 하위 버전에서도 동일하게 동작해야하기 때문이다.

primitive 타입을 제네릭에 사용하지 못하는 것은 Object 클래스를 상속받고 있지 않기 때문이다..

class GenericEx<T>{
    ...
    GenericEx(int size){
    //  arr = new T[size];
        arr = (T[]) new Object[size];
    }
    ...
}

위 코드에서 new T[size]가 안되는 이유도 같다. 제네릭은 컴파일타임에 적용되는 문법이므로 동적 메모리 할당인 new연산자는 바로 사용할 수 없으며 Object타입으로 생성한 다음 타입 캐스팅을 사용해야한다.

class GenericEx<T>{
    //private static T val;
    ...
}

static 변수에도 제네릭을 사용할 수 없다. static 키워드는 특정 객체에 종속되지 않고 클래스 이름으로 접근할 수 있는데 객체가 생성되기전에는 이 타입을 알수 없기 때문이다.

하지만 static 메소드에는 사용할 수 있다. 메소드의 경우 기능을 공유해서 사용되는 것이기 때문에 제네릭은 메소드 안에서 지역변수로만 활용되기 때문이다.

반응형

'Java > Java 기본' 카테고리의 다른 글

Java Lambda  (0) 2023.07.02
Java I/O  (0) 2023.07.02
Java Annotation  (0) 2023.07.02
Java Enum  (0) 2023.07.02
Java Thread  (0) 2023.07.02