인프런/자바 개발자를 위한 코틀린 입문

11) 코틀린에서 컬렉션을 함수형으로 다루는 방법

backend dev 2025. 1. 10.

1. 필터와 맵

 

자바에서 Stream과 함께 사용되는 filter, map 함수를

코틀린에서는 아래와 같이 사용한다.

 

// 익명함수를 전달해서 필터링
val apples = fruits.filter { fruit -> fruit.name == "사과" }

// 인덱스 또한 받아서 처리 가능
val apples2 = fruits.filterIndexed { index, fruit ->
    println(index)
    fruit.name == "사과"
}

// 자바에서 map의 반환은 Stream 이지만 코틀린에서는 List를 반환한다.
// 그래서 map 이후 .collect(Collectors.toList)를 안해도 된다.
val applePrices = fruits.filter{fruit -> fruit.name=="사과"}
    .map { fruit -> fruit.price }

 

 

 

 

 

코틀린에서는 filter와 map 같은 고차 함수들이 Stream을 사용하지 않고도 컬렉션에서 직접 사용할 수 있습니다.

코틀린은 컬렉션 API에 내장된 고차 함수들을 제공하여,

List, Set, Map 등과 같은 컬렉션에서 매우 쉽게 데이터를 처리할 수 있도록 합니다.

 

 

 

변경전 예시)

private fun filterFruits(
    fruits: List<Fruit>,
    funcName: (Fruit) -> Boolean
): List<Fruit> {

    val results = mutableListOf<Fruit>()
    
    for (fruit in fruits) {
        if (funcName(fruit)) {
            results.add(fruit)
        }
    }
    
    return results
    
}

 

변경후)

private fun filterFruits(
    fruits: List<Fruit>,
    funcName: (Fruit) -> Boolean
): List<Fruit> {

    return fruits
        .filter(funcName) // 바로 익명함수를 전달해줄수 있다.
//        .filter { fruit -> funcName(fruit) } 이처럼 작성할수도있지만 위가 더 깔끔하다.

}

 


 

2. 다양한 컬렉션 처리 기능

 

all  ->  조건을 모두 만족하면 true      그렇지 않으면 false

val isAllApple = fruits.all {fruit -> fruit.name == "사과"}

 

none ->  조건을 모두 불만족하면 true          그렇지 않으면 false

val isAllApple = fruits.none {fruit -> fruit.name == "사과"}

 

any ->  조건을 하나라도 만족하면 true          그렇지 않으면 false

val isAllApple = fruits.any {fruit -> fruit.name == "사과"}

 

val fruitCount = fruits.count();
//val fruitCount = fruits.size; count()와 동일

//  (오름차순) 정렬을 한다.
val sorted = fruits.sortedBy { fruit -> fruit.price }

//  (내림차순) 정렬을 한다.
val sortedDesc = fruits.sortedByDescending { fruit -> fruit.price }

// 변형된 값을 기준으로 중복을 제거한다. 이 예시는 이름을 기준으로 중복제거
val distinct = fruits.distinctBy { fruit -> fruit.name }

// 첫번째 값 가져오기 -> 무조건 null이 아니어야한다. null이라면 exception 발생
val first = fruits.first()

// 첫번째 값 또는 null을 가져온다.
val firstNullable = fruits.firstOrNull()

// 마지막 값 가져오기 -> 무조건 null이 아니어야한다. null이라면 exception 발생
val last = fruits.last()

// 마지막 값 또는 null을 가져온다.
val lastNullable = fruits.lastOrNull()

 


 

3. List를 Map으로

 

 

// List -> Map  ,  반환되는 값을 키값으로 하는 Map 반환 , 즉 과일이름으로 Map 만들기
val fruitMap = fruits.groupBy { fruit -> fruit.name }


fun main() {
    for (entry in fruitMap.entries) {
        println("key : ${entry.key} , value : ${entry.value}")
    }
}

 

 

/*
groupBy같이 map을 반환해준다.
groupBy는 value에 List가 들어갈 수 있지만 associateBy는 동일키를 가진 value를 만났을때
가장 마지막에 만난 요소로 대체해준다. 즉 value는 단일객체가 들어간다.
 */
val fruitMap2 = fruits.associateBy { fruit -> fruit.name }

fun main() {
    for (entry in fruitMap2.entries) {
        println("key : ${entry.key} , value : ${entry.value}")
    }
}

 

 

사과라는 동일한 키를 가지고 있는 value를 만나면 가장 마지막에 만나는 요소로 넣어준다.

 

 

 

// map을 만들때 키에 대한 설정말고 value에 대한 설정도 할 수 있다.
val fruitMap3 = fruits.groupBy ({fruit -> fruit.name},{fruit -> fruit.price})

fun main() {
    for (entry in fruitMap3.entries) {
        println("key : ${entry.key} , value : ${entry.value}")
    }
}

 

 

 

// map 또한 위에서 사용한 filter 같은 기능을 사용할 수 있다.
val fruitMap3 = fruits.groupBy ({fruit -> fruit.name},{fruit -> fruit.price})
    .filter { (key,value) -> key == "사과" }

fun main() {
    for (entry in fruitMap3.entries) {
        println("key : ${entry.key} , value : ${entry.value}")
    }
}

 

 

 

구조 분해 선언

val fruitMap3 = fruits.groupBy ({fruit -> fruit.name},{fruit -> fruit.price})
    .filter { (key,value) -> key == "사과" }

위의 코드는 잘 동작하지만  [ 구조 분해 선언 덕분에 ]

val fruitMap3 = fruits.groupBy ({fruit -> fruit.name},{fruit -> fruit.price})
    .filter { key -> key == "사과" }

위의 코드는 동작하지않는다. [ 에러메시지 ->  Operator '==' cannot be applied to 'Map.Entry<String!, List<Int>>' and 'String' ]

 

key에 Map의 Entry가 들어가기 때문이다. 

 

리스트였으면 요소의 순서대로 하나씩 요소가 key에 들어갔을텐데 

 

Map이니까 하나의 entry가 순서대로 들어가게 된다. 

 

val fruitMap3 = fruits.groupBy ({fruit -> fruit.name},{fruit -> fruit.price})
    .filter { entry -> entry.key == "사과" }

이렇게 코드를 작성하면 잘 들어간다.

 

 

코틀린(Kotlin) - 구조 분해 선언과 component 함수

데이터 클래스의 특성 중 convention 원리와 관련된 특성인 구조 분해 선언(destructuring declaration)에 대해 살펴보도록 하겠습니다. 데이터 클래스에 대한 기본적인 내용은 아래 포스팅을 참고하시면

0391kjy.tistory.com

 

 

 


4. 중첩된 컬렉션 처리

 

val fruitsInList: List<List<Fruit>> = listOf(
    listOf(
        Fruit("사과", 1000),
        Fruit("사과", 1200),
        Fruit("사과", 1200),
        Fruit("사과", 1500),
        Fruit("바나나", 2500),
        Fruit("수박", 10000)
    ),
    listOf(
        Fruit("사과", 1000),
        Fruit("사과", 1200),
        Fruit("사과", 1200),
        Fruit("바나나", 3000)
    ),
    listOf(
        Fruit("사과", 1000),
        Fruit("사과", 1200),
        Fruit("수박", 10000)
    )
)

 

 

중첩 컬렉션을 처리할때 java 에서는 flatmap을 이용해서 처리했다.

 

코틀린에서 가능하다.

val samePriceFruits = fruitsInList.flatMap { list->
    list.filter { fruit -> fruit.name =="사과" }
}

람다안에 람다가 들어가는 형식인데

 

위의 코드는 리팩토링이 가능하다.

 

public class Fruit {

  private final String name;
  private final int price;

  public Fruit(String name, int price) {
    this.name = name;
    this.price = price;
  }

// 중략
  public boolean isApple() {
    return this.name.equals("사과");
  }
  
//중략

}

Fruit 클래스에 isApple()라는 이름이 사과임을 체크하는 메서드를 만들고

 

val List<Fruit>.appleFilter:List<Fruit>
    get() = this.filter({ fruit -> fruit.isApple })
    
val applesList = fruitsInList.flatMap { list->
    list.appleFilter
}

List<Fruit>에 확장함수인 appleFilter를 생성해준다.

 

그리고 flatMap 내부에서 해당 확장함수를 쓰는형식으로 리팩토링 가능하다. 

 

위의 Fruit는 자바클래스로 진행했지만 코틀린 Fruit 클래스라면 isApple을 프로퍼티로 만들어서 사용하면된다.

val isApple:Boolean
    get() = this.name == "사과"

 

 

 

 

중첩 컬렉션을 1차원 컬렉션으로 바꿀때

List<List<Fruit>>를  List<Fruit>바꾸려고할때

flatten()을 사용한다.

val resultList = fruitsInList.flatten()

for (fruit in resultList) {
    println(fruit.toString())
}

 

 

모든 리스트를 펼쳐서 하나의 리스트로 만들어준다.

 

댓글