Post

Generic과 Object

Generic

데이터 타입을 일반화하여 클래스, 인터페이스, 메서드를 정의할 때 사용하는 기능

클래스나 메서드를 선언할 때 타입을 명시적으로 지정하지 않고, 사용하는 시점에 실제 타입을 지정할 수 있다.

제네릭을 이용하여 특정 타입에 의존하지 않고 데이터 구조나 알고리즘을 정의할 수 있으며, 코드의 재사용성과 타입 안정성을 향상 시킬 수 있다.

컴파일 타임에 타입을 체크할 수 있어 런타임에 발생할 수 있는 오류를 예방할 수 있으며 형 변환의 번거로움을 줄일 수 있다.


T(Type), E(Element), K(Key), V(Value)

동작방식은 우선 클래스나 메서드 선언시 타입 매개변수를 선언한다.
선언된 제네릭 클래스를 사용할 때는 타입 인자를 지정하여 구체적인 타입을 명시한다.

그 후 컴파일러가 타입 안정성을 보장하기 위해 타입 체크를 수행하여 잘못된 타입에 대한 경고나 오류를 찾는다.

제네릭을 사용하면 타입 변환을 자동으로 처리할 수 있어 코드가 간결해지고 가독성이 높아지며, 잘못된 타입 변화로 인한 런타임 오류를 줄일 수 있다.

동일한 코드를 참조 타입에 대해 재사용할 수 있는 장점이 있다. 또한 타입 캐스팅을 줄여 코드 가독성과 유지보수성을 높일 수 있다. 원시타입에 대해서는 사용할 수 없다.

하지만 다소 복잡하고, 컴파일 후 타입 소거로 인해 런타임에는 제네릭 타입 정보가 소실된다. (실행시점에는 Object 타입으로 다뤄짐)


제네릭에서 와일드카드를 사용하는 경우, 어떤 상황에서 어떻게 사용하는지

와일드카드는 제네릭 타입의 유연성을 높이고, 특정 타입에 구애받지 않고 여러 타입을 처리할 수 있게 한다.

제네릭 타입을 구체적으로 알 필요 없이 데이터를 처리하거나, 상위, 하위 타입을 제한하고자 할 때 사용할 수 있다. extends는 데이터를 읽기 전용으로 사용하는 상황에 적합하다.
super는 쓰기 전용 작업이 필요한 경우에 적합하다.




와일드카드의 종류

1. 무제한 와일드카드: <?>

  • 아무 타입이나 올 수 있을 때 사용한다.
  • List<?> 의 경우 어떤 타입의 리스트도 받을 수 있다.
  • 타입에 상관없이 메서드에서 모든 타입의 제네릭을 처리할 때 사용.

2. 상한 제한 와일드카드: <? extends T>

  • T타입과 그 하위 타입들을 나타낼 수 있습니다.
  • List<? extends Number>Number 타입과 그 하위 타입들을 모두 받을 수 있습니다.

3. 하한 제한 와일드카드: <? super T>

  • T 타입과 그 상위 타입들을 나타냅니다.
  • List<? super Integer>은 Integer타입과 그 상위타입들을 받을 수 있습니다.

하한 제한 와일드카드는 특정 타입 T 와 그 타입의 상위 클래스만 허용하는 제네릭이다.

주로 메서드의 매개변수 타입으로 사용되어 메서드가 여러 관련 타입을 처리할 수 있다.

상한 제한 와일드카드는 특정 타입의 하위 타입만 허용하는 경우에 사용되며, 주로 컬렉션에서 요소를 읽을 때 유용하다.

와일드 카드는 매개변수로만 사용될 수 있으며, 제네릭 클래스나 제네릭 메서드의 반환 타입으로는 사용할 수 없다.

와일드 카드를 사용하면 컬렉션의 요소를 읽는 것은 가능하지만 쓰는것은 불가능합니다. List<?> 에는 null 만 추가 가능하다.


사용상황

메서드에서 특정 제네릭 타입을 받아들이지만 실제 타입에 대해 정확히 알 필요가 없는 경우 사용한다.

클래스의 일부 멤버 변수가 제네릭 타입일 때 다양한 타입의 객체를 다룰 수 있도록 하기 위해 사용한다.

특징 <? super T> <? extends T>
허용타입 T와 T의 상위 타입 T와 T의 하위 타입
쓰기가능 T와 T의 하위 타입 추가 가능 추가 불가능
읽기가능 Object 타입으로 읽기 T 타입으로 읽기


와일드카드를 사용 시 주의할 부분

와일드카드 타입은 읽기 전용으로 요소를 추가하려면 null 만 추가할 수 있다. 따라서 컬렉션에 요소를 추가하는 용도로는 사용할 수 없다.

또한 와일드 카드를 사용하면 특정한 타입을 명시하지 않고 다양한 타입을 처리할 수 있다.
하지만, 그 범위는 명확하지 않아 정확한 타입을 추론하거나 타입을 확인하는 작업이 어렵다.

예를 들어 List<?>는 모든 타입을 처리할 수 있지만, 이 리스트에 어떤 타입의 요소가 들어있는지 정확히 알 수 없다.
따라서, 직접 타입 변환이 필요할 수 있다.

또한 컬렉션의 타입에 따라 사용할 수 있는 메서드에 제한이 생긴다.
List<? extends Number>에서는 값을 읽는 것은 가능하지만, 값을 추가하는 작업은 불가능한다. 반대로, ? super T를 사용한 컬렉션에서는 값을 추가할 수 있지만, 타입 안정성을 위해 읽기 작업이 제하된다. (주로 Object 타입으로 읽어야 한다).

또한 와일드카드를 반환하는 메서드를 사용할 때도 주의해야 한다.


와일드카드를 사용하지 않고 어떻게 대체할 수 있는지

대체하는 방법에는 타입 매개변수를 사용하는 것입니다. 이 방법을 통해 정확한 타입 지정과 메서드 호출이 가능하며 코드의 가독성과 유지보수성을 높일 수 있다.

이로써, 컴파일러가 타입을 추론할 수 있고 실제로 해당 타입과 관련된 메서드를 호출할 수 있다.

또한 와일드 카드의 범위가 너무 넓은 경우 상위 타입을 지정하여 구체화할 수 있다.


와일드카드와 타입 매개변수를 사용하는 것 간에 차이

타입 매개변수를 사용한 제네릭 메서드는 해당 타입에 대해 정확한 메서드 호출이 가능하다.

또한 타입 매개변수를 사용하는 경우, 메서드 호출 시 컴파일러가 타입을 추론할 수 있다.
반면, 와일드카드는 추론이 어려울 수 있다.

또한 타입 매개변수는 구체적인 타입을 지정할 수 있어 컴파일러가 타입 체크를 수행하고 필요한 경우 캐스팅 없이 안전하게 요소를 사용할 수 있다.
반면 와일드 카드는 런타임에 타입이 정확히 무엇인지 알 수 없으며 이로 인해 타입 안정성이 보장되지 않을 수 있다.




Object랑 비슷한 거 같은데?

Object

Java의 최상위 클래스로 모든 클래스는 Object를 상속받는다. 따라서 Object 타입의 변수는 어떤 객체도 참조할 수 있다.
하지만 객체의 구체적인 타입에 대한 정보가 없기 때문에, 사용시 캐스팅이 필요하다.

1
2
Object obj = "Hello";
String str = (String) obj; // 캐스팅 필요

둘 다 다양한 타입의 객체를 저장할 수 있어 유연성을 제공하며, 객체를 저장하는 데 사용될 수 있다.

하지만 Object는 어떤 객체든 저장할 수 있지만, 꺼낼 때마다 캐스팅이 필요하기 때문에 타입 오류가 발생할 수 있다.
반면, 제네릭은 사용시 특정 타입으로 제한할 수 있어서 컴파일 시 타입 체크가 이루어지기 때문에 타입 안정성이 높다

또한 Object는 타입에 대한 정보가 없기 때문에 코드의 가독성이 떨어지지만, 제네릭은 어떤 타입이 사둉될지 명확히 알 수 있어서 가독성이 좋다.


그러면 가독성도 떨어지고, 캐스팅도 해줘야되고 복잡한데 왜 써?

Object는 모든 타입의 최상위 타입으로, 모든 타입이 가져야 하는 요소를 갖는다.

또한 List<Object>를 사용하면 문자열, 숫자, 사용자 정의 객체 등을 같은 리스트에 저장할 수 있다.
예를 들어, 모든 객체를 처리해야 하는 경우 Object를 사용하여 다양한 타입을 처리할 수 있다. 기존 코드와의 호환성 때문에 Object를 사용할 수도 있다.

예를 들어, List<T>는 특정 타입의 리스트라는 것을 명확하게 나타내지만, List<Object>는 다양한 타입의 객체를 저장할 수 있다.

Object 클래스는 Java의 모든 객체에 대한 기본적인 동작을 정의하고, 모든 타입을 수용할 수 있는 가장 일반적인 형태로 다형성을 제공한다. 반면, 제네릭은 타입 파라미터를 통해 컴파일 시점에 타입 안전성을 보장하고, 코드의 재사용성을 높인다.


Map에 숫자를 저장한다고 해보자.
Object 타입으로 저장하면, read할 때 어떤 타입인지 모른다. 왜냐면 타입이 Object 이니까. 다양한 타입을 저장할 수 있으니까. 뭐가 들은지 모르기 때문에 형 변환을 해줘야 한다.

하지만 숫자만 저장할 수 있도록 제네릭타입을 선언하면??
반드시 숫자 타입만 저장하도록 단정지을 수 있기 때문에 read시 형변환이 필요없다. 숫자만 있을 테니까..

반대로 Map의 개발자라고 생각해보자.
이걸 사용하는 사람은 맵에 어떤 타입의 데이터를 저장하려고 할까??
사용하는 사람이 원하는 타입으로(동적으로) Map에 저장하고 읽어올 수 있도록 하기 위해 제네릭으로 만든다.

그렇지 않으면 숫자를 저장하고 읽고 싶은 사람, 문자를 저장하고 읽고 싶은 사람 모두 각각 그들이 원하는 타입으로 변환해야 한다.

따라서 제네릭은 사용하는 사람이 원하는 대로 타입 체크를 동적으로 받을 수 있도록 해준다.

This post is licensed under CC BY 4.0 by the author.