🔖Java

Java 중급 2-1 (제네릭)

Jerry_K 2025. 4. 17. 11:49

 

이번에는 김영한님의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