컴파일러는 두 리스트를 서로 다른 타입으로 인식하지만 실행 시점에 그 둘은 완전히 같은 타입의 객체
타입 파라미터가 2개 이상이라면 모든 타입 파라미터에 *를 포함
@Testfun`실행 시점의 제네릭`() {funprintSum(c: Collection<*>): Int {val intList = c as? List<Int> ?: throwIllegalArgumentException("List is expected")return intList.sum() }val actual =listOf(1, 2, 3)assertEquals(6, printSum(actual))// 실행 시점에는 제네릭 타입의 타입 인자를 알 수 없으므로 캐스팅은 항상 성공assertThrows<IllegalArgumentException> {printSum(setOf(1, 2, 3)) }// 잘못된 타입의 원소가 들어있는 리스트를 전달하면 실행 시점에 ClassCaseException 발생assertThrows<ClassCastException> {printSum(listOf('a', 'b', 'c')) }}
코틀린 컴파일러는 컴파일 시점에 타입 정보가 주어진 경우에는 is 검사를 수행하게 허용
funprintSum(c: Collection<Int>): Int {if (c is List<Int>) {return c.sum() }throwIllegalArgumentException("is not list")}assertEquals(6, printSum(listOf(1, 2, 3)))assertThrows<IllegalArgumentException> {printSum(setOf(1, 2, 3))}
실체화한 타입 파라미터의 제약
아래의 경우 실체화한 타입 파라미터 사용 가능
타입 검사와 캐스팅(is, !is, as, as?)
코틀린 리플렉션 API(::class) → 10장에서 설명
코틀린 타입에 대응하는 java.lang.Class를 얻기(::class.java)
다른 함수를 호출할 때 타입 인자로 사용
하지만 아래와 같은 일은 할 수 없음
타입 파라미터 클래스의 인스턴스 생성하기
타입 파라미터 클래스의 동반 객체 메소드 호출하기
실체화한 타입 파라미터를 요구하는 함수를 호출하면서 실체화하지 않은 타입 파라미터로 받은 타입을 타입 인자로 넘기기
클래스, 프로퍼티, 인라인 함수가 아닌 함수의 타입 파라미터를 refied로 지정하기
변성: 제네릭과 하위 타입
List<String>와 List<Any>와 같이 기저 타입이 같고 타입 인자가 다른 여러 타입이
서로 어떤 관계가 있는지 설명하는 개념
✅ 변성을 잘 활용하면 사용에 불편하지 않으면서 타입 안전성을 보장하는 API를 만들 수 있다.
변성이 있는 이유: 인자를 함수에 넘기기
List<Any> 타입의 파라미터를 받는 함수에 List<String>을 넘기면 안전할까❓
String 클래스는 Any를 확장하므로, Any 타입 값을 파라미터로 받는 함수에 String 값을 넘겨도 안전
하지만 Any와 String이 List 인터페이스의 타입 인자로 들어가는 경우 자신 있게 안전성을 말할 수 없음
val strings =mutableListOf(1, 2.0, "abc", "bac")strings.add("asbc")println(strings.maxBy { it.length }) // Type mismatch 에러
공변성: 하위 타입 관계를 유지
A가 B의 하위 타입일 때 Producer<A>가 Producer<B>의 하위 타입이면 Peoducer는 공변적
이를 하위 타입 관계가 유지된다고 설명
예를 들어 Cat가 Animal의 하위 타입이기 때문에 Producer<Cat>은 Producer<Animal>의 하위 타입
코틀린에서 제네릭 클래스가 타입 파라미터에 대해 공변적임을 표시하려면 타입 파라미터 이름 앞에 out을 명시
interfaceProducer<outT> { // 클래스가 T에 대해 공변적이라고 선언funproduce(): T}
클래스의 타입 파라미터를 공변적으로 만들면 함수 정의에 사용한 파라미터 타입과 타입 인자의 타입이 정확히 일치하지 않더라도 그 클래스의 인스턴스를 함수 인자나 반환값으로 사용할 수 있다.
openclassAnimal {funfeed() { ... }}// T 타입 파라미터에 대해 아무 변성도 지정하지 않았기 때문에(무공변성)// 고양이 무리는 동물 무리의 하위 클래스가 아니다.classHerd<T : Animal> {val size: Intget() =...operatorfunget(i: Int): T { ... }}// 고양이 무리를 넘기면 타입 불일치(type mismatch) 오류 발생funfeedAll(animals: Herd<Animal>) {for (i in0 until animals.size) { animals[i].feed() }}classCat : Animal() { funcleanLitter() { ... }}funtakeCareOfCats(cats: Herd<Cat>) {for (i in0 until cats.size) { cats[i].cleanLitter()// feedAll(cats) // type mismatch }}---// TOBE// Herd를 공변적인 클래스로 만들고classHerd<outT : Animal> { ...}// 호출 코드를 적절히 변경funtakeCareOfCats(cats: Herd<Cat>) {for (i in0 until cats.size) { cats[i].cleanLitter() }feedAll(cats) }
클래스 멤버를 선언할 때 타입 파라미터를 사용할 수 있는 지점은 모두 인(in)과 아웃(out)위치로 나뉜다.
MutableList<Any?>는 모든 타입의 원소를 담을 수 있다는 사실을 알 수 있는 리스트
반면 MutableList<>는 어떤 정해진 구체적인 타입의 원소만을 담는 리스트지만 그 원소의 타입을 정확히 모른다는 사실을 표현
@Testfun`타입 인자 대신 * 사용`() {val anyList: MutableList<Any?> =mutableListOf('a', 1, "qwe") anyList.add(42) // Any 타입의 원소 추가 가능assertEquals<MutableList<Any?>>(mutableListOf('a', 1, "qwe", 42), anyList)val chars =mutableListOf('a', 'b', 'c')// 어떤 구체적인 타입의 원소를 담는 리스트이지만 그 타입을 모름val unknownElements: MutableList<*> = chars // unknownElements.add(42) // 컴파일러는 이 메소드 호출을 금지(The integer literal does not conform to the expected type Nothing)assertEquals('a', unknownElements.first()) // 원소를 가져오는 것은 안전}
타입 파라미터를 시그니처에서 전혀 언급하지 않거나 데이터를 읽기는 하지만 그 타입에는 관심이 없는 경우와 같이 타입 인자 정보가 중요하지 않을 때도 스타 프로젝션 구문을 사용
funprintFirst(list: List<*>): Any? { // 모든 리스트를 인자로if (list.isNotEmpty()) { // isNotEmpty()에서는 제네릭 타입 파라미터를 사용하지 않음return list.first() // first()는 Any?를 반환하지만 여기서는 그 타입만으로 충분 }throwIllegalArgumentException("list is empty")}assertEquals("Svetlana", printFirst(listOf("Svetlana", "Dmitry")))
애노테이션 선언과 적용
애노테이션 적용
애노테이션의 인자로는 아래 항목들이 들어갈 수 있다.
원시 타입의 값
문자열
enum
클래스 참조
다른 애노테이션 클래스
그리고 지금까지 말한 요소들로 이뤄진 배열
애노테이션 인자를 지정하는 문법은 자바와 약간 다르다.
클래스를 애노테이션 인자로 지정할 때는 @MyAnnotation(MyClass::class)처럼 ::class를 클래스 이름 뒤에 뒤에 넣어야 한다.
다른 애노테이션을 인자로 지정할 때는 인자로 들어가는 애노테이션의 이름 앞에 @를 넣지 않아야 한다.
배열을 인자로 지정하려면 @RequestMapping(path = arrayOf("/foo", "/bar"))처럼 arrayOf 함수를 사용한다.
자바에서 선언한 애노테이션 클래스를 사용한다면 value라는 이름의 파라미터가 필요에 따라 자동으로 가변 길이 인자로 변환된다.
따라서 그런 경우에는 @JavaAnnotationWithArrayValue("abc", "foo", "bar")처럼 arrayOf 함수를 쓰지 않아도 된다.
privatefunStringBuilder.serializeObject(obj: Any) {val kClass = obj.javaClass.kotlin // 객체의 KClass를 얻는다. val properties = kClass.memberProperties // 클래스의 모든 프로퍼티를 얻는다. properties.joinToStringBuilder(this, prefix ="{", postfix ="}") { prop ->serializeString(prop.name) // 프로퍼티 이름을 얻는다. append(": ")serializePropertyValue(prop.get(obj)) // 프로퍼티 값을 얻는다. }}
애노테이션을 활용한 직렬화 제어
JSON 직렬화 과정에서 @JsonExclude를 사용하여 특정 필드들을 제외할 수 있다.