본문 바로가기

Kotlin

JVM 언어 의 공변

728x90

 

1. 공변이란?

 

제네릭에서는 3가지 공변 성질을 제공한다.

 

공변(Variance) : A가 B의 하위 타입일 때, T <A> 가 T<B>의 하위 타입이면 T가 공변의 성질을 가지고 있다고 말한다.

반공변(Contravariance) : A가 B의 하위 타입일 떄, T<B>가 T<A> 의 하위 타입이면 T가 반공변의 성질을 가지고 있다고 말한다.

무공변(Invariance) A가 B의 하위 타입일 때, T<A>와 T<B>간의 아무 관계도 없다면 무공변이라고 말한다.

 

이러한 공변, 반공변, 무공변으로 메소드의 인자로 들어오는 파라미터의 타입에 제한을 걸수 있다.

공변의 경우 Java에서는 extends, Kotlin에서는 out이라는 키워드를 사용한다.(Producer패턴 : 해당 타입을 생산하는 패턴에 사용)

반공변의 경우 Java에서는 super, Kotline에서는 in이라는 키워드를 사용한다.(Consumer패턴 : 파라미터로 읽어서 소비를 하는 패턴에 사용)

 

2. 그럼 공변을 대체 왜 쓰는가?

   처음에 공변이라는 것을 보고 이것을 대체 왜 쓰지 이런걸 왜 신경 써야하는지 생각했다. 실제로 변성으로 가능한 것들은 대부분 무공변으로 처리할 수 있다. 하지만 공변을 선언함으로써 얻는 이득은 크다. 간단히 요약하자면 내가 만든 인터페이스, 클래스, 메소드, 라이브러리를 다른 사람이 잘못 사용할 여지를 줄여줘서 원하는 의도대로 프로그래밍 할 수 있다. 아래의 간단한 예시로 설명하겠다.

 

 

간단하게 자바 클래스 구조를 그려봤다. 최상위에 Object 클래스가 있을거고 그 아래에 Number와 String

그리고 Number 아래에 Integer과 Double 클래스가 있다. 이 그림을 본 상태에서 아래 코드를 보자

 

Object A = new Object();
Object AA = new Number();
Object AAA = new String():
Object AAAA = new Integer();
/*하위 타입이므로 위는 성립한다.
그럼 아래의 공변관계는?
*/

List<Object> B = new ArrayList<Object>();
// List<Object> BB = new ArrayList<Integer>();
// List<Object> BBB = new ArrayList<Number>();
// List<Object> BBBB = new ArrayList<String>();

//List<Number> B = new ArrayList<Object>();
// List<Number> BB = new ArrayList<Integer>();
 List<Number> BBB = new ArrayList<Number>();
// List<Number> BBBB = new ArrayList<String>();

void doSomething(List <Object>){/**/}

   위의 그림을 보면 위의 코드 4줄은 당연히 성립한다. 아래의 코드에서 Java의 List는 무공변하므로 하위 타입관계와 상관없이 아래주석은 컴파일 에러가 나게 된다. (Infered type is ... type is ... 에러) 그러므로 doSomething과 같은 시그니처를 가진 함수에서 Object 의 어떠한 하위 타입을 파라미터로 사용하면 컴파일 에러가 난다. 오직 List <Object> 타입만 허용된다. 프로그래머의 의도는 Object로 다형성을 가지고 유연하게 프로그래밍하려고 했지만 의도와 다르게 List <Object> 만 받게 된것이다.(알다시피 Object 타입으로는 객체로써 할수 있는 게 거의 없다.) 사용자 입장에서도 마찬가지다. 이를 위해 쓰는것이 공변과 반공변이다. 공변은 upperBound로 그 객체의 하위 타입만 허용하는 것이고, 반공변은 lowerBound로 그 객체의 상위 타입만 허용하는 것이다. 이로인해 생기는 결과를 보자

 

 

List<? extends Number> foo3 = new ArrayList<Number>();  // Number "extends" Number (in this context)
List<? extends Number> foo3 = new ArrayList<Integer>(); // Integer extends Number
List<? extends Number> foo3 = new ArrayList<Double>();  // Double extends Number

우선 공변이다. extends(kotlin : out)를 사용하면 Number를 upperbound로 제한할수 있다. 이렇게 되면 아까와 같은 컴파일 에러가 사라진다. 그렇다고해서 List를 자유롭게 사용할수 있는 건 아니다. 이 제약으로 인해 foo3은 readOnly한 List가 되버린다. 각각의 예시로 살펴보자.

 

Read

  • 각각의 foo3에 들어갈수 있는 최상위 타입은 Number이다. 그러므로 Number, Integer, Double 모두 적어도 Number라는 타입을 가지고 있다. 그러므로 Number 타입을 읽을 수 있다.
  • 하지만 Integer나 Double 타입은 읽을수 없다. 왜냐면 foo3 이 각각 List<Double> , List<Integer>일수도 있기 때문이다.

Write

  • Write는 어떠한 경우에도 할수 없다. Integer, Double 은 read와 마찬가지로 각각 List<Double> , List<Integer>일수도 있기 때문에 안된다. Number인 경우에도 foo3이 List<Integer>를 가르킬수 있기 때문에 안 된다. (하위 타입인 Integer에 상위 타입인 Number을 저장하는 것이다.)

즉 선언만으로 우리는 메소드에 Number라는 upperBound 타입으로  read 할수 있게 제약을 걸었다. (프로그래머가 잘못 사용할 여지를 없앴다.)

 

반공변은 공변과 정반대다. 

List<? super Integer> foo3 = new ArrayList<Integer>();  // Integer is a "superclass" of Integer (in this context)
List<? super Integer> foo3 = new ArrayList<Number>();   // Number is a superclass of Integer
List<? super Integer> foo3 = new ArrayList<Object>();   // Object is a superclass of Integer

 

Read

  • read는 할수 없다. Integer의 경우 Number, Object일수 도있기 때문에 안된다. Number도 마찬가지. 반면, Object의 경우 제한적으로 가능하다. Object의 메소드만 사용가능한...)

Write

  • Write는 Integer만 가능하다. Integer, Number , Object 모두 Integer의 상위 타입이므로 Integer타입의 write를 받아들일수 있다. 반면 Number Object는 불가능하다. (ArrayList <Integer>를 가르킬수도 있기 때문)

반공변도 공변과 마찬가지로 메소드에 <? super Integer>를 붙임으로써 메소드에 제약이 생겼다. lowerBound 타입에 대한 write만 가능해졌다. 코틀린의 경우 삽입이 가능한 MutableList에 out를 설정할 경우 , add, addAll과 같은 write 메소드들이 모두 disable된다. 사용할 경우 인텔리제이 경고와 함께 컴파일 에러가 난다. 이렇게 공변 반공변으로 인해 무공변떄는 할수 없었던 일을 할수 있게 되었다.

 

3. 공변 시점

 

JVM 언어에서 공변을 적용하는 시점은 크게 선언 시점 변성과 사용시점 변성으로 나뉜다.

 

자바에서는 사용 시점 변성만 제공한다.

반면 코틀린에서는 선언 시점 변성과 사용시점 변성 모두를 제공한다.

public interface List<E> extends Collection<E> {
    // Query Operations

    /**
    ....
     */
   boolean addAll(int index, Collection<? extends E> c);
   boolean addAll(Collection<? extends E> c);
   }

java.util.List 인터페이스를 보면 선언부에 어떠한 제한도 없고, 다만 필요한 메소드마다 ? extends E 와 같이 파라미터에 제약을 건 것을 볼수 있다. 이 때문에 프로그래머는 클래스의 메소드마다 일일히 변성을 지정해줘야 되고 이는 잘못 사용할수 있는 여지를 남길수 있다는 단점을 가진다고 생각한다.

반면 코틀린에서는 선언 시점 변성과 사용시점 변성을 제공한다.

 

public interface List<out E> : Collection<E> {
/*
....
*/
    }

  코틀린의 List(읽기 전용 리스트)를 보면 선언부에 out E라고 명시를 해준것을 볼수 있다. 이로 인해 인터페이스 내의 메소드에서 타입을 producer로만 사용할 수 있다. 선언시점 변성으로 인터페이스나 클래스에 일관된 변성을 제공할수 있어 메소드에서 타입에 대한 예측을 하기가 더 쉽고 실수를 할 여지가 줄어든다. 또한 코틀린은 선언시점 변성외에 사용시점 변성 또한 제공하여 프로그래머에게 선택의 유연성을 준다.(무공변 클래스에 일부 함수만 공변, 반공변으로 함수를 만들수 있다.)

  코틀린에서 공변이나 반공변을 선언 시점에서 설정한 경우 코틀린 컴파일러는 타입 메소드에 제약을 건다. out (공변) 로 설정한 파라미터를 반환형이 아닌 함수 파라미터로 사용할 경우 에러를 내게 하고, in(반공변)로 설정한 파라미터를 함수 파라미터가 아닌 반환형으로 사용할 경우 컴파일 에러를 발생시킨다. 

 

stackoverflow.com/questions/4343202/difference-between-super-t-and-extends-t-in-java

 

Difference between and in Java

What is the difference between List and List ? I used to use List, but it does not allow me to add elements to it list.add(e), whereas the Li...

stackoverflow.com

 

 

'Kotlin' 카테고리의 다른 글

Kotlin 익명 클래스  (0) 2021.08.21
시스템  (0) 2021.06.13
by lazy vs lateinit  (0) 2021.03.10
코틀린 제네릭스  (0) 2021.02.19