코틀린 표준 라이브러리는 기본 컬렉션 타입인 List, Set, Map을 제공합니다.
또한 컬렉션은 두가지 종류로 나뉩니다.
불변 컬렉션(Immutable) : 읽기 전용 컬렉션
가변 컬렉션(Mutable) : 삽입, 수정, 삭제와 같은 쓰기 작업이 가능한 컬렉션
컬렉션을 생성할때 가장 일반적인 방법은 표준 라이브러리 함수를 사용입니다.
Immutable List 생성
val currencyList: List<String> = listOf("달러", "유로", "원")
Mutable List 생성
val mutableCurrencyList = mutableListOf<String>()
mutableCurrencyList.add("달러")
mutableCurrencyList.add("유로")
mutableCurrencyList.add("원")
Mutable 리스트에서 apply 메서드를 사용하면 가독성이 좋아집니다.
val mutableCurrencyList = mutableListOf<String>().apply {
add("달러")
add("유로")
add("원")
}
Immutable Set 생성
val numberSet = setOf(1, 2, 3, 4)
Mutable Set 생성
val mutableSet = mutableSetOf<Int>()
Immutable Map 생성
코틀린의 Map에서는 to 라는 중위 함수로 key - value 구조를 전달합니다.
val numberMap = mapOf("one" to 1, "two" to 2)
Mutable Map 생성
코틀린에서는 map.put("key",value)보다 아래와 같은 문법으로 key value를 선언하는 것을 선호합니다.
val mutableNumberMap = mutableMapOf<String, Int>()
mutableNumberMap["one"] = 1
mutableNumberMap["two"] = 2
mutableNumberMap["three"] = 3
코틀린은 컬렉션 빌더를 사용하여 컬렉션을 생성할 수 있습니다.
buildList, buildSet, buildMap 3종류를 제공하며 build 내부에선 Mutable 즉 가변이고 반환시엔 Immutable 불변입니다.
// numberList는 ImmutableList
val numberList: List<Int> = buildList {
// buildList 내부는 MutableList
add(1)
add(2)
add(3)
}
linkedList나 ArrayList 등 특정 구현체를 사용하고 싶은 경우 구현체의 생성자를 사용하면 됩니다.
val linkedList = LinkedList<Int>().apply {
add(1)
add(2)
add(3)
}
코틀린의 컬렉션은 Iterable의 구현체이므로 순차적 반복이 가능합니다.
val iterator = currencyList.iterator()
while (iterator.hasNext()) {
println(iterator.next())
}
하지만 위와 같은 코드는 가독성이 좋지 않습니다.
코틀린도 자바에서 가장 많이 쓰이는 문법인 foreach를 사용한 iterable을 구현할 수 있기에 더 간결한 코드를 생성할 수 있습니다.
for (currency in currencyList) {
println(currency)
}
코틀린 표준 라이브러리에는 컬렉션 사용시 자주 사용되는 패턴인 forEach, map, filter 와 같은 유용한 인라인 함수를 제공합니다.
currencyList.forEach {
println(it)
}
inline method - map
val lowerList = listOf("a", "b", "c", "d")
val upperList = mutableListOf<String>()
val upperList = lowerList.map { it.uppercase() }
println(upperList)
// [A, B, C, D]
inline method - filter
val filteredList = upperList.filter { it == "A" || it == "C" }
println(filteredList)
// [A, C]
자바8의 stream
자바의 스트림은 중간 연산자(map, filter, flatMap 등)만 사용했을땐 아무런 동작도 하지 않고 terminal operator를 사용해야 값을 얻어낼 수 있습니다.
val filteredList = upperList.stream().filter { it == "A" || it == "C" }; //
println(filteredList)
java.util.stream.ReferencePipeline$2@77b52d12
val filteredList = upperList.stream().filter { it == "A" || it == "C" }.collect(Collectors.toList());
println(filteredList)
// [A, C]
코틀린에서도 sequence를 사용해 자바의 stream과 같이 Lazy하게 동작시킬 수 있습니다.
또 시퀀스 API도 자바의 stream API와 같이 최종 연산자를 사용해야 중간 연산자가 동작합니다.
val filteredList = upperList.asSequence().filter { it == "A" || it == "C" }
println(filteredList)
// kotlin.sequences.FilteringSequence@7f560810
val filteredList = upperList.asSequence().filter { it == "A" || it == "C" }.toList()
println(filteredList)
// [A, C]
일반적으로 인라인 함수는 각각 함수가 동작할때마다 조건에 맞는 컬렉션을 생성합니다.
upperList
.filter { it == "A" } //컬렉션 생성
.filter { it == "C" } //컬렉션 생성
.filter { /.../ } //컬렉션 생성
// 총 3개의 컬렉션 생성, 원본 데이터 * 3의 메모리 사용
시퀀스 API는 각각의 함수가 동작할때 시퀀스를 생성하고 최종 연산자를 호출할때 1개의 컬렉션을 생성합니다.
upperList.asSequence()
.filter { it == "A" }
.filter { it == "C" }
.filter { /.../ }
.toList() //컬렉션 생성
// 하나의 컬렉션만 생성
일반적으론 인라인 함수가 빠르기 때문에 인라인 함수를 쓰고 대량의 데이터를 다룰때는 시퀀스 API를 사용을 권장합니다.
데이터를 보관하거나 전달하는 목적을 가진 객체를 만들때 사용합니다. ex) DTO
data class Person(val name: String, val age: Int)
데이터 클래스를 사용하면 컴파일러가 equals(), hashCode(), toString(), componentN(), copy()와 같은 메서드들을 자동으로 생성해줍니다.
기존 자바에선 주로 Lombok을 사용
@Data
public class Person {
private final String name;
private final int age;
}
또 제가 가장 좋아하는 record 클래스도 JDK 15 이상부터 지원해줍니다.
public record Person(String name, int age) {
}
코틀린의 데이터 클래스는 데이터 저장을 목적으로 하는 클래스는 일반적으로 3가지 함수 toString, equals, hashCode를 재정의하는데 데이터 클래스를 사용하면 자동으로 생성해줍니다.
일반 클래스에서 toString, equals, hashCode를 쓸 경우 직접 구현하거나 IDE를 통해 생성해야합니다.
일반적으로 두개의 인스턴스의 동등성 비교를 위해 equals를 재정의합니다.
객체의 동등성 비교시 결과에 대한 차이 일반 클래스
class Person(val name: String, val age: Int)
fun main() {
val person1 = Person(name = "tony", age = 12)
val person2 = Person(name = "tony", age = 12)
println(person1 == person2)
} // false
data class Person(val name: String, val age: Int)
fun main() {
val person1 = Person(name = "tony", age = 12)
val tony2 = Person(name = "tony", age = 12)
println(person1 == person2)
} // true
equals를 재정의할때 반드시 hashCode도 재정의 해야합니다.
JVM 언어 기준으로 객체 비교시 equals로 true를 반환하는 객체는 hashCode도 같아야합니다.
equals가 true인데 hashCode가 다르다면 Hash계열 자료구조에서 정상적으로 동작하지 않습니다. // ex) HashSet
copy 미사용
data class Person(var name: String, var age: Int)
fun main() {
val person1 = Person(name = "tony", age = 12)
val set = hashSetOf(person1)
println(set.contains(person1))
// true
person1.name = "strange"
println(set.contains(person1))
// false
}
copy 사용
data class Person(val name: String, val age: Int)
fun main() {
val person1 = Person(name = "tony", age = 12)
val person2 = person1.copy(name= "strange")
println(person2.toString())
// Person(name=strange, age=12)
}
componentN은 데이터 클래스에 정의된 프로퍼티를 정의된 순서대로 가져올 수 있습니다.
fun main() {
val person1 = Person(name = "tony", age = 12)
println("이름=${person1.component1()}, 나이=${person1.component2()}")
// 이름=tony, 나이=12
}
코틀린에서는 구조분해할당을 사용해 좀 더 쉽고 안전하게 변수를 선언할 수 있습니다.
data class Person(val name: String, val age: Int)
fun main() {
val person1 = Person(name = "tony", age = 12)
val (name, age) = person1
println("이름=${name}, 나이=${age}")
// 이름=tony, 나이=12
}
출처 : fastcampus