- 컬렉션, 문자열, 정규식을 다루기 위한 함수
- 이름 붙인 인자, 디폴트 파라미터 값, 중위 호출 문법 사용
- 확장 함수와 확장 프로퍼티를 사용해 자바 라이브러리 적용
- 최상위 및 로컬 함수와 프로퍼티를 사용해 코드 구조화
val set = hashSetOf(1, 7, 53)
val list = arrayListOf(1, 7, 53)
val map = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty-threee")
println(set.javaClass) // class java.util.HashSet
println(list.javaClass) // class java.util.ArrayList
println(map.javaClass) // class java.util.HashMap
val strings = listOf("first", "second", "fourteenth")
println(strings.last()) // fourteenth
val numbers = setOf(1, 14, 2)
println(numbers.max()) // 14
val list = listOf(1, 2, 3)
println(list) // [1, 2, 3]
// joitnToString() 함수의 초기 구현
fun <T> joinToString(
collection: Collection<T>,
separator: string,
prefix: String,
postfix: String
): String {
val result = StringBuilder(prefix)
for ((index, element) in collection.withIndex()) {
if (index > 0) result.append(separator)
result.append(element)
}
result.append(postfix)
return result.toString()
}
✅ 제네릭 : 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법
val list = listOf(1, 2, 3)
println(joinToString(list, "; ", "(", ")")) // (1; 2; 3)
joinToString(collection, separator = " ", prefix = " ", postfix = ".")
💡 함수 선언에서 파라미터의 디폴트 값을 지정할 수 있어 오버로드 중 상당수를 피할 수 있도록 도와준다.
// 디폴트 파라미터 값을 사용해 joinToString() 정의하기
fun <T> joinToString(
collection: Collection<T>,
separator: string = ", ",
prefix: String = "",
postfix: String = ""
): String
joinToString(list, ", ", "", "") // 1, 2, 3
joinToString(list) // 1, 2, 3
joinToString(list, "; ") // 1; 2; 3
joinToString(list, postfix = ";", prefix = "# ") // # 1, 2, 3;
함수의 디폴트 파라미터 값은 함수를 호출하는 쪽이 아니라 함수 선언 쪽에서 지정된다는 사실을 기억하라. 따라서 어떤 클래스 안에 정의된 함수의 디폴트 값을 바꾸고 그 클래스가 포함된 파일을 재컴파일하면 그 함수를 호출하는 코드 중에 값을 지정하지 않은 모든 인자는 자동으로 바뀐 디폴트 값을 적용받는다.
객체의 인스턴스 API = 객체가 가지는 인스턴스 메서드와 프로퍼티
// 비슷하게 중요한 역할을 하는 클래스가 둘 이상 있는 경우
// 다양한 정적 메서드를 모아두는 역할만 담당하며, 특별한 상태나 인스턴스 메소드는 없는 클래스가 생겨난다.
class Rectangle(val width: Double, val height: Double) {
fun calculateArea(): Double {
return width * height
}
}
class Circle(val radius: Double) {
fun calculateArea(): Double {
return Math.PI * radius * radius
}
}
// 정적 유틸리티 클래스
class GeometryUtil {
companion object {
fun calculateArea(shape: Any): Double {
return when (shape) {
is Rectangle -> shape.calculateArea()
is Circle -> shape.calculateArea()
else -> throw IllegalArgumentException("Unknown shape")
}
}
}
}
fun main() {
val rectangle = Rectangle(5.0, 3.0)
val circle = Circle(2.0)
val area1 = GeometryUtil.calculateArea(rectangle)
val area2 = GeometryUtil.calculateArea(circle)
println("Area of rectangle: $area1")
println("Area of circle: $area2")
}
// 중요한 객체는 하나뿐이지만 그 연산을 객체의 인스턴스 API에 추가해서 API를 너무 크게 만드는 경우
// 다양한 정적 메서드를 모아두는 역할만 담당하며, 특별한 상태나 인스턴스 메소드는 없는 클래스가 생겨난다.
class MathUtility {
companion object {
fun add(a: Int, b: Int): Int {
return a + b
}
fun subtract(a: Int, b: Int): Int {
return a - b
}
// 다른 수학 함수들도 추가 가능
}
}
fun main() {
val result1 = MathUtility.add(5, 3)
val result2 = MathUtility.subtract(8, 2)
println("Addition: $result1") // 출력: Addition: 8
println("Subtraction: $result2") // 출력: Subtraction: 6
}
// joinToString() 함수를 최상위 함수로 선언하기
package strings
fun joinToString(...): String { ... }
Q1. 새로운 클래스? : package 내에 선언된 함수를 클래스의 정적 메서드로 만드는 별도의 유틸리티 클래스를 자동 생성해준다. (생성된 클래스명 = 해당 패키지의 이름 + "Kt" 접미사가 추가된 이름) 즉, strings 패키지에 속하는 클래스에서 joinToString 함수를 호출할 때 사용하는 별도의 유틸리티 클래스인 StringsKt 클래스를 생성한다.
var opCount = 0
fun performOperation() {
opCount++
// ...
}
fun reportOperationCount() {
println("Operation performed $opCount times")
}
✅ 상수 : 변하지 않고, 항상 일정한 값을 갖는 수
val UNIX_LINE_SEPARATOR = "\n"
const val UNIX_LINE_SEPARATOR = "\n"
앞의 코드는 다음 자바 코드와 동등한 바이트코드를 만들어낸다.
public static final String UNIX_LINE_SEPARATOR = "\n";
💡 클래스의 멤버 함수처럼 호출되지만, 해당 클래스의 정의나 상태를 변경하지 않는 함수이다. 특별한 상태나 인스턴스 메소드가 없는 클래스가 생기지 않도록 모든 다른 클래스 밖에 선언되며, 수신 객체를 첫 번째 인자로 받는다.
Q2. 확장 함수를 쓴다면? : 원본 객체를 수정하지 않고도 해당 객체에 새로운 기능을 추가할 수 있고, 관련된 함수들을 함께 묶어서 확장 함수로 정의하면 해당 객체와 관련된 기능들이 하나의 모듈로 묶여 있어 응집도가 높아진다.
package strings
fun String.lastChar(): Char = this.get(this.length - 1)
println("Kotlin".lastChar()) // n
수신 객체 타입(receiver type)
이라 부르며, 확장 함수가 호출되는 대상이 되는 값(객체)을 수신 객체(receiver object)
라고 부른다.package strings
fun String.lastChar(): Char = get(length - 1)
import strings.lastChar
val c = "Kotlin".lastChar()
import strings.*
val c = "Kotlin".lastChar()
import strings.lastChar as last
val c = "Kotlin".last()
물론 일반적인 클래스나 함수라면 그 전체 이름을 써도 된다. 하지만 코틀린 문법상 확장 함수는 반드시 짧은 이름을 써야 한다. 따라서 임포트할 때 이름을 바꾸는 것이 확장 함수 이름 충돌을 해결할 수 있는 유일한 방법이다.
fun <T> joinToString(
collection: Collection<T>,
separator: string = ", ",
prefix: String = "",
postfix: String = ""
): String {
val result = StringBuilder(prefix)
for ((index, element) in this.withIndex()) {
if (index > 0) result.append(separator)
result.append(element)
}
result.append(postfix)
return result.toString()
}
val list = listOf(1, 2, 3)
println(list.joinToString(separator = "; ", prefix = "(", postfix = ")")) // (1; 2; 3)
// 문자열 컬렉션에 대해서만 호출할 수 있는 join 함수 정의
fun Collection<String>.join(
separator: string = ", ",
prefix: String = "",
postfix: String = ""
) = joinToString(separator, prefix, postfix)
println(listOf("one", "two", "eight").join(" ")) // one two eight
// 맴버 함수 오버라이드하기
open class View {
open fun click() = println("View clicked")
}
class Button: View() {
override fun click() = println("Button clicked")
}
val view: View = Button()
view.click() // Button clicked
확장 함수는 클래스의 일부가 아니다. 확장 함수는 클래스 밖에 선언된다.
// 확장 함수는 오버라이드할 수 없다.
fun View.showOff() = println("I'm a view!")
fun Button.showOff() = println("I'm a button!")
val view: View = Button()
view.showOff() // I'm a view!
어떤 클래스를 확장한 함수와 그 클래스의 멤버 함수의 이름과 시그니처가 같다면 확장 함수가 아니라 멤버 함수가 호출된다(멤버 함수의 우선 순위가 더 높다).
💡 기존 클래스 객체에 대한 프로퍼티 형식의 구문으로 사용할 수 있는 API(기능)을 추가할 수 있으며, 뒷받침하는 필드가 없기 때문에 최소한 게터를 꼭 정의해야 한다.
// 확장 프로퍼티 선언하기
val String.lastChar: Char
get() = get(length - 1)
var StringBuilder.lastChar: Char
get() = get(length - 1)
set(value: Char) {
this.setCharAt(length - 1, value)
}
println("Kotlin".lastChar) // n
val sb = StringBuilder("Kotlin?")
// sb.lastChar.set('!')
sb.lastChar = '!'
println(sb) // Kotlin!
varage 키워드를 사용하면 호출 시 인자 개수가 달라질 수 있는 함수를 정의할 수 있다.
중위(infix) 함수 호출 구문을 사용하면 인자가 하나뿐인 메소드를 간편하게 호출할 수 있다.
구조 분해 선언을 사용하면 복합적인 값을 분해해서 여러 변수에 나눠 담을 수 있다.
val strings: List<String> = listOf("first", "second", "fourteenth")
strings.last() // fourteenth
val numbers: Collection<Int> = setOf(1, 14, 2)
numbers.max() // 14
자바 라이브러리 클래스의 인스턴스인 컬렉션에 대해 코틀린에서는 어떻게 새로운 기능을 추가할 수 있었을까?
- last와 max는 모두 확장 함수였던 것이다!
// last는 List 클래스의 확장 함수다. fun <T> List<T>.last(): T { /* 마지막 원소를 반환함 */ } fun Collection<Int>.max(): Int { /* 컬렉션의 최댓값을 찾음 */ }
var list = listOf(2, 3, 5, 7, 11)
fun listOf<T>(vararg values: T): List<T> { ... }
💡 가변 길이 인자(vararg) : 메소드를 호출할 때 원하는 개수만큼 값을 인자로 넘기면 자바 컴파일러가 배열에 그 값들을 넣어주는 기능이다.
fun main(args: Array<String>) {
val list = listOf("args: ", *args)
println(list)
}
val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
중위 호출(infix call)
이라는 특별한 방식으로 to라는 일반 메소드를 호출한 것이다.1.to("one") // "to" 메소드를 일반적인 방식으로 호출함
1 to "one" // "to" 메소드를 중위 호출 방식으로 호출함
infix fun Any.to(other: Any) = Pair(this, other)
Pair
는 코틀린 표준 라이브러리 클래스로 그 이름대로 두 원소로 이뤄진 순서쌍을 표현한다. 실제로 to는 제네릭 함수다.val (number, name) = 1 to "one"
구조 분해 선언
이라고 부른다. Pair 인스턴스 외 다른 객체에도 구조 분해를 적용할 수 있다.for ((index, element) in collection.withIndex()) {
println("$index: $element")
}
// mapOf 함수의 선언
fun <K, V> mapOf(vararg values: Pair<K, V>): Map<K, V>
// 마침표나 대시(-)로 문자열을 분리하는 예
println("12.345-6.A".split("\\.|-".toRegex())) // [12, 345, 6, A]
println("12.345-6.A".split(".", "-")) // [12, 345, 6, A]
// String 확장 함수를 사용해 경로 파싱하기
fun parsePath(path: String) {
val directory = path.substringBeforeLast("/")
val fullName = path.substringAfterLast("/")
val fileName = fullName.substringBeforeLast(".")
val extension = fullName.substringAfterLast(".")
println("Dir: $directory, name: $fileName, ext: $extension")
}
parsePath("/Users/yole/kotlin-book/chapter.adoc")
// 디렉터리 경로 = Dir, 파일 이름 = name, 파일 확장자 = ext
// 결과: Dir: /Users/yole/kotlin-book, name: chapter, ext: adoc
// 경로 파싱에 정규식 사용하기
fun parsePath(path: String) {
val regex = """(.+)/(.+)\.(.+)""".toRegex()
val matchResult = regex.matchEntire(path)
if (matchResult != null) {
// destructured 프로퍼티 : 그룹별로 분해한 매치 결과
val (directory, filename, extension) = matchResult.destructured
println("Dir: $directory, name: $filename, ext: $extension")
}
}
val kotlinLogo = """| //
.| //
.|/ \"""
// trimMargin : 해당 문자열과 그 직전의 공백을 제거
println(kotlinLogo.trimMargin("."))
//// 결과
// | //
// | //
// |/ \
val price = """${'$'}99.9"""
반복하지 말라(DRY, Don't Repeat Yourself)
✅ 메소드 추출 리팩토링 : 한 메서드에 세세한 처리가 많을 때 그런 처리를 묶어서 나누고 독립된 메서드로 추출하는 것
class User(val id: Int, val name: String, val address: String)
fun saveUser(user: User) {
if (user.name.isEmpty()) {
throw IllegalArgumentException(
"Can't save user ${user.id}: empty Name")
}
if (user.address.isEmpty()) {
throw IllegalArgumentException(
"Can't save user ${user.id}: empty Address")
}
// user를 데이터베이스에 저장한다.
}
saveUser(User(1, "", ""))
// 결과: java.lang.IllegalArgumentException: Can't save user 1: empty Name
// 로컬 함수를 사용해 코드 중복 줄이기
class User(val id: Int, val name: String, val address: String)
fun saveUser(user: User) {
fun validate(user: User, value: String, fieldName: String) {
if (value.isEmpty()) {
throw IllegalArgumentException(
"Can't save user ${user.id}: empty $fieldName")
}
}
// 로컬 함수를 호출해서 각 필드를 검증한다.
validate(user, user.name, "Name")
validate(user, user.address, "Address")
// user를 데이터베이스에 저장한다.
}
// 로컬 함수에서 바깥 함수의 파라미터 접근하기
class User(val id: Int, val name: String, val address: String)
fun saveUser(user: User) {
// 이제 saveUser 함수의 user 파라미터를 중복 사용하지 않는다.
fun validate(value: String, fieldName: String) {
if (value.isEmpty()) {
throw IllegalArgumentException(
// 바깥 함수의 하라미터에 직접 접근할 수 있다.
"Can't save user ${user.id}: empty $fieldName")
}
}
validate(user.name, "Name")
validate(user.address, "Address")
// user를 데이터베이스에 저장한다.
}
class User(val id: Int, val name: String, val address: String)
fun User.validateBeforeSave() {
fun validate(value: String, fieldName: String) {
if (value.isEmpty()) {
throw IllegalArgumentException(
// User의 프로퍼티를 직접 사용할 수 있다.
"Can't save user $id: empty $fieldName")
}
}
validate(name, "Name")
validate(address, "Address")
}
fun saveUser(user: User) {
user.validateBeforeSave() // 확장 함수를 호출한다.
// user를 데이터베이스 저장한다.
}
이 경우 검증 로직은 User를 사용하는 다른 곳에서는 쓰이지 않는 기능이기 때문에 User에 포함시키고 싶지는 않다. User를 간결하게 유지하면 생각해야 할 내용이 줄어들어서 더 쉽게 코드를 파악할 수 있다.
반면 한 객체만을 다루면서 객체의 비공개 데이터를 다룰 필요 없는 함수는 이와 같이 확장 함수로 만들면
객체.멤버
처럼 수신 객체를 지정하지 않고도 공개된 멤버 프로퍼티나 메소드에 접근할 수 있다.