[Java] Generic

이정우·2022년 1월 3일
0

Generics add stability to your code by making more of your bugs detectable at compile time. - Oracle Java Doc

Generic이란?

Generic은 "일반적인"이라는 뜻을 가지고 있다.

뜻만 놓고 보자면 프로그래밍에서 일반적인 경우가 뭐가 있을까 하는 생각이 들 수 있는데, 데이터의 타입을 일반화하여 여러 데이터 타입을 사용하는 것을 의미한다.

자바의 기초적인 자료구조인 ArrayList를 살펴보자.

ArrayList<Integer> list1 = new ArrayList<Integer>();

ArrayList<String> list2 = new ArrayList<String>();

ArrayList<Something> list3 = new ArrayList<Something>();

<> 안에 들어가는 데이터 타입이 매번 다른데도 똑같이 작동했던 경험을 다들 가지고 있을 것이다.

자바에서 제공해주는 Wrapper Class는 그렇다 쳐도, 내가 임의로 만든 클래스까지 모두 사용이 가능하다.

자바를 만든 사람들이 NormalClass, AwesomeClass, SuperSexyClass 등 클래스 이름으로 만들 수 있는 모든 경우의 수를 생각해서 하나하나 다 지정을 한 것일까?

만약, 열심히 연구를 해서 새롭게 자료구조를 만들었다고 치자. 그 때도 Integer, String, NormalClass 등 모든 데이터 타입에 따라 구현을 해야할까?

그런 비효율적인 일을 막기 위해서 Generic(제네릭)이라는 기법을 사용한다.

제네릭은 클래스를 정의할 때 데이터의 타입을 확정하지 않고, 사용자가 객체를 생성할 때 지정하는 기법을 뜻한다.


Why Generic?

제네릭이 무엇인지는 어느 정도 알았으니, 사용하는 이유에 대해서 더 자세하게 알아보자.

컴파일시 에러 체크가 가능

런타임시 에러가 발생하면 에러를 수정하는 것이 어렵다. 심지어 어디서 발생했는지 찾는 것도 어렵다.

이 때, 제네릭을 사용하면 컴파일을 할 때 타입에 대한 에러 체크를 하기 때문에 보다 안전하다.

class FruitBox{
    Object item;
    public void store(Object item){
    	this.item = item;
    }
    public Object pullOut(){
    	return item;
    }
}

public static void main(String []args){
    FruitBox box = new FruitBox();
    box.store(new Apple(10));
    
    // 컴파일 시에는 에러가 발생하지 않지만, 실행하면 에러가 발생
    Orange orange = (Orange)box.pullOut();
}

자바에서 모든 클래스는 Object 클래스의 하위 클래스이다.

위의 코드에서 FruitBox 클래스는 여러 과일 객체를 저장하기 위해 Object 객체로 item을 멤버 변수로 지니고 있다.

Apple 객체를 item으로 넣었지만, 꺼낼 때는 Orange 객체를 꺼냈다. 컴파일을 할 때 에러가 발생할 것 같지만, 실제로는 런타임에 에러가 발생한다.

따라서, Object 객체를 사용하는 것보다 제네릭을 사용해 컴파일 시점에서 오류를 잡는 것이 좋다.

형 변환의 생략

앞선 예시에서 Orange 객체가 아닌 Apple 객체를 꺼낸다고 가정하자.

class FruitBox{
    Object item;
    public void store(Object item){
    	this.item = item;
    }
    public Object pullOut(){
    	return item;
    }
}

public static void main(String []args){
    FruitBox box = new FruitBox();
    box.store(new Apple(10));
    
    Apple apple = (Apple)box.pullOut();
}

분명히 Apple 객체를 넣었고, Apple 객체를 그대로 가져와서 사용하지만 형 변환이 필요하다.

이렇게 형 변환을 할 경우에는, 오버헤드가 발생하기 때문에 시스템의 성능을 저하시킬 수 있다.

오버헤드 발생 원인

Object o = someObject;
String s = (String)o; // 명시적인 형변환

// s와 o 두 객체의 데이터 타입을 확인해야 하고,
// 형 변환이 불가능한 경우에는 JVM에서 ClassCastException을 throw해야 하기 때문에 오버헤드 발생

코드의 재사용성 증가

앞서 새로운 자료구조를 만들었을 때, 다양한 데이터 타입을 어떻게 적용해야할까? 라는 구문이 있었다.

제네릭을 이용하면 이러한 의문점도 해결이 된다.

class IntArray{
    int[] array = new int[10];
    
    int getElement(int idx){
    	return array[idx];
    }
}

class StringArray{
    String[] array = new String[10];
    
    String getElement(int idx){
    	return array[idx];
    }
}

위의 코드에서 각 클래스에 존재하는 getElement 메소드는 배열에서 인덱스에 해당하는 원소를 반환한다.

차이점은 원소의 데이터 타입인데, 제네릭을 이용하면 위와 같은 비슷한 기능에 대한 코드의 재사용이 가능하다.


Generic Types

Generic Type(제네릭 타입)은 파라미터로 데이터 타입을 가지는 클래스나 인터페이스를 의미한다.

제네릭 타입은 클래스나 인터페이스 이름 뒤에 <>가 붙으며, 그 안에 타입 파라미터가 들어간다.

public class SuperClass<T> { ... }
public interface AwesomeInterface<T> { ... }

Generic Types 적용

앞서 들었던 FruitBox 예제에 제네릭 타입을 적용해보자.

class FruitBox<T>{
    T item;
    public void store(T item){
    	this.item = item;
    }
    public T pullOut(){
    	return item;
    }
}

public static void main(String []args){
    FruitBox<Apple> appleBox = new FruitBox<Apple>();
    FruitBox<Orange> orangeBox = new FruitBox<>();
    
    appleBox.store(new Apple(10));
    orangeBox.store(new Orange(5));
    
    Apple apple = appleBox.pullOut();
    Orange orange = orangeBox.pullOut();
}

제네릭 타입을 이용해 런타임 에러과 형 변환을 없앴다. 또한, 같은 역할을 하는 메소드를 중복 선언하지 않음으로써 코드의 재사용성도 높아졌다.

Diamond

사용하고자 하는 클래스를 <> 안에 넣으면 사용할 수 있으며, orangeBox의 선언 부분을 보면 뒤쪽의 <>에는 데이터 타입이 생략된 것을 볼 수 있다.

이는 Java SE 7 버전 이후 도입된 문법으로, FruitBox<Orange> 부분에서 데이터 타입을 명시했기 때문에 생략이 가능한 것이다.

Type Parameter

타입 파라미터로는 주로 T를 사용하는데, 이는 정해진 것이 아니라 Type을 의미하기 때문에 사용하는 것이다.

규칙은 없지만 일반적으로 대문자 알파벳 한 글자로 표현하며, 주로 사용하는 타입 파라미터는 다음과 같다.

타입 파라미터의미
<T>Type
<E>Element
<K>Key
<N>Number
<V>Value

Generic Class

클래스를 설계할 때 타입을 명시하지 않고 타입 파라미터로 지정한 뒤, 해당 클래스를 사용할 때 구체적인 타입을 지정한다.

class GenericClass<T> {
    private T t;
    
    public void setT(T t){
    	this.t = t;
    }
    
    public T getT(){
    	return T;
    }
}

Generic Interface

클래스와 마찬가지로 인터페이스에서도 제네릭을 사용할 수 있다. 인터페이스를 구현하는 클래스의 선언 시 데이터 타입을 명시하면 된다.

interface GenericInterface<T>{
	T doSomething();
}

class GenericImpl implements GenericInterface<String>{
    @Override
    String doSomething(){
    	return "";
    }
}

Multi Type Parameters

자바의 HashMap 같은 경우에는 Key 값과 Value를 받는다. 이 때도 제네릭의 사용이 가능한데, 여러 타입 파라미터를 사용하기만 하면 된다.

// 실제 HashMap 자료구조는 다음과 같이 선언되어 있다
public class HashMap<K, V>;

참조

Oracle Java Doc - Generics
Does Java casting introduce overhead?

0개의 댓글