Java/라이브스터디

제네릭

제네릭?

 

제네릭, 어노테이션, 스트림, 람다.. 

자바를 접하게되면서 "이게 뭐지?" 라는 생각이 들게만든 키워드이다. 

 

어노테이션은 기능을 가진 주석.

람다는 함수형 인터페이스를 간편하게 표현.

스트림은 작업의 흐름을 표현하는 방법. 

 

자바 공부를 하게되면서 각각의 키워드를 나름대로 위와 같이 생각하게 되었다.

 

그렇다면 제네릭은 무엇일까?

 

제네릭은 클래스나 인터페이스 또는 메소드 정의 시에 이 자리에 사용될 타입을 호출이나 선언 시 지정해라. 를 표현하는 방법이다.

 

여기서 이런 종류에 프리미티브 타입은 올 수 없다.

 

 

제네릭이 사용된 List인터페이스를 보면 <E> 와 같이 표현된 부분을 찾을 수 있으며, 이 부분이 바로 제네릭이다. 

이는 List를 선언할 때 E타입의 값을 가지도록 지정할 수 있다는 의미를 가진다고 볼 수 있다.

 

여기서 이해가지 않는 부분은 <E> 가 뭘 의미하는가? 였다.

 

이는 자바 튜토리얼에 의하면 Type Parameter라고 불리우며, 단일 대문자로 표현된다.

 

  • E - Element ( Collection 에서 주로 사용 )
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S, U, V 등등 - 2번째, 3번째 4번째 타입

일반적으로 사용되는 타입파라미터 이름들은 위와 같다.

 

각각의 이름들은 특정 타입이 와야함을 강제한다기 보다는 이 이름은 이러한 의미로 쓰세요. 라는 성격이 더 강한 것 같다.

 

 

public static class TypeParam<K, V>{

    K key;

    V value;

    TypeParam(K key, V value){
        this.key = key;
        this.value = value;
    }

    public K getKey(){
        return key;
    }

    public V getValue(){
        return value;
    }
}

 

예를 들어 이런 제네릭 타입의 클래스가 있다고 가정해보자.

 

 

TypeParam<String, Integer> StringInteger = new TypeParam<>("Willow",10);
TypeParam<Integer, Long> IntegerLong = new TypeParam<>(10,20L);

StringInteger에게 있어서 key값, 즉 K는 String타입을 의미하고 V는 Integer타입을 의미하는 반면에

IntegerLong에게 있어서 K는 Integer타입을 의미하고 V는 Long타입을 의미하게 된다.

 

상황에 따라서 Key값이 클래스타입이 되거나 인터페이스 타입이 될 수도 있는 것이다.

 

 

 

추가적으로 제네릭이 동작하는 방식을 확인해 봤다.

더보기
public static void main(String[] args) {
    List<String> names = new ArrayList<>();
    names.add("Willow");
    names.add("Woody");

    for (String name : names) {
        System.out.println(name);
    }


    List<Integer> nums = new ArrayList<>();
    nums.add(10);
    nums.add(20);

    for (Integer num : nums) {
        System.out.println(num);
    }
}

이 코드는 바이트코드로 변환되면 어떻게 변할까?

 

 

public static void main(String[] args) {
    List<String> names = new ArrayList();
    names.add("Willow");
    names.add("Woody");
    Iterator var2 = names.iterator();

    while(var2.hasNext()) {
        String name = (String)var2.next();
        System.out.println(name);
    }

    List<Integer> nums = new ArrayList();
    nums.add(10);
    nums.add(20);
    Iterator var6 = nums.iterator();

    while(var6.hasNext()) {
        Integer num = (Integer)var6.next();
        System.out.println(num);
    }

}

내부적으로 타입캐스팅이 일어나는 것을 확인할 수 있다.

 

즉, 제네릭을 사용하지 않은 아래의 코드와 동일하게 동작한다.

public static void main(String[] args) {
    List names = new ArrayList();
    names.add("Willow");
    names.add("Woody");

    for (Object name : names) {
        System.out.println((String)name);
    }


    List nums = new ArrayList();
    nums.add(10);
    nums.add(20);

    for (Object num : nums) {
        System.out.println((Integer)num);
    }
}

 

 

바운디드 타입 파라미터

 

앞서서 제네릭은 특정 타입을 강제하지 않으며, 이 이름은 이런 뜻으로 쓰세요. 라고 권장하는 것이었다.

바운디드 타입 파라미터란, 이 제네릭 타입의 범위를 제한하는 기술이라고 볼 수 있다.

 

public class GENERIC {

    public static <V> void show(V value){
        System.out.println(value);
    }

    public static void main(String[] args) {
        show(10);
        show("ddings");
    }
}

제네릭은 기본적으로는 프리미티브 타입을 제외한 어떠한 타입이라도 올 수 있다.

위 코드에서도 Integer값과 String값 모두를 정상적으로 출력하는 모습을 보인다.

 

이를 특정 클래스의 인스턴스 또는 그 하위클래스만 받아들이게 만들 수 있다.

 

 

 

이렇게 extends를 통하여 제네릭 타입이 특정 클래스를 상속하는 모양새로 바꾸어주면 해당 클래스와 관련없는 타입의 값은 받아들일 수 없게 만들 수 있다.

 

 

public class GENERIC {

    static class A{

    }

    interface B{

    }

    interface C{

    }

    static class D{

    }

    public static <V extends A & B & C> void show(V value){
        System.out.println(value);
    }

    public static void main(String[] args) {
       
    }
}

이러한 바운드는 여러개를 줄 수 있으며, 맨 처음에는 클래스나 인터페이스가 올 수 있고 그 후에는 인터페이스를 &로 구분하여 줄 수 있다.

 

 

 

 

와일드카드

 

제네릭 코드에서 와일드카드는 ? 를 의미합니다.

알 수 없는 타입을 의미하며 어떤 타입이 와야하는지 명확하지 않을 때 사용할 수 있습니다.

 

와일드카드엔 Upper Bounded , Unbounded, Lower Bounded 가 존재합니다.

 

Upper Bounded : 지정 타입의 하위타입만 사용가능하도록 제한하는 것을 의미,  extends 키워드를 사용합니다.

UnBounded : 타입의 제한을 주지 않은 경우. 어떠한 타입이 와도 상관없을 때 사용

Lower Bounded : 특정 타입의 상위타입만 허용하는 것, super 키워드를 사용

 

 

자바 튜토리얼에서는 Upper Bounded와 Lower Bounded 중 어떤걸 사용해야 하는지 혼란이 생길 때의 가이드라인을 제공합니다.

 

  1. 데이터를 제공받는 변수의 경우 upper bounded wildcard의 사용을 권장
  2. 데이터를 다른 곳에 사용하기 위해 보관하는 경우 lower bounded wildcard의 사용을 권장
  3. Object 클래스에 정의된 메소드로 데이터를 제공받는 변수에 접근할 경우 Unbounded wildcard의 사용을 권장
  4. 1번과 3번의 경우에 모두 해당하는 경우 와일드카드를 사용하지 않는 것을 권장
  5. 리턴타입으로의 와일드카드 사용은 권장하지 않음.

 

자바 튜토리얼

 

Guidelines for Wildcard Use (The Java™ Tutorials > Learning the Java Language > Generics (Updated))

The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Java Language Changes for a summary of updated

docs.oracle.com

 

 

 

와일드카드 capture와 헬퍼 메소드

 

와일드카드 capture 오류는 컴파일단계에서 컴파일러가 코드가 정상적인지 검증할 수 없다는 것을 의미합니다.

 

위 이미지의 코드에서 EvenNumber는 NaturalNumber을 상속하고있는 상태입니다.

 

le는 EvenNumber타입의 리스트를 의미하고 EvenNumber는 NaturalNumber를 상속하고 있기 때문에 

ln = le 명령까지는 컴파일단계에서 아무런 문제가 없습니다.

 

이후 add명령에서 에러가 발생하는데 해당 메소드를 확인해보면 다음과 같이 선언되어 있습니다.

 

 

여기서 E는 ln변수이기 때문에 ? extends NaturalNumber 를 의미합니다.

사람이 생각하기엔 EvenNumber가 NaturalNumber를 상속하도록 코드를 작성했으니 아무런 문제가 없습니다.

 

그럼 컴파일러 입장에서는 어떨까요?

 

ln의 경우 NatrualNumber를 포함한 하위타입들이 전부 가능합니다. 

그 말은 상황에 따라 List<NaturalNumber>도 될 수 있고 List<EvenNumber>도 될 수 있다는 의미가 됩니다.

 

이러한 상황에서 add메소드에 EvenNumber타입의 객체나 NaturalNumber타입의 객체를 주게된다면, 그 상황에서 ln의 E는 EvenNumber인지 NatrualNumber인지 확실하게 알 수가 없습니다.

 

이럴때 발생하는 에러가 capture에러 입니다.

 

 

 

이 에러를 해결할 수 있는 방법중 하나가 헬퍼 메소드를 사용하는 방법으로 대리자를 세우는 느낌과 비슷합니다.

위와 같이 타입을 특정짓게 만들 수 있는 메소드를 만들어서 코드를 작성하는 방법이 있습니다.

 

 

 

 

제네릭 메소드

제네릭 메소드는 제네릭 타입의 파라미터를 사용하는 메소드를 의미합니다.

메소드의 선언부에 사용될 제네릭 이름을 명시하고 파라미터 선언부에서 제네릭 타입의 변수를 지정하는 것을 의미합니다.

 

말로만 들으면 어려울 수 있으니 예시 코드를 살펴봐야겠죠?

 

코드자체는 매우 이상한 코드에 속하지만 이런 식으로 작성하는 것 또한 가능은 합니다.

 

이렇게 접근지정자와 리턴 타입 사이에 사용될 제네릭 타입의 이름을 기재하고 파라미터에서 해당 타입들을 사용하는 메소드를 제네릭 메소드라고 부릅니다.

 

이때 사용되는 제네릭 타입은 따로 Bounded를 지정해주지 않는 한 Object를 상속하는 모든 클래스가 올 수 있습니다.

 

 

 

 

 

타입 Erasure

 

제네릭 타입은 코드의 실행 시 해석이 되며 해당 과정을 타입 Erasure라고 부릅니다.

 

제네릭 타입 ( 클래스나 인터페이스 )과 메소드 모두 바운드가 지정되어있으면 해당 바운드, 그렇지 않으면 Object로 인식된다고 합니다.

 

public class GENERIC<T> {
    T data;

    public GENERIC(T data) {
        this.data = data;
    }

    public void setData(T data){
        System.out.println("SUPER's setData");
        this.data = data;
    }
}

이 코드의 경우 바운드가 지정되어있지 않으므로 Object로 인식하게 되어 아래 코드와 같이 변한다고 생각할 수 있습니다.

 

 

public class GENERIC {
    Object data;

    public GENERIC(Object data) {
        this.data = data;
    }

    public void setData(Object data){
        System.out.println("SUPER's setData");
        this.data = data;
    }
}

 

 

 

 

그렇다면 이런 코드는 어떻게 될까요?

 

package me.ddings.java8to11;

public class GENERIC<T> {
    T data;

    public GENERIC(T data) {
        this.data = data;
    }

    public void setData(T data){
        System.out.println("SUPER's setData");
        this.data = data;
    }
}

 

package me.ddings.java8to11;

public class Sub_Generic extends GENERIC<Integer>{

    public Sub_Generic(Integer data) {
        super(data);
    }

    public void setData(Integer data){
        System.out.println("Sub's setData");
        super.setData(data);
    }

    public static void main(String[] args) {
        Sub_Generic sg = new Sub_Generic(10);
        GENERIC g = sg;
        g.setData("Hi");
        System.out.println(sg.data);
    }
}

위 코드는 ClassCastException이 발생합니다.

 

그 이유는 sg의 선언과 동시에 GENERIC클래스의 T타입이 Integer로 변환되었기 때문입니다.

이미 Integer 타입의 data에 String값을 넣으려고 하니 에러가 발생하는 것이죠.

 

에러의 원인 이전에 Sub_Generic 클래스의 setData메소드는 어떻게 오버라이딩이 성립된걸까요? 

 

GENERIC 클래스의 setData는 바운드가 없기 때문에 파라미터의 타입이 Object이지만 Sub_Generic의 setData의 파라미터타입은 Integer입니다.

 

이럴떄 컴파일러가 생성해주는 메소드가 존재하며 이를 브릿지메소드라고 부릅니다.

 

해당 메소드에 대한 설명은 자바튜토리얼의 예시코드를 확인하면 이해할 수 있습니다.

 

 

 

 

 

 

 

 

 

 

 

 

출처 

docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html