2 분 소요

제네릭이란?

상황에 따라 다른 형의 데이터형을 받아야된느 변수가 있다. 이처럼 어떤 값이 하나의 참조 자료형이 아닌 여러 참조 자료형을 사용할 수 있도록 프로그래밍하는 것을 제네릭 프로그래밍 이라고 한다.

제네릭의 필요성

얘를 들어서 3D프린터의 재료는 다양할 수 있다. 하지만 그 재료에 따라서 매번 변수(재료)만 다른 코드를 작성하기에는 너무 비효율적이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class TreeDPrinter {
    private Powder material; //재료가 파우더형인 경우
 
    public void setMaterial(Powder material) {
        this.material = material;
    }
 
    public Powder getMaterial() {
        return material;
    }
 
}
 
public class TreeDPrinter {
    private Plastic material; //재료가 플라스틱형인 경우
 
    public void setMaterial(Plastic material) {
        this.material = material;
    }
 
    public Plastic getMaterial() {
        return material;
    }
 
}
cs

이때 필요한 것이 제네릭이다. 자료형 Object를 사용하면 달라지는 자료형에 따라서 처리를 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TreeDPrinter {
    private Object material; 
 
    public void setMaterial(Object material) {
        this.material = material;
    }
 
    public Object getMaterial() {
        return material;
    }
 
}
 
cs

하지만 Object형의 단점은 setMaterial메서드 같은 경우는 자동으로 형 변환이 되지만, getMaterial메서드 같은 경우는 직접 형 변환을 해주어야 된다.

1
2
3
4
5
6
ThreeDPrinter printer = new ThreeDPrinter();
 
Powder p1 = new Powder();
printer.setMaterial(p1); //자동으로 Powder형으로 형변환
 
Powder p2 = (Powder)printer.getMaterial(); //직접 형 변환이 필요
cs

결국 getMaterial 메서드 같은 경우는 Object형으로 리턴을 해주기 때문에 이 데이터형을 또 Powder형으로 바꾸어주어야 한다는 것이다. 하지만 이 점은 굉장히 번거롭다. 그래서 사용하는 것이 제네릭이라는 것이다.

다이아몬드 연산자 < >

1
2
ArrayList<String> list = new ArrayList<> ();
 
cs

클래스를 사용할 때, 이런 각괄호를 많이 봤다. 그리고 클래스에 데이터형을 넣어주었으면 뒤에 인스턴스에는 넣어주지 않아도 잘 작동했던 경험도 있다. 근데 이게 왜 정확히 그러는지 몰랐는데, 알고 봤더니 선언된 자료형을 보고 생략된 부분이 String임을 컴파일러가 유추할 수 있었기 때문이었다.

자료형 매개변수 T와 static

앞에서 사용자 정의 메서드를 만들때, public static ~ 이렇게 썼던 경우가 많다. static 변수나 메서드는 인스턴스를 생성하지 않아도 클래스 이름으로 호출할 수 있다. static변수는 인스턴스 변수가 생성되기 이전에 생성되기 때문이다. 그래서 static 메서드에서는 인스턴스 변수를 사용할 수 없다. static이 먼저 생성되기 때문에 static이 생성될 때에는 아직 인스턴스가 없어서 코드가 작동할 수가 없다. 같은 원리로 자료형 매개변수 T는 static보다 먼저 생성된다. 그래서 static 변수나 메서드의 자료형을 T를 사용할 수 없다. T가 생성될 때, 아직 static은 없는 존재기 때문이다. 그래서 생성 순서를 보면 T -> static -> 인스턴스 이렇게 되므로 코드를 작성할 때, 먼저 생성되는 변수 뒤에 나중에 생성되는 자료형, 인스턴스를 사용하지 않도록 주의해야 된다.

제네릭 클래스 사용하기

1
2
3
4
5
6
7
8
9
10
//Powder.java
public class Powder {
    public void doPrinting() {
        System.out.println("Powder 재료로 출력합니다.");
    }
    public String toString() {
        return "재료는 Powder입니다.";
    }
}
 
cs
1
2
3
4
5
6
7
8
9
10
11
//Plastic.java
public class Plastic {
    public void doPrinting() {
        System.out.println("Plastic 재료로 출력합니다.");
    }
    public String toString() {
        return "재료는 Plastic입니다.";
    }
    
}
 
cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//GenericPrinter.java
public class GenericPrinter<T> { //제네릭 자료형 사용하기
    private T material;
 
    public void setMaterail(T material) {
        this.material = material;
    }
 
    public T getMaterial() {
        return material;
    }
 
    public String toString() {
        return material.toString();
    }
}
 
cs

다이아몬드 연산자 사이에 T를 넣어줌으로써 만약 코드에서 T자리에 Powder이 들어간다면 위 코드에 있는 T를 모두 Powder로 바꾸어 컴파일 한다. 오호!

추상 클랙스를 상속받은 제네릭 데이터형 / 제네릭 데이터형 활용

먼저 추상 클래스 Material.java를 만들어 줍니다.

1
2
3
4
5
//Material.java
public abstract class Material {
    public abstract void doPrinting();
}
 
cs

그런다음 GenericPrinter.java를 수정해줍니다. 왜냐하면 추상클래스 자체가 메서드만 선언되어있지 코드가 작성되지 않았으므로 무슨 역할을 하는 메서드인지를 알려주어야 하기 때문입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//GenericPrinter.java Material상속받음
public class GenericPrinter<extends Material> { //제네릭 자료형 사용하기 + Material 상속
    private T material;
 
    public void setMaterial(T material) {
        this.material = material;
    }
 
    public T getMaterial() {
        return material;
    }
 
    public String toString() {
        return material.toString();
    }
 
    public void printing() {
        material.doPrinting(); //상위 클래스 Material의 메서드 호출 / 오버라이딩
    }
}
 
cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//GenericPriner.java
public class GenericPrinterTest2 {
    public static void main(String[] args) {
        GenericPrinter<Powder> powderPrinter = new GenericPrinter<Powder>();
 
        powderPrinter.setMaterial(new Powder());
        powderPrinter.printing();
 
        GenericPrinter<Plastic> plasticPrinter = new GenericPrinter<Plastic>();
 
        plasticPrinter.setMaterial(new Plastic());
        plasticPrinter.printing();
    }
}
 
cs

제네릭 메서드 활용하기

제네릭 메서드와 제네릭 클래스는 엄청난 차이가 있다. 위에서 제네릭 타입인 T를 보면 클래스 내에서는 전역 변수로 사용될 수 있다. 하지만 메서드 같은 경우는 메서드에서만 사용할 수 있으므로 지역 변수 형태를 띄고 있다. 제네릭 메서드를 선언하는 방법은 다음과 같다.

**public <자료형 매개변수=""> 반환형 메서드 이름(자료형 매개변스 ...) {}**

public static <T, V> double makeRectangle(Point<T, V> p1, Point<T, V> p2) {}

이렇게 선언을 하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//Point.java
public class Point<T, V> {
    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;
    }
}
 
cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//GenericMethod.java
public class GenericMethod {
    public static <T, V> double makeRectangle(Point<T, V> p1, Point<T, V> p2) {
        double left = ((Number)p1.getX()).doubleValue();
        double right = ((Number)p2.getX()).doubleValue();
        double top = ((Number)p1.getY()).doubleValue();
        double bottom = ((Number)p2.getX()).doubleValue();
 
        double width = right - left;
        double height = bottom - top;
 
        return width * height;
    }
 
    public static void main(String[] args) {
        Point<Integer, Double> p1 = new Point<>(00.0);
        Point<Integer, Double> p2 = new Point<>(1010.0);
 
        double rect = GenericMethod.<Integer, Double>makeRectangle(p1, p2);
        System.out.println(rect);
    }
}
 
cs

코드를 보면, 제네릭 메서드를 호출할 때 역시 반환형 앞에 <Integer, Double>과 같은 데이터형을 알려주는 다이아몬드 연산자가 필요하다. 하지만 JDK 1.7 이상부터는 <>를 생략해도 된다고 한다.

태그:

카테고리:

업데이트:

댓글남기기