Kotlin Collection - associateBy 와 groupBy

1. Kotlin Docs

associateBy 와 groupBy 의 코드를 비교해보자.

public inline fun <T, K> Iterable<T>.associateBy(keySelector: (T) -> K): Map<K, T> {
    val capacity = mapCapacity(collectionSizeOrDefault(10)).coerceAtLeast(16)
    return associateByTo(LinkedHashMap<K, T>(capacity), keySelector)
}

public inline fun <T, K> Iterable<T>.groupBy(keySelector: (T) -> K): Map<K, List<T>> {
    return groupByTo(LinkedHashMap<K, MutableList<T>>(), keySelector)
}

두 함수의 공통점은, Map 을 반환한다는 것이다. 차이점은, associateBy 는 T 를 value 로 반환하는 반면, groupBy 는 List 를 value 로 반환한다는 것이다. 즉, associateBy 는 Iterable 에서 T 가 unique 하다고 가정하는 반면, groupBy 는 duplicates 가 있다고 가정하고 있다. 아래 예제를 살펴보자.

2. Examples

아래와 같이 Person class 를 정의하고, personList 에 Person 객체들을 집어넣자.

class Person(
    val name: String,
    val isGood: Boolean
) {
    override fun toString(): String {
        return "Person(name='$name', isGood=$isGood)"
    }
}

fun main() {

    val personList = mutableListOf<Person>()
    for (i in 1..3) {
        val name = "name$i"
        val boolean = i % 2 == 0
        val person = Person(name, boolean)
        personList.add(person)
    }
}

위의 코드에서 name 은 unique 하고, isGood 은 dupicates 가 있다. 그러므로 associateBy 는 name 에, groupBy 는 isGood 에 적합하다. 아래와 같이 말이다.

val personAssociateByName = personList.associateBy { it.name }
val personAssociateByIsGood = personList.associateBy { it.isGood }

println(personAssociateByName)
println(personGroupByIsGood)
{name1=Person(name='name1', isGood=false), name2=Person(name='name2', isGood=true), name3=Person(name='name3', isGood=false)}
{false=[Person(name='name1', isGood=false), Person(name='name3', isGood=false)], true=[Person(name='name2', isGood=true)]}

associateBy 를 쓸 때 key 가 unique 하지 않아도 괜찮을까? 아래에서 확인해보자.

val personAssociateByIsGood = personList.associateBy { it.isGood }
println(personAssociateByIsGood)
{false=Person(name='name3', isGood=false), true=Person(name='name2', isGood=true)}

unique 하지 않아도 Exception 은 발생하지 않는다. 하지만 Map 의 key 는 unique 해야 하기 때문에 각 key 에 해당하는 마지막 values 만 반영된다.

groupBy 를 key 가 unique 할 때 사용하면 associateBy 와 같은 결과를 반환할까? 아래에서 확인해보자.

val personAssociateByName = personList.associateBy { it.name }
val personGroupByName = personList.groupBy { it.name }
pritnln(personAssociateByName)
println(personGroupByName)
{name1=Person(name='name1', isGood=false), name2=Person(name='name2', isGood=true), name3=Person(name='name3', isGood=false)}
{name1=[Person(name='name1', isGood=false)], name2=[Person(name='name2', isGood=true)], name3=[Person(name='name3', isGood=false)]}

언뜻 보면 같은 것을 반환하는 것으로 보인다. 하지만 1. Kotlin Docs 를 보면 알 수 있듯이, associateBy 가 반환하는 Map 의 value 는 T, 여기서는 Person 객체이며, groupBy 의 그것은 List, 여기서는 List 이다.

3. Applications

associateBy 에는 여러 용례가 있지만 여기서는 서로 다른 두 컬렉션을 합칠(merge) 때 사용한다. Person 객체와 Address 객체를 합쳐서 PersonAddress 객체를 만들어보자.

class Person(
    val name: String,
    val isGood: Boolean
) {
    override fun toString(): String {
        return "Person(name='$name', isGood=$isGood)"
    }
}

class Address(
    val name: String,
    val address: String
) {
    override fun toString(): String {
        return "Address(name='$name', address='$address')"
    }
}

class PersonAddress(
    val name: String,
    val address: String,
    val isGood: Boolean
) {
    override fun toString(): String {
        return "PersonAddress(name='$name', address='$address', isGood=$isGood)"
    }
}

fun main() {
    val personList = mutableListOf<Person>()
    for (i in 1..3) {
        val name = "name$i"
        val boolean = i % 2 == 0
        val person = Person(name, boolean)
        personList.add(person)
    }

    val addressList = mutableListOf<Address>()
    for (i in 1..3) {
        val name = "name$i"
        val address = "address$i"
        val addressElement = Address(name, address)
        addressList.add(addressElement)
    }

    val personAssociateByName = personList.associateBy { it.name }
    val addressAssociateByName = addressList.associateBy { it.name }

    val personWithAddress = personAssociateByName.map {
        (name, person) ->
        val correspondingAddress = addressAssociateByName[name]!!
        PersonAddress(
            name = name,
            address = correspondingAddress.address,
            isGood = person.isGood
        )
    }
    println(personWithAddress)
}
[PersonAddress(name='name1', address='address1', isGood=false), PersonAddress(name='name2', address='address2', isGood=true), PersonAddress(name='name3', address='address3', isGood=false)]

groupBy 는 key 별로 갯수를 세고 싶을 때 사용한다.

    println(
        personList
            .groupBy { it.isGood }
            .mapValues { it.value.size }
    )
{false=2, true=1}