☕️ 제네릭(Generic)이란
제네릭이란 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미한다.
예를 들면 자바에서 자주 사용되는 List클래스는 제네릭 사용이 가능한 클래스 중 하나로 List에서 사용될 데이터 타입을 외부에서 지정할 수 있다. 아래와 같이 List 내부에서 데이터 타입을 지정하는 것이 아니라 외부에서 타입을 지정하는 형태로 사용된다.
// String 타입으로 선언 및 생성
List<String> stringList = new ArrayList<>();
// Integer 타입으로 선언 및 생성
List<Integer> integerList = new ArrayList<>();
☕️ 제네릭(Generic)의 장점
1. 컴파일 시점에 타입 검사를 통한 예외 방지
제네릭을 사용하게 되면 컴파일 시점에 타입 검사를 통해 형변환 시 발생할 수 있는 오류를 사전에 방지할 수 있다.
가령 List에 담긴 숫자를 모두 더하는 프로그램이 있다고 가정해 보자.
아래의 코드와 같이 의도치 않게 문자열이 담기게 된다면, 컴파일 시점에는 오류가 발생하지 않고 런타임 시점에 오류가 발생하여 프로그램이 예기치 않게 종료가 될 수 있다.
List integerList = new ArrayList<>();
integerList.add(1);
integerList.add(3);
integerList.add(7);
integerList.add("abc"); // 의도하지 않은 문자가 들어간 케이스
int sum = 0;
for (int i = 0; i < integerList.size(); i++) {
sum += (int)integerList.get(i);
}
반면 제네릭을 사용하여 Integer타입을 보장해 준다면, 위와 동일하게 문자가 들어가더라도 컴파일 시점에 오류가 발생하여 런타임 오류를 사전에 방지할 수 있게 된다.
2. 불필요한 캐스팅을 제거
이전과 동일하게 List에 담긴 숫자를 모두 더하는 프로그램이 있다고 가정해 보자.
제네릭을 사용하지 않은 경우, 누적 연산을 할 때 강제로 형변환을 해줘야 한다.
List integerList = new ArrayList<>();
integerList.add(1);
integerList.add(3);
integerList.add(7);
int sum = 0;
for (int i = 0; i < integerList.size(); i++) {
sum += (int)integerList.get(i); // 강제 형변환 필요
}
반면, 제네릭을 사용할 경우 타입을 미리 지정해 놓기 때문에 형변환의 번거로움을 줄일 수 있으며 가독성도 좋아진다.
List<Integer> integerList = new ArrayList<>();
integerList.add(1);
integerList.add(3);
integerList.add(7);
int sum = 0;
for (int i = 0; i < integerList.size(); i++) {
sum += integerList.get(i);
}
☕️ 제네릭(Generic) 선언
제네릭 사용법을 익히기에 앞서 제네릭 표현법과 선언하는 방법에 대해서 알아보자.
제네릭은 통상적으로 아래의 규칙에 의해 타입을 표현한다.
타입 | 설명 |
<T> | Type |
<E> | Element |
<K> | Key |
<V> | Value |
<N> | Number |
제네릭은 클래스와 메서드에서 사용 가능하다.
먼저 클래스에서의 제네릭 선언 방법에 대해서 알아보자.
아래의 코드와 같이 클래스 옆에 <T>와 같이 제네릭을 명시해 주고, 리턴 타입과 매개변수 모두 타입을 T로 선언해 준다.
class GenericClass<T> {
private T t;
public T get() {
return t;
}
public void set(T t) {
this.t = t;
}
}
다음은 메서드에서의 사용 방법에 대해 알아보자.
클래스와 유사하게 리턴 타입과 매개변수에 제네릭 형태로 사용하면 된다.
class GenericMethod {
public <T> T method1(T t) {
return t;
}
public <K, V> void method2(K k, V v) {
System.out.println(k + "=" + v);
}
}
☕️ 제네릭(Generic) 사용예시
앞서 클래스와 메서드에서 제네릭을 선언하는 방법에 대해서 알아보았다.
실제 예제 코드를 통해 사용하는 방법에 대해서 알아보자.
사용예시 1. 제네릭을 사용하지 않은 코드와 사용한 코드
아래의 코드는 Apple 클래스와 Watch 클래스가 존재하며, Goods 클래스의 setter와 getter를 통해 값을 저장하고 가져올 수 있다.
이때 Goods 클래스는 내부적으로 Object를 통해 값을 저장할 수 있다.
반면, GoodsUsingGeneric 클래스는 제네릭의 형태로 만들어진 클래스이다.
Goods 클래스와 GoodsUsingGeneric 클래스의 차이를 보면,
Goods 클래스의 경우, getter를 통해 값을 가져올 때 매번 다운캐스팅이 필요하며 잘못된 형변환 시 오류가 발생할 가능성도 있다.
반면 제네릭을 사용한 GoodsUsingGeneric 클래스의 경우, 명시적으로 형변환을 해줄 필요가 없으며 형변환이 잘못되면 컴파일 시점에 오류가 발견된다는 장점도 가지고 있다.
class Apple {}
class Watch {}
class Goods {
private Object obj = new Object();
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
}
class GoodsUsingGeneric<T> {
private T t;
public T get() {
return t;
}
public void set(T t) {
this.t = t;
}
}
public class DifferenceObjectAndGeneric {
public static void main(String[] args) {
/* 다양한 타입을 제어하기 위하여 Object 사용 */
Goods goods1 = new Goods();
goods1.setObj(new Apple());
Apple apple1 = (Apple)goods1.getObj(); // 다운캐스팅 필요
Goods goods2 = new Goods();
goods2.setObj(new Watch());
Watch watch1 = (Watch)goods2.getObj(); // 다운캐스팅 필요
Goods goods3 = new Goods();
goods3.setObj(new Apple());
/* java.lang.ClassCastException.
문법상으로는 오류가 없지만 실행 예외 발생(사용자의 실수에 의하여 발생 가능)
*/
Watch watch2 = (Watch)goods3.getObj();
/* 다양한 타입을 제어하기 위하여 Generic 사용 */
GoodsUsingGeneric<Apple> goodsUsingGeneric1 = new GoodsUsingGeneric<>();
goodsUsingGeneric1.set(new Apple());
Apple appleUsingGeneric1 = goodsUsingGeneric1.get(); // 다운캐스팅 필요 없음
GoodsUsingGeneric<Watch> goodsUsingGeneric2 = new GoodsUsingGeneric<>();
goodsUsingGeneric2.set(new Watch());
Watch watchUsingGeneric1 = goodsUsingGeneric2.get(); // 다운캐스팅 필요 없음
GoodsUsingGeneric<Apple> goodsUsingGeneric3 = new GoodsUsingGeneric<>();
goodsUsingGeneric3.set(new Apple());
// Watch watchUsingGeneric2 = goodsUsingGeneric3.get(); // 문법 오류로 표시되어 사전에 오류 방지
}
}
사용예시 2. 제네릭 메서드 사용
class GenericMethod {
public <T> T method1(T t) {
return t;
}
public <K, V> void method2(K k, V v) {
System.out.println(k + "=" + v);
}
}
public class GenericMethodSample {
public static void main(String[] args) {
GenericMethod gm = new GenericMethod();
String str = gm.method1("문자열");
System.out.println("str : " + str); // str : 문자열
int num = gm.method1(1);
System.out.println("num : " + num); // num : 1
gm.method2("사과", "1개"); // 사과=1개
gm.method2("시력", 1.0); // 시력=1.0
gm.method2(3, 3); // 3=3
}
}
사용예시 3. 제네릭 클래스 타입제한
제네릭을 사용할 경우, 클래스 혹은 메서드에 들어올 타입에 대한 제한을 할 수 있다.
아래의 코드를 보면 C와 JAVA 클래스는 부모로 Lang 클래스 상속받고 있으며,
Eclipse와 IntelliJ 클래스는 Tool 클래스를 상속받고 있다.
이때, DevTool클래스를 보면 제네릭표현식에서 extends를 사용하여 Tool 또는 그의 자식 클래스만 대입가능하도록 제한을 두고 있다.
이렇게 타입제한을 하게 되면 DevTool에는 Tool, Eclipse, IntelliJ만 대입이 가능하고, 그 외 클래스를 대입하려고 하면 컴파일 오류가 발생한다.
class Lang {}
class C extends Lang {}
class Java extends Lang {}
class Tool {}
class Eclipse extends Tool {}
class IntelliJ extends Tool {}
class DevTool<T extends Tool> {} // 제네릭 타입으로 Tool 또는 그 자식 클래스만 대입 가능
public class TypeRestrictOnClass {
public static void main(String[] args) {
DevTool<Tool> devTool1 = new DevTool<>();
DevTool<Eclipse> devTool2 = new DevTool<>();
DevTool<IntelliJ> devTool3 = new DevTool<>();
/* 불가
DevTool<Lang> devTool4 = new DevTool<>();
DevTool<C> devTool5 = new DevTool<>();
DevTool<Java> devTool6 = new DevTool<>();
*/
}
}
사용예시 4. 제네릭 메서드 타입제한
class GenericMethd2 {
/*
GenericMethod.java의 GenericMethd 클래스에서 메서드의 타입 제한을 위하여 extends 키워드를 추가하였다.
*/
public <T extends String> T method1(T t) {
return t;
}
public <K, V extends Number> void method2(K k, V v) {
System.out.println(k + "=" + v);
}
}
public class TypeRestrictOnMethod {
public static void main(String[] args) {
GenericMethd2 gm2 = new GenericMethd2();
String str = gm2.method1("문자열");
System.out.println("str : " + str);
/* 메서드의 타입이 String으로 제한되어 숫자 타입은 사용하지 못하게 되었다.
int num = gm2.method1(1);
System.out.println("num : " + num);
*/
/*
메서드의 두 번째 파라미터 타입이 Number로 제한되어 문자열 타입은 사용하지 못하게 되었다.
gm2.method2("사과", "1개");
*/
gm2.method2("시력", 1.0);
gm2.method2(3, 3);
}
}
사용예시 5.와일드카드
와일드카드란 제네릭 타입을 매개 값이나 리턴 타입으로 사용할 때 구체적인 타입 대신에 사용하는 것으로 코드에서는 ?로 표현된다.
와일드카드는 아래와 같이 사용할 수 있다.
- <?> (Unbound WildCard, 제한 없음)
: 제한 없이 모든 타입이 가능하다. - <? super 하위타입> (Lower Bounded Wildcard, 하위 클래스로 제한)
: 하위 타입만 사용 가능하다. - <? extends 상위타입> (Upper Bounded Wildcard, 상위 클래스로 제한)
: 상위 타입만 사용 가능하다.
import java.util.ArrayList;
import java.util.List;
class Fruit {}
class Banana extends Fruit {}
class Melon extends Fruit {}
class Keyboard {}
public class Wildcard {
public static void main(String[] args) {
// 1. <?> (제한 없음)
List<?> all1 = new ArrayList<Fruit>();
List<?> all2 = new ArrayList<Banana>();
List<?> all3 = new ArrayList<Melon>();
List<?> all4 = new ArrayList<Keyboard>();
// 2. <? super 하위타입> (상위 클래스로 제한)
List<? super Banana> bananas1 = new ArrayList<Banana>();
List<? super Banana> bananas2 = new ArrayList<Fruit>();
// List<? super Banana> bananas3 = new ArrayList<Melon>(); // 오류 발생
// 3. <? extends 상위타입> (하위 클래스로 제한)
// List<Fruit> bananas = new ArrayList<Banana>(); // 오류 발생
// List<Fruit> melons = new ArrayList<Melon>(); // 오류 발생
List<? extends Fruit> fruits1 = new ArrayList<Fruit>();
List<? extends Fruit> fruits2 = new ArrayList<Banana>();
List<? extends Fruit> fruits3 = new ArrayList<Melon>();
// List<? extends Fruit> fruits4 = new ArrayList<Keyboard>(); // 오류 발생
}
}
References.
1. Inpa - 자바 제네릭(Generics) 개념 & 문법 정복하기
2. Stranger's LAB - 자바 [JAVA] - 제네릭(Generic)의 이해
3. Dev.GA - [JAVA] Java 제네릭(Generics)이란?
'Java' 카테고리의 다른 글
[Java] 람다식(Lambda Expressions) (0) | 2023.08.08 |
---|---|
[Java] 컬렉션 프레임워크(Collection Framework) (0) | 2023.08.07 |
[Java] 오버로딩(Overloading)과 오버라이딩(Overriding) (0) | 2023.08.02 |
[Java] 추상클래스와 인터페이스 비교 (0) | 2023.07.29 |
[Java] 인터페이스(Interface) (0) | 2023.07.27 |