본문 바로가기
Java

[자바 스터디] #14 - 제네릭

by zannew 2021. 3. 10.

 목표

자바의 제네릭에 대해 학습하세요.

 

▣ 학습할 내용

  • 제네릭 사용법
  • 제네릭 주요 개념 (바운디드 타입, 와일드 카드)
  • 제네릭 메소드 만들기
  • Erasure

 

▶ 14-1 제네릭 사용법

▷ 제네릭스란?

제네릭스는 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스 컴파일 시 타입 체크를 해주는 기능이다. 객체 타입을 컴파일 시 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움을 줄일 수 있다.

타입 안정성을 높인다는 것은 의도하지 않은 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준다는 뜻이다. 

▷ 제네릭스의 장점

  • 타입 안정성을 제공한다.
  • 타입 체크와 형변환을 생략할 수 있으므로 코드가 간결해 진다.

▷ 제네릭 클래스 선언하기

제네릭 타입은 클래스와 메서드에 선언할 수 있다. 먼저 클래스에 선언하는 경우를 보면,

14-1-1) 일반적인 클래스 선언

위 그림과 같이 선언이 되어있다면, 이 GenericsTest클래스를 제네릭 클래스로 변경하려면 클래스 옆에 '<T>'를 붙이면 된다. 그리고 Object타입들을 'T'로 바꿔주면 된다.

14-1-2) 제네릭 클래스로 선언

 

그럼 도대체 'T'는 무엇을 의미할까..? Type의 첫 글자에서 따온 것이고, 타입 변수라고도 한다. 타입 변수는 'T'가 아닌 다른 것을 사용해도 된다. ArrayList의 경우 Element의 '<E>'를 사용하고, Map의 경우 Key와 Value의 '<K, V>'를 사용하기도 한다. 상황에 맞게 유추하기 쉬운 알파벳을 사용하면 될 것 같다. 

제네릭 클래스로 바꿔준 GenericsTest클래스의 객체를 생성하면,

public class GenericsMain {

    public static void main(String[] args) {
        
        GenericsTest<String> gt = new GenericsTest<>();
        //gt.setItem(new Object());
        gt.setItem("HELLO");
        //String item=(String)gt.getItem();
        String item=gt.getItem();
        System.out.println(item);	//"HELLO"가 출력된다.
    }
}

위 코드에서 주석처리한 부분이 제네릭 클래스를 활용하지 않고 Object로 객체를 생성하는 코드이다. (에러가 난다....) 제네릭 클래스의 객체를 생성하면서 타입은 String으로 정해주었고 setter를 바로 사용하고 출력까지 할 수 있다. 만약에 기존에 객체 생성하던 방법을 쓴다면..??

14-1-3) 제네릭 아닌 일반 객체 생성 방법

하이라이트된 부분에 마우스를 얹어보면 "Raw use of parameterized class 'GenericsTest' "라고 뜬다. 매개변수화된 클래스의 원시적 사용을 의미한다. 타입 안정성을 위해 제네릭 클래스는 객체 생성시 코드 작성에 주의해야할 것 같다...

 

위의 코드블럭에서 작성해둔 GenericsMain클래스 올바른 예시의 바이트코드를 실행해보면 Object가 사용됨을 알 수 있다.

14-1-4) GenericsMain 바이트코드

 

 

 

▶ 14-2 제네릭 주요 개념 (바운디드 타입, 와일드 카드)

 

▷ 바운디드 타입(bounded type : 제한된 타입)

아래와 같이 제네릭 타입에 'extends' 키워드를 사용하면 특정 타입의 자손 타입만 사용할 수 있게 제한할 수 있다.

public class BoundedTypeTest<T extends Test>{

	List<T> list = new ArrayList<>();
    ...
}

Test클래스를 상속받은 자손 클래스 타입들만 담을 수 있다는 조건을 둔 것이다.

 

TestMain<BoundedTypeTest> main = new TestMain<BoundedTypeTest>();
TestMain<AnotherTest> main = new TestMain<AnotherTest>();// 에러 : AnotherTest는 Test를 상속받지 않는다.

 

 BoundedTypeTest클래스는 Test를 상속받기 때문에 에러가 나지 않는다. 매개변수화된 타입의 자손 타입도 다형성에 의해 객체 생성이 가능해진 것이다. 타입 매개변수 'T'에 Object를 넣는다면 모든 타입의 객체를 저장할 수 있게 된다.

 만약 클래스가 아닌 인터페이스를 구현해야하는 조건이라면 제네릭의 경우 상속과 같은 키워드 'extends'를 사용한다. 만약 어떤 부모클래스를 상속받으면서 구현해야하는 인터페이스가 있다면..? '&'기호를 사용하여 예를 들면, 'extends ParentClass & Writable'로 연결하여 작성할 수 있다.

 

▷ 와일드 카드(Wild Card)

구분 표기 설명
Unbounded  <?> 제한이 없음. 모든 타입 가능.
<? extends Object>와 동일
Upper Bounded <? extends T> 상한 제한. T와 그 자손 타입만 가능
Lower Bounded <? super T> 하한 제한. T와 그 조상 타입만 가능

 

와일드 카드는 기호 '?'로 표현하는데 어떠한 타입도 올 수 있다. '<?>'만 쓸 경우는 Object타입과 다를 바 없기 때문에 'extends'와 'super' 키워드를 상요해서 상항과 하한을 제한할 수 있다.

※ 다른 제네릭 클래스와 달리 와일드 카드에는 '&'를 사용할 수 없다. ('<? extends A & B>'가 불가능하다.)

 

 

▶ 14-3 제네릭 메소드 만들기

 

▷ 제네릭 메서드 선언하기

메서드 선언부에 제네릭타입이 선언된 메서드를 제네릭 메서드라고 부른다. 제네릭 타입의 선언 위치는 리턴타입 바로 앞이다.

// Collections의 sort()예시
class GenericsTest<T>{

	static <T> void test(Object obj){
    	...
    }

}

 

GenericsTest클래스에 선언된 '<T>'와 test메서드에 선언된 '<T>'는 전혀 다른 타입이 될 수 있다. 꼭 같은 타입이 아닐 수 있다는 것!

※ 참고로 제네릭 메서드는 제네릭 클래스가 아닌 일반 클래스에서도 정의될 수 있다.

 

▷ 제네릭을 static메서드에 선언하면?

제네릭은 런타임이 아닌 컴파일 타임에 타입을 체크하기 때문에 인스턴스 생성시 new연산자를 쓰는 경우 heap영역에 객체가 할당되고 경우 결국 타입을 알지 못하게 된다. 하지만 static메서드는 인스턴스가 없어도 클래스로 바로 접근을 할 수 있기 때문에 제네릭을 사용할 수 있다. static변수의 경우는 값의 타입을 알아야 사용할 수 있기 때문에 불가능하다.

 

 

▶ 14-4 Erasure

 

제네릭 타입은 컴파일된 파일(*.class)에서는 정보를 찾을 수 없다. (Erasure: 소거된다.)제네릭이 도입되기 이전의 소스 코드와의 호환성을 유지하기 위해서 원시 타입을 사용해서 코드를 작성하는 것을 허용한다. 하지만 가능하면 원시 타입 사용은 지양하는 것이 좋다. 분명히 하위 호환성을 포기해야하는 때가 올 것이기 때문이다.

앞서 바이트코드를 확인했을 때 어떤 제네릭 타입을 사용해도 바이트코드에서는 Object를 확인할 수 있었다. 

14-4-1) 제네릭 타입 소거 테스트

아래 바이트코드를 보면,,

 

14-4-2) ErasureTest.class 바이트코드

 

필드에 선언된 List는 Object타입으로, main메서드에서 생성된 Erasure객체로 호출한 setList메서드가 ArrayList객체를 생성하고 있다. 소스 코드에 어떤 타입이 작성된다해도 런타임에는 Object로 바뀌어 읽히고 기존의 제네릭 타입 정보는 소거되는 것이다.

추가적으로 덧붙이자면 List등 컬렉션 클래스의 객체 생성시 primitive타입은 쓰이지 않는데 이 또한 primitive타입(int, double, float, long 등..)은 Object클래스의 자손이 아니기 때문이다. 따라서 우리가 이제껏 'List<int>'가 아닌 'List<Integer>'로 사용해온 것이다. (Integer wrapper클래스는 Object클래스의 자손이므로 int가 아닌 Integer를 사용해야된다.)

 

 

 

 

▷ Reference

- 자바의 정석

- sujl95.tistory.com/73

- blog.naver.com/hsm622/222251602836

 

백기선님의 자바 라이브 스터디 커리큘럼을 따라 공부하고 있습니다. 

잘못된 점이나 보충할 부분이 있으면 코멘트 남겨주세요

작은 조언이 저에겐 성장의 원동력이 됩니다 :-)

댓글