3. 함수 정의와 호출
_ 컬렉션 / 문자열 / 정규식을 다루기 위한 함수
_ 이름 붙인 인자 / 디폴트 파라미터 값 / 중위 호출 문법 사용
_ 확장 함수와 확장 프로퍼티를 사용해 자바 라이브러리 적용
_ 최상위 및 로컬 함수와 프로퍼티를 사용해 코드 구조화
3-1. 기본중의 기본 - 자료형 확인
- 변수.javaClass로 변수의 자료형 확인이 가능하며, 이는 java의 getClass() 커스텀 getter와 매칭되어 있다.
3-2. joinToString 함수 구현부 분석
// Kotlin 1.6 버전 기준 code
fun <T> joinToString(
collection: Collection<T>, // 요소
separator: String, // 구분자
prefix: String, // 접미
postfix: String // 접두
): String {
// sb 초기화 시 접미 먼저 넣고 초기화
val result = StringBuilder(prefix)
// 컬렉션을 순회하며 구분자와 요소를 삽입
for ((index, element) in collection.withIndex()) {
if (index > 0) result.append(separator)
result.append(element)
}
// sb에 접두를 마지막에 추가해준뒤, String으로 형변환 하여 결과값 반환
result.append(postfix)
return result.toString()
}
- 현재 내가 사용중인 kotlin 1.19.12 버전에서는 joinToString이 여러 종류의 Array타입의 확장함수로 구현이 되어 있다.
-> ByteArray, ShortArray, IntArray, LongArray, FloatArray, DoubleArray, BooleanArray, CharArray 등
3-3. 디폴트 값과 자바
- 자바에는 default parameter value의 개념이 없다.
-> 그러므로 코틀린 함수를 자바에서 호출하는 경우에는 그 코틀린 함수의 각 파라미터에 따른 디폴트 값이 모두 지정되어 있더라도, 모든 인자를 명시적으로 넣어줘야 한다.
-> 이런 경우 @JvmOverloads 어노테이션을 Kotlin 함수의 상단에 추가하면, 코틀린 컴파일러에 의해 자동으로 뒤에부터 하나씩 파라미터를 제거한 오버로딩된 자바 메서드를 추가해준다.
말로는 이해가 안될 수 있으니 코드로 분석해 보자
1. 아래와 같이 자바에서 호출해야 하는 코틀린 메서드가 필요하다고 할 때, 자바에서도 디폴트 파라미터를 사용하기 위해 아래 처럼 @JvmOverloads 어노테이션을 추가한다.
@JvmOverloads
fun callMethodFromJava(a: String = "", b: Int = -1, c: Boolean = flase): String {
// ..
}
2. 그러면 아래의 java 메서드들이 코틀린 컴파일러에 의해 자동적으로 오버로딩된다.
// 각각의 오버로딩된 함수들은 시그니처에서 생략된 파라미터에 의해 코틀린 함수의 디폴트 파라미터 값을 사용한다.
String callMethodFromJava(String a, Int b, Boolean c) { callMethodFromJava(a, b, c) }
String callMethodFromJava(String a, Int b) { callMethodFromJava(a, b, false) }
String callMethodFromJava(String a) { callMethodFromJava(a, -1, false) }
- 의문점. 그러면 매개변수 a와 c 만 매개값으로 두고 함수를 호출할 수 있는가? Kotlin에서는 명명된 인자를 이용하여 원하는 변수의 값만으로도 함수를 호출할 수 있다. (누락된 인자는 default value로 지정된다.)
-> java에서 kotlin 메서드를 호출한 결과 a와 c만을 호출할 수는 없었다.
-> [Q&A]. TODO
3-4. 코틀린에서 최상위 함수의 사용이 가능한 이유(by. 컴파일러)
- JVM(Java Virtual Machine)은 클래스 안에 들어있는 코드만을 실행할 수 있다.
그렇다면, 아래의 최상위 함수는 어떻게 실행되는 걸까?
// class 외부에 위치한 메서드 joinToString
fun joinToString(data: CharArray) { ... }
위 코드는 컴파일러에 의해 아래와 같이 변경된다.
// 컴파일러가 JoinKt라는 클래스 추가해서 kotlin의 최상위 함수를 감싼다.
public Class JoinKt {
public static String joinToString(CharArray data) { ... }
}
이때 새롭게 생성된 class의 이름인 JoinKt는 해당 파일의 이름과 확장자명(join.kt -> JoinKt)을 따른다.
JoinKt라는 클래스의 이름을 지정하고 싶다면, 아래 처럼 파일의 최상단에 @JvmName 어노테이션을 추가하면 된다.
@file:JvmName("StringFunctions") // 추가 시, joinToString를 감싸는 class의 이름은 StringFunctions이 된다.
package 패키지명
fun joinToString(data: CharArray) { ... }
3-5. 확장 함수
- 정의 : 어떤 클래스의 멤버 메서드인 것 처럼 호출할 수 있지만, 그 클래스의 밖에 선언된 함수
- 특징 : [1], [2]
[1]. 확장 함수는 class의 캡슐화를 깨지 않는다.
- class의 외부에 선언하기 때문에 class 내부에 private 이나 protected의 접근지정자를 부여한 변수 또는 메서드에 접근할 수 없기 때문이다.
[2]. 확장 함수는 오버라이딩이 불가능하다 (static 메서드와 같은 특징을 가짐)
- 주의사항 : 코틀린 문법상 확장 함수는 반드시 짧은 이름을 써야한다. (처음 알게된 사실)
// 문자열의 제일 마지막 문자(Char)를 가져오는 확장함수
fun String.lastChar(): Char = this.get(this.length - 1)
// this 생략 가능!! (처음 알게된 사실)
fun String.lastChar(): Char = get(length - 1)
// 사용 예시
println("context".lastChar()) // 출력: t
* 수신 객체 타입 : 확장된 Class의 이름, 위의 경우에는 String을 의미함
* 수신 객체 : 단일 표현식에서 사용된 this를 지칭하며, 이는 생략이 가능하다.
- 확장 함수를 호출할 때, 수신 객체로 지정한 변수의 정적 타입에 의해 어떤 확장함수가 호출될지 결정된다.
-> 확장 함수는 정적 디스패치
* 정적 디스패치(static dispatch) : 컴파일 시점에 알려진 변수 타입에 따라 정해진 메서드를 호출하는 방식
* 동적 디스패치(dynamic dispatch) : 런타임에 객체 타입에 따라 동적으로 호출될 대상 메서드를 결정하는 방식
3-6. as 키워드
- python과 같이 특정 라이브러리를 import 할 때, as 키워드를 사용하여 클래스나 함수를 다른 이름으로 지정할 수 있다.
- 2개의 외부 라이브러리를 import 해서 쓸 때, 메서드나 class의 이름이 겹친다면 as를 이용하여 name 충돌을 막을 수 있다. (언젠가 문제의 해결방안이 될 수 있을 것 같아서 기억을 위해 남김)
3-7. 가변 길이 인자, vararg
- vararg 키워드를 사용하면 호출 시 인자 개수가 달라질 수 있는 함수를 정의할 수 있다.
val numbers1 = listOf(1, 2, 3, 4, 5)
val numbers2 = mapOf("one" to 1, "two" to 2)
위 코드 처럼 일반적으로 많이 사용하던 list나 map에서 원하는 인자의 개수만큼 생성할 수 있었던 이유는
listOf와 mapOf 등이 인자값을 받을 때, vararg 키워드를 사용하는 오버로딩된 함수가 구현되어 있기 때문이다.
fun listOf<T> (vararg values: T): List<T> { ... }
public fun <K, V> mapOf(vararg pairs: Pair<K, V>): Map<K, V> =
if (pairs.size > 0) pairs.toMap(LinkedHashMap(mapCapacity(pairs.size))) else emptyMap()
*(스프레드) 연산자 : 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되도록 해주는 기능
- 사용법 : 단순히 배열이나 컬렉션 앞에 * 키워드를 붙이면 된다!!
- 사용 예시
val number1 = arrayOf(100, 200, 300)
// *를 사용했기 때문에, 실제로 listOf(100, 200, 300, -1)와 같음
val number2 = listOf(*number1, -1) // same code : listOf(number1[0], number1[1], number1[2], -1)
println(number2) // [100, 200, 300, -1]
number1의 요소가 3개라면 스프레드 연산자 대신에 뒤의 코드처럼 index를 사용해서 초기화해도 되겠지만,
number1의 요소가 몇 개인지 모른다면? 또는 요소의 개수가 너무 많다면, listOf에 어떻게 요소를 기입할 것인가?
(물론 값을 하나 추가하는 방법은 다양하지만, vararg와 *의 느낌과 특장점을 체감하고자 쓴 예시이다.)
- *number1은 자바 코드에서 number...과 같다. (구조 분해 할당)
3-8. String 확장 함수(새롭게 알게된 메소드)
val path = "/Users/yole/kotlin-book/chapter.adoc"
val directory = path.substringBeforeLast("/") // /Users/yole/kotlin-book
val fullName = path.substringAfterLast("/") // chapter.adoc
val fileName = fullName.substringBeforeLast(".") // chapter
val extension = fullName.substringAfterLast(".") // adoc
- 정규식만큼 강력한 파싱 함수는 없지만, 나중에 알아보기가 힘들어 수정이 조금 어려울 수도 있다는 단점이 있는 만큼
어느 정도의 대체재로 사용되지 않을까 한다.
-> 하지만, 정규식이 이메일, 생년월일, 폰번호 등에 많이 사용되는 만큼 정규표현식을 수정할 경우는 많이 없다고 판단됨
언젠가 쓰긴 할 것 같지만, 또 그렇게 많이 쓸 것 같지는 않아서. 이런 함수가 있다는 것만 기억하고 넘어간다.
3-9. 여러 줄 문자열 (""" """ : 3중 따옴표 문자열)
- 내 생각에 거의 유일한 사용처 : 전체 로직 단위의 테스트 코드 작성 시, response값과 비교하기 위한 문자열로 사용
- 특징 : 3중 따옴표 문자열에서는 *이스케이프를 하지 않아도 된다.
// 마침표(.)를 구분자로 사용하여 띄워쓰기 및 \n등 전체 형식을 유지할 수 있도록 했다.
val kotlinLogo = """| //
.|//
.|/ \"""
// 띄워쓰기로 각 데이터의 시작 라인을 맞춰, 가독성을 높일 수 있음 -> trimMargin 활용으로 스페이스는 추후 제거
println(kotlinLogo.trimMargin("."))
* escape(이스케이프) : 특수 문자가 컴파일 과정에서 오류가 나지 않도록 '\'를 앞에 추가하는 것, 특수 문자는 주로 특별한 기능(문자열 안에서 $가 변수의 출력을 담당하는 것 처럼)을 하기 때문에 그 기능을 제거시키는 것이다.
3-a. 몰랐던 사실
- map타입을 초기화 하기 위해 사용했던 to의 반환 타입은 Pair(first, second)다.
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
- 확장 함수도 중위(infix) 호출에 활용할 수 있다.
- java 에서는 split 함수에 delimiters(구분자)를 "."으로 넣으면 빈 배열을 반환한다.
-> 마침표(.)은 모든 문자를 나타내는 정규식으로 해석되기 때문 ( Java 17.0.2버전 에서도 같았음)
- split 함수도 delimiters에 vararg 키워드가 붙어있어서 여러 요소의 구분자를 입력할 수 있었다.
오늘의 계획을 다 이루지 못했다고, 그리고 책 진도가 잘 안나간다고 조급해하지 말자.
새로운 내용을 학습하는 것도 좋지만, 배운 내용을 정리하며 내용을 복습하는 시간도 그만큼 중요하다.
그냥 오늘 열심히 학습했다면 그걸로 만족하자.
'Kotlin' 카테고리의 다른 글
[Kotlin in Action 4장] 정주행 (0) | 2024.03.20 |
---|---|
Kotlin Scope Functions 정리 (1) | 2024.03.11 |
[Kotlin in Action 1, 2장] 정주행 시작 (0) | 2024.02.27 |
shuffle()와 shuffled()의 차이점 및 배열(Array), 리스트(List) 정리 (0) | 2024.02.19 |