JAVA 제네릭 정복하기
💡 제네릭(Generic)이란
📃 결정되지 않은 타입을 파라미터로 처리하고 실제 사용할 때 파라미터를 구체적인 타입으로 대체 시키는 기능이다.
💬 제네릭의 대표 예시
제네릭을 사용하는 가장 대표적인 예시는 컬렉션 프레임워크이다.
예를 들어 ArrayList 클래스는 제네릭을 사용하여 여러 타입의 객체를 저장할 수 있다.
제네릭을 사용하지 않은 경우 Object 타입으로 모든 객체를 다루어야 했지만,
제네릭을 사용하면 타입 안정성을 확보하면서 특정 타입의 객체만 다룰 수 있게 된다.
public class Main {
public static void main(String[] args) {
// 제네릭을 사용하지 않은 경우
ArrayList list = new ArrayList();
list.add("hello world")
list.add(1);
// 각 요소를 가져올 때 형변환이 필요하다.
String str = (String) list.get(0);
int intValue = (int)list.get(1);
System.out.println(str);
System.out.println(intValue);
// 제네릭을 사용하는 경우
ArrayList<String> stringArrayList = new ArrayList<>();
stringArrayList.add("hello world");
//stringArrayList.add(100); 컴파일오류
// 형변환 없이 각 요소를 안전하게 가져올 수 있다.
String string = stringArrayList.get(0);
}
}
위 소스코드에서 제네릭의 장점을 확인할 수 있다.
-
타입 안정성
제네릭을 사용하면 컴파일러가 컴파일 시간에 코드를 검사하여 타입 관련 오류를 잡아낸다.
따라서 런타임에 발생하는 형변환 오류를 방지하고 코드의 안정성을 높일 수 있다.ArrayList을 사용하여 문자열만을 저장하도록 명시했기 때문에, 컴파일러가 정수를 추가하려는 시도를 오류로 처리한 것이다. -
코드 가독성 및 유지보수 향상
타입 변환 없이 코드를 작성할 수 있으므로 코드가 간결해지고 유지보수가 쉬워진다.
💡 타입 파라미터
타입 파라미터는 클래스나 메서드를 정의할 때 사용하는 결정되지 않은 타입을 나타낸다.
이렇게 정의된 클래스나 메서드는 실제 사용시에 이 타입 파라미터를 구체적인 타입으로 대체하여 사용한다.
다음 소스코드는 Box 클래스에서 결정되지 않은 content
의 타입을 T 라는 타입 파라미터로 정의한 것이다.
public class Box <T> {
public T content;
}
-
<T>
는 타입 파라미터를 나타내며, Box 클래스의 Content 필드의 타입으로 사용된다. -
Box
클래스는 T가 어떤 타입인지 모르지만Box
객체가 생성될 시점에는 다른 타입으로 대체된다는것을 알고있다.
📌 만약에 Box객체의 내용물로 문자열을 저장하고 싶다면?
Box<String> box = new Box<String>();
box.content = "안녕하세요";
String content = box.content;
Box
객체를 생성할 때 타입 파라미터 대신 String
을 대입한다.
📌 만약에 Box객체의 내용물로 정수값을 저장하고 싶다면?
Box<Integer> box = new Box<Integer>();
box.content = 100;
int content = box.content;
Box
객체를 생성할 때 타입 파라미터 대신 Integer
을 대입한다.
타입 파라미터는 A-Z 까지 어떤 알파벳을 사용해도 허용한다.
주의할 점은 타입 파라미터를 대체하는 타입은 클래스 및 인터페이스라는 것이다.
Box<int>
라고 하지 않는 이유는 기본 타입은 타입 파라미터의 대체 타입이 될 수 없기 때문이다.
💡 제네릭 타입
제네릭 타입은 결정되지 않은 타입을 파라미터로 가지는 클래스 또는 인터페이스를 의미한다.
제네릭 타입은 클래스나 인터페이스 이름 뒤에 <T>
와 같은 형태로 타입 파라미터를 선언한다.
public class Box<T> {...}
public interface Box<T> {...}
이 타입 파라미터는 클래스 내부의 필드, 메서드, 생성자 등에서 사용된다.
외부에서 제네릭 타입을 사용하려면 타입 파라미터에 구체적인 타입을 지정해야한다.
만약 지정하지 않으면 Object
타입이 암묵적으로 사용된다.
제네릭 타입을 이용하여 작성한 소스코드
class Tv {}
class Car {}
class Product<K, M> {
private K kind;
private M moder;
public K getKind() {
return kind;
}
public void setKind(K kind) {
this.kind = kind;
}
public M getModer() {
return moder;
}
public void setModer(M moder) {
this.moder = moder;
}
}
public class Main {
public static void main(String[] args) {
Product<Tv, String> product1 = new Product<>();
product1.setKind(new Tv());
product1.setModer("스마트 Tv");
System.out.println(product1.getKind());
System.out.println(product1.getModer());
Product<Car, String> product2 = new Product<>();
product2.setKind(new Car());
product2.setModer("봉고차");
System.out.println(product2.getKind());
System.out.println(product2.getModer());
}
}
제네릭 타입을 이용하여 Tv와 Car를 저장하고 얻어올 수 있는 코드를 작성하였다.
💡 제네릭 메서드
제네릭 메소드는 타입 파라미터를 가지고 있는 메소드를 말한다.
타입 파라미터가 메소드 선언부에 정의된다는 점에서 제네릭 타입과 차이가 있다.
public <T> 리턴타입 메소드명(매개변수...) { ... }
class Box<T> {
T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
}
class GenericExample {
public static <T> Box<T> boxing(T t){
Box<T> box = new Box<T>();
box.setT(t);
return box;
}
}
public class Main {
public static void main(String[] args) {
Box<Integer> box1 = GenericExample.boxing(100);
int intValue = box1.getT();
System.out.println(intValue);
Box<String> box2 = GenericExample.boxing("홍길동");
String strValue = box2.getT();
System.out.println(strValue);
}
}
💡 제한된 타입 파라미터
제한된 타입 파라미터는 타입 파라미터를 대체하는 구체적인 타입을 제한할때 사용한다.
public <T extends 상위타입> 리턴타입 메소드 (매개변수 ...) {...}
예를 들어, “숫자를 연산하는 제네릭 메소드” 를 작성한다고 가정하여보자.
이 메소드는 특정 타입과 그 타입의 하위 클래스들만을 처리할 수 있도록 제한하고 싶다.
이를 위해 제한된 타입 파라미터를 사용한다.
public <T extends Number> boolean compare(T t1, T t2) {
double v1 = t1.doubleValue();
double v2 = t2.doubleValue();
return (v1 == v2);
}
여기서
따라서 이 메소드는 Byte, Short, Integer, Long, Double과 같은 숫자 타입에 대해서만 동작하게 된다.
💡 와일드카드 타입 파라미터
와일드 카드 타입 파라미터는 제네릭 코드를 작성할 때, 더 유연하게 다룰 수 있도록 하는 방법이다.
와일드 카드는 ?
기호로 표현되며, 크게 <?>
, <? entends 상위타입>
, <? super 하위타입
> 3 종류가 있다.
- <?> : 어떤 타입이든 모두 허용한다.
- <? extends 상위타입> : 특정 상위 타입 및 그 상위 타입의 하위타입들만 허용한다.
- <? super 하위타입> : 특정 하위 타입 및 그 하위 타입의 상위 타입들만 허용한다.
📝 <?> 예제 코드
class Wildcard {
public static void printList(List<?> list) {
for (Object element : list) {
System.out.println(element + " ");
}
System.out.println();
}
}
public class Main {
public static void main(String[] args) {
List<Integer> integerList = List.of(1, 2, 3);
List<String> stringList = List.of("A", "B", "C");
List<Double> doubleList = List.of(1.5, 2.5, 3.5);
Wildcard.printList(integerList);
Wildcard.printList(stringList);
Wildcard.printList(doubleList);
}
}
📝 <? exnteds 상위타입> 예제 코드
class Wildcard {
public static void printList(List<? extends Number> list) {
for (Object element : list) {
System.out.println(element + " ");
}
System.out.println();
}
}
public class Main {
public static void main(String[] args) {
List<Integer> integerList = List.of(1, 2, 3);
List<String> stringList = List.of("A", "B", "C");
List<Double> doubleList = List.of(1.5, 2.5, 3.5);
Wildcard.printList(integerList);
// Wildcard.printList(stringList); 컴파일 오류 발생
Wildcard.printList(doubleList);
}
}
Number타입과 Number의 자식타입들만 허용하였으므로 String은 컴파일 오류가 발생한다.
📝 <? super 하위타입> 예제 코드
class Wildcard {
public static void addNumbers(List<? super Integer> list) {
for(int i = 1; i <= 5; i++){
list.add(i);
}
}
}
public class Main {
public static void main(String[] args) {
List<Object> objectList = new ArrayList<>();
List<Number> numberList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
List<Double> DoubleList = new ArrayList<>();
Wildcard.addNumbers(objectList);
Wildcard.addNumbers(numberList);
Wildcard.addNumbers(integerList);
//Wildcard.addNumbers(DoubleList); 컴파일 오류 발생
System.out.println("Object List: " + objectList);
System.out.println("Number List: " + numberList);
System.out.println("Integer List: " + integerList);
}
}
Interger 타입과 Integer타입의 상위 타입들만 허용하였으므로 double은 컴파일 오류가 발생한다.
댓글남기기