programming language/Java

제네릭 이해하기

공대키메라 2022. 3. 26. 14:02

사실 제네릭은 어렴풋이 대~강 사용하는 법을 알고있지만,

 

무언가 내가 직접 어떤것을 만들고 적용하려면 그렇게 바로 할 수 있지는 않았다. 

 

이번에 제대로 제네릭에 대해서 이해하도록 하고, 실제로 어떻게 쓰이는지도 알아볼 것이다. 

 

참고 서적은 Java의 정석이다. 

 

 

 

1. Generic란?

 

다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크를 해주는 기능이다. 

=>  객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다. 

 

 

2. 제네릭 예시 1

 

Person.java => 일반 class

package com.example.generic;

public class Person {

    Object pocket;

    public Object getPocket() {
        return pocket;
    }

    public void setPocket(Object pocket) {
        this.pocket = pocket;
    }

}

 

Person 클래스를 선언했다. 

 

이것을 이번에는 제네릭 타입을 적용해서 선언해보겠다.

 

Person.java => 제네릭 이용

package com.example.generic;

public class Person<T> {

    T pocket;

    public T getPocket() {
        return pocket;
    }

    public void setPocket(T pocket) {
        this.pocket = pocket;
    }

}

 

Person<T>에서 T를 '타입 변수(type variable)'라고 하며, 'Type'의 첫글자 T를 따온 것이다. 

 

이와 관련된 관습적인 타입 변수가 더 있다.

 

E : Element
K : Key
V : Value

 

사실 이 기호의 종류만 다를 뿐 임의의 참조형 타입을 의미하는 것은 모두 같다. 그냥 기호로 무언가를 확인하기 편하도록 이렇게 여지를 제공하는 것이다. 

 

이것을 이번에는 선언해서 사용해보도록 하겠다.

 

package com.example.generic;

public class Person<T> {

    T pocket;

    public T getPocket() {
        return pocket;
    }

    public void setPocket(T pocket) {
        this.pocket = pocket;
    }

}

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

        // generic을 이용한 선언에서는 인스턴스 생성시 default 값이 object인 것 같다.
        Person person = new Person();
        person.setPocket("Test");
        Object pocket = person.getPocket();

        //원하는 타입을 지정할 수 있다.
        Person<String> person1 = new Person<String>();
        person1.setPocket("Test");
        String pocket1 = person1.getPocket();
        System.out.println("pocket1 = " + pocket1);
    }
}

 

3. 용어

 

public class Person<T> 의 용어에 대해 간단히 집고 가자.

 

  • Person<T> => 제네릭 클래스. T Person 혹은 .T의 Person이라고 한다.
  • T => 타입 변수 혹은 타입 매개변수
  • Person => 원시타입

 

4. 제네릭 제한

 

위에서는 기본적으로 제공하는 타입만을 사용했지만 우리가 선언한 타입도 선언해서 사용이 가능하다.

 

 Person<Orange> person = new Person<Orange>(); // Orange 객체만 사용 가능

 

그리고 모든 객체에 대해 동일하게 작동해야 하는 static 멤버에 타입 변수 T를 사용할 수 는 없다. 

 

 

에러로 static을 사용하지 말라는 문구를 확인할 수 있다.

 

또한, 제네릭은 배열 참조변수 선언은 가능하지만 배열을 생성하는 것은 안됀다. 

 

 

5. 제네릭 예시 2


Person.java

package com.example.generic;

import java.util.ArrayList;

public class Person<T> {
    ArrayList<T> list = new ArrayList<T>();

    void add(T item){
        list.add(item);
    }
    T get(int i){
        return list.get(i);
    }

    ArrayList<T> getList(){
        return list;
    }
    int size(){
        return list.size();
    }
    public String toString(){
        return list.toString();
    }

}

 

TestGeneric1.java

package com.example.generic;

class Family{
    public String toString(){
        return "Family";
    }
}
class Father extends Man{
    public String toString(){
        return "Father";
    }
}
class Boy {
    public String toString(){
        return "Boy";
    }
}

class Son extends Man{
    public String toString(){
        return "Son";
    }
}

public class TestGeneric1 {
    public static void main(String[] args) {
        Person<Family> familyBox = new Person<Family>();
        Person<Father> fatherBox = new Person<Father>();
        Person<Boy> boyBox = new Person<Boy>();
//        Person<Son> sonBox = new Person<Boy>(); // 타입 에러

        familyBox.add(new Father()); // 부모 클래스를 상속한 자식 클래스는 받을 수 있음.
        familyBox.add(new Son());

        fatherBox.add(new Father());
        fatherBox.add(new Father());
//        fatherBox.add(new Boy()); Father안에는 father만 들어올 수 있음.
        
        //manBox.add(new Boy()); // Man 클래스 혹은 그 자식들만 담을 수 있음.
        boyBox.add(new Boy());

        System.out.println("familyBox = " + familyBox);
        System.out.println("fatherBox = " + fatherBox);
        System.out.println("boyBox = " + boyBox);

    }
}

 

결과

familyBox = [Father, Son]
fatherBox = [Father, Father]
boyBox = [Boy]

 

주의점은 다운캐스팅은 되지 않는다는 것이다. 

 

부모 class에서 자식 class를 받을 수 는 있지만 이것을 다운 캐스팅해서 자식 클래스에 넣으면 오류가 난다. 

 

6. 제네릭 클래스 제한하기

 

사실 이 글을 정리하는 이유는 이제 정리할 내용을 제대로 이해하기 위한 목적이다. 

 

위에서 Person을 선언했고, 각 Person의 타입으로 우리가 Family, Father, Boy, Son을 선언했었다.

 

물론 선언된 타입만 사용할 수 있지만 한 번 더 거르로 싶다면 어떻게 할까?

 

예를 들어 Family의 범주에 속하지 않는 Boy는 넣기 싫다는 것이다.

 

이럴 때에 제네릭 타입에 extends를 사용하면 특정 타입의 자손만 대입할 수 있게 제한이 가능하다. 

 

한번 사용해보도록 하겠다. 

 

TestGenericAll.java

package com.example.generic;

import java.util.ArrayList;


class Family implements GoFishing {
    public String toString(){
        return "Family";
    }
}

interface GoFishing {}

class Mother extends Family{
    public String toString(){
        return "Mother";
    }
}

class Father extends Family {
    public String toString(){
        return "Father";
    }
}
class Boy {
    public String toString(){
        return "Boy";
    }
}

class Son extends Family {
    public String toString(){
        return "Son";
    }
}

public class TestGenericAll{
    public static void main(String[] args) {
        FamilyBox<Family> familyBox = new FamilyBox<Family>();
        FamilyBox<Mother> motherBox = new FamilyBox<Mother>();
//        FamilyBox<Family> fatherBox = new FamilyBox<Son>(); 타입 불일치 에러
        FamilyBox<Boy> boyBox = new FamilyBox<Boy>();
    }


}

class FamilyBox<T extends Family & GoFishing> extends Person<T> {

}

class Person<T> {
    ArrayList<T> list = new ArrayList<T>();

    void add(T item){
        list.add(item);
    }
    T get(int i){
        return list.get(i);
    }

    ArrayList<T> getList(){
        return list;
    }
    int size(){
        return list.size();
    }
    public String toString(){
        return list.toString();
    }

}

 

 

에러를 읽어보면 not within its bound 라고 적혀있다. 해당 바운더리에 속하지 않으니까 안된다는 것이다!

 

이것은 내가 선언한 FamilyBox 때문이다

 

class FamilyBox<T extends Family & GoFishing> extends Person<T> {}

 

여전히 한 타입만 담을 수 있지만, 이제는 Family 클래스의 자손들만 담을 수 있다. 모르는 Boy 안녕! 

 

7. 와일드 카드

이 표현이 참 재미있다. 와일드 카드... 무언가 필살기의 느낌이다.

 

와일드 카드는 어떤 컬렉션이든지 받아서 출력하는 메소드를 만들도록 도와준다.(오버로딩이 아닌 메소드 중복 정의)

 

그러면 이것을 표현하는 방법을 알아보자. 

 

상한과 하한, 그리고 둘다
  • <? extends T> : 와일드 카드의 상한 제한. T와 그 자손들만 가능
  • <? super T> : 와일크 카드의 하한 제한. T와 그 조상들만 가능
  • <?> : 제한 없음. 모든 타입이 가능. <? extends Object>와 동일


GenericTest2.java - extends

package com.example.generic;

import java.util.ArrayList;

class Family {
    public String toString(){
        return "Family";
    }
}

class Father extends Family {
    public String toString(){
        return "Father";
    }
}

class Son extends Family {
    public String toString() {
        return "Son";
    }
}

public class TestGeneric2 {
    public static void main(String[] args) {
        FamilyBox<Father> fatherFamilyBox = new FamilyBox<Father>();
        FamilyBox<Son> sonFamilyBox = new FamilyBox<Son>();


        fatherFamilyBox.add(new Father());
        sonFamilyBox.add(new Son());

        System.out.println(DoMovePlan.letGoMoveOut(fatherFamilyBox));
        System.out.println(DoMovePlan.letGoMoveOut(sonFamilyBox));
    }
}

class MovePlan{
    String plan;
    MovePlan(String plan){
        this.plan = plan;
    }
    public String toString(){
        return plan;
    }
}

class DoMovePlan{
    static MovePlan letGoMoveOut(FamilyBox<? extends Family> box){
        String test = "";

        for(Family member : box.getList()){
            test +=member + " ";
        }
        return new MovePlan(test);
    }
}

class FamilyBox<T extends Family> extends Person<T> { }

class Person<T> {
    ArrayList<T> list = new ArrayList<T>();

    void add(T item){
        list.add(item);
    }
    T get(int i){
        return list.get(i);
    }

    ArrayList<T> getList(){
        return list;
    }
    int size(){
        return list.size();
    }
    public String toString(){
        return list.toString();
    }
}

 

8. 제네릭 메서드

메서드의 선언부에 제네릭 타입이 선언된 메서드를 제네릭 메서드라고 한다. 

매개 타입과 리턴 타입으로 타입 파라미터를 갖는 메소드다.

 

TestGeneric3.java

package com.example.generic;

public class TestGeneric3 {

    public static void main(String[] args) {
        Box<Integer> box1 = Util.<Integer>boxing(100);// 구체적 타입 명시
        int intValue = box1.getT();

        Box<String> box2 = Util.boxing("암묵적 호출"); //암묵적 호출
        String stringValue = box2.getT(); // 

        System.out.println("intValue = " + intValue);
        System.out.println("stringValue = " + stringValue);
    }

}

class Box<T>{
    private T t;
    public T getT() {
        return t;
    }
    public void setT(T t){
        this.t = t;
    }
}

class Util{
    public static <T> Box<T> boxing(T t){
        Box<T> box = new Box<T>();
        box.setT(t);
        return box;
    }
}