제네릭스
애노테이션을 통해서는 라이브러리가 요구하는 의미를 클래스에서 부여하는 것이 가능하고시점에 컴파일러 내부 구조를 분석하는 것이 가능하다 ㅈㅈ
실체화한 타입 파라미터를 사용하면 인라인 함수 호출에서 타입인자로 쓰인 구체적인 타입을 실행한 시점에 알 수 있다 선언 지점 변성을 사용하면 기저 타입은 같지만 타입 인자가 다른 두 제네릭 타입 A, B가 있다고 가정할 때 타입 인자 A, B의 상위/하위 타입 관계에 따라 두 제네릭 타입의 상위/하위 타입 관계가 어떠한 관계인지 지정하는 것이 가능하다
제네릭 타입 파라미터
제네릭스를 사용하면 타입 파라미터를 받는 타입을 정의하는 것이 가능하다 제네릭 타입의 인스턴스를 만들기 위해서는 타입 파라미터를 구체적인 타입 인자로 치환해주어야 한다 자바에서오 동일하다 그냥 Map<K, V> 이러한 제네릭 타입을 가진 자료구조 타입이 있는데, Map<Int, String> 이런 식으로 구체적인 타입을 넘김으로써 타입을 인스턴스화할 수 있다
제네릭 함수를 사용할 때는 반드시 구체적 타입으로 타입 인자를 넘겨야만 사용하는 것이 가능하다 하지만 특정 리스트에 대한 함수를 사용하는 경우에는 컴파일러로 하여금 추론하게 할 수 있다
char 리스트인 letters에 대한 slice 함수를 사용하면서 첫 번째로 사용할 때는 타입을 명시해줌으로써 리턴타입을 명시해주었지만 두 번째로 해당 리스트의 한 번 추론해주었던 타입을 명시하는 순간 컴파일러는 slice함수의 타입을 Char라고 인식하고 작동하게 된다
제네릭 클래스의 선언은 자바에서 사용하는 것과 동일하게 <> 을 통해서 선언을 해주고 사용할 때는 특정 타입임을 명시해주면서 사용한다 제네릭 클래스를 확장하는 클래스를 정의할 때는 기반 타입의 제네릭 파라미터에 에 대한 타입 인자를 지정해주어야 함 - 하위 클래스도 제네릭이면 그 클래스의 구체적 타입을 주는 것도 가능하고 그냥 받은 파라미터로 받은 타입을 넘길 수도 있다
코틀린만의 특정 제네릭 특성에 대해서 보자
타입 파라미터 제약 타입 파라미터 제약이라은 클래스나 함수에 사용할 수 있는 타입 인자를 제한하는 기능이다 sum이라는 함수가 있을 때 -> Int,Double은 허용가능한 타입이고 String은 허용불가능한 타입으로 지정하고 허용불가능한 타입은 사용할 수 없게하는 것이 가능한 것이다 그 방법으로는 제네릭 타입을 선언하는데 있어서 허용하고 싶은 파라미터의 상한으로 지정하면 > 제네릭 타입을 인스턴스화할 때 사용하는 타입 인자를 반드시 그 상한 타입이거나 그 상한타입의 하위 타입이어야만 사용할 수 있다 그래서 위의 sum 예시에 적용하면 sum 제네릭 함수를 사용시 Int, Double 을 허용할 것이기 때문에 Number 타입을 상한타입으로 정해주면 가능하다 fun List.sum(): T 이렇게 사용하고 리턴하는 값인 T 같은 경우에는 그 상한 타입으로 값으로 취급하는 것도 가능하다는 점
이렇게 일차원적으로 넣어서 사용하는 값 말구 Compare함수와 같은 제네릭 함수를 생각해보자 compare 같은 경우에는 값을 2개 받아야하고, 그 2개의 값의 타입을 비교하고 그 타입을 비교하는 과정을 거치게 되는데, 그냥 아무런 타입이나 선언해두고 부등호(>,<) 이렇게 비교하는 걸 넣어두고 String 과 같은 타입을 넣어버리면 max을 비교할 수 없는 값 사이에 호출하면 컴파일 오류가 난다 파라미터에 2가지 이상의 타입 제약을 거는 것은 함수 내부에서 where T: Number, String 뭐 이렇게 타입 파라미터를 선언하는 곳에서 :을 통해서 선언해주는 것이 아닌 함수 내부에서 where 문을 통해서 특정 타입을 지정해준다
다음은 Nullable 이다 제네릭을 인스턴스화할 때 nullable 타입을 포함하는 어떤 타입으로 타입인자를 지정해도 타입 파라미터를 치환하는 것이 가능하다 만약에 아무런 상한을 정하지 않은 타입 파라미터는 결과적으로 Any? 을 상한으로 정한 파라미터와 같다 그래서 따로 ?으로 타입 파라미터를 지정해주지 않아도 기본적으로 Any? 으로부터 만들어지기 때문에 내부에서 해당 파라미터를 사용할때 nullable의 체크인 ?. 을 통해서 사용하는 것이 가능하다 그래서 만약에 non-nullable 타입을 제네릭 타입으로 받고 싶다면, T 와 같이 무명으로 타입 파라미터를 지정해주는 것이 아니라 Any 이 타입을 선언해주자 그러면 내부에서 non-nullable 방식으로 컴파일을 진행해준다 근데 any로 해두면 문제가 있는게 non-nullable 을 위해서 any를 설정한 것이지만, 모든 객체는 any로부터 파생되었기 떄문에 결국은 nullable 객체를 제네릭에 넣어주어도 들어간다는 것이고 그 들어간다는 의미는 즉 npe를 내뱉는다는 의미이다 그러니까 그냥 any가 아닌 그냥 non-nullable 타입을 사용해서 상한을 지정해주자
제네릭스 동작
JVM 에서 제네릭스는 타입 소거를 사용해서 구현되는데, 이건 실행 시점에 제네릭 클래스의 인스턴스에 타입 인자 정보가 들어있지 않다는 의미이다 코틀린에서도 그럼 결국 제네릭 클래스의 타입 인자 정보는 런타임시에는 지워진다는 의미인데, 예시로 2개의 List가 있다고 보자 List, List 이렇게 2개가 있는데 컴파일러는 두 리스트를 서로 다른 타입으로 인식하고 각각의 리스트에는 지정해준 타입의 객체만 채워질 수 있도록 보장해준다 근데 타입 소거로부터 발생하는 한계가 있는데, 타입인자를 따로 저장하지 않기 때문에 실행 시점에 타입 인자를 검사하는 것이 불가능하다 즉, is 검사와 같은 타입 인자로 지정한 타입을 검사할 수는 없다 실행 시점에는 List인지 아닌지에 대한 여부는 체크할 수 있지만, 그 List의 타입이 String 인지 Int 인지 이러한 여부는 알 수가 없다는 것이다 물론 이렇게 타입 소거에 의해서 메모리 사용량과 같은 이점을 볼 수 있다는 점도 있다 as, as? 에도 제네릭은 여전하게 사용하는 것이 가능하지만 기저 클래스는 같지만 타입 인자가 다른 타입으로 캐스팅해도 여전히 캐스팅이 가능하다는 점을 조심해주어야 한다 실행 시점에는 모르고 무조건 성공하기 때문에 컴파일러에서 unchecked cast으로 경고를 해주기 때문에 문제 없다고 생각해도 그정도는 고려할 수 있어야 한다 그냥 실행은 되어버리지만 만약에 잘못된 타입의 원소가 들어가면 ClassCastException이 발생 그래서 as 을 사용해서 바로 타입 캐스팅하는 것이 아니라 is 을 통해서 검사를 해주면 좋게 사용할 수 있다
제네릭 함수가 호출되어도 그 함수의 본문에서는 호출 시 쓰인 타입 인자를 알 수 없지만 이러한 제약을 피할 수 있는 경우가 있는데, 그 방법은 인라인이다 인라인 함수의 타입 파라미터는 실체화되기 때문에 실행 시점에 인라인 함수의 타입 인자를 알 수 있다 inline 이라는 키워드는 컴파일러는 그 함수를 호출한 식을 모두 함수 본문으로 변경해주는 키워드이다 함수가 람다를 인자로 사용하는 경우 그 함수를 인라인 함수로 만들면 람다 코드도 함께 인라이닝되고 그에 따라 무명 클래스와 객체가 생성되지 않아서 성능이 더 좋아질 수도 있다 인라인 키워드는 본문에 구현한 바이트 코드를 그 함수가 호출되는 모든 지점에 삽입하는 키워드이기 때문에 컴파일러는 실체화한 타입 인자를 사용해서 인라인 함수를 호출하는 각 부분의 타입을 실행시점에 보는 것이 가능하기 때문이다 인라인의 특징은 이전에도 인라인을 보면서 봤지만, 인라인 함수의 크기에 항상 주의를 해야한다는 점을 기억
실체화한 타입 파라미터로 클래스 참조 대신 java.lang.Class 타입 인자를 파라미터로 받는 api에 대한 코트린 어뎁터를 구축하는 경우에는 실체화한 타입 파라미털르 사용한다 JDK의 ServiceLoader는 어떤 추상 클래스나 인터페이스를 표현하는 java.lang.Class를 받아서 그 클래스나 인스턴스를 구현한 인스턴스를 반환한다 ServiceLoader으로 부터 서비스를 가져오는 방식은 이러하다 var service = ServiceLoader.load(Service::class.java) ::class.java 는 코틀린에서 java.lang.Class 참조를 사용하는 방법이다 근데 이걸 var service = loadService() 이렇게 줄여서도 쓸 수 있다 이렇게 말고도 클래스를 타입 인자로 지정하면 ::class.java 라고쓰는거보다 간단하게 아래와 같이 사용할 수 있다
실체화한 타입 파라미터를 사용할 수 있는 경우는 이러하다
타입 검사와 캐스팅(is, !is, as, as?)
코틀린 리플렉션(::class)
코클린 타입에 대응하는 java.lang.Class (::class.java)
다른 함수를 호출할 때 타입 인자로 사용
하지만 다음과 같은 일을 할 수 없다
타입 파라미터 클래스의 인스턴스 생성하기
타입 파라미터 클래스의 동반 객체 메소드 호출하기
실체화한 타입 파라미터를 요구하는 함수를 호출하면서 실체화하지 않은 타입 파라미터로 받은 타입을 타입 인자로 넘기기
클래스, 프로퍼티, 인라인 함수가 아닌 함수의 타입 파라미터를 reified로 지정하기
변성: 제네릭과 하위 타입
변성이라는 개념은 List 이나 List와 같이 기저 타입이 같고 (List), 타입인자가 다른 여러 타입이 서로 어떠한 관계가 있는지를 설명하는 개념이다 직접 제네릭 클래스나 함수를 정의하는 경우 변성을 이해하고 사용하는게 좋다고 하네요 변성의 이유는 인자를 함수에 넘길 수 있어서이다 List 을 파라미터로 받는 놈에다가 List 을 보냈을 떄 문제가 없이 안전하게 작동하는 것과 같다 근데 mutableListOf 같은걸로 해서 any로 선언해두고 String 을 집어넣으려고하면 컴파일러에서 일단은 막는걸로 보인다 리스트의 원소를 바꾸거나 넣거나 할때는 타입에 대해서 불일치가 생길 수 있기 때문에 Any 대신에 String 을 넘기는 것은 불가능하지만 그렇게 수정이나 추가가 없는 경우에는 그냥 상위 타입인 Any를 사용하는 것도 가능하다
여기서 타입과 클래스의 차이는 무엇인걸까? 제네릭이 아닌 경우에는 클래스 이름을 바로 타입으로 사용하는 것이 가능하다 var x: String 이렇게 사용하면 String 클래스의 인스턴스를 저장하는 변수를 저장한 것이다 근데 var x: String? 이렇게 클래스 이름을 널이 될 수 있는 타입에도 사용할 수 있다는 점을 기억하자 = 모든 코틀린 클래스가 적어도 둘 이상의 타입을 구성할 수 있다는 의미 제네릭에서 올바른 타입을 얻기 위해서는 제네릭 타입의 파입 파라미터를 구체적인 타입 인자로 바꿔주어야 한다 그렇게 다양한 클래스 타입을 만들다 보면 타입간의 관계를 고려해봐야 한다 타입 사이의 관계를 정의하기 위해서 하위 타입이라는 개념도 알아야하는데 개념은 이러하다 타입 A, B 가 있는데, A의 값에 필요한 모든 장소에 B을 넣을 수 있다면, 즉 A을 대신해서 B를 사용해도 문제가 없다면 그것을 보고 B가 A의 하위 타입이다 위 상황의 반대는 A같은 경우에는 상위 타입이다. 더 구체적인 예시로 들면서 Number로 짜여져있는 곳에다가 Number 대신 Int 을 넣어도 정상 동작을 하게 되는데 그래서 Number 가 상위타입, Int가 하위 타입 이렇게 생각하면 된다 이렇게 타입에 대한 다른 점을 확인 하는 이유는 컴파일러가 변수를 대입하거나 함수 인자를 전달하는 시점에 하위 타입 검사를 진행하기 때문이다 그래서 예를 들어서 함수의 파라미터로 Int 클래스를 받았는데, 내부에서 구현할 때 Number 변수에 넣어도 문제가 없다는 것이다 대신 String 같은 변수에 넣으면 문제가 생기지? 추가로 nullable 요것도 봐야 한다 코틀린에서 기본적으로 모든 타입은 non-nullable 이고, ? 을 붙힘을 통해서 nullable 하게 돌아간다 여기서도 하위 타입 개념이 들어가는데 non-nullable 이 상위타입, nullable이 하위타입이다 Int가 상위타입, Int? 이 하위 타입 이렇게 상위하위를 왜 따지냐 -> 제네릭에서는 아주 중요하다. 제네릭 타입을 인스턴스화할 때 타입 인자로 서로 다른 타입이 들어가면 인스턴스 타입 사이의 하위 타입 관계가 성립하지 않으면 그 제네릭 타입을 무공변이라고 한다
그럼 또 반대로 A가 B의 하위 타입이라면 List는 List의 하위 타입이게 되고 이러한 클래스나 인터페이스르 보고 공변적이라고 한다 그래서 공변적이라는 의미는 하위 타입 관계를 유지하는 것이다 예를 들어서 Animal 하위에 Cat 이 있다고 했을 때 Producer에 넣어서 사용한다고 가정하자 그럼 Producer은 Producer 의 하위 타입이게 된다 그리고 코틀린에서 제네릭 클래스가 타입 파라미터에 대해 공변적임을 표시하기 위해서는 타입 파라미터 이름 앞에 out 을 넣어주어야 한다
Last updated
Was this helpful?