일반적으로 배열을 순회할 때 표준 for-in 루프를 사용하지만 배열의 인덱스 값도 같이 사용하고 싶다면, withIndex 함수를 사용하자
fun <T> Array<out T>.withIndex(): Iterable<IndexedValue<T>>dataclassIndexedValue<outT>(publicval index: Int, publicvalvalue: T)
👉🏻 withindex를 사용해 배열 값이 접근하기
@Testfun`withIndex returns IndexValues`() {val strings =arrayOf("this", "is", "an", "array", "of", "strings")/** * Index 0 maps to this * Index 1 maps to is * Index 2 maps to an * Index 3 maps to array * Index 4 maps to of * Index 5 maps to strings * */for ((index, value ) in strings.withIndex()) {println("Index $index maps to $value") // withIndex 호출assertTrue(index in0..5) // 각각의 인덱스와 값에 접근 }}
컬렉션 생성하기
List, Set, Map은
listOf, setOf, mapOf 처럼 변경 불가능한 컬렉션을 생성하기 위해 만들어진 함수나
mutableListOf, mutableSetOf, mutableMapOf 처럼 변경 가능한 컬렉션을 생성하기 위해 고안된 함수 중 하나를 사용
asList 구현
asList 구현은 읽기 전용 리스트를 리턴하는 자바의 Arrays.asList에 위임
👉🏻 불변 List, Set, Map 생성하기
val numList =listOf(3, 1, 4, 1, 5, 9) // 불변 리스트 생성val numSet =setOf(3, 1, 4, 1, 5, 9) // 불변 세트 생성(중복 포함 X)val map =mapOf(1 to "one", 2 to "two", 3 to "three") // 불변 맵 생성
기본적으로 코틀린 컬렉션은 불변이다.
컬렉션은 원소를 추가하거나 제거하는 메소드를 지원하지 않는다.
컬렉션을 변경하는 메소드는 팩토리 메소드에서 제공하는 가변 인터페이스에 들어 있다.
mutableListOf
mutableSetOf
mutableMapOf
👉🏻 가변 List, Set, Map 생성하기
val numList =mutableListOf(3, 1, 4, 1, 5, 9)val numSet =mutableSetOf(3, 1, 4, 1, 5, 9)val map =mutableMapOf(1 to "one", 2 to "two", 3 to "three")
👉🏻 List, Set, Map 인터페이스를 직접 구현한 클래스의 인스턴스 생성
@Testfun`instantiating a linked list`() {val list =LinkedList<Int>() list.add(3) // addLast 의 별칭 list.add(1) list.addLast(999) list[2] =4// 배열 타입 접근은 get or set 호출 list.addAll(listOf(1, 5, 9, 2, 6, 5))assertThat(list, contains(3, 1, 4, 1, 5, 9, 2, 6, 5))}
컬렉션에서 읽기 전용 뷰 생성하기
toList, toSet, toMap 메소드를 사용해 새로운 읽기 전용 컬렉션을 생헝하자
👉🏻 List 타입의 레퍼런스를 리턴하는 toList 메소드 호출
@Testfun`toList on mutableList makes a readOnly new list`() {val mutableNums =mutableListOf(3, 1, 4, 1, 5, 9)val readOnlyNumList: List<Int> = mutableNums.toList()assertEquals(mutableNums, readOnlyNumList)assertNotSame(mutableNums, readOnlyNumList)}
👉🏻 독립된 객체를 생성하는데 독립된 객체의 내용은 원본과 같지만 더 이상 같은 객체를 나타내지는 않는다.
@Testfun`modify mutable list does not change read-only list`() {val mutableNums =mutableListOf(3, 1, 4, 1, 5, 9)val readOnly: List<Int> = mutableNums.toList() mutableNums.add(2)assertThat(readOnly, not(contains(2)))}
👉🏻 내용이 같은 읽기 전용 뷰를 생성하고 싶다면, List 타입의 레퍼런스에 가변 리스트를 할당하자.
@Testfun`read-only view of a mutable list`() {val mutableNums =mutableListOf(3, 1, 4, 1, 5, 9)// List 타입의 레퍼런스에 가변 리스트 할당val readOnlySameList: List<Int> = mutableNumsassertEquals(mutableNums, readOnlySameList)assertSame(mutableNums, readOnlySameList) mutableNums.add(2)assertEquals(mutableNums, readOnlySameList)assertSame(mutableNums, readOnlySameList) // 여전히 같은 기저 객체}
컬렉션이나 문자열이 비어 있는 경우 ifEmpty, ifBlank 함수를 사용해 기본값을 리턴하자.
👉🏻 기본값 테스트
dataclassProduct(val name: String,var price: Double,var onSale: Boolean=false,)// 판매 중인 상품 이름 얻기funsameOfProductsOnSale(products: List<Product>) = products.filter { it.onSale } .map { it.name } .joinToString(separator =", ")// 빈 컬렉션에 기본 리스트 제공funonSaleProductsIfEmptyCollection(products: List<Product>) = products.filter { it.onSale } .map { it.name } .ifEmpty { listOf("none") } .joinToString(separator =", ")// 빈 문자열에 기본 문자열 제공funonSaleProductsIfEmptyString(products: List<Product>) = products.filter { it.onSale } .joinToString(separator =", ") { it.name } .ifEmpty { "none" }privateval overthruster =Product("Oscillation Overthruster", 1_000_000.0)privateval fluxcapacitor =Product("Flux Capacitor", 299_999.95, onSale =true)privateval tpsReportCoverSheet =Product("TPS Report Cover Sheet", 0.25)@Testfun`asis_productsNotOnSale`() {val sameOfProductsOnSale =sameOfProductsOnSale(listOf(overthruster, tpsReportCoverSheet))assertEquals("", sameOfProductsOnSale) // 판매 중인 상품이 없는 경우 빈 컬렉션을 리턴하고 빈 문자열로 반환}@TestfunproductsOnSale() {val products =listOf(overthruster, fluxcapacitor, tpsReportCoverSheet)assertAll("On sale products", { assertEquals("Flux Capacitor", onSaleProductsIfEmptyCollection(products)) }, { assertEquals("Flux Capacitor", onSaleProductsIfEmptyString(products)) })}@TestfunproductsNotOnSale() {val products =listOf(overthruster, tpsReportCoverSheet)assertAll("No products on sale", { assertEquals("none", onSaleProductsIfEmptyCollection(products)) }, { assertEquals("none", onSaleProductsIfEmptyString(products)) })}
코틀린도 Optional<T>를 지원하지만 ifEmpty 함수를 사용해 특정한 값을 리턴하는 방법이 더 사용하기 쉽다.
주어진 범위로 값 제한하기
값이 주어졌을 때, 주어진 값이 특정 번위 안에 들면 해당 값을 리턴하고,
그렇지 않다면 범위의 최솟값 또는 최댓값을 리턴하려면
kotlin.ranges의 coerceIn 함수를 범위 인자 또는 구체적인 최솟값, 최댓값과 함께 사용하자.
👉🏻 coerceIn 함수는 값이 범위 안에 있으면 해당 값을 리턴하고 그렇지 않다면 범위의 경계 값을 리턴
@Testfun`coerceIn given a range`() {val range =3..8assertEquals(5, 5.coerceIn(range))assertEquals(range.start, 1.coerceIn(range))assertEquals(range.endInclusive, 9.coerceIn(range))}
👉🏻 원하는 최대/최솟값이 있다면 범위를 생성하지 않아도 된다.
@Testfun`coerceIn given a range`() {val min =2val max =6assertEquals(5, 5.coerceIn(min, max))assertEquals(min, 1.coerceIn(min, max))assertEquals(max, 9.coerceIn(min, max))}
리스트 구조 분해하기
최대 5개의 원소를 가진 그룹에 리스트를 할당하기
👉🏻 리스트의 원소를 구조 분해하기
val list =listOf("a", "b", "c", "d", "e", "f", "g")val (a, b, c, d, e) = listprintln("$a $b $c $d $e")
코틀린 표준 라이브러리의 List 클래스에 N이 1부터 5까지인 componentN 이라는 확장 함수가 정의되어 가능한 동작
package kotlin.collections@kotlin.internal.InlineOnlypublicinlineoperatorfun <T> List<T>.component1(): T {returnget(0)}@kotlin.internal.InlineOnlypublicinlineoperatorfun <T> List<T>.component2(): T {returnget(1)}@kotlin.internal.InlineOnlypublicinlineoperatorfun <T> List<T>.component3(): T {returnget(2)}@kotlin.internal.InlineOnlypublicinlineoperatorfun <T> List<T>.component4(): T {returnget(3)}@kotlin.internal.InlineOnlypublicinlineoperatorfun <T> List<T>.component5(): T {returnget(4)}
데이터 처리를 위해 시퀀스를 사용하면, 각각의 원소는 자신의 다음 원소가 처리되지 전에 전체 파이프라인을 완료한다.
지연 처리 방식은 데이터가 많거나 first 같은 쇼트 서킷 연산의 경우 도움이 되고, 원하는 값을 찾았을 때 시퀀스를 종료할 수 있게 도와준다.
지연 시퀀스 사용하기
특정 조건을 만족시키는 데 필요한 최소량의 데이터만 처리하고 싶다면
코틀린 시퀀스를 쇼트 서킷 함수와 함께 사용하자
쇼트 서킷: 특정 조건에 다다를 때까지 오직 필요한 데이터만을 처리하는 방식
👉🏻 filter 함수 사용하기
/** * AS-IS */(100 until 200).map { it *2 } // 100개의 계산 .filter { it %3==0 } // 100개의 계산 .first() /** * TO-BE */(100 until 200).map { it *2 } // 100개의 계산 .first { it %3==0 } // 3개의 계산
👉🏻 시퀀스 사용하기
시퀀스가 비었다면 first 함수는 예외를 던지므로 firstOrNull을 사용하자
(100 until 200).asSequence() // 범위를 시퀀스로 변경(6개의 연산만 수행) .map { println("doubling $it"); it *2 } .filter { println("filtering $it"); it %3==0 } .firstOrNull()
시퀀스 API
시퀀스 API는 컬렉션에 들어 있는 함수와 똑같은 함수를 가지고 있지만, 시퀀스에 대한 연산은 중간 연산과 최종 연산이라는 범주로 나뉜다.
다음 소수를 찾기 위해 얼마나 많은 수를 확인해야 하는지 알 수 없는 이러한 경우 시퀀스를 사용하기 좋은 이유
@Testfun`test`() {funInt.isPrime() =// 2인지 확인 후 2가 아니면 2부터 해당 수의 제곱근 값을 반올림한 수까지 범위로 생성this==2|| (2..ceil(sqrt(this.toDouble())).toInt())// 주어진 수를 이 범위의 각각의 수로 나누어 정확히 떨어지는 수를 범위에서 찾을 수 없다면 .none { divisor ->this% divisor ==0 }/** * 무한대의 정수를 생성하고 첫 번째 소수를 찾을 때까지 생성한 정수를 하나씩 평가 */funnextPrime(num: Int) =generateSequence(num +1) { it +1} // 주어진 수보다 1 큰 수에서 시작하고 1 증가 반복 .first(Int::isPrime) // 첫 소수 값을 리턴assertEquals(10007, nextPrime(9973))}
무한 시퀀스 다루기
무한대의 원소를 갖는 시퀀스의 일부분이 필요하다면,
널을 리턴하는 시퀀스 생성기를 사용하거나, 시퀀스 확장 함수 중 takeWhile 같은 함수를 사용하자.
@Testfun`처음 N개의 소수 찾기`() {funInt.isPrime() =this==2|| (2..ceil(sqrt(this.toDouble())).toInt()) .none { divisor ->this% divisor ==0 }funnextPrime(num: Int) =generateSequence(num +1) { it +1} .first(Int::isPrime)/** 처음 N개의 소수 찾기 */funfirstNPrimes(count: Int) =generateSequence(2, ::nextPrime) // 2부터 시작하는 소수의 무한 시퀀스 .take(count) // 요청한 수만큼만 원소를 가져오는 중간 연산 .toList() // 최종 연산/** 주어진 수보다 작은 모든 소수 Ver.1 (널을 리턴하는 시퀀스 생성기) */funprimesLessThanV1(max: Int): List<Int> =generateSequence(2) { n ->if (n < max) nextPrime(n) elsenull } .toList() .dropLast(1)/** 주어진 수보다 작은 모든 소수 Ver.2 (takeWhile 사용) */funprimesLessThanV2(max: Int): List<Int> =generateSequence(2, ::nextPrime) .takeWhile { it < max } .toList()assertEquals(listOf(2, 3, 5, 7, 11), firstNPrimes(5))assertEquals(listOf(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97), primesLessThanV1(100))assertEquals(listOf(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97), primesLessThanV2(100))}
시퀀스에서 yield하기
구간을 지정해 시퀀스에서 값을 생성하려면
yield 중단 함수와 함께 sequence 함수를 사용하자
sequence 함수는 주어진 블록에서 평가되는 시퀀스를 생성
이 블록은 인자 없는 람다 함수이며 void를 리턴하고 평가 후 SequenceScope 타입을 받는다.
👉🏻 apply를 사용하여 단 하나의 구문으로, 저장해야 할 인스턴스를 받아 새로운 키로 한번에 갱신
apply 블록은 이미 인스턴스화된 객체의 추가 설정을 위해 사용하는 가장 일반적인 방법
@RepositoryclassJdbcOfficerDAO(privateval jdbcTemplate: JdbcTemplate) {privateval insertOfficer =SimpleJdbcInsert(jdbcTemplate) .withTableName("OFFICERS") .usingGeneratedKeyColumns("id")// Officer의 id 속성은 apply 블록 안에서 갱신된 다음// Officer 인스턴스가 리턴funsave(officer: Officer) = officer.apply {// excuteAndReturnKey 메소드는 컬럼 이름과 같으로 이뤄진 맵을 인자로 받아// 데이터베이스에서 생성된 기본 키 값을 리턴 id = insertOfficer.excuteAndReturnKey(mapOf("rank" to rank,"first_name" to first,"last_name" to last))// ... 추가적인 초기화 }// ...}
@Testfun`let test`() {funprocessString_asis(str: String) = str.let {when { it.isEmpty() ->"Empty" it.isBlank() ->"Blank"else-> it.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } } }funprocessString_tobe(str: String?) = str?.let { // 안전 호출 연산자와 let 을 같이 사용when { it.isEmpty() ->"Empty" it.isBlank() ->"Blank"else-> it.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } } } ?: "Null"// 널인 경우 처리하는 엘비스 연산자assertEquals("Abcdef", processString_asis("abcdef"))// assertEquals("", processString_asis(null)) // Null can not be a value of a non-null type StringassertEquals("Abcdef", processString_tobe("abcdef"))assertEquals("Null", processString_tobe(null))}
임시 변수로 let
연산 결과를 임시 변수에 할당하지 않고 처리하고 싶다면,
연산에 let 호출을 연쇄하고 let에 제공된 람다 또는 함수 레퍼런스 안에서 그 결과를 처리하자
클래스에서 해당 인터페이스를 구현한 후 by 키워드를 사용해 바깥쪽에 래퍼 클래스를 만들자
코틀린에서 by 키워드는 포함된 객체에 있는 모든 public 함수를 이 객체를 담고 있는 컨테이너를 통해 노출할 수 있다.
컨테이너에 노출하려면 포함된 객체의 public 메소드의 인터페이스를 생성해야 한다.
생성자에서 객체를 인스턴스화하고 모든 public 함수를 위임하도록 정의할 수 있다.
오직 포함된 객체의 public 함수만 노출
interfaceDialable { // 연산을 위임할 메소드가 포함된 인터페이스fundial(number: String): String}classPhone : Dialable {overridefundial(number: String) ="Dialing $number..."}interfaceSnappable { // 연산을 위임할 메소드가 포함된 인터페이스funtakePicture(): String}classCamera : Snappable {overridefuntakePicture() ="Taking picture..."}classSmartPhone(// 생성자에서 객체를 인스턴스화하고 모든 public 함수를 위임하도록 정의privateval phone: Dialable=Phone(),privateval camera: Snappable=Camera()) : Dialablebyphone, Snappable by camera // by 키워드를 사용해서 위임// 내부적으로 SmartPhone 클래스는 위임된 속성을 인터페이스 타입으로 정의privateval smartPhone: SmartPhone=SmartPhone() // SmartPhone 인스턴스화@Testfun`by keyword test`() {// Dialing delegates to internal phoneassertEquals("Dialing 555-1234...", smartPhone.dial("555-1234")) // 위임 함수 호출// Taking picture delegates to internal cameraassertEquals("Taking picture...", smartPhone.takePicture()) // 위임 함수 호출}
lazy 대리자 사용하기
어떤 속성이 필요할 때까지 해당 속성의 초기화를 지연시키고 싶다면 lazy 대리자를 사용하자
코틀린은 어떤 속성의 획득자와 설정자가 대리자라고 불리는 다른 객체에서 구현되어 있다는 것을 암시하기 위해 속성에 by 키워드를 사용
코틀린 표준 라이브러리에 다수의 대리자 함수가 있는데,
그 중 가장 인기 있는 함수는 lazy
👉🏻 처음 접근까지 속성의 초기화를 대기
첫 호출은 lazy가 받은 람다를 실행하고 그 다음 변수에 저장될 값을 리턴
내부적으로 이 값을 캐시하는 Lazy 타입의 ultimateAnswer$delegate라는 특별한 속성을 생성
@Testfun`lazy test`() {val ultimateAnswer: Intbylazy {println("computing the answer")42 }assertEquals(42, ultimateAnswer) // 여기서 초기화assertEquals(42, ultimateAnswer)}
lazy는 LazyThreadSafetyMode 타입의 이넘도 인자로 받는다.
SYNCHRONIZED
오직 하나의 스레드만 Lazy 인스턴스를 초기화할 수 있게 락 사용
PUBLICATION
초기화 함수가 여러 번 호출될 수 있지만 첫 번째 리턴값만 사용
NONE
락이 사용되지 않음
LazyThreadSafetyMode
lazy의 lock 인자를 제공하면 값 계산 시 이 객체가 대리자를 동기화
lock 인자가 없을 경우 대리자는 자신 스스로 동기화
값이 널이 될 수 없게 만들기
notNull 함수를 이용해 값이 설정되지 않았다면 예외를 던지는 대리자를 제공하자.
속성 초기화를 지연시키는 한 가지 방법은 속성에 처음 접근하기 전에 속성이 사용되면 예외를 던지는 대리자를 제공하는 notNull 함수를 사용하는 것이다.
👉🏻 속성에 값이 제공되기 전 접근을 시도하면 IllegalStateException
var shouldNotBeBull: StringbyDelegates.notNull()@Testfun`uninitialized value throws exception`() {assertThrows<IllegalStateException> { shouldNotBeBull }}@Testfun`initialize value then retrieve it`() { shouldNotBeBull ="Hello, World!"assertDoesNotThrow { shouldNotBeBull }assertEquals("Hello, World!", shouldNotBeBull)}
observable, vetoable 대리자
변경 감지에는 observable 함수를
변경 적용 여부를 결정할 때는 vetoable 함수와 람다를 사용하자.
var watched: IntbyDelegates.observable(1) { prop, old, new ->// 변수의 값이 변경될 때마다 메시지 출력println("${prop.name} changed from $old to $new")}var checked: IntbyDelegates.vetoable(0) { prop, old, new ->// 오직 양수 값으로만 변경 가능println("Trying to change ${prop.name} from $old to $new") new >=0}@Testfun`watched variable prints old and new values`() {assertEquals(1, watched) watched *=2// watched changed from 1 to 2assertEquals(2, watched) watched *=2// watched changed from 2 to 4assertEquals(4, watched)}@Testfun`veto values less than zero`() {assertAll( { assertEquals(0, checked) }, { checked =42; assertEquals(42, checked) }, // Trying to change checked from 0 to 42 { checked =-1; assertEquals(42, checked) }, // Trying to change checked from 42 to -1 { checked =17; assertEquals(17, checked) } // Trying to change checked from 42 to 17 )}
observable, vetoable 함수 구현은 개발자가 대리자를 작성할 때 참고할 만한 좋은 패턴이다.
classDelegate {operatorfungetValue(thisRef: Any?, property: KProperty<*>): String {return"$thisRef, thank you for delegating '${property.name}' to me!" }operatorfunsetValue(thisRef: Any?, property: KProperty<*>, value: String) {println("$value has been assigned to '${property.name}' in $thisRef.") }}@Testfun`delegate test`() {classExample {var p: StringbyDelegate() }val e =Example()println(e.p) // ScopeFunction$delegate test$Example@710636b0, thank you for delegating 'p' to me! e.p ="NEW"// NEW has been assigned to 'p' in ScopeFunction$delegate test$Example@710636b0.}
완전히 다른 예제 집합으로 그레이들 빌드 도구는 위임된 속성을 통해 컨테이너와 상호작용할 수 있게 도와주는 Kotlin DSL을 제공한다.
프로젝트 자체(org.gradle.api.Project)와 연관된 속성의 집합
프로젝트 전체에 사용할 수 있는 extra 속성
val myProperty; String by project // project 속성 myProperty를 사용 가능하게 만들기val myNullableProperty: String? by project // 널이 될 수 있는 속성을 사용 가능하게 만들기val myNewProperty by extra("initial value") // extra 속성 myNewProperty를 만들고 초기화val myOtherNewProperty by extra { "lazy initial value" } // 처음 접근이 일어날 때 초기화되는 속성 생성
속성 대리자 생성
속성 대리자 생성 방법은 간단하지만 이미 표준 라이브러리에 들어 있거나 또는 그레이들과 같은 서드파티가 제공하는 대리자를 사용하게 될 가능성이 높다.