이번에는 김영한님의Java 중급2에 내용들을 정리하고 보충하였다.
이번 내용들의 핵심은 제네릭과 컬렉션 프레임워크이다.
컬렉션 프레임워크 같은 경우, 자료 구조를 일부 구현하기 때문에,
깃허브에 정리 해두었다.
따라서 이번 포스팅은 비교적 짧게 제네릭만 흩어볼 수 있다.
제네릭
제네릭이 필요한 예시 1)
public class IntegerBox {
private Integer value;
public void set(Integer value){
this.value = value;
}
public Integer get(){
return value;
}
}
- get과 set이 가능한 IntegerBox 클래스
public class StringBox {
private String value;
public void set(String value) {
this.value = value;
}
public String get() {
return value;
}
}
- get과 set이 가능한 StringBox 클래스
예시1 같은 경우에는 타입의 안정성은. 높지만. 코드의 재사용성이 떨어진다.
다형성을 통해서 재사용성을. 높혀보자.
제네릭이 필요한 예시 2)
public class ObjectBox {
private Object value;
public void set(Object object){
this.value = object;
}
public Object get(){
return value;
}
}
-
- get과 set이 가능한 ObjectBox 클래스
public class BoxMain2 {
public static void main(String[] args) {
ObjectBox integerBox = new ObjectBox();
integerBox.set(10);
Integer integer = (Integer) integerBox.get();
System.out.println("integer = " + integer);
ObjectBox stringBox = new ObjectBox();
stringBox.set("hello");
String str = (String) stringBox.get();
System.out.println("str = " + str);
// 문제 발생 위치
integerBox.set("문자 100");
Integer result = (Integer)integerBox.get();
System.out.println("result = " + result);
}
}
-
- 매개변수가 Object이기 때문에 다른 타입의 값도 입력이된다.
다형성을 활용하여 코드의 중복을 제거하였지만, 실수로 원하지 않는 타입이 들어갈 수도 있다.
(문제점)
- IntegerBox 클래스와 StringBox 클래스를 사용하면 코드 재사용성이 떨어지고 타입 안정성은 높아진다.
- ObjectBox를 사용해서 다형성으로 하나의 클래스만 정의하는 경우는 코드 재사용은 높아지지만 타입 안정성이 떨어진다.
코드 재사용성과 타입 안정성, 이러한 문제들을 해결하는 방법이 제네릭이다.
제네릭 적용
public class GenericBox <T>{
private T value;
public void set(T value){
this.value = value;
}
public T get(){
return value;
}
}
- <> (다이아몬드) 를 사용한 클래스를 제네릭 클래스라고 한다.
- 제네릭 클래스를 사용할 때는 Integer, String 같은 타입을 미리 결정하지 않는다.
- <T> 와 같이 선언했을때, T를 타입 매개변수라고 한다,
GenericBox <Integer> integerBox = new GenericBox<Integer>();
integerBox.set(4);
Integer integer = integerBox.get();
System.out.println("integer = " + integer);
GenericBox<String> stringBox = new GenericBox<String>();
stringBox.set("hello");
String str = stringBox.get();
System.out.println("str = " + str);
- 제네릭으로 생성 시점에 원하는 타입 지정 가능하다.
- 참고로 기본형은 안되고 래퍼형만 제네릭에 담을 수 있다.
- 이렇게 원하는 모든 타입 사용이 가능하다.
GenericBox<Integer> integerBox2 = new GenericBox<>();
- 자바는 이렇게 왼쪽에서 타입을 추론해, 오른쪽에 있는 객체를 생성할 때 타입 정보를 생략할 수 있다.
이렇게 제네릭을 사용한 덕분에 코드 재사용과 타입 안정성이라는 두 마리 토끼를 다 잡을 수 있었다.
제네릭 용어와 관례
- 제네릭의 핵심은 사용할 타입을 미리 결정하지 않는다는 점이다.
- 어떻게보면 메서드랑 비슷하게 작동한다.
- 메서드의 매개변수는 사용할 값에 대한 결정을 나중으로 미룸
- 제네릭의 타입 매개변수는 사용할 타입에 대한 결정을 나중으로 미룸
제네릭 타입 (Generic Type)
public class GenericBox <T>{
. . .
}
- 여기에서 GenericBox<T>를 제네릭 타입이라고 한다.
타입 매개변수 (Type Parameter)
- GenericBox<T>에서 T를 타입 매개변수라고 한다.
타입 인자 (Type Argument)
- 제네릭 타입을 사용할 때 제공되는 실제 타입
- GenericBox<Integer>에서 Integer를 타입 인자라고 한다.
제네릭은 매개변수, 인자에 "타입"이 추가로 붙는다는 점을 기억하자 !
타입 매개변수 제한이 필요한 이유
public class Animal {
private String name;
private int size;
public Animal(String name,int size){
this.name = name;
this.size = size;
}
public int getSize() {
return size;
}
public String getName() {
return name;
}
public void sound(){
System.out.println("동물 울름 소리");
}
@Override
public String toString() {
return "Animal{" +
"name='" + name + '\'' +
", size=" + size +
'}';
}
}
- Animal 클래스
public class Cat extends Animal {
public Cat(String name, int size) {
super(name, size);
}
@Override
public void sound() {
System.out.println("냐옹");
}
}
- Animal 클래스를 상속 받는 Cat 클래스
public class Dog extends Animal{
public Dog(String name, int size) {
super(name, size);
}
@Override
public void sound() {
System.out.println("멍멍");
}
}
- Animal 클래스를 상속 받는 Dog 클래스
public class AnimalHospitalV2 <T> {
private T animal;
public void set(T animal){
this.animal = animal;
}
public void checkup (){
System.out.println("동물 이름 : " + animal.getName());
System.out.println("동물 크기 : " + animal.getSize());
animal.sound();
}
public T bigger(T target){
return animal.getSize() > target.getSize() ? animal : target;
}
}
- 타입 매개변수 T로 제네릭 타입 선언
- T에는 Animal에 대한 아무런 정보가 없다.
- 따라서 T에는 Integer가 들어올 수도 있고, Dog가 들어 올 수있다. (다 들어올 수 있음)
- 자바 컴파일러는 어떤 타입이 들어올 지 알 수 없기 때문에 Object 타입으로 가정한다.
- Object의 기능만 사용 가능
- 이렇게 될 경우, Animal 타입이 제공하는 getName(), getSize 같은 메서드를 호출 할 수 없게 된다.
이러한 문제는 아래의 방법으로 해결이 가능하다.
타입 매개변수 제한
public class AnimalHospitalV3<T extends Animal>
AnimalHospitalV3<Dog> dogHospital = new AnimalHospitalV3<>();
AnimalHospitalV3<Cat> catHospital = new AnimalHospitalV3<>();
- 이렇게 함으로써 T에 입력될 수 있는 값의 범위를 예측할 수 있다.
- T 또한 입력 값의 범위를 예측 가능해진다.
제네릭에 타입 매개변수 상한을 사용해서 타입 안정성을 지키면서,
상위 타입의 원하는 기능까지 사용할 수 있게 되었다.
제네릭 메서드
"제네릭 타입과 제네릭 메서드는 다른 것이다."
public class GenericMethod {
public static Object objMethod(Object obj){
System.out.println("Object print: " + obj);
return obj;
}
public static <T> T genericMethod(T t){
System.out.println("generic print: " + t);
return t;
}
public static <T extends Number> T numberMethod(T t){
System.out.println("bound print: " + t);
return t;
}
}
Integer numberInteger = GenericMethod.<Integer>numberMethod(10);
Double numberDouble = GenericMethod.<Double>numberMethod(20.0);
- 특정 메서드에 제네릭을 적용하는 것을 제네릭 메서드라고 한다,
- 제네릭 타입, 제네릭 메서드 모두 제네릭을 사용하지 서로 다른 기능을 제공한다.
- 한가지 주의해야 할 점은 타입 인자는 컴파일 타임에만 존재하고 런타임에는 완전히 사라진다.
- 제네릭 타입
- 객체를 생성하는 시점에 타입 인자 전달
- ex) GenericCalss<T>
- 제네릭 메서드
- 매서드를 호출하는 시점에 타입 인자 전달
- <T> T genericMethod(T t)
- 제네릭 타입
- 제네릭 메서드는 클래스 전체가 아니라 특정 메서드 단위로 제네릭을 도입할 때 사용한다.
- GenericMethod.<Integer>numberMethod(10) 와 같이 호출 시점에서 타입을 정하여 호출
제네릭 메서드 - 인스턴스 메서드, static 메서드
class Box<T> {
static <V> V staticMethod(V v){ } //static 메서드에 제네릭 메서드 도입
<Z> Z instanceMethod(Z z){ } //인스턴스 메서드에 제네릭 메서드 도입 가능
}
- 제네릭 메서드는 인스턴스 메서드와 static 메서드에 모두 적용할 수 있다.
제네릭 타입 - 인스턴스 메서드, static 메서드
class Box<T> {
static T staticMethod(T t){ } // 제네릭 타입의 T 사용 불가능
T instanceMethod(T t){ } // 가능
}
- 하지만 제네릭 타입에서 static 메서드는 사용이 불가능하다.
- 제네릭 타입은 객체를 생성하는 시점에 타입이 정해지는데, static 메서드는 인스턴스 단위가 아니라 클래스 단위로 작동하기 때문에 사용이 불가능하다.
- static 메서드에 제네릭을 도입하고 싶으면 제네릭 메서드를 사용해야 한다.
static <T> T staticMethod(T t)는 메서드가 자기 타입(T)을 스스로 정의했기 때문에,
클래스의 제네릭이 없어도 사용 가능하고, 메서드 호출 시점에 T가 결정된다.
즉, static 메서드가 자기가 만든 타입에만 의존하는 구조다.
반면 static T staticMethod(T t)는 클래스의 제네릭 타입 <T>에 의존하려고 하는데,
static 메서드는 클래스 인스턴스 없이도 실행되기 때문에
컴파일 시점에 T의 정체를 알 수 없어 에러가 난다.
즉, static 메서드가 클래스가 만든 타입에 의존하려고 해서 문제가 생긴다.
제네릭 메서드 타입 추론
Integer numberInteger = GenericMethod.<Integer>numberMethod(10);
Integer numberInteger = GenericMethod.numberMethod(10); // 타입 추론 가능
- 자바 컴파일러가 10의 타입을 Integer라는 것을 추론하여 제네릭 타입 인자를 추론하여 생략 가능하다.
제네릭 타입과 제네릭 메서드의 우선순위
- static(정적) 메서드는 제네릭 메서드만 적용 가능하지만, 인트턴스 메서드는 모두 적용 가능
class ComplexBox <T extends Animal>{
public <T> T printAndReturn (T t){ }
}
- 만일 이렇게 될 경우 제네릭 메서드가 제네릭 타입보다 더 높은 우선순위를 가진다.
- 위의 코드에서 제네릭 메서드는 제한(상한)이 없기 때문에, Object 클래스로 취급된다.
- 따라서 해당 제네릭 메서드에서는 Animal과 Animal 자식 메서드를 활용 할 수 없다
근데 애초에 이렇게 이름을 겹치게 짓는것이 문제 !!
와일드카드
public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
//이것은 제네릭 메서드이다.
//Box<Dog> dogBox를 전달한다. 타입 추론에 의해 타입 T가 Dog가 된다.
static <T> void printGenericV1(Box<T> box) {
System.out.println("T = " + box.get());
}
//이것은 제네릭 메서드가 아니다. 일반적인 메서드이다.
//Box<Dog> dogBox를 전달한다. 와일드카드 ?는 모든 타입을 받을 수 있다.
static void printWildcardV1(Box<?> box) {
System.out.println("? = " + box.get());
}
- 와일드카드는 제네릭 타입이나, 제네릭 메서드를 선언하는 것이 아니다.
- 와일드카트는 이미 만들어진 제네릭 타입을 활용할 때 사용한다.
- 즉, 와일드카드는 제네릭 타입 없이는 쓸 수 없다.
- 와일드 카드는 타입 인자가 정해진 제네릭 타입을 전달 받아서 활용할 때 사용
와일드 카드에 대해 생각 해볼 점
와일드카드는 일반적인 메서드에 사용할 수 있고, 단순 매개변수로 제네릭 타입을 받을 수 있는 것 뿐이다.
제네릭 메서드처럼 타입을 결정하거나 복잡하게 작동하지 않는다.
또한 와일드 카드는 제네릭 타입을 사용하는 쪽(외부 클래스)에서 쓰는 것으로 ,
제네릭 타입을 정의하는(클래스 내부)에서는 안쓴다.
그냥 단순히 일반 메서드에 제네릭 타입을 받을 수 있는 매개변수가 하나 있는 것 뿐이다.
제네릭 타입이나 제네릭 메서드 정의가 꼭 필요한 상황이 아니라면, 단순한 와일드 카드 사용을 권장 !
타입 매개변수가 꼭 필요한 경우 (와일드 카드의 한계)
// 제네릭 메서드
static <T extends Animal> T printAndReturnGeneric(Box<T> box){
T t = box.get();
System.out.println("이름 = " + t.getName());
return t;
}
// 와일드카드
static Animal printAndReturnWildcard(Box<? extends Animal> box){
Animal animal = box.get();
System.out.println("이름 = " + animal.getName());
return animal;
}
- 제네릭 메서드와 와일드카드가 있는 일반 메서드이다.
Dog dog = WildcardEx.printAndReturnGeneric(dogBox);
Animal animal = WildcardEx.printAndReturnWildcard(dogBox);
- 위와 같이 호출했다고 해보자.
- dogBox를 타입 매개변수 또는 매개변수로 넣었다.
- 내가 원하는 반환 타입은 dog인데, 와일드카드 경우 Animal 타입으로 반환이 된다.
// Box <? extends Animal> 와일드 카드는 모두를 받을 수 있다.
Box<Dog>
Box<Cat>
Box<Lion>
. . .
- 위와 같이 와일드 카드는 모두를 받을 수 있기 때문에, 컴파일러 입장에서는 Dog인지 Cat인지 모른다.
- 따라서 컴파일러 입장에서는 Animal 타입으로 생각하는 것이다.
- 이러한 이유는 와일드카드는 이미 만들어진 제네릭 타입을 전달 받아서 활용하기 때문이다.
- 즉, 동적이 아닌 정적으로 처리하기 때문에, 타입 인자를 통해 메서드의 타입을 변경 할 수 없다.
와일드카드를 복잡하게 생각하지 말고, 그냥 일반적인 메서드라고 생각하자 !
T는 이름이 있는 타입
? 는 이름이 없는 타입
상한 와일드카드
static Animal printAndReturnWildcard(Box<? extends Animal> box){
Animal animal = box.get();
System.out.println("이름 = " + animal.getName());
return animal;
}
- 와일드카드도 제네릭 메서드와 마찬가지로 상한 제한을 둘 수 있다.
- 해당 코드는 " <? extends Animal >" 로 상한을 제한 두었다.
- 컴파일 입장에서는 ?가 어떤 타입인지 모르므고, Animal 타입을 반환형으로
하한 와일드카드
static void writeBox(Box<? super Animal> box){
box.set(new Dog("멍멍이", 100)); //업캐스팅
}
- 타입이 Animal이지만, 업캐스팅
writeBox(objectBox); // 최상위 Object이므로 가능
writeBox(animalBox);
// writeBox(dogBox); // Animal 자식 클래스여서 불가능
// writeBox(catBox); // Animal 자식 클래스여서 불가능
- 상한은 extends를, 하한은 super 키워드를 사용한다.
- Animal 이상이므로 그 아래 자식들을 인자로 넣을 수 없다.
자바의 제네릭을 단순하게 생각하면 개발자가 직접 캐스팅 하는 코드를 컴파일러가 대신 처리해주는 것 !
실무에서는 제네릭을 사용해서 무언가를 설계하거나 만드는 일은 드물다고 한다 !
이미 만들어진 코드의 제네릭을 읽고 이해하는 정도만 이해해보자.
타입 이레이저
public class GenericBox<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
- 제네릭 타입에 Integer 타입 인자 전달 전
public class GenericBox<Integer> {
private Integer value;
public void set(Integer value) {
this.value = value;
}
public Integer get() {
return value;
}
}
- 컴파일 시점에서는 Integer 전달되어 위와 같이 컴파일러가 이해한다.
- 제네릭은 자바 컴파일 단계에서만 사용되고, 컴파일 이후에는 제네릭 정보가 삭제
class EraserBox<T> {
public boolean instanceCheck(Object param) {
return param instanceof T; // 오류
}
public T create() {
return new T(); // 오류
}
}
- 컴파일 이후에는 제네릭의 타입 정보가 존재하지 않는다.
- .class로 자바를 실행하는 런타임에는 우리가 지정한 타입 정보가 모두 제거된다.
- 따라서 위와 같이 런타임에 타입을 활용하는 코드는 작성 할 수 없다.
'🔖Java' 카테고리의 다른 글
Java 기본 (객체 지향 프로그래밍) (0) | 2025.03.16 |
---|---|
Java 중급-1 (0) | 2025.03.16 |
Java 기초 정리 (0) | 2025.03.09 |