아이템 30

Choi Wang Gyu·2023년 11월 4일
0

제네릭

제네릭이란 데이터의 타입을 클래스 내부에서 지정하는 것이 아닌 외부에서 사용자에 의해 지정되는 것을 의미합니다.

제네릭 들어가기전 타입 매개변수란

타입매개변수는 아래와 같이 T가 타입 매개변수입니다

public class GenericClass<T> {
    private T data;

    public GenericClass(T data) {
        this.data = data;
    }
}

GenericClass<Integer> intObject = new GenericClass<>(42);
GenericClass<String> stringObject = new GenericClass<>("Hello, world!");

첫 번째 예에서 TInteger로 대체되어 정수형 데이터를 저장하도록 인스턴스를 만듭니다. 두 번째 예에서 TString으로 대체되어 문자열 데이터를 저장하도록 인스턴스를 만듭니다. 이것이 타입 매개변수의 역할이며, 다양한 데이터 유형을 처리하기 위해 사용됩니다.

Collection의 알고리즘 (binarySearch, sort등) 모두 제네릭이라고 합니다

Colletion의 메서드들이 제네릭인 이유(제네릭 장점 가까움)

  • 타입 안정성: 제네릭을 사용하면 컴파일 시에 타입 안정성을 보장할 수 있습니다. 즉, 컴파일러가 컬렉션과 알고리즘을 사용하는 동안 잘못된 데이터 유형을 사용하려는 시도를 미리 방지할 수 있습니다. 이로써 런타임에 발생할 수 있는 타입 관련 오류를 방지하고 안정성을 확보할 수 있습니다.

  • 재사용성: 제네릭을 사용하면 컬렉션과 알고리즘을 여러 다른 데이터 유형에 대해 재사용할 수 있습니다. 특정 데이터 유형에 의존하지 않고 다양한 데이터 유형에서 작동하는 코드를 작성할 수 있습니다.

  • 가독성과 유지보수성: 제네릭을 사용하면 코드가 더 읽기 쉽고 유지보수가 쉽습니다. 메서드 및 알고리즘의 매개변수 및 반환 값이 명확하게 정의되므로 코드의 의도를 이해하기가 더 쉽습니다.

메서드도 제네릭으로 만들 수 있다

매개변수화 타입을 받는 정적 유틸리티 메서드는 보통 제네릭입니다.
제네릭 메서드 코드예시입니다

public class GenericUtils {
    // 제네릭 메서드: 배열을 리스트로 변환하는 메서드
    public static <T> List<T> arrayToList(T[] array) {
        List<T> list = new ArrayList<>();
        for (T element : array) {
            list.add(element);
        }
        return list;
    }
}

제네릭 메서드 만들기 - 1 (raw타입 사용 - 수용 불가)

경고 발생
원인 : 경고를 없애려면 메서드를 타입 안전하게 만들어야 한다
수정 방법 : 입력 2개, 반환 1개 원소타입을 타입 매개변수로 명시하고, 메서드 안에서도 이 타입 매개변수만 사용하게 수정하면 된다

    public static Set union(Set s1, Set s2){
        Set result = new HashSet(s1);
        result.addAll(s2);
        return  result;
    }

제네릭 메서드 만들기 - 2 (타입 매개변수로 수정 완료)

    public static <E> Set<E> union(Set<E> s1, Set<E> s2){
        Set<E> result = new HashSet(s1);
        result.addAll(s2);
        return  result;
    }

불변 객체를 여러 타입으로 활용할 때가 있다.

자바에서 불변 객체를 여러 타입으로 활용할 수 있게 만들어야 할 때까 있다 라는게 무슨말인가요(책 177페이지 9줄)

자바에서 불변(immutable) 객체란 한 번 생성되면 그 상태를 변경할 수 없는 객체를 말합니다.
불변 객체를 여러 타입으로 활용한다는 말은, 같은 불변 객체를 여러 가지 다른 데이터 타입처럼 다루거나 활용하는 것을 의미합니다.

예시코드입니다

public final class ImmutablePerson {
    private final String name;
    private final int age;

    public ImmutablePerson(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

이제 이 불변 객체 ImmutablePerson을 여러 다른 타입으로 활용할 수 있습니다. 즉, 이 객체를 다양한 상황에서 사용할 수 있도록 다른 클래스나 인터페이스와 함께 작동시킬 수 있습니다.

// 다양한 인터페이스를 구현
class PersonInfo implements Nameable, Ageable {
    private ImmutablePerson person;

    public PersonInfo(ImmutablePerson person) {
        this.person = person;
    }

    public String getName() {
        return person.getName();
    }

    public int getAge() {
        return person.getAge();
    }
}
interface Nameable {
    String getName();
}

interface Ageable {
    int getAge();
}
// 다른 클래스에서 ImmutablePerson을 사용
class PersonProcessor {
    public void processPerson(ImmutablePerson person) {
        // 이곳에서 ImmutablePerson을 사용하여 다양한 작업을 수행
        String name = person.getName();
        int age = person.getAge();
        // ...
    }
}

이렇게 하면 ImmutablePerson 객체를 다양한 타입으로 다룰 수 있게 됩니다. 예를 들어, PersonInfo 클래스는 Nameable 및 Ageable 인터페이스를 구현하여 ImmutablePerson을 이름과 나이 정보를 가져오는 데 사용할 수 있습니다. 또한 PersonProcessor 클래스는 ImmutablePerson을 처리하는 다른 동작을 수행할 수 있습니다.

즉, 불변 객체를 여러 타입으로 활용한다는 것은 동일한 불변 객체를 여러 다른 컨텍스트에서 유연하게 활용할 수 있는 능력을 가리킵니다. 이것은 객체 지향 프로그래밍의 다형성 개념과 관련이 있으며, 불변 객체를 인터페이스, 상속, 또는 다른 클래스와 결합하여 코드를 더 유연하게 작성하고 재사용성을 높일 수 있게 합니다.

제네릭 싱글턴 팩터리

제네릭은 런타임시 타입 정보가 소거 되므로 하나의 객체를 어떤 타입으로든 매개변수화 할 수 있다.
->
객체를 매개변수화하려면 요청한 타입 매개변수에 맞게 매번 그 객체의 타입을 바꿔주는 정적 팩터리가 필요하다.
이 정적 팩터리를 제네릭 싱글턴 팩터리라고 한다

public class GenericFactoryMethod {
public static final Set EMPTY_SET = new HashSet();

    public static final <T> Set<T> emptySet() {
        return (Set<T>) EMPTY_SET;
    }
}

제네릭으로 타입설정 가능한 인스턴스를 만들어두고, 반환 시에 제네릭으로 받은 타입을 이용해 타입을 결정하는 것이다.

예제 코드

@Test
public void genericTest() {
    Set<String> set = GenericFactoryMethod.emptySet();
    Set<Integer> set2 = GenericFactoryMethod.emptySet();
    Set<Elvis> set3 = GenericFactoryMethod.emptySet();

    set.add("ab");
    set2.add(123);
    set3.add(Elvis.INSTANCE);

    String s = set.toString();
    System.out.println("s = " + s);
}

위와 같이 여러 타입으로 내부 객체를 받아도 에러가 나지 않는다.
큰 유연성을 제공한다.

결과
s = [ab, item3.Elvis@3439f68d, 123]
제네릭 싱글턴 팩터리가 아니라, 고정된 타입으로 생성했다면 에러가 났을 것이다.

항등함수를 담은 클래스

항등 함수란? 입력 값 수정 없이 그대로 반환하는 함수

    private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;
    
    @SuppressWarnings("unchecked")
    public static <T> UnaryOperator<T> identityFunction() {
        return (UnaryOperator<T>) IDENTITY_FN;
    }

제네릭에서 항등함수가 무슨상관인가?

제네릭을 사용하지 않은 경우:

public class WithoutGenerics {
    private Object data;

    public WithoutGenerics(Object data) {
        this.data = data;
    }

    public Object getData() {
        return data;
    }

    public static void main(String[] args) {
        WithoutGenerics stringContainer = new WithoutGenerics("Hello, World");
        WithoutGenerics intContainer = new WithoutGenerics(42);

        String str = (String) stringContainer.getData(); // 강제 형변환 필요
        int num = (int) intContainer.getData();           // 강제 형변환 필요

        System.out.println("문자열: " + str);
        System.out.println("정수: " + num);
    }
}

위의 코드에서는 data 필드에 Object 유형을 사용하고, 값을 가져올 때 강제 형변환을 수행해야 합니다. 이로 인해 컴파일 시간에 오류를 찾기 어렵고 런타임 오류가 발생할 수 있습니다.

제네릭을 사용한 경우:

public class WithGenerics<T> {
    private T data;

    public WithGenerics(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }

    public static void main(String[] args) {
        WithGenerics<String> stringContainer = new WithGenerics<>("Hello, World");
        WithGenerics<Integer> intContainer = new WithGenerics<>(42);

        String str = stringContainer.getData(); // 형변환 필요 없음
        int num = intContainer.getData();       // 형변환 필요 없음

        System.out.println("문자열: " + str);
        System.out.println("정수: " + num);
    }
}

제네릭을 사용한 코드에서는 WithGenerics 클래스의 형식 매개변수 T를 사용하여 다양한 데이터 유형을 처리합니다. 이로 인해 강제 형변환이 필요하지 않으며, 컴파일 시간에 타입 안전성이 유지되며 런타임 오류가 줄어듭니다.
앞에서 계속나오듯이 제네릭을 사용하면 코드의 가독성과 안정성을 향상시키며, 잘 정의된 인터페이스 및 컬렉션 클래스와 함께 사용하여 보다 효율적인 코드를 작성할 수 있습니다.

재귀적 타입 한정

<E extends Comparable<E>> 제네릭 클래스나 메서드가 Comparable 인터페이스를 구현한 객체 타입 E만을 허용하도록 하는 것을 의미합니다. 이것은 제네릭을 사용하여 객체를 정렬하거나 비교할 때 컴파일 시간에 타입 안전성을 제공합니다.

우리가 가장 많이 사용하는 제네릭

API공통 포맷을 사용할때 저희는 제네릭을 가장 많이 보게 되는거같아서 넣었습니다

@Getter
public class ApiResponse<T> {

    private int code;
    private HttpStatus status;
    private String message;
    private T data;

    public ApiResponse(HttpStatus status, String message, T data) {
        this.code = status.value();
        this.status = status;
        this.message = message;
        this.data = data;
    }

    public static <T> ApiResponse<T> of(HttpStatus httpStatus, String message, T data) {
        return new ApiResponse<>(httpStatus, message, data);
    }

    public static <T> ApiResponse<T> of(HttpStatus httpStatus, T data) {
        return of(httpStatus, httpStatus.name(), data);
    }

    public static <T> ApiResponse<T> ok(T data) {
        return of(HttpStatus.OK, data);
    }

}

느낀점

이번 아이템은 내용은 사실 쉬운데 용어들이 자주사용하지 않는게 많아서 헷갈렸다
제네릭은 안정적인 프로그램을 위해 존재하는 느낌이 강했다.
컴파일 시점에 타입 검사를 통해 오류를 미리 잡기때문이다

0개의 댓글

관련 채용 정보