Developing Myself Everyday
article thumbnail

Collection


컬렉션은 일련의 객체를 담는 컨테이너로 사용됩니다. 컬렉션은 여러 객체를 저장하고 관리하며, 필요에 따라 데이터를 추가, 삭제, 검색, 정렬하는 등의 작업을 수행할 수 있습니다. Java와 Kotlin에서 제공하는 컬렉션 인터페이스와 클래스는 다양한 형태의 컬렉션을 다룰 수 있도록 다양한 기능을 제공합니다.컬렉션은 크게 List, Set, Map로 분류될 수 있습니다

 

이 3가지 Collection은 중복을 허용하는지, 순서가 보장되는지에 따라 구분할 수 있습니다.

 

 

Mutable(가변) vs Immutable(불변)

변수에서의 가변과 불변은 해당 값이 변경될 수 있는가에 따라 나뉩니다. 쉽게 말하자면  ‘read-only’와 ‘read&write’로 보면 됩니다. 렉션도 마찬가지입니다. 

 

읽기 전용으로 사용되는 리스트는 ‘List<T>’이며, 변경까지 가능한 리스트는 ‘MutableList<T>’입니다.

 

 List, Set, Map 모두 Mutable과 Immutable Collection을 지원합니다.

 

 

 

아래는 코틀린 컬렉션을 나타내는 그림입니다.

 

 

 

 

 

 

List


MutableList

List는 기본적으로 불변이지만 이를 변경할 수 있게 만든 것이 바로 MutableList입니다.

val mutableList: MutableList<Int> = mutableListOf(1, 2, 3)
mutableList.add(4)
mutableList[1] = 10

 

 

ArrayList

ArrayList는 중복을 허용하고 순서를 가지며 인덱스로 원소들을 관리합니다. 이 말을 들으면 우리는 바로 ArrayList는 Array와 매우 유사한 기능을 가지고 있다고 생각할 수 있습니다. ArrayList란 이름에서도 알 수 있듯이 Array의 이런 점, MutableList의 데이터가 저장될 때 필요에 의해 자동으로 늘어나며 순서를 가지는 점을 가지고 있습니다.

 

 

 Array와 가장 큰 차이점은 Array는 처음에 크기를 지정하면 그 크기는 변하지 않지만, ArrayList는 크기를 마음대로 조정할 수 있다는 차이가 있습니다. 다만 ArrayList는 내부적으로 Array들의 List로 구성되어 있기에 default 값인 10의 크기보다 더 많은 값을 추가하려 할 때에는 추가되는 값보다 더욱 더 큰 Array를 만들어서 데이터들을 옮기는 과정을 가져야 합니다. 이런 점에서 Array보다 느리기 때문에 Array로 처리가 가능할때는 가능하면 Array를 사용하는것이 좋습니다.

 

 

MutableList vs ArrayList

그렇다면 이 둘의 가장 큰 차이는 무엇일까요?

 

바로 MutableList는 클래스가 아닌 인터페이스이고 ArrayList가 이 MutableList 인터페이스를 구현하고 있다는 것입니다.

/**
 * Returns an empty new [MutableList].
 * @sample samples.collections.Collections.Lists.emptyMutableList
 */
@SinceKotlin("1.1")
@kotlin.internal.InlineOnly
public inline fun <T> mutableListOf(): MutableList<T> = ArrayList()

/**
 * Returns an empty new [ArrayList].
 * @sample samples.collections.Collections.Lists.emptyArrayList
 */
@SinceKotlin("1.1")
@kotlin.internal.InlineOnly
public inline fun <T> arrayListOf(): ArrayList<T> = ArrayList()

위의 코드를 보겠습니다. mutableListOfarrayListOf 모두 ArrayList의 인스터스를 생성합니다.

 

두 함수의 유일한 차이점은 arrayListOf()ArrayList를 실제 ArrayList로 반환하는 반면, mutableListOf()ArrayListMutableList로 반환하여 ArrayList의 일부만이 MutableList 인터페이스에 의해 표현한다는 것입니다.

 

실제로 구현상의 차이는 ArrayList에는 MutableList 인터페이스에 포함되지 않는 몇 가지 메서드(trimToSize 및 ensureCapacity)가 있는 것입니다.

 

철학적으로 볼 때, MutableList는 반환되는 객체의 "동작"에만 관심을 둡니다. 그저 "MutableList처럼 작동하는 것"을 반환합니다. 하지만 ArrayList는 객체의 "구조"에 관심을 둡니다. 이는 객체가 할당한 메모리를 직접 조작하는 것을 허용하며(trimToSize), 메모리 구조에 관여합니다.

 

이를 직관적으로 이해하면, 정확한 내부 구조에 신경을 쓸 이유가 없다면 인터페이스 버전인 mutableListOf()을 선호하는 것이 좋습니다. 즉, 어떤 것을 선택해야 할지 모르는 경우, 먼저 mutableListOf를 선택하는 것이 좋습니다.

 

 

LinkedList

 ArrayList는 데이터들이 순서대로 쭉 늘어선 배열의 형식을 취하고 있는 반면 LinkedList는 자료의 주소값으로 서로 서로 연결되어 있는 구조를 하고 있습니다. 

LinkedList는 양방향의 연결 리스트로 구성되어서 참조하려는 원소에 따라 처음부터 순방향이나 역순으로 순회할 수 있습니다.

 

다만, 큰 단점이 존재하는데  만약 데이터를 검색하려면 처음부터 끝까지 찾아가야 하기 때문에 시간이 많이 걸립니다.

 

순차접근을 하는 경우에도 LinkedList보다는 ArrayList가 훨씬 빠릅니다. n개의 자료를 저장할 때, ArrayList는 하나의 연속적인 묶음으로 묶어 자료를 저장하는 반면, LinkedList는 자료들을 저장 공간에 불연속적인 단위로 저장하게 됩니다. 그렇기 때문에 LinkedList는 메모리 이곳저곡에 산재해 저장되어 있는 노드들을 접근하는데 ArrayList보다는 긴 지연시간이 소모됩니다.

 

 

 

Map


Map은 key와 value를 짝지어 저장하는 Kotlin Collection입니다. Map의 key는 유일해야하며 동일한 key는 허용되지 않으며 순서를 보장하지 않습니다. Map은 get(index)도 지원하고 배열처럼 [index]도 지원합니다.

fun main() {
    val numbersMap = mapOf<String, String>("1" to "one", "2" to "two", "3" to "three")
    println("numbersMap: $numbersMap") // 출력: numbersMap: {1=one, 2=two, 3=three}
// val numbersMap = mutableMapOf<String, String>("1" to "one", "2" to "two", "3" to "three")
    val numbersMap2 = mapOf(Pair("1", "one"), Pair("2", "two"), Pair("3", "three"))
    println("numbersMap2: $numbersMap2") // 출력 numbersMap2: {1=one, 2=two, 3=three}
}

 

 

Map의 종류

Map에서 자주 사용하는 종류로는 TreeMap, HashMap, LinkedHashMap이 있습니다.

 

 

TreeMap

트리(Tree) 구조는 그래프의 일종으로, 한 노드에서 시작해서 다른 정점들을 순회하여 자기 자신에게 돌아오지 못하는 연결 그래프입니다. 

 

 

이진 탐색 트리의 삽입은 아래의 과정을 가집니다.

 

  1. 삽입할 값을 루트 노드와 비교해 같다면 오류를 발생한다( 중복 값 허용 X )
  2. 삽입할 값이 루트 노드의 키보다 작다면 왼쪽 서브 트리를 탐색해서 비어있다면 추가하고, 비어있지 않다면 다시 값을 비교한다.
  3. 삽입할 값이 루트노드의 키보다 크다면 오른쪽 서브트리를 탐색해서 비어있다면 추가하고, 비어있지 않다면 다시 값을 비교한다.

 

 

이렇게 만들어진 이진 탐색 트리를 출력하려고 해 보겠습니다. 출력을 하려면 전체 노드를 순회해야 합니다. 여기서 TreeMap은 중위 순회를 사용합니다.

 

중위 순회는 트리의 각 노드를 왼쪽 서브트리, 현재 노드, 오른쪽 서브트리 순으로 순회하는 방식입니다.

 

중위 순회의 방식과 이진 탐색 트리의 특성을 조합하면 우리는 정렬된 값을 얻을 수 있게 됩니다.

 

위의 트리를 중위 순회 방식으로 순회하면 1 -> 3 -> 4 -> 6 -> 7 -> 8 -> 10 -> 13 -> 14 를 얻을 수 있습니다.

 

 

TreeMap은 이진 검색 트리의 변형으로 2개의 각 노드의 자식 노드가 최대 2개인 이진 탐색 트리입니다. 그렇기에 TreeMap은 Key를 기준으로 정렬된 값을 가지게 됩니다.

fun main() {
    val treeMap = TreeMap<Int, String>()  //빈 treeMap 생성

    treeMap[3] = "사과"
    treeMap[2] = "바나나"
    treeMap[1] = "파인애플"

    println(treeMap) // 출력: {1=파인애플, 2=바나나, 3=사과}
}

 

 

HashMap

해시(Hash)는 임의의 길이를 가진 데이터를 고정된 길이의 값으로 매핑하는 것을 의미합니다. 해시 함수(hash function)는 이러한 매핑을 수행하는 함수로, 어떤 입력 데이터에 대해 고유한 해시 값을 생성합니다. 해시 함수를 통해 얻은 해시 값은 해시 코드 또는 해시 값이라고도 불립니다. 해시 함수는 일반적으로 빠르게 계산되어야 하며, 입력 데이터가 약간만 변경되어도 다른 해시 값을 생성하도록 설계되어야 합니다.

 

 

HashMap은 이러한 해시를 기반으로 만들어진 자바에서 제공하는 데이터 구조입니다.

val hashMap = HashMap<String, Int>()  //빈 HashMap 생성
val hashMap2 = HashMap<String, Int>(5) //초기 용량이 5인 HashMap 생성
val hashMap3 = HashMap<String, Int>(5,0.8f)   //초기 용량이 5이고 load factor가 0.8인 HashMap 생성
val hashMap4 = HashMap<String, Int>(hashMap)  //Map을 인자로 받아 HashMap 생성

 

HashMap은 빠른 접근 속도를 보이지만, 데이터를 입력 순서대로 저장하거나 정렬하여 저장하지 않기 때문에 이를 원하지 않을 때에는 사용하지 않습니다.

 

 

LinkedHashMap

LinkedHashMap 역시 해시 기법을 사용하여 구현된 Map입니다. HashMap과 다른 점은 기본적으로 데이터를 추가한 순서대로 데이터들을 보관한다는 점입니다. 

 

 

 

 

Set(집합)


Set는 순서가 없고 중복을 허용하지 않습니다.

fun main() {
    val set = setOf<String>("a", "b", "c", "d")
// val set = mutableSetOf<String>("a", "b", "c", "d") - Mutable Collection
    println(set) // 출력: [a, b, c, d]
    println("set.size: ${set.size}") // 출력: set.size: 4
    println("set.contains(a): ${set.contains("a")}") // 출력: set.contains(a): true
    println("set.isEmpty(): ${set.isEmpty()}") // 출력: set.isEmpty(): false
}

Set은 List와 같이 Index를 통한 객체 접근은 불가능합니다. 대신 iterator를 이용하여 객체에 차례대로 접근할 수 있습니다.

 

 

Set의 종류

Set에서 자주 사용하는 종류로는 TreeSet, HashSet. LinkedHashSet이 있습니다.

 

TreeSet은 TreeMap을 기반으로, HashSet은 HashMap을 기반으로, LinkedHashSet은 LinkedHashMap을 기반으로 구현되어 있습니다.

 

 

LinkedHashSet과 HashSet의 차이

LinkedHashSetHashSet의 가장 큰 차이는 LinkedHashSet이 바로 요소들의 순서를 유지한다는 것입니다. Set에 어떤 요소가 주입되거나 제거되는지에 상관없이 순서를 유지합니다. 반면 HashSet은 그렇지 않습니다.

 

아래의 예시를 보면 이해가 됩니다.

fun main() {
    val set: LinkedHashSet<Int> = linkedSetOf(1, 3, 2)

    println(set) // [1, 3, 2]

    set.remove(3)
    set += listOf(5, 4)
    println(set) // [1, 2, 5, 4]

    val hashSet: HashSet<Int> = hashSetOf(1, 3, 2)

    println(hashSet) // [1, 2, 3]

    hashSet.remove(3)
    hashSet += listOf(5, 4)
    println(hashSet) // [1, 2, 4, 5]
}

 

위의 예시에서 알 수 있듯이 HashSet과 달리 LinkedHashSet이 요소들의 순서를 유지하고 있다는 것을 알 수 있습니다. 

 

 

 

 

Stack(스택)


스택은 먼저 들어간 자료가 나중에 나오는 자료구조입니다. 자바 패키지에 있는 java.util.Stack을 활용해서 코틀린에서도 스택을 구현할 수 있습니다.

 

 

 

Method Description
empty() Stack이 비어있다면 true, 아니면 false를 반환한다.
peek() Stack에서 제거하지 않고 스택의 최상단(맨 위)에 있는 객체를 확인한다.
pop() Stack의 최상단에 있는 객체를 제거하고 해당 객체를 return한다.
push(E item) Stack에 최상단 항목으로 삽입한다.
serach(Object o) Object를 Stack에서 찾는 method로 최상단 객체를 1로 하여(1-based-position) 위치(position)를 찾아주며, 객체가 없다면 -1을 return한다.

 

import java.util.Stack

fun main() {
    val stack = Stack<Int>()

    // 스택에 요소 추가 (push)
    stack.add(1)
    stack.add(2)
    stack.add(3)

    // 스택의 상단 요소 확인 (peek)
    val topElement = stack.peek()
    println("상단 요소: $topElement") // 출력: 상단 요소: 3

    // 스택에서 요소 삭제 (pop)
    val poppedElement = stack.pop()
    println("삭제된 요소: $poppedElement") // 출력: 삭제된 요소: 3

    // 스택의 현재 상태 출력
    println("스택의 현재 상태: $stack") // 출력: 스택의 현재 상태: [1, 2]
}

 

 

 

 

Queue(큐)


큐는 먼저 들어간 자료가 먼저 나오는 자료구조입니다. FIFO(First In First Out, 선입선출) 또는 LILO(Last In Last Out, 후입후출) 라는 구조를 갖습니다.

 

 

코틀린에서는 Queue 인터페이스LinkedList를 사용해서 Queue를 구현합니다.

 

Method Description
add(E e) Queue에 객체를 추가한다. Queue의 남아 있는 space가 없을 경우 exception을 throw한다.
element() Queue에 가장 먼저 들어간 객체를 return한다.
offer(E e) Queue에 객체를 추가한다. 이때는 exception을 발생시키지 않고 성공 여부에 대해 return한다.
peek() Queue에 가장 먼저 들어간 객체를 제거하지 않고 확인한다.
poll() Queue에 가장 먼저 들어간 객체를 제거하고 해당 객체를 return한다. 이때 queue가 비어 있다면 null을 return한다.
remove() Queue에 가장 먼저 들어간 객체를 제거하고 해당 객체를 return한다. 이때 queue가 비어있다면 exception을 throw한다.

 

import java.util.LinkedList
import java.util.Queue

fun main() {
    // 링크드 리스트로 구현된 큐 생성
    val queue: Queue<Int> = LinkedList()

    // 큐에 요소 추가 (enqueue)
    queue.add(1)
    queue.add(2)
    queue.add(3)

    // 큐의 가장 앞쪽 요소 확인 (peek)
    val frontElement = queue.peek()
    println("가장 앞쪽 요소: $frontElement") // 출력: 가장 앞쪽 요소: 1

    // 큐에서 요소 제거 (dequeue)
    val removedElement = queue.poll()
    println("제거된 요소: $removedElement") // 출력: 제거된 요소: 1

    // 큐의 현재 상태 출력
    println("큐의 현재 상태: $queue") // 출력: 큐의 현재 상태: [2, 3]
}

 

 

 

 

Dequeue(덱)


dequeue는 양쪽 끝에서 삽입과 삭제가 모두 가능한 자료 구조입니다. Dequeue은 Stack처럼 쓰일 수도 있고, Queue처럼 쓰일 수도 있으며, Stack과 Queue의 기능을 합친 것처럼 사용할 수도 있습니다. 

 

자바 패키지에 있는 java.util.ArrayDeque를 활용해서 코틀린에서도 Dequeue을 구현할 수 있습니다.

import java.util.ArrayDeque

fun main() {
    val deque = ArrayDeque<Int>()

    // 덱의 뒷쪽에 요소 추가
    deque.addLast(1)
    deque.addLast(2)
    deque.addLast(3)

    // 덱의 앞쪽에 요소 추가
    deque.addFirst(0)

    // 덱의 앞쪽 요소 확인
    val frontElement = deque.peekFirst()
    println("앞쪽 요소: $frontElement") // 출력: 앞쪽 요소: 0

    // 덱의 뒷쪽 요소 확인
    val rearElement = deque.peekLast()
    println("뒷쪽 요소: $rearElement") // 출력: 뒷쪽 요소: 3

    // 덱의 앞쪽 요소 제거
    val removedFrontElement = deque.pollFirst()
    println("앞쪽 요소 제거: $removedFrontElement") // 출력: 앞쪽 요소 제거: 0

    // 덱의 뒷쪽 요소 제거
    val removedRearElement = deque.pollLast()
    println("뒷쪽 요소 제거: $removedRearElement") // 출력: 뒷쪽 요소 제거: 3

    // 덱의 현재 상태 출력
    println("덱의 현재 상태: $deque") // 출력: 덱의 현재 상태: [1, 2]
}

 

 

 

 

컬렉션 메서드 - 검색과 조건검사


컬렉션 상태 확인

메서드로 컬렉션의 상태를 확인할 수 있습니다. 

val myList = listOf(1, 2, 3)
println(myList.isEmpty())     // false
println(myList.isNotEmpty())  // true
println(myList.contains(1))  // true
println(myList.containsAll(listOf(1, 2, 4)))  // false
println(myList.size)          // 3

 

 

컬렉션 내부 순환

컬렉션의 내부 순환은 forEachforEachIndexed 메서드를 사용해 처리합니다. 인자로 람다 표현식을 받습니다.

// List 반복  
val myList = listOf("apple", "banana", "cherry")

myList.forEach { fruit ->
    print("$fruit ") // apple banana cherry 
}

println()

myList.forEachIndexed { index, fruit ->
    print("Index $index: $fruit ") // Index 0: apple Index 1: banana Index 2: cherry 
}

 

 

컬렉션 원소 조회

컬렉션의 인덱스로 원소를 조회하거나 여러 조건을 가지고 원소를 조회할 수 있습니다. 아래는 인덱스와 조건을 가지고 컬렉션의 원소를 조회한 예시입니다.

val myList = listOf("apple", "banana", "cherry")

val element1 = myList.get(1) // "banana"

val element2 = myList[1] // "banana"

val element3 = myList.elementAtOrElse(3) { "not found" } // "not found"

val firstElement = myList.first() // "apple"
val lastElement = myList.last()   // "cherry"

val foundElement = myList.find { it.startsWith("b") } // "banana"
val foundLastElement = myList.findLast { it.contains("e") } // "cherry"

 

 

Map 원소 조회

맵은 key와 value로 값이 나눠져 있기 때문에 원소를 조회하는 방법이 조금 다릅니다.

 

맵의 key를 기준으로 원소를 조회하려면 get를 사용하고 value를 기준으로 원소를 조회하려면 getValue를 사용합니다.

val myMap = mapOf("a" to 1, "b" to 2, "c" to 3)

val value1 = myMap.get("b")
println(value1) // 출력: 2

val value2 = myMap["c"]
println(value2) // 출력: 3

val value3 = myMap.getValue("a")
println(value3) // 출력: 1

 

 

그리고 맵의 key와 value들을 각각 Set의 형태와 Collection 형태로 만들 수 있습니다.

val keys = myMap.keys // 모든 키를 가져옴 (Set 형태)
val values = myMap.values // 모든 값을 가져옴 (Collection 형태)
println(keys) // 출력: [a, b, c]
println(values) // 출력: [1, 2, 3]

 

 

List를 Map으로 병합

2개의 리스트를 가지고 맵을 만들 수도 있습니다. `zip` 함수를 사용하면 두 리스트의 요소를 일대일로 연결하여 하나의 쌍으로 만들 수 있습니다. 

val keys = listOf("a", "b", "c")
val values = listOf(1, 2, 3)

val map = keys.zip(values).toMap()

// 결과 맵 출력
println(map) // {a=1, b=2, c=3}

 

 

컬렉션 조건 검사

컬렉션이 조건에 충족하는지를 Boolean으로 반환할 수 있습니다.

val numbers = listOf(1, 2, 3, 4, 5)

// 모든 요소가 조건을 충족하는지 검사하여 boolean 반환
val allEven = numbers.all { it % 2 == 0 }
println(allEven) // false

// 어떤 요소라도 조건을 충족하는지 검사하여 boolean 반환
val anyEven = numbers.any { it % 2 == 0 }
println(anyEven) // true

// 모든 요소가 조건을 충족하지 않는지 검사하여 boolean 반환
val noneEven = numbers.none { it % 6 == 0 }
println(noneEven) // true

// 조건을 충족하는 요소의 수를 세어 boolean 반환 (0이 아니면 true)
val hasEvenNumbers = numbers.count { it % 2 == 0 } > 0
println(hasEvenNumbers) // true

 

 

 

 

컬렉션 메서드 - 정렬, 삭제, 조인


컬렉션 정렬

컬렉션은 sort를 사용해 오름차순 정렬을 수행하고 sortDescending을 사용해 내림차순 정렬을 수행할 수 있습니다.

val mutableNumbers = mutableListOf(5, 1, 3, 2, 4)

// 오름차순 정렬 (원본 컬렉션 변경)
mutableNumbers.sort()
println(mutableNumbers) // 출력: [1, 2, 3, 4, 5]

// 내림차순 정렬 (원본 컬렉션 변경)
mutableNumbers.sortDescending()
println(mutableNumbers) // 출력: [5, 4, 3, 2, 1]

 

 

Drop

drop을 사용하여 특정 인덱스까지 컬렉션의 원소를 제외할 수 있습니다. dropWhile을 사용하면 조건을 만족할 때까지 삭제합니다.

val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

// drop 함수를 사용하여 처음 5개의 요소를 제외한 나머지 요소 가져오기
val drop = numbers.drop(5)
println(drop) // 출력: [6, 7, 8, 9, 10]

// dropWhile 함수를 사용하여 조건을 만족하는 요소 제외하기
val dropWhile = numbers.dropWhile { it <= 5 }
println(dropWhile) // 출력: [6, 7, 8, 9, 10]

 

 

Take

take

을 사용하여 특정 인덱스까지 컬렉션의 원소를 조회할 수 있습니다. takeWhile을 사용하면 조건을 만족할 때까지 조회합니다.

val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

// take 함수를 사용하여 처음 3개의 요소 가져오기
val take = numbers.take(3)
println(take) // 출력: [1, 2, 3]

// takeWhile 함수를 사용하여 조건을 만족하는 요소 가져오기
val takeWhile = numbers.takeWhile { it <= 5 }
println(takeWhile) // 출력: [1, 2, 3, 4, 5]

 

 

조인

joinToString을 사용하여 컬렉션을 문자열로 조인할 수 있습니다. 그리고 커스텀 구분자와 Prefix, Postfix를 사용해 원하는 대로 컬렉션을 조인할 수 있습니다.

 

joinTo를 사용하면 StringBuilder에 문자열을 추가할 수 있습니다.

val fruits = listOf("apple", "banana", "cherry")

// 요소를 쉼표로 구분하여 문자열로 조인
val joinedString = fruits.joinToString()
println(joinedString) // 출력: apple, banana, cherry

// 커스텀 구분자 및 프리픽스/서픽스 사용
val customJoinedString = fruits.joinToString(separator = " | ", prefix = "Fruits: ", postfix = "!")
println(customJoinedString) // 출력: Fruits: apple | banana | cherry!

// joinTo 사용
val joinTo = fruits.joinTo(buffer = StringBuilder(), separator = " | ", prefix = "Fruits: ", postfix = "!")
println(joinTo) // 출력: Fruits: apple | banana | cherry!

 

 

 

 

맵 리듀스 처리 - Map Task


맵 리듀스(Map-Reduce)는 대규모 데이터 집합을 병렬로 처리하는 데 사용되는 분산 컴퓨팅 모델입니다.

 

 

 

이제부터 코틀린에서 맵 리듀스를 처리하기 위한 컬렉션 메서드를 알아보겠습니다.

 

 

Map 연산

Map 연산은 컬렉션의 각 요소에 함수를 적용하여 새로운 컬렉션을 생성하는 과정입니다.

val numbers = listOf(1, 2, 3, 4, 5)

// 맵 연산: 각 요소를 제곱하여 새 리스트 생성
val squaredNumbers = numbers.map {
    print("$it ") // 출력: 1 2 3 4 5 
    it * it
}
println()

// 결과 출력
println(squaredNumbers) // 출력: [1, 4, 9, 16, 25]

 

 

filter

filter 함수를 사용해 컬렉션에서 특정 조건을 만족하는 요소만 필터링해 새로운 컬렉션을 사용할 수 있습니다

val numbers = listOf(1, 2, 3, 4, 5)

// 필터 연산: 각 요소를 방문해 리턴이 true인 값만 필터링
val filterNumbers = numbers.filter { it >= 3 }

// 결과 출력
println(filterNumbers) // 출력: [1, 4, 9, 16, 25]

 

 

flatMap

flatMap을 사용하면 중첩된 컬렉션을 하나의 평평한 컬렉션으로 변환할 수 있습니다.

 

아래의 예시는 string을 list로 변환하고 변환된 list들을 하나의 list로 변환하는 예시입니다.

val strings = listOf("abc", "de")
val flatMap = strings.flatMap { it.toList() }

println(flatMap) // [a, b, c, d, e]

 

 

 

 

맵 리듀스 처리 - Reduce Task


Reduce, fold

컬렉션 내의 원소들이 모두 변환이 된 후에 결과를 하나의 값으로 처리할 때 리듀스를 사용합니다.

 

이 때 사용되는 함수는 reducefold입니다. 두 함수의 차이는 reduce 함수는 초기값을 제공하지 않고 컬렉션의 첫 요소부터 축적을 시작하며, fold 함수는 초기값을 제공하고 초기값부터 컬렉션의 요소를 축적합니다.

val numbers = listOf(7, 4, 8, 1, 9)

val sum = numbers.reduce { total, num -> total + num }
println("reduced: $sum") // reduced: 29
val sumFromTen = numbers.fold(10) { total, num -> total + num }
println("folded: $sumFromTen") // folded: 39

 

 

이 둘의 사용이 확실하게 구분지어질 때는 컬렉션이 비어있을 가능성이 있을 때입니다.

val numbers = emptyList<Int>()

val sum = numbers.reduce { total, num -> total + num }
println("reduced: $sum") // Exception in thread "main" java.lang.UnsupportedOperationException
val sumFromTen = numbers.fold(10) { total, num -> total + num }
println("folded: $sumFromTen") // folded: 10

 

위의 예시를 보면 알 수 있습니다. 빈 컬렉션에 reduce를 사용하면 빈 컬렉션에 연산을 수행하려고 하기 때문에 컴파일러는 `UnsupportedOperationException` 예외를 발생합니다. 

 

반면에 fold는 초기값이 있기 때문에 컬렉션이 비어 있을 때 초기값을 그대로 반환하게 됩니다.

 

 

그룹화

컬렉션의 특정 원소를 기준으로 여러 그룹으로 나누고, 각 그룹에 해당하는 요소를 묶어서 새로운 데이터 구조를 생성할 수 있습니다. 아래는 groupBy를 사용해서 User의 age를 기준으로 그룹화한 예시입니다.

data class User(val name: String, val age: Int)

val users = listOf(
    User("Alice", 30),
    User("Bob", 25),
    User("Charlie", 30),
    User("David", 25)
)

fun main() {
    val groupedUsers = users.groupBy { it.age }

    groupedUsers.forEach {
        println(it)
    }
}
// 30=[User(name=Alice, age=30), User(name=Charlie, age=30)]
// 25=[User(name=Bob, age=25), User(name=David, age=25)]

 

 

groupByTo 함수를 사용하면 그룹화한 결과를 새로운 맵에 저장할 수 있습니다.

val ageGroups = mutableMapOf<Int, MutableList<User>>()

users.groupByTo(ageGroups) { it.age }

 

 

groupingBy 함수를 사용하면 Grouping이라는 별도 객체를 제공해서 다양한 처리 연산을 수행할 수 있습니다. 아래는 eachCount를 사용해 그룹화된 요소의 개수를 출력하는 예시입니다.

val ageGrouping: Grouping<User, Int> = users.groupingBy { it.age }

println(ageGrouping.eachCount()) // {30=2, 25=2}

 

 

 

 

시퀀스(Sequence)


Kotlin 표준 라이브러리에는 컬렉션과 다른 유형인 시퀀스가 있습니다. 

 

컬렉션과 달리 시퀀스는 요소를 포함하지 않고 반복(iterate)하는 동안 요소를 생성합니다. 시퀀스는 Iterable과 동일한 기능을 제공하지만 다단계 컬렉션 처리에 대한 다른 접근 방식을 구현합니다.

 

Iterable의 처리가 여러 단계를 포함하는 경우, 각 처리 단계는 즉시 실행됩니다. 각 처리 단계는 중간 컬렉션을 완료하고 반환합니다. 그 다음 단계는 이 컬렉션에서 실행됩니다. 반면 시퀀스의 다단계 처리는 가능한 한 지연 실행(Lazy Evaluation)됩니다. 실제 계산은 전체 처리 체인의 결과가 요청될 때만 발생합니다.

 

 

연산 실행 순서도 다릅니다. Iterable은 전체 컬렉션에 대해 각 단계를 완료한 다음 다음 단계로 진행합니다.

 

Interable의 연산

 

 

반면, 시퀀스는 각 요소에 대해 모든 처리 단계를 하나씩 순차적으로 수행합니다. 

 

Sequence의 연산

 

 

 

따라서 시퀀스를 사용하면 중간 단계의 결과를 만들지 않고 전체 컬렉션 처리 체인의 성능을 향상시킬 수 있습니다. 그러나 시퀀스의 지연 실행 특성은 더 작은 컬렉션을 처리하거나 간단한 계산을 수행할 때 상당한 오버헤드가 발생할 수 있습니다. 따라서 Sequence와 Iterable을 모두 고려하고 어떤 것이 해당 상황에 더 적합한지 결정해야 합니다.


지연 실행(Lazy Evalutation)이란?
지연 실행(Lazy Evaluation)은 프로그램이 요청되거나 필요한 시점에만 계산이 이루어지는 것을 의미합니다. 지연 실행은 데이터 처리나 계산 작업에서 성능 최적화와 메모리 관리를 개선하기 위해 사용되는 중요한 개념 중 하나입니다.

 

val numbersSequence = sequenceOf("four", "three", "two", "one")

 

 

시퀀스로 변환

asSequence를 사용하면 리스트를 시퀀스로 변환할 수 있습니다.

val numbers = listOf("one", "two", "three", "four")
val numbersSequence = numbers.asSequence()

 

 

시퀀스와 청크(chunk)

sequence {} 를 사용하면 일정한 요소 수 또는 크기로 시퀀스를 분할하여 처리할 수 있습니다.

 

이 함수는 yieldyieldAll 함수의 호출을 포함하는 람다 표현식을 인자로 사용합니다. 이러한 함수들은 시퀀스 소비자에게 요소를 반환하고, 다음 요소가 소비자에 의해 요청될 때까지 sequence()의 실행을 일시 중단합니다. yield()는 단일 요소를 인자로 사용하고, yieldAll()은 Iterable 객체, Iterator 또는 다른 Sequence를 인자로 사용할 수 있습니다.

val mySequence = sequence {
    println("반복")
    yield(1)
    yieldAll(listOf(2, 3))
    yield(false)
}

 

 

chunked를 사용하면 시퀀스를 일정한 크기로 나눌 수 있습니다.

val mySequence = sequence {
    for (i in 1..10) {
        yield(i)
    }
}

val chunks = mySequence.chunked(3)

for (chunk in chunks) {
    println(chunk)
}
// [1, 2, 3]
// [4, 5, 6]
// [7, 8, 9]
// [10]

 

 

 

 

 

 

Reference

 

From the Kotlin community on Reddit

Explore this post and more from the Kotlin community

www.reddit.com

 

Sequences | Kotlin

 

kotlinlang.org

 

profile

Developing Myself Everyday

@배준형

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!