기본적인 내용은 생략한다.
키워드 #
- (자바와 다른) 함수 정의 / 함수 호출
- 확장 함수 (+ 프로퍼티)
3.1 코틀린에서 컬렉션 만들기 #
핵심 : 자바 컬렉션을 사용한다.
다양한 방법으로 컬렉션을 만들 수 있다.
val hashSet = hashSetOf(1, 7, 53)
val arrayList = arrayListOf(1, 7, 53)
val hashMap = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
println(hashSet) // [1, 53, 7] | java.util.HashSet
println(arrayList) // [1, 7, 53] | java.util.ArrayList
println(hashMap) // {1=one, 53=fifty-three, 7=seven} | java.util.HashMap
- 코틀린만의 특별한 컬렉션이 있는 것이 아니다. 자바 컬렉션을 그대로 사용한다.
- 표준 자바 컬렉션 활용 → 자바와 호환/상호작용↑
- 코틀린에셔는 자바 컬렉션에서보다 더 많은 기능을 사용할 수 있다. (있는 것 처럼 보여준다.)
- 있는 것 처럼 보여준다. (= 확장함수를 이용하는 거니까 ‘있는 것 처럼 보인다’라고 말할 수 있지 않을까)
to
#
to
는 특별한 키워드가 아닌, 일반 함수(= 중위 함수 = infix
)이다.
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
3.2 함수를 호출하기 쉽게 만들기 #
코틀린에서는 함수를 클래스 안에 선언할 필요가 전혀 없다.
이름 붙인 인자 #
함수에 전달하는 (일부, 혹은 전부)인자의 이름을 명시할 수 있다.
단, 인자 이름 명시 후 그 뒤에 오는 모든 인자는 이름을 꼭 명시해야 한다.
joinToString(
collection,
separator = " ",
prefix = " ",
postfix = " "
)
" ‘이름 붙인 인자’는 ‘default 파라미터 값’과 함께 쓸모가 많다. “
디폴트 파라미터 값 #
자바 (너무 많아지는)오버로딩 문제를 해결 가능
fun <T> joinToString(
collection: Collection<T>,
separator: String = ", ",
prefix: String = "",
postfix: String = ""
)
/** 사용 예시 */
joinToString(list)
joinToString(list, "; ")
...
@JvmOverloads #
자바에서는 디폴트 파라미터 기능이 없다.
자바 → 코틀린 클래스 호출 시 모든 인자를 명시해줘야한다.
이때, @JvmOverloads
함수를 추가해주면 (컴파일 시)맨 마지막 파라미터부터 하나씩 생략한 오버로딩 메서드들을 추가해준다.
정적 유틸리티 클래스 없애기 : 최상위 함수, 프로퍼티 #
코틀린에서는 함수를 클래스 안에 선언할 필요가 전혀 없다.
자바에서는 특별한 클래스에 속하지 않는, 유틸리티성 함수를 만들기 위해 유틸리티 클래스/함수를 생성했었다.
코틀린에서는 이런 ‘무의미한 클래스(= 무분별한 정적 유틸리티 클래스)‘가 필요 없다.
함수를 소스 파일의 최상위 수준(클래스 밖)에 위치시키면 된다.
근데 소스를 보다보면 이게 가독성에 좋은지는 모르겠다. (아직 익숙하지 않아서일수도 있다.)
코틀린 컴파일러가 생성하는 ‘클래스명’은 최상위 함수가 들어있던 ‘코틀린 소스 파일의 이름’과 대응한다.
[컴파일 전]
// 파일명 : join.kt
// kotlin
package strings
fun joinToString(...): String { ... }
[컴파일 후]
// java
package strings;
public class JoinKt {
public static String joinToString(...) { ... }
}
[사용 예시]
import strings.JoinKt;
JoinKt.joinToString(...);
@JvmNames
#
기본적으로는 파일에 대응하여 생성되는 클래스명을 바꿀 수 있다.
@JvmNames
를 파일의 맨 앞(패키지 선언 전)에 위치시킨다.
@file:JvmNames("StringFunctions")
package strings
fun joinToString(...): String { ... }
package strings;
public class StringFunctions {
public static String joinToString(...) { ... }
}
최상위 프로퍼티 #
(함수와 마찬가지로) 프로퍼티도 최상위 수준에 놓을 수 있다.
= ‘정적 필드’에 대응/저장된다.
= ‘상수’를 선언할 때 사용할 수 있다.
” 어떤 데이터를 클래스 밖에 위치시켜야 하는 경우는 흔하지는 않지만, 그래도 가끔 유용할 때가 있다. “
var opCount = 0
fun performOperation() {
opCount++
}
val UNIX_LINE_SEPARATOR = "\n"
다만, 다른 프로퍼티와 동일하게 (var, val 에 따라) 접근자 메서드(getter, setter)가 생긴다.
‘상수’를 이용할 때 접근자 메서드가 생기는 것은 부자연스럽다.
= 즉, public static final
변수를 만들어야 한다.
= 이를 위해 const
키워드를 사용한다.
const val UNIX_LINE_SEPARATOR = "\n"
// public static final String UNIX_LINE_SEPARATOR = "\n";
3.3 메서드를 다른 클래스에 추가 : 확장 함수, 확장 프로퍼티 #
” 기존 코드와 코틀린 코드를 자연스럽게 통합하는 것은 코틀린의 핵심 목표 중 하나이다. “
자바 → 코틀린, 코틀린 → 자바로 전환/통합할 때 전혀 문제가 없어야한다.
전혀 문제가 없으면서도 추가적인 편리한 기능을 제공하고 싶다.
확장 함수 가 위 이슈를 해결한다.
확장 함수 #
어떤 클래스의 멤버 메서드인 것처럼 호출할 수 있지만, 클래스 밖에 선언된 함수이다.
멤버 함수와 확장 함수가 시그니처가 같다면, 멤버 함수가 우선적으로 호출된다.
용어 | 설명 |
---|---|
수신 객체 타입 | 확장이 정의될 클래스 타입 (확장 함수를 통해)확장할 대상 클래스 타입 |
수신 객체 | 그 클래스에 속한 인스턴스 객체 확장 함수가 호출되는 대상 객체 |
// 수신 객체 타입(String) 수신 객체(this) (+ this 는 생략 가능)
fun String.lastChar(): Char = this.get(this.length - 1)
println("kotlin".lastChar())
// 수신 객체 타입 : String
// 수신 객체 : "kotlin"
확장 함수가 캡슐화를 깨지는 않는다.
확장 함수는 확장 함수 내부에서 인스턴스의 메서드, 프로퍼티를 바로 사용할 수 있다.
단, 확장 함수가 캡슐화를 깨지는 않는다.
(멤버 메서드와 달리) 확장 함수 안에서는 클래스 내부에서만 사용할 수 있는 private, protected 멤버를 사용할 수 없다.
자바 변환 시 : ‘수신 객체를 첫 번째 인자로 받는 정적 메서드’
// java
char c = StringUtilKt.lastChar("kotin");
” 확장 함수는 단지 정적 메서드 호출에 대한 ‘문법적 편의’일 뿐이다. “
오버라이딩 X
정적 메서드와 같은 특징을 가지므로, 확장 함수를 하위 클래스에서 오버라이딩할 수 없다.
// View : super class
// Button : sub class
fun View.showOff() = println("view")
fun Button.showOff() = println("button")
val view:View = Button()
view.showOff() // "view"
확장 함수는 정적으로 결정된다.
확장 함수를 호출할 때 수신 객체의 정적 타입에 의해 어떤 확장함수가 실행될 지 결정된다.
- 동적 타입 = 런타임 타입 = 실 객체 타입에 의해 결정되지 않는다.
확장 프로퍼티 #
다시 읽어볼 것 (p123)
다른 클래스에 대해서, 프로퍼티 형식의 구문으로 사용할 수 있는 API를 추가할 수 있다.
프로퍼티라는 이름으로 불리긴 하지만 상태를 저장할 방법이 없기 때문에(기존 클래스/객체에 필드를 추가할 방법은 없다.), 실제로 확장 프로퍼티는 아무 상태도 가질 수 없다.
다만 문법 사용 시 더 짧은 코드(가독성↑)를 작성할 수 있게 도와줄 수 있다.
val String.lastChar: Char
get() = this.get(length - 1)
var StringBuilder.lastChar: Char
get() = get(length - 1)
set(value: Char) {
this.setCharAt(length - 1, value)
}
확장 함수의 경우와 마찬가지로 확장 프로퍼티도 일반적은 프로퍼티와 같은데, 단지 ‘수신 객체 클래스’가 추가되었을 뿐이다.
뒷받침하는 필드가 없어서 기본 게터 구현을 제공할 수 없으므로 최소한 게터는 꼭 정의해야 한다.
마찬가지로 초기화 코드에서 계산한 값을 담을 장소가 전혀 없으므로, 초기화 코드도 쓸 수 없다.
3.4 컬렉션 처리 : 가변 길이 인자, 중위 함수 호출, 라이브러리 지원 #
키워드 | 요약(설명) |
---|---|
vararg | 가변 인자를 처리한다. |
infix | 중위 함수 |
구조 분해 선언 (destructuring declaration) | 복합적인 값을 분해해서 여러 변수에 담을 수 있다. |
vararg #
- 코틀린 : vararg
- 자바 :
...
이미 배열에 들어있는 원소를 ‘가변 길이 인자’로 넘길 때에는 주의가 필요하다.
자바에서는 그냥 넘기면 되지만, 코틀린에서는 스프레드 연산자(spread)를 사용해야 한다. (실재로는 *
를 붙이기만 하면 된다고한다.)
스프레드 = 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되게 하는 것
fun main(args: Array<String>) {
val list = listOf("args:", *args) // 스프레드 연산자가 배열의 내용을 펼쳐준다.
println(list)
}
스프레드 연산자를 통해 ‘배열의 원소’ + ‘다른 여러 값’들을 함께 넘길 수 있다.
infix #
// 수신 객체 + 중위 함수 + 유일한 메서드 인자 (1개)
1 to "oen"
수신 객체, 메서드 이름, 유일한 인자 사이에는 ‘공백’이 들어가야 한다.
구조 분해 선언 #
val (number, name) = 1 to "one" // Pair<A, B>
for ((index, element) in collection.withIndex()) {
println("$index : $element")
}
3.5 문자열과 정규식 다루기 #
코틀린 문자열 = 자바 문자열 (같다.)
코틀린에서는 확장 함수를 통해 좀 더 명확하고 실수를 줄일 수 있는 함수를 제공한다.
예를 들어 split 의 정규표현식 / 문자열 인자
등
"12.345-6.A".split("\\.|-".toRegex())
"12.345-6.A".split(".", "-")
3중 따옴표 #
"”" 안에서는
- 역슬래시()를 포함하여 어떤 문자도 이스케이프할 필요가 없다.
- 줄바꿈이 그대로 표현된다.
3.6 코드 다듬기 : 로컬 함수와 확장 #
로컬 함수
- 함수 내부에 중첩된 함수를 만들 수 있다.
- (자신이 속한) 바깥 함수의 모든 파라미터와 변수를 사용할 수 있다.
- 일반적으로 1단계만 중첩시킬 것을 권장한다. (중첩의 중첩의 … => 더 어렵다.)
간혹 리팩토링의 의미로 메서드를 잘게잘게 쪼개는 경우가 있다. 잘되면 좋지만, 종종 코드를 이해하기 더 힘들게 한다. (예를 들며느 클래스에 파편화된 메서드가 많아지는 느낌)
이때 대체제로 유용하게 사용할 수 있을 것 같다.
책에서도 다음과 같이 말하고 있다.
" 작은 메서드가 많아지고 각 메서드 사이의 관계를 파악하기 힘들어서 코드를 이해하기 더 어려워질 수도 있다. “
fun saveUser(user: User) {
fun validate(user: User, value: String, filedName: String) {
...
}
validate(user, user.name, "Name")
validate(user, user.address, "Address")
...
}
fun saveUser(user: User) {
fun validate(value: String, filedName: String) {
...
}
validate(user.name, "Name")
validate(user.address, "Address")
...
}
이것을 더 확장해서 User 클래스의 확장함수로 만들 수도 있다.
fun User.validate(~) { ... }
3.7 요약 #
- 확장함수
- 편리한, 다양한 API 제공
- ‘이름 붙인 인자’ + ‘인자 디폴트 값’ 가능
- 함수, 프로퍼티 → 최상위 수준 작성 가능
- 중위 호출
- 구조 분해 선언
- 3중 따옴표 (
"""
) - 로컬 함수