프로그래밍 언어/Java

[Java] 제네릭(generic)의 개념과 활용법

백엔드 개발자 - 젤리곰 2023. 12. 19. 23:10
728x90

1. 제네릭(generic)이란?

자바에서 제네릭(generic)이란 데이터의 타입(data type)을 일반화한다(generalize)는 것을 의미합니다.

제네릭(Generic)은 Java 프로그래밍 언어에서 타입(type)의 파라미터화를 가능하게 하는 언어 기능입니다.

이는 클래스, 인터페이스, 메소드를 정의할 때 타입을 하나의 파라미터처럼 취급할 수 있게 해 줍니다.

즉, 구체적인 타입을 명시하지 않고도 타입을 사용할 수 있는 방법을 제공합니다.

 

 

 

2. 제네릭 사용 시 얻을 수 있는 이점

1) 타입 안정성 :  제네릭을 사용하면 컴파일 시점에 타입 체크를 할 수 있어서 런타임에 발생할 수 있는 ClassCastException과 같은 오류를 방지할 수 있습니다.

2) 코드의 재사용성 증가 

3) 유지보수성 향상

 

 

 

3. 제네릭 사용법

타입 매개변수를 꺾쇠괄호(< >) 안에 지정하여 선언합니다.

 

 

 

 

4. 제네릭을 사용하지 않은 코드 VS 제네릭 사용한 코드

 

4-1) 코드 중복 문제

 

*제네릭을 사용하지 않은 코드

package Day10_Generic;

public class _01_Generics {
    public static void main(String[] args) {
        int[] iArr = {1,2,3,4,5};
        double[] dArr = {1.0,2.0,3.0,4.0,5.0};
        String[] sArr = {"A","B","C","D","E"};

		//문제점 1.
        //타입에 따라 메서드를 따로 선언해서 사용하고 있다.
        //추후 타입이 추가되면 메서드를 또 만들어야한다.
        printIntArr(iArr);
        printDoubleArr(dArr);
        printStringArr(sArr);

    }

	//문제점 2.
    //코드가 중복되고 있다.
    //비효율적인 코드로 보인다.
    private static void printStringArr(String[] sArr) {
        for (String i: sArr) {
            System.out.print(i+ " ");
        }
    }

    private static void printDoubleArr(double[] dArr) {
        for (double i: dArr) {
            System.out.print(i+ " ");
        }
    }

    private static void printIntArr(int[] iArr) {
        for (int i: iArr) {
            System.out.print(i+ " ");
        }
    }
}

 

주석에도 설명을 달아놨지만, 타입의 갯수만큼 메서드를 생성해야하는 코드입니다.

따라서 코드 중복도 보입니다.

 

 

*제네릭을 사용한 코드

package Day10_Generic;

public class _01_Generics {
    public static void main(String[] args) {
	    Integer[] wrapiArr = {1,2,3,4,5};
        Double[] wrapdArr = {1.0,2.0,3.0,4.0,5.0};
        String[] sArr = {"A","B","C","D","E"};

        //제네릭스가 지원하는 건 객체!
        //기본자료형은 쓸 수 없다. 래퍼클래스를 써야된다. 첫문자가 대문자!
        printAnyArr(wrapiArr);
        printAnyArr(wrapdArr);
        printAnyArr(sArr);
    }

    // T : Type
    private static <T> void printAnyArr(T[] arr){
        for (T t: arr) {
            System.out.print(t+ " ");
        }
    }
}

 

훨씬 코드가 깔끔해 졌습니다.

타입의 갯수만큼 메서드를 만들지 않아도 됩니다.

유지보수성도 향상되고 코드의 중복도 없앴습니다.

 

다만, 주의해야할 점은 제네릭이 지원하는 건 객체라는 것입니다.

int, double등은 기본자료형이기 때문에 래퍼클래스로 값을 넘겨줘야 제네릭을 활용할 수 있습니다.

 

 

4-2) 코드 중복 및 형변환 실수

 

*제네릭을 사용하지 않은 코드

/*클래스 : CoffeeByNumber*/

public class CoffeeByNumber {
    public int waitingNumber;

    public CoffeeByNumber(int waitingNumber) {
        this.waitingNumber = waitingNumber;
    }

    public void ready(){
        System.out.println("커피 준비 완료: "+ waitingNumber);
    }

}


/*클래스 : CoffeeByNickname*/

package Day10_Generic.coffee;

public class CoffeeByNickname {
    public String nickname;


    public CoffeeByNickname(String nickname) {
        this.nickname = nickname;
    }

    public void ready(){
        System.out.println("커피 준비 완료 : " + nickname);
    }
}


/*클래스 : CoffeeByName*/

package Day10_Generic.coffee;

public class CoffeeByName {
    public Object name;

    public CoffeeByName(Object name) {
        this.name = name;
    }

    public void ready(){
        System.out.println("커피 준비 완료 : "+name);
    }

}


/*main메서드*/

public class _02_GenericClass {
    public static void main(String[] args) {
    
        //int
        CoffeeByNumber c1 = new CoffeeByNumber(33);
        c1.ready();
        
        //String
        CoffeeByNickname c2 = new CoffeeByNickname("우루루파도");
        c2.ready();
        
        //-------------------------------//
        //Object
        CoffeeByName c3 = new CoffeeByName(34);
        c3.ready();
        CoffeeByName c4 = new CoffeeByName("돌맹이");
        c4.ready();

        //c3는 Object타입이기때문에 Integer객체로 박싱됐다.
        // (int)로 언박싱해줘야한다.
        int c3Name = (int)c3.name;
        System.out.println("주문 고객 번호 : "+c3Name);
        
        // c4.name은 Object 타입이지만, 실제로 저장된 값은 String이다.
        // 그러나 컴파일러는 이것을 알지 못하기 때문에
        // 이것이 String임을 명시적으로 알려주어야 한다.
        String c4Name = (String)c4.name;
        System.out.println("주문 고객 이름 : "+c4Name);

        //⭐⭐⭐⭐⭐Integer객체를 (String)으로 형변환하려고 하면
        //ClassCastException 에러가 난다.
		c4Name = (String)c3.name;

    }
}

 

위 코드는 스타벅스 예제입니다.

커피가 준비되면 주문번호로 불리거나 닉네임으로 불립니다.

 

1️⃣ 자료형에 따라 CoffeeByNumber, CoffeeByNickname 두 클래스로 나눴습니다.

자료형에 따라 클래스를 계속 추가해줘야하니 번거롭습니다.

2️⃣ Object타입으로 받으니 클래스 하나로도 가능합니다.

그런데, 두가지 문제점이 보이네요.

- name을 받아올 때, 매번 형변환을 해줘야 한다는 점.

- 형변환을 할 때, 바꿀 수 없는 것을 형변환하면 ClassCastException에러가 나올 수 있다는 점.

 

위 문제들을 제네릭 클래스로 해결할 수 있습니다. 

 

*제네릭 사용한 코드

/* 클래스 : Coffee */
public class Coffee <T>{
    public T name;

    public Coffee(T name) {
        this.name = name;
    }

    public void ready() {
        System.out.println("커피 준비 완료 : "+name);
    }
}

/* main메소드 */
public class _02_GenericClass {
    public static void main(String[] args) {

        Coffee<Integer> c5 = new Coffee<>(35);
        c5.ready();
        int c5Name = c5.name;
        System.out.println("주문 고객 번호 : "+ c5Name);
        
        Coffee<String> c6 = new Coffee<>("카카오");
        c6.ready();
        String c6Name = c6.name;
        System.out.println("주문 고객 번호 : "+ c6Name);
    }
}

 

형변환을 따로 명시하지 않아도 됩니다.

컴파일 시 타입 체크를 수행하여, (빨간밑줄)

런타임 시 발생할 수 있는  ClassCastException에러를 사전에 방지할 수 있습니다.

 

 

 

5. 제네릭 타입 제한하는 법

 

상속을 활용하여 타입을 제한할 수 있습니다.

바로 예제 코드로 이해해보겠습니다.

/*클래스 : User*/

public class User {
    public String name;

    public User(String name) {
        this.name = name;
    }

    public void addPoint(){
        System.out.println(this.name + "님 포인트 적립되었습니다.");
    }
}

/*클래스 : VIPUser*/

public class VIPUser extends User{

    public VIPUser(String name) {
        super("특별한"+name);
    }
}


/*클래스 : CoffeeByUser*/

public class CoffeeByUser <T extends User>{//User클래스만 T에 들어올 수 있다.

    public T user;

    public CoffeeByUser(T user) {
        this.user = user;
    }

    public void ready(){
        System.out.println("커피 준비 완료 : "+user.name);
        user.addPoint();
    }
}


/*main 메서드*/

public class _02_GenericClass {
    public static void main(String[] args) {

        CoffeeByUser<User> c7 = new CoffeeByUser<>(new User("강호동"));
        c7.ready();//User클래스

        CoffeeByUser<User> c8 = new CoffeeByUser<>(new VIPUser("서장훈"));
        c8.ready(); //User를 상속받은 클래스
        
        CoffeeByUser<User> c9 = new CoffeeByUser<>(new OtherType("뿡뿡이"));
        //new OtherType은 User클래스 또는 User클래스를 상속받은 클래스가 아니기에 에러가 납니다.
 }

 

 

 

6. 제네릭 여러개 사용하기

public class _02_GenericClass {
    public static void main(String[] args) {
    
        orderCoffee("우루루파도","카페라떼");
        
    }

    public static <T,V> void orderCoffee(T name,V coffee){
        System.out.println(coffee+" 준비 완료 : " + name);
    }
}

 

쉼표로 구분해서 쓰면 됩니다. 참 쉽죠.

728x90