[Java] 지네릭스(Generics)

Jake·2022년 3월 22일
0

Java

목록 보기
1/8

1. 지네릭스를 사용하는 이유

지네릭스의 장점은 크게 2가지입니다.

  • 타입 안정성 (타입 체크)
  • 형변환 생략
ArrayList<Integer> integerList = new ArrayList<>();
integerList.add(2); //OK
integerList.add("String") //Compile Error

이렇게 지네릭스는 객체의 타입을 미리 명시해주어 객체를 꺼낼 때 마다 타입체크 및 형변환을 하지 않아도 되게 해주고
동시에 원하지 않는 타입의 객체가 포함되는 것 또한 컴파일 단계에서 알려준다는 장점이 있습니다.

컴파일 에러는 런타임 에러, 논리 에러 보다 훠어어어얼씬 좋습니다.


2. 지네릭 클래스

다음과 같은 상황을 가정해보겠습니다.

Sns

public abstract class Sns {

    String content;
    String url;

    public Sns(String content, String url) {
        this.content = content;
        this.url = url;
    }
}

Facebook

public class Facebook extends Sns {

    int likes;

    public Facebook(String content, String url, int likes) {
        super(content, url);
        this.likes = likes;
    }
}

Instagram

public class Instagram extends Sns {

    String likes;

    public Instagram(String content, String url, String likes) {
        super(content, url);
        this.likes = likes;
    }
}

Twitter

public class Twitter extends Sns{

    int likes;

    public Twitter(String content, String url, int likes) {
        super(content, url);
        this.likes = likes;
    }
}
  • SNS 매체에 따라 likes와 comments를 int 타입으로 제공하지 않을 수도 있다는 상황을 가정하여 likes와 comments를 부모 클래스에서 선언하지 않았습니다.

Member

Member 클래스는 snsList 컬렉션을 갖는 지네릭 클래스입니다.

public class Member<T> {

    String name;
    ArrayList<T> snsList;

    public Member(String name) {
        this.name = name;
        snsList = new ArrayList<>();
    }
}

Note1. 지네릭 타입은 컴파일 후 사라진다

//before compile
Member<Facebook>
Member<Instagram>

//after compile: 원시 타입인 Member로 바뀐다
Member
Member
  • 지네릭 타입 변수 T는 말 그대로 변수입니다. 변수에 대입되는 값이 다르다고 해서 다른 함수가 되는 것이 아니듯,
    Member 또한 어떠한 지네릭 타입이 대입되어 호출되는지에 관계 없이 같은 Member 클래스입니다.

  • 다만 인스턴스별로 다르게 동작하게 하고, 이 때 형변환과 타입안정성을 컴파일 단계에서 지원하기 위해 지네릭스가 존재하는 것입니다.


Note 2. 따라서 지네릭은 컴파일 시점에 타입 T를 알아야 한다

컴파일 단계 이후 사라지기 때문에, 컴파일 시점에 타입 T를 알고 있어야 합니다. 이러한 특성 때문에 다음과 같은 기능이 제한됩니다.

  1. static 맴버에 타입 변수 T를 사용할 수 없다. T는 인스턴스 변수로 간주되기 때문.

  2. 지네릭 타입의 배열을 생성하는 것은 허용되지 않는다.


3. 지네릭 클래스의 사용

  • 객체 생성 단계에서는 제네릭 타입이 상속관계에 있더라도 컴파일 에러가 발생합니다.
    • 단 제네릭 클래스 타입이 상속관계에 있을 때 대입된 타입이 같은 것은 가능
      ex) VIPMember가 Member를 상속한다고 했을 때
      VIPMember<Facebook> member = new Member<Facebook>(); //Compile Error 발생하지 않음
  • fMember.snsList에 instagram 객체를 넣으려고 하면 컴파일 에러 발생
  • 또한 제한된 지네릭 클래스를 사용하면, Member를 선언할 때 특정 클래스의 자식만 담을 수 있는 제한을 설정할 수 있습니다.

    public class Member<T extends Sns> {
    
      String name;
      ArrayList<T> snsList;
    
      public Member(String name) {
          this.name = name;
          snsList = new ArrayList<>();
      }
    }


    Toy 클래스는 Sns를 상속하지 않기 때문에 컴파일 에러가 발생하는 것을 확인할 수 있습니다.


4. 와일드 카드

Member가 갖고 있는 snsList의 모든 content를 출력해주는 Printer 클래스를 가정하겠습니다.

class Printer {
    static void print(Member<T> member) {
        for(T item : member.snsList) {
            System.out.println(item.content);
        }
    }
}

이 때 Printer 클래스는 지네릭 클래스도 아닐 뿐더러, static에서는 타입 매개변수를 사용할 수 없기 때문에 컴파일 에러가 발생합니다.
오버로딩으로 문제를 해결하려고 해도, 지네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않기 때문에 마찬가지로 컴파일 에러가 발생합니다.

이 문제를 해결하기 위해 고안된 것이 와일드 카드입니다.

<? extends T> : 와일드 카드의 상한 제한. T와 그 자손들만 가능
<? super T> : 와일드 카드의 하한 제한. T와 그 조상들만 가능
<?> : 제한 없음. <? extends Object>와 동일

이를 위 문제상황에 대입하면 다음과 같이 만들 수 있습니다.


5. 지네릭 메서드

메서드에 선언된 지네릭 타입은 지역 변수를 선언한 것과 같습니다. 이때 메서드에 선언된 지네릭 매게변수는 메서드 내에서만 사용되기 때문에, static 메서드에서도 사용할 수 있습니다.

따라서 위의 Printer 클래스의 print()메서드는 다음과 같이 변경할 수 있습니다.

static 메서드에서 지네릭 매게변수를 사용했음에도 컴파일 에러가 발생하지 않았습니다!

Note 3. 클래스에 정의된 타입 타입변수 != 메서드에 정의된 타입 타입변수

Printer 클래스에 지네릭 타입변수 T를 넣어 지네릭 클래스로 만들어도, Printer 옆에 있는 T가 회색으로 표시된 것 보이시나요?
IntelliJ에서 회색으로 표시된 변수는 한 번도 사용되지 않았음을 의미합니다
즉, print 메서드의 지네릭 매개변수 T와 Printer 클래스의 지네릭 타입변수 T는 글자만 같을 뿐, 다른 타입변수입니다.

profile
Java/Spring Back-End Developer

0개의 댓글