본문 바로가기
Backend/Java

Java 기초 문법 : 제네릭(generic)

by 코딩쥐 2024. 10. 8.

제네릭(Generic) 타입은 Java 5 이후에 추가된 기능으로, 클래스, 인터페이스, 메서드를 정의할 때 타입 파라미터를 사용하여 외부에서 지정할 수 있게 한다. 제네릭을 사용하면 같은 로직으로 다양한 타입의 객체를 처리할 수 있으며, 컴파일러 시에 타입 파라미터에 해당하는 타입 또는 해당 타입의 자식 클래스 객체만이 할당되도록 검증한다. 이로 인해 잘못된 타입의 객체가 사용될 경우 오류를 사전에 발견할 수 있다.

  • 제네릭 클래스 : class 클래스명<타입 파라미터>{}
  • 제네릭 메서드 : <타입 파라미터> 리턴타입 메서드명(매개변수){}

 

타입 파라미터 

제네릭을 정의할 때 사용하는 식별자는 아래와 같다. 원하는 대로 이름을 사용할 수 있지만, 아래의 약어들은 제네릭 프로그래밍에서 통상적으로 사용되는 약어이다. 

  • E : Element (컬렉션 요소 타입을 나타낼 때 사용)
  • K : Key (Map에서 키의 타입을 나타낼 때 사용)
  • N : Number (숫자와 관련된 타입을 정의할 때 사용)
  • T : Type 
  • V : Value (Map에서 값의 타입을 나타낼 때 사용)
  • S, U, V : 두번째, 세번째, 네번째

제네릭 타입 파라미터에는 기본 타입(primitive type)을 직접 사용할 수 없다. 대신 해당 기본 타입의 래퍼 클래스(wrapper class)를 사용해야 한다. 모든 객체는 Object 클래스의 자식이지만, 타입 파라미터로 Object를 사용하는 것은 타입 안정성을 떨어뜨리기 때문에 일반적으로 권장되지 않는다. 

 

Wrapper class

기본 타입(primitive type)의 데이터를 객체로 다루기 위해 사용되는 클래스이다.

기본 타입 래퍼 클래스
byte Byte
boolean Boolean
char Character
double Double
float Float
int Integer
long Long
short Short

 

제네릭 사용하기

제네릭 클래스

아래 예제에서 <T, S> 두개의 타입 파라미터를 사용하여 클래스를 정의했다. 각각의 타입 파라미터에는 다양한 타입이 들어갈 수 있다. 

package com.example;

// 제네릭 타입 파라미터 사용
public class Example01<T, S> {
    // 제네릭 타입을 가지고 있는 멤버변수 선언
    private T a;
    private S b;

    public T getA() {
        return a;
    }

    public void setA(T a) {
        this.a = a;
    }

    public S getB() {
        return b;
    }

    public void setB(S b) {
        this.b = b;
    }

    public static void main(String[] args) {
        //Example01의 타입을 String, Integer로 지정
        Example01<String, Integer> var1 = new Example01<>();
        var1.setA("안녕하세요");
        var1.setB(3);
        System.out.println(var1.getA());
        System.out.println(var1.getB());// "안녕하세요"

        //Example01의 타입을 Boolean, Character로 지정
        Example01<Boolean, Character> var2 = new Example01<>();
        var2.setA(true);
        var2.setB('a');
        System.out.println(var2.getA()); // true
        System.out.println(var2.getB()); // a
    }
}

 

제네릭 메서드

메서드에서도 파라미터 타입과 리턴 타입에 제네릭 객체를 사용할 수 있다. 

package com.example;

public class Example01{
    //제네릭 파라미터를 가지고 있는 메서드 생성, 어떤 타입의 배열이든 받을 수 있다.
    //제네릭 타입의 배열을 받아서 출력하는 메서드
    public static <T> void printArray(T[] arr){
        for ( T elem : arr){
            System.out.println(elem);
        }
    }
    
    public static void main(String[] args) {
        // Integer 배열 생성 및 출력
        Integer[] intArr = {1,2,3,4,5};
        printArray(intArr); // 1, 2, 3, 4, 5

        // String 배열 생성 및 출력
        String[] strArr = {"가","나","다","라"};
        printArray(strArr); // 가, 나, 다, 라
    }
}

 

제네릭 타입 제한하기

제네릭 클래스와 메서드에서 특정 타입 또는 자식 타입만을 타입 파라미터로 받을 수 있도록 제한할 수 있다. 

  • class 클래스명<타입 파라미터 extends 상위타입>{}
  •  <타입 파라미터> 리턴타입 메서드명(매개변수){}

 

제네릭 클래스

package com.example;

// 제네릭 타입 파라미터 제한
public class Example01<T extends CharSequence> {
    // 제네릭 타입을 가지고 있는 멤버변수 선언
    private T a;

    public T getA() {
        return a;
    }

    public void setA(T a) {
        this.a = a;
    }

    public static void main(String[] args) {
        // Example01의 타입을 String으로 지정
        Example01<String> var1 = new Example01<>();
        var1.setA("안녕하세요"); // String은 CharSequence의 자식 클래스
        System.out.println(var1.getA()); // "안녕하세요"

        // 잘못된 타입 사용 예시
        // Example01<Integer> var2 = new Example01<>(); // 오류 발생
        // var2.setA(4); // Integer는 CharSequence의 자식 클래스가 아님
    }
}

 

제네릭 메서드

package com.example;

public class Example01{
    //Number 클래스를 상위 타입으로 갖는 제네릭 타입
    public static <T extends Number> void printArray(T[] arr){
        for ( T elem : arr){
            System.out.println(elem);
        }
    }
    public static void main(String[] args) {
        // Number 클래스 상위 타입이기 때문에, Integer, Double, Float의 타입만 사용 가능
        Integer[] intArr = {1,2,3,4,5};
        printArray(intArr); // 1, 2, 3, 4, 5

        // String 타입 시에 에러
        // String[] strArr = {"가","나","다"};
        // printArray(strArr);

    }
}

 

printArray 메서드는 Number 클래스 또는 그 자식 클래스의 배열만 허용하며, String 배열을 전달하려고 할 때 타입 불일치로 인해 에러가 발생한다. 

 

제네릭 타입의 상속과 구현

제네릭 타입도 다른 타입과 마찬가지로 부모 클래스가 될 수 있다. 단 부모클래스가 제네릭 타입이 2개일 경우, 자식클래스는 2개 이상의 제네릭 타입을 가지고 있어야 한다. 

package com.example;

class Parent<T,S>{
    private T a;
    private S b;

    // 생성자 생성
    public Parent(T a, S b) {
        this.a = a;
        this.b = b;
    }

    public T getA() {
        return a;
    }

    public void setA(T a) {
        this.a = a;
    }

    public S getB() {
        return b;
    }

    public void setB(S b) {
        this.b = b;
    }
}

//부모클래스의 제네릭 타입이 2개이기 때문에, 자식클래스 제네릭 타입도 2개 이상이어야 한다.
public class Example01<T, S> extends Parent<T, S> {
    // 부모클래스의 생성자 호출
    public Example01(T a, S b) {
        super(a, b);
    }

    public static void main(String[] args) {
        //Example01 인스턴스 생성 
        Example01<Integer, String> var1 = new Example01<>(1, "안녕");
        System.out.println(var1.getA()); // 1
        System.out.println(var1.getB()); // "안녕"
    }
}