[JAVA]Generics - 기본 제네릭 클래스
분명 자바를 주로하고 있지만 인터넷에서 찾아보는 코드들에는 제네릭 클래스들이 들어있어 조금 당황스러울 때가 있다. 분명 자바에 대해서 배웠는데 뭐지? 라는 생각을 하였는데, 아마 시간 없어서 교수님이 안하신것 같다. 이게 맞나?
그래서 다시 공부해보려한다.
1. 기초 제네릭 클래스(자료구조형 기반)
2. 제네릭 선언
3. 컬렉션
4. 버퍼
5. 로그
평소 자바에서 부족하다고 생각한 부분을 공부할 것 이다!
📝 제네릭 왜 써야할까?
크게 2가지 이유가 있다.
- 타입 안정성
- 제네릭은 컴파일 시점에서 타입을 확정한다. 코드 실행 중에 발생하는 타입 관련 오류를 미연 방지할 수 있다.
- 추가로 컬렉션 클래스(List, Set, Map)와 같은 컨테이너 클래스에서 특정한 타입의 요소를 다룰 때 편리하다.
// 제네릭을 사용하지 않는 경우
List myList = new ArrayList();
myList.add("문자열");
Integer intValue = (Integer) myList.get(0); // 컴파일은 되지만 런타임에 ClassCastException 발생
// 제네릭을 사용하는 경우
List<String> myGenericList = new ArrayList<>();
myGenericList.add("문자열");
Integer intValue = myGenericList.get(0); // 컴파일 오류 발생
2. 코드의 재사용성
- 제네릭을 사용하면 일반적인 알고리즘 또는 컴포넌트를 여러 데이터 타입에서 재사용할 수 있다.
- 동일한 로직을 다양한 타입에 대해 작성할 필요가 없어지므로 중복을 줄여 가독성이 좋아진다.
// 제네릭을 사용하지 않는 경우
// 처리마다 형변환이 필요
public class NonGenericList {
private Object value;
public void setValue(Object value) {
this.value = value;
}
public Object getValue() {
return value;
}
}
// 제네릭을 사용하는 경우
// 선언 시에 타입 설정을 통한 재사용
public class GenericList<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
📝 제네릭 선언 타입
제네릭 타입은 클래스, 인터페이스에 모두 적용할 수 있다.
public class MyClass<T>{}
public interface MyInterface<T>{}
Generic Type Parameter | 용도 |
T | Type |
E | Element의 약자로 주로 컬렉션에 사용 |
K | Key의 약자로 주로 맵(Map)에서 키를 표현할 때 사용 |
V | Value의 약자로 주로 맵(Map)에서 값을 표현할 때 사용 |
S, U, V | 2, 3, 4번째 타입을 지칭할 때 사용 |
단일 제네릭 예제
public class ObjectBox {
private Object content;
public void setContent(Object content) {
this.content = content;
}
public Object getContent() {
return content;
}
public static void main(String[] args) {
ObjectBox box = new ObjectBox();
// 문자열을 설정
box.setContent("Hello, Object!");
// 문자열을 가져오고 형변환 필요
String strValue = (String) box.getContent();
System.out.println("String Value: " + strValue);
// 정수를 설정
box.setContent(42);
// 정수를 가져오고 형변환 필요
int intValue = (int) box.getContent();
System.out.println("Integer Value: " + intValue);
}
}
public class GenericBox<T> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
public static void main(String[] args) {
// 제네릭을 사용하여 Box를 생성하면서 타입을 지정
GenericBox<String> strBox = new GenericBox<>();
// 문자열을 설정
strBox.setContent("Hello, Generics!");
// 문자열을 가져올 때 형변환 필요 없음
String strValue = strBox.getContent();
System.out.println("String Value: " + strValue);
// 다른 타입을 설정하려고 하면 컴파일 타임 오류 발생
// strBox.setContent(42); // 에러: Type mismatch
}
}
위의 코드에서 ObjectBox 클래스는 Object 를 사용하여 모든 종류의 객체를 저장할 수 있지만, 가져올 때 형변환이 필요하다. 반면에 GenericBox 클래스는 제네릭을 사용하여 특정 타입을 지정하여 사용할 수 있으며 형변환이 필요없다.
위 코드로 보는 장점
- 제네릭을 사용하면 코드가 간결해지고, 컴파일 타임에 안정성이 보장
- 코드 가독성이 향상, 사용자는 더 적은 형변환을 다룬다.
복수 제네릭 타입 사용
public class Pair<K, V> {
private K first;
private V second;
public Pair(K first, V second) {
this.first = first;
this.second = second;
}
public K getFirst() {
return first;
}
public V getSecond() {
return second;
}
public static void main(String[] args) {
// 문자열과 정수를 저장하는 Pair 객체 생성
Pair<String, Integer> pair1 = new Pair<>("One", 1);
// Pair 객체에서 값을 가져와 출력
String strValue = pair1.getFirst();
int intValue = pair1.getSecond();
System.out.println("Pair 1 - First: " + strValue + ", Second: " + intValue);
}
}
K,V의 의미는 키-벨류 방식으로 맵이나 JSON에서 사용한다.
그외 제네릭 타입 사용 예시
- 매개변수화된 타입
Pair<String, List<Integer>> p = new Pair<>("name", new List<Integer>());
해당 코드처럼 간단한 튜플이나 임시적인 데이터 구조를 표현한 매개변수 타입을 넣을 수 있다.
📝제네릭 메소드
제네릭 메소드는 하나 이상의 타입 매개변수를 사용하여 정의된 메소드이다. 이를 통해 메소드가 여러 종류의 타입에 대해 동작할 수 있도록 일반화된 형태로 작성할 수 있다.
public class GenericMethodExample {
// 제네릭 메소드 정의
public <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
public static void main(String[] args) {
GenericMethodExample example = new GenericMethodExample();
// 제네릭 메소드 호출
Integer[] intArray = { 1, 2, 3, 4, 5 };
example.printArray(intArray);
String[] strArray = { "apple", "orange", "banana" };
example.printArray(strArray);
}
}
📝Bounded Type Parameters
Bounded Type Parameters는 제네릭 클래스나 메서드에서 사용되는 타입 매개변수에 대한 제약을 설정하는 방법이다.
Bounded Type Parameters는 크게 두가지로 나눌 수 있다.
1. 상한 제한
- 특정 클래스 또는 인터페이스의 서브타입만을 허용하는 방법이다. 키워드는 extends
public class Box<T extends Number> {
// T는 Number 또는 Number의 서브타입이어야 함
// Number은 모든 숫자 타입의 최상위 클래스
}
2. 하한 제한
- 특정 클래스의 상위 타입만을 허용하는 방법이다. 키워드는 super
public static <T super MyClass> void process(List<T> list) {
// T는 MyClass의 상위 타입이어야 함
}
여러 타입 바운드
제네릭은 제한된 타입 변수에 여러 타입을 제한할 수 있다.
extends 키워드나 super 키워드 뒤에 타입을 & 를 이용해 나열이 가능하다.
public class MultiBoundExample<T extends Number & List<T>>{}
//해당 타입은 List<>이면서 number의 타입을 가지고 있어야한다.
// List<Integer>, List<Long>
📝Wildcards
와일드카드는 ? 로 사용되며 unknown 타입을 의미한다. 와일드 카드는 제네릭 타입을 좀 더 유연하게 다룰 수 있도록 도와준다.
Wildcards를 이해하려면 제네릭의 문제점을 이해해야한다.
제네릭의 등장으로 타입이 달라도 메소드를 사용할 수 있는 재사용성과 안정성을 보장 받을 수 있게 되었다.
그러나 제네릭의 불변성에서 문제가 발생했다.
불변성?
제네릭의 불변성
제네릭의 불변성이란? 제네릭 타입 간의 하위 타입 관계가 유지되지 않는 성질을 의미한다.
예시를 일단 보자
List<Object> objects = new ArrayList<>();
List<String> strings = new ArrayList<>();
objects.add("Hello"); // List<Object>에는 어떤 객체든 추가 가능
strings.add("World"); // List<String>에는 문자열만 추가 가능
objects = strings; // 컴파일 에러! List<Object>와 List<String>은 불변성이 유지되지 않음
Object는 모든 타입에 대해 상위 관계를 가지고 있다. 그러나 제네릭은 불변성으로 인해 string을 받아들일 수 없다.
이러한 문제를 해결하기 위해 와일드 카드라는 타입이 탄생하게 되었다.
와일드카드의 핵심 특징
와일드카드에서 중요한 점은 모든 타입이 아니라 타입을 모른다는 점이다.
List<Object> objectList = new ArrayList<>();
List<?> wildcardList = new ArrayList<>();
objectList.add("Hello"); // 가능
wildcardList.add("Hello"); // 불가능
Object obj1 = objectList.get(0); // 가능
Object obj2 = wildcardList.get(0); // 가능
위 코드에서 wildcardList.add()가 안되는 이유는
아무 제한 없이 ?로 선언된 리스트는 알 수 없는 타입이기 때문에 넣는 것이 불가능하다.
그러나 Object를 이용해서 받아오는 것은 가능하다. (모든 타입은 Object의 하위 타입이기 때문이다.)
궁금증.
받아오는 건 가능한데 왜 넣는 건 안되는 거야?
나는 이런 생각이 들었다.
Object는 모든 타입의 상위 인자이다. 그러면 넣는 것도 Object로 하면 가능해야 하는거 아니야?
이 질문에 대한 답은 자바 제네릭 설계의 장점으로 알 수 있다.
1. 타입 안전성 유지
- 와일드카드로 선언된 리스트는 여러 타입을 수용하기 위한 유연성을 제공하지만, 제네릭 기본 원칙인 타입 안전성도 유지해야한다.
- 값을 추가하는 경우 컴파일러가 타입을 정확히 추론할 수 없어 값 추가가 금지되었다.
📝Wildcards 데이터 add 방법
와일드카드는 제한을 두어 안전성을 지키면서, 타입 수용이 가능해진다.
상한 와일드 카드(읽기 전용)
- 예) List<? extends T">
- 제네릭 방법과 같이 키워드는 extends로 특정 타입과 그 하위 타입만 받아올 수 있다.
- 읽기 작업을 주를 이루며, 값의 추가는 선언 시에만 가능하다.
import java.util.*;
class Test{
void printList(List<? extends Number> values){
for(Number value : values){
System.out.println(value);
}
}
public static void main(String[] args){
Test test = new Test();
List<Integer> integers = new ArrayList<>(List.of(1,2,3));
test.printList(integers);
}
}
위의 코드처럼 extends는 unknown 타입에 대해 최대 Number 클래스이거나 Number 클래스의 자식이어야 하는 타입의 상한을 지정한다.
하한 와일드카드(쓰기 전용)
- 예) List<? super T">
- 제네릭 방법과 같이 키워드는 super로 특정 타입과 그 상위 타입만 받아올 수 있다.
- 쓰기 작업을 주를 이루며, 읽기 작업은 Object로 제한되어 있다.
class Animal {}
class Mammal extends Animal {}
class Cat extends Mammal {}
class Dog extends Mammal {}
public class WildcardExample {
public static void main(String[] args) {
// List<? super Mammal>는 Mammal 또는 Mammal의 상위 타입을 받아올 수 있음
List<? super Mammal> mammalList = new ArrayList<>();
// 쓰기 작업: Mammal 및 하위 타입 객체 추가 가능
mammalList.add(new Mammal());
mammalList.add(new Cat());
mammalList.add(new Dog());
// 읽기 작업: Object로 받아와야 함
for (Object mammal : mammalList) {
System.out.println(mammal);
}
}
}
위 코드처럼 ? super Mammal 를 통해 리스트가 갖는 타입은 적어도 Mammal 위기 때문에 안전하게 추가할 수 있다.
그러나 읽기는 컴파일러가 정확한 타입을 알 수 없기 때문에 모든 타입에 대해 상위인 Object 타입만 가능하다.