인생을 코딩하다.

[Java] Generic 본문

Java

[Java] Generic

Hyung1 2020. 11. 19. 18:08
728x90
반응형

Java Collection Framework의 Generic programming

 

Java Collection Framework은 java에서 여러가지 자료구조와 알고리즘을 구현해놓은 라이브러리라고 할 수 있다.

 

 

제네렉 프로그래밍이란? "클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법"

- 변수의 선언이나 메서드의 매개변수를 하나의 참조 자료형이 아닌 여러 자료형을 변환 될 수 있도록 프로그래밍 하는 방식

- 메소드 호출시 리턴값으로 넘어오는 타입과 다를 때 각각 형변환을 해주는 문제점이 있다. 그래서 타입 형 변환에서 발생할 수 있는 문제점을 "사전에' 없애기 위해서 만들어졌다

- 실제 사용되는 참조 자료형으로 변환은 컴파일러가 검증하므로 안정적인 프로그래밍 방식

- java5 부터 추가 됨.

 

자바에서 정의한 제네릭의 기본 규칙

- E : 요소 (자바 컬렉션에서 주로 사용)

- K :키

- N : 숫자

- T : 타입

- V : 값

- S, U, V : 두 번째. 세 번째, 네 번째에 선언된 타입

 

꼭 이 규칙을 지켜야 컴파일이 되는 것은 아니다. 하지만, 다른 어떤 사람이 보더라도 쉽게 이해할 수 있도록 하려면 이 규칙을 따르는 것이 좋다.

 

자료형 매개변수 T

public class Generic<T> {
    private T material;   // T -> 제네릭클래스, Type의 약자, 자료형 매개변수

    public void setMaterial(T material) {
        this.material = material;
    }

    public T getMaterial() {
        return material;
    }
}

T에 어떤 것이 들어가느냐는 실제 클래스를 쓸 때 결정하면 된다, 여러개의 클래스가 쓰일 수 있다.

package java8.generic;

public class Plastic {

    public String toString() {
        return "재료는 Plastic 입니다";
    }
}
package java8.generic;

public class Powder {

    public String toString() {
        return "재료는 Powder 입니다";
    }
}
package java8.generic;

public class Water {
     ...
}
package java8.generic;

public class GenericPrinter<T> {

    // Printer을 쓸 때, 재료를 정하자, T type에 적용이 되서 클래스가 생성 될 때, T타입에 모두 대입이 된다.
    private T material;


    public T getMaterial() {
        return material;
    }

    public void setMaterial(T material) {
        this.material = material;
    }

    public String toString() {
        return material.toString();
    }

}
package java8.generic;

public class GenericPrinterTest {

    public static void main(String[] args) {

        GenericPrinter<Powder>  powderGenericPrinter = new GenericPrinter<>();
        Powder powder = new Powder(); // 파우더에 관한 재료를 넣어준다.
        // GenericPrinter 클래스에서 제네릭 타입만 생성했다고 해서 되는게 아니다. 생성을 해서 set으로 넣어줘야 한다.
        powderGenericPrinter.setMaterial(powder);
        System.out.println(powderGenericPrinter);

        GenericPrinter<Plastic>  plasticGenericPrinter = new GenericPrinter<>();
        Plastic plastic = new Plastic();
        plasticGenericPrinter.setMaterial(plastic);
        System.out.println(plasticGenericPrinter);
        
        // GenericPrinter<Water> waterGenericPrinter = new GenericPrinter<>();

    }
}

두 개의 관해 각각 다른 프린터를 생성을 할 때, 자료형을 대입해서 쓸 수 있다. 이게 제네릭 프로그래밍이다.

 

여기서 하나 생각해볼것은, 만약 재료가 물이 있다면 물을 3D 프린터 재료로 할 수는 없다. 만약 제약 조건이 없다면 위에 주석 처리 한 로직처럼 그냥 쓸 수 있다. 나중에 기술이 발전해서 물로도 3D 프린터를 할 수 있다면 상관 없겠지만, 위 주석처리 한 로직을 주석처리 하지 않고 실행하게 된다면 문제가 발생 할 수 있다.

 

그래서 이를 해결하기 위한 방법은,

 

Meterial 클래스를 만들고

package java8.generic;

public class Meterial {
}

GenericPrinter 클래스에서 T type 옆에 Meterial 클래스를 상속 받아 Meterial 클래스를 상속 받은 애들만 GenericPrinter의 재료로 사용할 수 있게 해준다.

public class GenericPrinter<T extends Meterial> {

그리고 Powder, Plastic 클래스에서 상속을 해주어야 한다.

// Powder 클래스
public class Powder extends Meterial{

//Plastic 클래스
public class Plastic extends Meterial{

이렇게 하면 

Water는 당연히 쓸 수 없게 된다.



또 이런 방법도 있다. 

package java8.generic;

public abstract class Meterial {

    public abstract void doPrinting();

}

Powder, Plastic 클래스에 

package java8.generic;

public class Plastic extends Meterial {

    public String toString() {
        return "재료는 Plastic 입니다";
    }

    @Override
    public void doPrinting() {
        System.out.println("Plastic로 프린팅 합니다.");
    }
}
package java8.generic;

public class Powder extends Meterial{

    public String toString() {
        return "재료는 Powder 입니다";
    }

    @Override
    public void doPrinting() {
        System.out.println("Powder로 프린팅 합니다.");

    }
}

추가해준다.

package java8.generic;

public class GenericPrinter<T extends Meterial> {

    // Printer을 쓸 때, 재료를 정하자, T type에 적용이 되서 클래스가 생성 될 때, T타입에 모두 대입이 된다.
    private T material;


    public T getMaterial() {
        return material;
    }

    public void setMaterial(T material) {
        this.material = material;
    }

    public String toString() {
        return material.toString();
    }

    public void printing() {
        material.doPrinting();
    }

}
package java8.generic;

public class GenericPrinterTest {

    public static void main(String[] args) {

        GenericPrinter<Powder>  powderGenericPrinter = new GenericPrinter<>();
        Powder powder = new Powder(); // 파우더에 관한 재료를 넣어준다.
        // GenericPrinter 클래스에서 제네릭 타입만 생성했다고 해서 되는게 아니다. 생성을 해서 set으로 넣어줘야 한다.
        powderGenericPrinter.setMaterial(powder);
        System.out.println(powderGenericPrinter);

        GenericPrinter<Plastic>  plasticGenericPrinter = new GenericPrinter<>();
        Plastic plastic = new Plastic();
        plasticGenericPrinter.setMaterial(plastic);
        System.out.println(plasticGenericPrinter);

       // GenericPrinter<Water> waterGenericPrinter = new GenericPrinter<>();

        powderGenericPrinter.printing();
        plasticGenericPrinter.printing();
    }
}

 

맨 마지막 아래에 두 줄을 추가해 줌으로써

재료는 Powder 입니다
재료는 Plastic 입니다
Powder로 프린팅 합니다.
Plastic로 프린팅 합니다.

이런 결과를 얻을 수 있다.

 

정리하자면, <T extends 클래스> = "Bounded Wildcards" -> T 대신에 사용될 자료형을 제한하기 위해 사용한다

 

Material <----- Powder, Plastic

Material에 정의된 메서드를 공유할 수 있다. 상속을 통해 제한을 구현할 수 있다.

즉, 매개 변수로 넘어오는 제네릭 타입의 경계를 지정하는데 사용한다는 의미

 

그런데 이 방법에는 큰 단점이 있다. 매개 변수로 사용된 객체에 값을 추가할 수 가 없다는 것이다.

    public <T> void genericMethod(wildcardGeneric<T> c, T addValue) {   
        c.setWildcard(addValue);
        T value = c.getWildcard();
        System.out.println(value);
    }
    
 /*  
     // 이렇게도 쓸 수 있다.
     public <T extends Car> void boundGenericMethod(wildcardGeneric<T> c, T addValue) {
     // 중간 생략
     }
 */
    
   

메소드 선언부를 잘 보면 리턴 타입 앞에<>로 제네릭 타입을 선언해 놓았다.  그리고, 매개변수에서는 그 제네릭 타입이 포함된 객체를 받아서 처리한 것을 볼 수 있다. 게다가 메소드의 첫 문장을 보면 setWildcard() 메소드를 통하여 값을 할당까지 했다. 이처럼 메소드 선언시 리턴 타입 앞에 제니릭한 타입을 선언해주고, 그 타입을 매개변수에서 사용하면 컴파일 할 때 전혀 문제가 없다. 게다가 값도 할당할 수 있다.

 

?를 사용하는 Wildcard 처럼 타입을 두리뭉실하게 하는 것보다는 이처럼 명시적으로 메소드 선언시 타입을 지정해주면 보다 견고한 코드를 작성할 수 있다.

 

그러면 "내가 만든 메소드는 제네릭 타입이 두 개인데, 이럴 땐 어떻게 하지?" 라고 추가 질문을 던질 수 있을 것이다. 그럴땐 제네릭 타입의 선언을 콤마로 구분하여 나열해주면 된다.

    public <S, T extends Car> void multiGenericMethod(wildcardGeneric<T> c, T addValue, S another) {
    // 중간생략
    {

 

 

 

============================================================================

 

 

자료형 매개 변수가 두 개 이상일 때

public class Point<T, V> { //<T, V> 하나는 Integer이고 하나는 Double, 다르게 쓸 수 있다.
    T x;
    V y;
    
    Point(T x, V y) {
        this.x = x;
        this.y = y;
    }
    
    // 제네릭 메서드
    public T getX() {
        return x;
    }
    public V getY() {
        return y;
    }
}

 

제네릭 메서드를 일반 클래스에서도 쓸 수 있다.

 

메서드 내에서의 자료형 매개 변수는 메서드 내에서만 유효함 (지역변수와 같은 개념)

class Shape<T> { // 여기에서 T는 멤버변수나 반환값으로 쓰일 수 있다. 
    public static <T, V> double makleRectangle(Point<T, V> p1, Point<T, V> p2) { // 지역 변수
        .....
    }
}

shape의 T와 makeRectangle의 T는 전혀 다른 의미

 

728x90
반응형
Comments