-
4. 코틀린에서의 FPKotlin/자바 개발자를 위한 코틀린 입문 2023. 7. 26. 19:20
15. 코틀린에서 배열과 컬렉션을 다루는 방법
1. 배열
- 사실 배열은 잘 사용하지 않는다.
for (i in array.indices) { println("${i} ${array[i]}") }
- array.indices 는 0부터 마지막 index까지의 Range이다.
for ((idx, value) in array.withIndex()) { println("$idx $value") }
- withIndex() 를 사용하면, 인덱스와 값을 한 번에 가져올 수 있다.
2. 코틀린에서의 Collection - List, Set, Map
- 컬렉션을 만들어줄 때 불변인지, 가변인지를 설정해야 한다.
ㆍ가변(Mutable) 컬렉션 : 컬렉션에 element를 추가, 삭제할 수 있다.
ㆍ불변 컬렉션 : 컬렉션에 element를 추가, 삭제할 수 없다.
- 불변 컬렉션은 Collection을 만들자마자 Collection.unmodifiableList() 등을 붙여준다!
- 불변 컬렉션이라 하더라도 Reference Type인 Element의 필드는 바꿀 수 있다.
- Kotlin은 불변/가변을 지정해 주어야 한다느 사실을 기억하자!
List
val numbers = listOf(100, 200) val emptyList = emptyList<Int>()
- listOf를 통해 '불변 리스트'를 만든다.
- emptyList<타입>()
fun main() { printNumbers(emptyList()) } private fun printNumbers(numbers: List<Int>) { }
- 타입을 추론할 수 있다면 생략 가능하다.
- 가변리스트를 만들고 싶다면?
// 가변 리스트 val numbers2 = mutableListOf(100, 200) numbers2.add(300)
- 기본 구현체는 ArrayList이고 기타 사용법은 Java와 동일하다.
* 간단한 TIP
: 우선 불변 리스트를 만들고, 꼭 필요한 경우 가변 리스트로 바꾸자!
Set
- 집합은 List와 다르게 순서가 없고, 같은 element는 하나만 존재할 수 있다.
- 자료구조적 의미만 제외하면 모든 기능이 List와 비슷하다.
// 3. Set val numbers3 = setOf(100, 200) // For Each for (number in numbers3) { println(number) } // 전통적인 For 문 for ((index, number) in numbers3.withIndex()) { println("$index $number") } // 가변집합을 만들고 싶다면? val numbers4 = mutableSetOf(100, 200)
- 기본 구현체는 LinkedHashSet 이다.
Map
- Kotlin도 MutableMap을 만들어 넣을 수도 있고, 정적 팩토리 메소드를 바로 활용할 수도 있다.
val oldMap = mutableMapOf<Int, String>() oldMap[1] = "MONDAY" oldMap[2] = "TUESDAY" mapOf(1 to "MONDAY", 2 to "TUESDAY")
- 타입을 추론할 수 없어, 타입을 지정해주었다.
- 가변 Map 이기 때문에 (key, value)를 넣을 수 있다.
- Java처럼 put을 쓸 수도 있고, map[key] = value를 쓸 수도 있다.
- mapOf(key to value)를 사용해 불변 map을 만들 수 있다.
3. 컬렉션의 null 가능성, Java와 함께 사용하기
- List<Int?> : 리스트에 null이 들어갈 수 있지만, 리스트는 절대 null이 아님
- List<Int>? : 리스트에는 null이 들어갈 수 없지만, 리스트는 null일 수 있음
- List<Int?>? : 리스트에 null이 들어갈 수도 있고, 리스트가 null일 수도 있음
- ? 위치에 따라 null 가능성 의미가 달라지므로 차이를 잘 이해해야한다.
- Java는 읽기 전용 컬렉션과 변경 가능 컬렉션을 구분하지 않는다.- Java는 nullable 타입과 non-nullable 타입을 구분하지 않는다.
- Kotlin 쪽의 컬렉션이 Java에서 호출되면 컬렉션 내용이 변할 수 있음을 감안해야 한다.
- 코틀린 쪽에서 Collections.unmodifiableXXX() 를 활용하면 변경 자체를 막을 수는 있다!
- Kotlin에서 Java 컬렉션을 가져다 사용할 때 플랫폼 타입을 신경써야 한다.
- Java 코드를 보며, 맥락을 확인하고 Java 코드를 가져오는 지점을 wrapping 한다.
정리
- 배열의 사용법이 약간 다르다!
- 코틀린에서는 컬렉션을 만들 때도 불변/가변을 지정해야 한다.
- List, Set, Map 에 대한 사용법이 변경, 확장되었다.
- Java와 Kotlin 코드를 섞어 컬렉션을 사용할 때에는 주의해야 한다.
ㆍJava에서 Kotlin 컬렉션을 가져갈 때는 불변 컬렉션을 수정할 수도 있고, non-nullable 컬렉션에 null을 넣을 수도 있다.
ㆍKotlin에서 Java 컬렉션을 가져갈 때는 플랫폼타입을 주의해야 한다.
16. 코틀린에서 다양한 함수를 다루는 방법
1. 확장함수
- 어떤 클래스안에 있는 메소드처럼 호출할 수 있지만, 함수는 밖에 만들 수 있게 하자!
fun String.lastChar(): Char { return this[this.length - 1] }
- String 클래스를 확장
- 함수 안에서는 this를 통해 인스턴스에 접근 가능하다!
fun 확장할려는클래스(수신객체타입).함수이름(파라미터): 리턴타입 { // this(수신객체)를 이용해 실제 클래스 안의 값에 접근 }
- 확장함수는 클래스에 있는 private 또는 protected 멤버를 가져올 수 없다!!
public class Person { private final String firstName; private final String lastName; private int age; public Person(String firstName, String lastName, int age) { this.firstName = firstName; this.lastName = lastName; this.age = age; } public int nextYearAge() { System.out.println("멤버 함수"); return this.age + 1; } }
fun Person.nextYearAge(): Int { println("확장 함수") return this.age + 1 } fun main() { val person = Person("A", "B",100) println(person.nextYearAge()) // 멤버 함수 }
- 멤버함수가 우선적으로 호출된다!
- 확장함수를 만들었지만, 다른 기능의 똑같은 멤버함수가 생기면?! 오류가 발생할 수 있다.
val train: Train = Train() train.isExpensive() // Train의 확장함수 val srt1: Train = Srt() srt1.isExpensive() // Train의 확장함수 val srt2: Srt = Srt() srt2.isExpensive() // Srt의 확장함수
- 해당 변수의 현재 타입. 즉, 정적인 타입에 의해 어떤 확장함수가 호출될지 결정된다.
확장함수 중간정리
1. 확장함수는 원본 클래스의 private, protected 멤버 접근이 안된다!
2. 멤버함수, 확장함수 중 멤버함수에 우선권이 있다!
3. 확장함수는 현재 타입을 기준으로 호출된다.
- Java에서는 Kotlin 확장함수를 가져다 사용할 수 있나?!
public static void main(String[] args) { StringUtilsKt.lastChar("ABC"); }
- 정적 메소드를 부르는 것처럼 사용 가능하다.
- 확장함수 라는 개념은 확장 프로퍼티와도 연결
fun String.lastChar(): Char { return this[this.length - 1] } val String.lastChar: Char get() = this[this.length - 1]
- 확장 프로퍼티의 원리는 확장함수 + custom getter와 동일하다!
2. infix 함수
- 중위함수, 함수를 호출하는 새로운 방법!!
- downTo, step 도 함수이다! ( 중위 호출 함수 )
- 변수.함수이름(argument) 대신 변수 함수이름 argument
fun Int.add(other: Int): Int { return this + other } infix fun Int.add2(other: Int): Int { return this + other } fun main() { 3.add(4) 3.add2(4) 3 add2 4 }
- Infix는 멤버함수에도 붙일 수 있다!!
3. inline 함수
- 함수가 호출되는 대신, 함수를 호출한 지점에 함수 본문을 그대로 복붙하고 싶은 경우!
- 함수를 파라미터로 전달할 때에 오버헤드를 줄일 수 있다.
- 하지만 inline 함수의 사용은 성능 측정과 함께 신중하게 사용되어야 한다.
4. 지역함수
- 함수안에 함수를 선언할 수 있다.
fun createPerson(firstName: String, lastName: String): Person { // if(firstName.isEmpty()) { // throw IllegalArgumentException("firstName은 비어있을 수 없습니다! 현재 값 : $firstName") // } // if(lastName.isEmpty()) { // throw IllegalArgumentException("lastName 비어있을 수 없습니다! 현재 값 : $lastName") // } fun validateName(name: String, fieldName: String) { if (name.isEmpty()) { throw IllegalArgumentException("$firstName 은 비어있을 수 없습니다! 현재값 : $name") } } validateName(firstName, "firstName") validateName(lastName, "lastName") return Person(firstName, lastName, 1) }
- 함수로 추출하면 좋을 것 같은데, 이 함수를 지금 함수 내에서만 사용하고 싶을 때
- 하지만 depth가 깊어지기도 하고, 코드가 그렇게 깔끔하지 않다..
정리
- Java 코드가 있는 상황에서, Kotlin 코드로 추가 기능 개발을 하기 위해 확장함수와 확장프로퍼티가 등장했다.
- 확장함수는 원본 클래스의 private, protected 멤버 접근이 안된다!
- 멤버함수, 확장함수 중 멤버함수에 우선권이 있다!
- 확장함수는 현재 타입을 기준으로 호출된다!
- Java에서는 static 함수를 쓰는 것처럼 Kotlin의 확장함수를 쓸 수 있다.
- 함수 호출 방식을 바꿔주는 infix 함수가 존재한다.
- 함수를 복사-붙여넣기 하는 inline 함수가 존재한다.
- Kotlin에서는 함수 안에 함수를 선언할 수 있고, 지역함수라고 부른다.
17. 코틀린에서 람다를 다루는 방법
1. Java에서 람다를 다루기 위한 노력
- java에서는 함수는 변수에 할당되거나 파라미터로 전달할 수 없다. ( 2급 시민 )
2. 코틀린에서의 람다
- Java와는 근본적으로 다른 한 가지가 있다.
- 코틀린에서는 함수가 그 자체로 값이 될 수 있다. 변수에 할당할수도, 파라미터로 넘길 수도 있다. (1급 시민)
// 람다를 만드는 방법 1 val isApple: (Fruit) -> Boolean = fun(fruit: Fruit): Boolean { return fruit.name == "사과" } // 람다를 만드는 방법2 val isApple2: (Fruit) -> Boolean = { fruit: Fruit -> fruit.name == "사과" } // 람다를 직접 호출하는 방법1 isApple(fruit[0]) // 람다를 직접 호출하는 방법2 isApple.invoke(fruit[0])
- 함수의 타입: (파라미터 타입...) -> 반환타입
private fun filterFruits( fruits: List<Fruit>, filter: (Fruit) -> Boolean ): List<Fruit> { val results = mutableListOf<Fruit>() for (fruit in fruits) { if(filter(fruit)) { results.add(fruit) } } return results }
fun main() { val isApple: (Fruit) -> Boolean = fun(fruit: Fruit): Boolean { return fruit.name == "사과" } filterFruits(fruits, isApple) filterFruits(fruits) { it.name == "사과" } filterFruits(fruits) { fruit -> println("사과") fruit.name == "사과" } }
- 마지막 파라미터가 함수인 경우, 소괄호 밖에 람다 사용가능
- 람다를 작성할때, 람다의 파라미터를 it으로 직접 참조할 수 있다.
- 람다를 여러줄 작성할 수 있고, 마지막 줄의 결과가 람다의 반환값이다.
3. Closure
String targetFruitName = "바나나"; targetFruitName = "수박"; filterFruits(fruits, (fruit) -> targetFruitName.equals(fruit.getName())); // Variable used in lambda expression should be final or effectively final
- Java에서는 람다를 쓸 때 사용할 수 있는 변수에 제약이 있다
var targetFruitName = "바나나" targetFruitName = "수박" filterFruits(fruits) { it.name == targetFruitName }
- 코틀린에서는 아무런 문제 없이 동작한다!
- 코틀린에서는 람다가 시작하는 지점에 참조하고 있는 변수들을 모두 포획하여 그 정보를 가지고 있다.
- 이렇게 해야만, 람다를 진정한 일급 시민으로 간주할 수 있다.
ㆍ이 데이터 구조를 Closure라고 부른다
4. 다시 try with resources
- use 구현체
public inline fun <T: Closeable?, R> T.use(block: (T) -> R): R {
ㆍCloseable 구현체에 대한 확장함수이다.
ㆍInline 함수이다.
ㆍ람다를 받게 만들어진 함수이다.
fun readFile(path: String) { BufferedReader(FileReader(path)).use { reader -> println(reader.readLine()) } }
정리
- 함수는 Java에서는 2급시민이지만, 코트린에서는 1급 시민이다.
ㆍ때문에, 함수 자체를 변수에 넣을 수도 있고, 파라미터로 전달할 수도 있다.
- 코틀린에서 함수 타입은 (파라미터 타입, ...) -> 반환타입 이었다.
- 코틀린에서 람다는 두 가지 방법으로 만들 수 있고, {} 방법이 더 많이 사용된다.
- 함수를 호출하며, 마지막 파라미터인 람다를 쓸 때는 소괄호 밖으로 람다를 뺄 수 있다.
ㆍit을 쓰기보다는 화살표를 쓰는 방법을 더 추천한다. ( it 데이터가 어떤 데이터인지 모를 수 도 있다. )
- 람다의 마지막 expression 결과는 람다의 반환 값이다.
- 코트린에서는 Closure를 사용하여 non-final 변수도 람다에서 사용할 수 있다.
18. 코틀린에서 컬렉션을 함수형으로 다루는 방법
1. 필터와 맵
- filter / filterIndexed / map / mapIndexed / mapNotNull
2. 다양한 컬렉션 처리 기능
- all : 조건을 모두 만족하면 true 그렇지 않으면 false
// all val isAllApple = fruits.all {fruit -> fruit.name == "사과" }
- none : 조건을 모두 불만족하면 true 그렇지 않으면 false
// none val isNoApple = fruits.none {fruit -> fruit.name == "사과" }
- any : 조건을 하나라도 만족하면 true 그렇지 않으면 false
// any val isNoApple2 = fruits.any {fruit -> fruit.factoryPrice >= 10_000 }
// count val fruitCount = fruits.count() // sortedBy : (오름차순) 정렬을 한다. val fruitSort1 = fruits.sortedBy { fruit -> fruit.currentPrice } // sortedByDescending : (내림차순) 정렬을 한다 val fruitSort2 = fruits.sortedByDescending { fruit -> fruit.currentPrice } // distinctBy : 변형된 값을 기준으로 중복을 제거한다. val distinctFruitNames = fruits.distinctBy { fruit: Fruit -> fruit.name } .map { fruit: Fruit -> fruit.name } // first : 첫번째 값을 가져온다 (무조건 null이 아니어야함) fruits.first() // firstOrNull : 첫번째 값 또는 null을 가져온다 fruits.firstOrNull() // last : 마지막 값을 가져온다 (무조건 null이 아니어야함) fruits.last() // lastOrNull : 첫번째 값 또는 null을 가져온다 fruits.lastOrNull()
3. List를 Map으로
- groupBy / associateBy
// 과일이름 -> List<과일> 의 Map val map: Map<String, List<Fruit>> = fruits.groupBy { fruit -> fruit.name } // id -> 과일 의 Map val map2: Map<Long, Fruit> = fruits.associateBy { fruit -> fruit.id } // 과일이름 -> List<출고가> Map val map3: Map<String, List<Long>> = fruits.groupBy({fruit -> fruit.name}, {fruit -> fruit.factoryPrice}) // id -> 출고가 Map val map4: Map<Long, Long> = fruits.associateBy({fruit -> fruit.id}, {fruit -> fruit.factoryPrice}) val map5: Map<String, List<Fruit>> = fruits.groupBy { fruit -> fruit.name } .filter { (key, value) -> key == "사과" }
4. 중첩된 컬렉션 처리
- flatMap
val samePriceFruits = fruitsInList.flatMap { list -> list.filter { fruit -> fruit.factoryPrice == fruit.currentPrice } }
data class Fruit( val id: Long, val name: String, val factoryPrice: Long, val currentPrice: Long, ) { fun nullOrValue(): Unit? { TODO("Not yet implemented") } val isSamePrice: Boolean get() = factoryPrice == currentPrice } val List<Fruit>.samePriceFilter: List<Fruit> get() = this.filter(Fruit::isSamePrice) fun main() { val samePriceFruits2 = fruitsInList.flatMap { list -> list.samePriceFilter } }
- flatten
// List<List<Fruit>>를 List<Fruit>로 변경 fruitsInList.flatten()
정리
- filter / filterIndexed
- map / mapIndexed / mapNotNull
- all / none / any
- count / sortedBy / sortedByDescending / distinct
- first / firstOrNull / last / lastOrNull
- groupBy / associateBy
- flatMap / flatten
'Kotlin > 자바 개발자를 위한 코틀린 입문' 카테고리의 다른 글
5. 추가적으로 알아두어야 할 코틀린 특성 (0) 2023.07.29 3. 코틀린에서의 OOP (0) 2023.07.10 2. 코틀린에서 코드를 제어하는 방법 (0) 2023.07.06 1. 코틀린에서 변수와 타입, 연산자를 다루는 방법 (0) 2023.07.05