GeehDev

[Java] 제네릭 본문

Study/Java

[Java] 제네릭

geehyun 2024. 9. 15. 16:05

velog에서 이관해온 글


제네릭(Generic)

제네릭이란, 기본적으로 클래스나 메서드를 정의할 때 정의한 타입에 대해서만 사용할 수 있는 구조에 대해 타입을 미리 정하지 않고 타입자체를 타입변수로 지정해 어떤 타입이 들어오든 활용할 수 있도록 하는 문법 구조입니다.

// 기본 클래스 활용
class UseNormal {
    int[] arr;                // int형만 담을 수 있는 배열 필드 arr
    String function1() {      // String형만 리턴할 수 있는 function1 메서드
        ...;
    }
    void function2(int e) {  // int형만 매개변수로 받을 수 있는 function2 메서드
        ...;
    }
}
// 제네릭 활용
class UseGeneric<T> {
    T[] arr;              // 객체 생성할 때 정한 자료형을 담을 수 있는 배열 필드 arr
    T function1() {       // 객체 생성할 떄 정한 자료형을 리턴할 수 있는 function1 메서드
        ...;
    }
    void function2(T e) { // 객체 생성할 떄 정한 자료형의 매개변수로 받을 수 있는 function2 메서드
        ...;
    }
}
UseGeneric<String> generic1 = new UseGeneric<String>();
// 제네릭 변수에 String형을 담아 자료형 확정 후 사용
UseGeneric<Integer> generic1 = new UseGeneric<Integer>();
// 제네릭 변수에 Integer 클래스를 담아 자료형 확정 후 사용

위 처럼 자료형을 클래스, 메서드를 정의할 때 명시하지 않고 해당 클래스로 객체를 생성할 때 자료형을 기입해서 활용하는 문법구조 입니다.

기본구조

// Generic 클래스를 생성법
class 클래스명<제네릭 변수,,,> {...}
// 사용예시
class UseGeneric1<T> {...}
class UseGeneric2<K, V> {...}

// Generic 클래스 객체 생성 예시
제네릭클래스명<제네릭변수에 대입할 타입> 참조변수 = new 제네릭클래스명<제네릭변수에 대입할 타입>();
// 사용예시
UseGeneric1<String> = new UseGeneric1<String>();
UseGeneric2<Integer, String> = new UseGeneric2<Integet, String>();
  1. 제네릭 클래스를 생성합니다.
    생성할 때는 클래스명 뒤에 <제네릭변수,,,>로 작성해주며, 제네릭 변수는 여러개가 들어갈 수도 있고 ,(쉼표)로 구분해줍니다.
💡 제네릭 변수명은 정해져있나요?
사실 상 제제릭 변수도 그냥 변수명과 마찬가지로 작성자가 임의로 지을 수 있습니다. 다만, 일반적으로 영문 대문자 한글자를 주로 사용합니다.
제네릭 변수 의미
T 타입(Type)
K 키(Key)
V 값(Value)
N 숫자(Number)
E 원소(Element)

 

  1.  생성된 제네릭 클래스의 객체를 생성합니다.
    객체를 생성할 때는 일반 클래스 객체 생성법과 동일하지만 클래스명 우측에 <실제 제네릭 변수에 대입될 타입>을 명시해준다는 차이점이 있습니다.
    UseGeneric1<String> = new UseGeneric1<String>();
    UseGeneric2<Integer, String> = new UseGeneric2<Integet, String>();

    위 처럼 제네릭 클래스는 해당 클래스의 객체를 생성하는 시점에서 사용할 타입(자료형)을 확정해주는 개념입니다.
💡 제네릭 변수에 대입될 수 있는 타입
제네릭 변수에 담을 수 있는 타입은 기본자료형을 제외한 참조자료형만을 담을 수 있습니다. 따라서 기본 자료형으로 제네릭 클래스를 사용하고 싶다면, 랩퍼클래스(Wrapper Class)를 이용해야합니다.
Byte, Short, Character, Integer, Long, Float, Double 이 있습니다.

 

제네릭 문법의 활용범위

제네릭 클래스

클래스를 정의 할 때 제네릭 문법을 적용하여 해당 클래스내 전체에 대해 제네릭 타입변수를 활용할 수 있습니다.

class UseGeneric1<제네릭 타입변수> {...}

제네릭 인터페이스

인터페이스를 정의 할 때 제네릭 문법을 활용하여 해당 인터페이스 내 전체에 대해 제네릭 타입변수를 활용할 수 있습니다.

interface UseGeneric2<제네릭 타입변수> {...}

제네릭 메서드

일반 클래스내 특정 메서드에만 제네릭 타입변수를 활용할 수도 있습니다.
메서드 내에서 리턴타입으로 활용, 매개변수로 활용하거나 둘 모두에서 활용할 수 있습니다.

  • 리턴타입으로 제네릭 타입 활용
    //사용법
    접근지정자 <제네릭 타입변수> 제네릭타입변수(리턴타입) 메서드명() {...;}
    // 사용예시
    class A {
      public <T> T function1() {
          T t;
          // t에 값을 받는 코드;
          return t;
      }
    }
    // 호출법
    A a = new A();
    a.<String> function1();

 

  • 매개변수로 제네릭 타입 활용
    //사용법
    접근지정자 <제네릭 타입변수, 제네릭 타입변수,,,> void 메서드명(제네릭 타입 변수, 제네릭 타입 변수,,,) {...;}
    // 사용예시
    class A {
    public <K, V> void function2(K k, V v) {
        //
    }
    }
    // 호출법
    A a = new A();
    a.<Integer, String> function2(1, "바보");

 

  • 리턴타입, 매개변수 모두 제네릭 타입 활용
    //사용법
    접근지정자 <제네릭 타입변수, 제네릭 타입변수,,,> 제네릭타입변수(리턴타입) 메서드명(제네릭 타입 변수, 제네릭 타입 변수,,,) {...;}
    // 사용예시
    public <K, V, T> T function3(K k, V v) {
      T t;
      // t에 값을 받는 코드;
      return t;
    }
    // 호출법
    A a = new A();
    a.<Integer, String, List> List function3(1, "바보");

제네릭 문법의 특징

    • (제네릭 클래스의 경우) 객체를 생성한 시점, (제네릭 메서드일 경우) 메서드를 호출한 시점에 사용할 타입(자료형)이 확정된다는 특징이 있습니다.
    • 강한타입체크를 할 수 있습니다.

💡 강한 타입 체크란? 

    • 약한타입체크
      Object를 자료형으로 작성할 경우 Object는 최상위 클래스이기 때문에 제네릭 타입변수와 마찬가지로 어떠한 자료형이 들어와도 활용할 수 있긴합니다.
      다만, 마찬가지로 모든 클래스를 포함하는 클래스이기 때문에, 실제 스스로 쓰고싶은 타입 외 다른 타입이 들어와도 이를 걸러내지 못한다는 단점이 있으며, 제대로된 타입이 들어오더라도 실제 사용 시에는 Object 자료형으로 들어온 값이기 때문에 상황에 따라 다운캐스팅이 필요할 수 있습니다.
      이를 약한 타입체크라고 합니다.
    • 강한타입체크
      제네릭 문법의 경우 클래스(또는 메서드) 작성시에는 타입을 확정하지 않고 작성 하지만 실제 객체 생성(또는 메서드호출) 시에는 타입을 명시해줌으로써 타입을 확정하여 사용하는 개념 입니다.
      따라서 Object클래스를 이용할 때와는 달리 실제 사용 시 명사한 타입 외 다른 타입이 들어올 시 오류를 발생하게 되며 명시한 타입 그대로 들어오니 다운캐스팅이 필요 없습니다.
      이를 강한 타입체크라고 합니다.
  • 제네릭 타입 변수로 클래스, 메서드를 정의 시에는 실제 사용할 타입이 확정되지 않은 상태에서 정의하기 때문에 특정 타입에서만 사용할 수 있는 메서드는 기본적으로 사용이 불가하면, 최상위 클래스인 Object 클래스의 메서드만 사용 가능합니다.
  • 제네릭 클래스의 객체 생성 시 대입할 제네릭 타입을 명시 해주지 않을 경우 자동으로 최상위 클래스인 Object가 대입됩니다.

제네릭 타입 범위 제한

제네릭 타입의 범위제한(bound)은 말그대로 제네릭 타입을 작성하면서 사용할 수 있는 타입의 범위를 제한하는 것입니다.

💡 제네릭 타입 범위 제한을 왜 사용하는 걸까?

  • 제네릭 타입을 제한함에 따라 원하는 특정 클래스 또는 특정 클래스의 하위 클래스만 올 수 있도록 제한 할 수 있습니다.
  • 특정 클래스로 타입을 제한 함으로써 해당 클래스의 메서드 사용 가능합니다.
    제네릭 타입을 사용할 경우 해당 클래스, 메서드의 타입이 정의 시 확정되지 않기 때문에 정의할 떄 특정 타입에서만 사용할 수 있는 메서드는 사용이 불가하며 기본적으로 최상위 클래스인 Object클래스의 메서드만 활용이 가능합니다.
    제네릭 타입의 범위 제한을 통해 사용할 수 있는 타입의 범위를 제한하여 해당 제한된 범위까지의 타입의 메서드는 사용가능하게끔 할 수 있습니다.

 

제네릭 타입의 범위를 제한하는 방법은, 아래 3가지로 나뉘어집니다.
1. 제네릭 클래스/인터페이스에서 제한하는 방법
2. 제네릭 메서드에서 제한하는 방법
3. 일반 메서드의 매개변수로 제네릭 클래스의 타입을 제한하는 방법

 

제네릭 클래스/인터페이스에서의 타입제한

제네릭 클래스/인터페이스에서 제네릭 타입 제한은,

  • 제네릭 타입에 대입될 수 있는 타입의 최상위 클래스를 제한합니다.
    => 해당 클래스/인터페이스 사용시 제한되어있는 최상위 클래스를 포함하여 해당 클래스의 하위클래스로만 대입이 가능합니다.
  • 제네릭 클래스, 제네릭 인터페이스 모두 제네릭 타입 제한에서 extends 키워드를 사용합니다.
  • 클래스명 <제네릭 타입변수 extends 제한할 최상위 클래스/인터페이스>
    인터페이스명 <제네릭 타입변수 extends 제한할 최상위 클래스/인터페이스>
// 사용법
접근지정자 class 클래스명 <제네릭 타입변수 extends 제한할 최상위 클래스/인터페이스> {...}
interface 인터페이스명 <제네릭 타입변수 extends 제한할 최상위 클래스/인터페이스> {...}
// 사용예시
class A {}
class B extends A {}
class C extends B {}

class D<T extends B> {
    ...;
}

D<A> da = new D<A>(); // 불가 => D의 제네릭 타입의 최상위 클래스는 B로 제한되어있기 때문에 B 또는 B의 하위 클래스만 대입 가능하나 A클래스는 B의 상위클래스 이므로 불가
D<B> db = new D<B>(); // 가능
D<C> dc = new D<C>(); // 가능 => C는 B의 하위 클래스
D d = new D(); 
// 제네릭 타입으로 대입해줄 타입을 생략한 경우 자동으로 최상위 클래스로 대입됩니다. 
// 기본적으로 최상위 클래스는 Object지만, 해당 D클래스는 최상위 클래스를 B클래스 제한하였기 때문에 B클래스로 대입됩니다.

제네릭 메서드에서의 타입제한

제네릭 메서드에서 타입제한은, 기본적으로 제네릭 클래스/인터페이스에서의 타입제한과 방법이 유사합니다.

  • 제네릭 타입에 대입될 수 있는 타입의 최상위 클래스를 제한합니다.
    => 해당 클래스/인터페이스 사용시 제한되어있는 최상위 클래스를 포함하여 해당 클래스의 하위클래스로만 대입이 가능합니다.
  • 제네릭 클래스, 제네릭 인터페이스 모두 제네릭 타입 제한에서 extends 키워드를 사용합니다.
  • 제네릭 타입의 최상위 클래스를 제한하면서 해당 최상위 클래스의 메서드 사용 가능하게 됩니다.
  • 접근지정자 <제네릭 타입변수 extends 제한할 최상위 클래스/인터페이스> 리턴타입 메서드명() {}
//사용법
접근지정자 <제네릭 타입변수 extends 최상위> 리턴타입 메서드명() {...;}
//사용예시
public <T> void function1(T t) {
    System.out.println(t.intValue());   // X intValue() Number 클래스의 메서드 사용불가
}
public <T extends Number> void function2(T t) {
    System.out.println(t.intValue());   // o intValue() Number 클래스의 메서드 사용가능
}

일반 메서드의 매개변수로 제네릭 클래스의 타입제한

일반 메서드에서 매개변수로 제네릭 클래스를 받아 이에 대해 타입을 제한하는 방법은 크게 4가지가 있습니다.

  • CASE 1 :  메서드명(제네릭 클래스명<제네릭 타입명> 참조변수)
    대입할 수 있는 제네릭 타입을 특정 타입으로 확정하는 방법입니다.
  • CASE 2 :  메서드명(제네릭 클래스명<?> 참조변수)
    대입할 수 있는 제네릭 타입을 확정하지 않고 메서드 사용 시 대입하는 값으로 제네릭 객체 생성하는 방법 입니다.
  • CASE 3 :  메서드명(제네릭 클래스명<? extends 최상위 클래스/인터페이스> 참조변수)
    대입할 수 있는 제네릭 타입의 최상위 클래스/인터페이스를 제한하여 해당 클래스/인터페이스 또는 그 하위 클래스/인터페이스만 대입 가능하게끔 하는 방법입니다.
  • CASE 4 :  메서드명(제네릭 클래스명<? super 최하위 클래스/인터페이스> 참조변수)
    위와 반대로 대입할 수 있는 제네릭 타입의 최하위 클래스/인터페이스를 제한하여 해당 클래스/인터페이스 도는 그 상위 클래스/인터페이스만 대입 가능하게끔 하는 방법입니다.
    //사용법
    접근지정자 리턴타입 메서드명(제네릭클래스명 <제네릭 타입명> 참조변수) {...;}                     // CASE 1
    접근지정자 리턴타입 메서드명(제네릭클래스명 <?> 참조변수) {...;}                               // CASE 2
    접근지정자 리턴타입 메서드명(제네릭클래스명 <? extends 최상위 클래스/인터페이스> 참조변수) {...;}  // CASE 3
    접근지정자 리턴타입 메서드명(제네릭클래스명 <? super 최하위 클래스/인터페이스> 참조변수) {...;}    // CASE 4
    
    //사용예시
    class A {}
    class B extends A {}
    class C extends B {}
    class D extends C {}
    
    class GenericClass<T> {...}
    
    class Test {
    	void function1(GenericClass <A> g) {...;}         // CASE 1
        void function2(GenericClass <?> g) {...;}         // CASE 2
        void function3(GenericClass <? extends B> g) {...;}  // CASE 3
        void function4(GenericClass <? super B> g) {...;}    // CASE 4
    }
    
    public class Main{
    	public static void main(String[] args) {
            Test t = new Test();
            
            // CASE 1
            t.function1(new GenericClass<A>()); //가능 (o)
            t.function1(new GenericClass<B>()); //불가 (X)
            t.function1(new GenericClass<C>()); //불가 (X)
            t.function1(new GenericClass<D>()); //불가 (X)
            // => 대입가능한 클래스를 A로 제한했기 때문에 A클래스 이외에는 전부 불가
    
            // CASE 2
            t.function2(new GenericClass<A>()); //가능 (o)
            t.function2(new GenericClass<B>()); //가능 (o)
            t.function2(new GenericClass<C>()); //가능 (o)
            t.function2(new GenericClass<D>()); //가능 (o)
            // => 대입가능한 클래스를 제한하지 않아서 모든 타입 전부 대입가능
    
            // CASE 3
            t.function3(new GenericClass<A>()); //불가 (X)
            t.function3(new GenericClass<B>()); //가능 (o)
            t.function3(new GenericClass<C>()); //가능 (o)
            t.function3(new GenericClass<D>()); //가능 (o)    
            // => 대입가능한 클래스에 대해 B클래스를 최상위 클래스로 제한하여서 B 또는 B의 하위 클래스만 대입 가능
    
            // CASE 4
            t.function4(new GenericClass<A>()); //가능 (o)
            t.function4(new GenericClass<B>()); //가능 (o)
            t.function4(new GenericClass<C>()); //불가 (X)
            t.function4(new GenericClass<D>()); //불가 (X)    
            // => 대입가능한 클래스에 대해 B클래스를 최하위 클래스로 제한하여서 B 또는 B의 상위 클래스만 대입 가능    
        }
    }

제네릭의 상속

제네릭의 상속에는, 크게 아래 두가지로 구분됩니다.

  • 제네릭 클래스/인터페이스 자체의 상속
  • 제네릭 메서드를 포함하고 있는 일반 클래스의 상속

제네릭 클래스/인터페이스의 상속

제네릭 클래스 또는 인터페이스를 상속 받을 때 해당 자식 클래스도 제네릭 클래스가 됩니다.
부모의 제네릭 타입 변수를 그대로 자식 클래스가 상속 받게 되며, 자식 클래스는 물려받은 제네릭 타입변수 외 본인만의 제네릭 타입변수를 추가할 수도 있습니다.

// 부모 클래스
class Parent<K, V> {...}
// 자식 클래스 ① : 부모 클래스의 제네릭 타입 변수 그대로 상속
class Child1<K, V> extends Parent<K, V> {...}
// 자식 클래스 ② : 부모 클래스의 제네릭 타입 변수 + 본인만의 제네릭 타입변수 추가
class CHild2<K, V, T> extends Parent<K, V> {...}

제네릭 메서드를 포함하고 있는 일반 클래스의 상속

제네릭 메서드를 포함하고 있는 일반 클래스를 상속받을 경우 해당 자식클래스에서는 부모의 제네릭 메서드를 상속받게 됩니다.
이 때 자식 클래스는 해당 제네릭 메서드를 오버라이딩할 수 있는데, 오버라이딩 규칙에 따라 부모클래스에서 정한 제네릭 타입 변수를 모두 사용해야합니다.

// 부모 클래스
class Parent {
    public <T> void print(T t) {
        System.out.println(t);
    }
}
// 자식 클래스
class Child {
    @Override
    public <T> void print(T t) {
        System.out.println(t +" 오버라이딩함");
    }
}
// 메서드 사용
public class Main {
    public static void main(String[] args) {
        Parent p = new Parent();
        Child c = new Child();
        p.<String>print("안녕");    // 출력 : "안녕"
        c.<String>print("안녕");    // 출력 : "안녕 오버라이딩함"
    }
]

💡 제네릭 객체 생성 시 <타입>생략
제네릭 타입의 경우 제네릭클래스<타입> 참조변수 = new 생성자<타입>();에서 제네릭 클래스에서 <타입>을 명시한 경우 생성자<타입>에서 <타입> 부분을 생략할 수 있지만... 위 학습노트에서는 가능한 기본 문법을 지키는 방향으로 작성되었습니다!
생략가능하다는 점도 참고 부탁드립니다!

class GenericClass<T> {...}
GenericClass<String> g1 = new GenericClass<String>(); //기본사용법
GenericClass<String> g2 = new GenericClass<>();       //생성자 쪽 <> 제네릭 타입변수 값   생략가능

 

참고

Do it! 진짜 개발자가 되는 Java 프로그램 입분서 자바 완전 정복 - 김동형
위 책을 공부하며 작성하고 있습니다!

'Study > Java' 카테고리의 다른 글

[Java] 컬렉션 - 1) 컬렉션과 List(ArrayList, LinkedList, Vector)  (0) 2024.09.15
[Java] JavaBeans  (0) 2024.09.15
[Java] 예외 처리  (1) 2024.09.15
[Java] 이너 클래스  (0) 2024.09.15
[Java] 추상 클래스와 인터페이스  (1) 2024.09.15