프로그래밍 기초/Kotlin

코틀린 완벽 가이드 - 7장 컬렉션과 I/O 자세히 알아보기

sumni0530 2023. 4. 16. 23:07

7장 - 컬렉션과 I/O 자세히 알아보기

이 장에서는 컬렉션 타입을 이해하고, 코틀린 표준 라이브러리를 사용해 컬렉션 데이트를 간결하고 코틀린답게 조작하는 방법을 배우며, I/O 스트림 API 확장을 이해한다.

 

7.1 컬렉션

코틀린 공식 문서 내 collection type 설명 - https://kotlinlang.org/docs/collections-overview.html#collection-types

컬렉션은 엘리먼트(동일한 유형의 객체)들로 이뤄진 그룹을 저장하기 위해 설계된 객체다. 여러 가지 데이터 구조(배열, 연결 리스트, 해시 테이블 등)에 기반한 다양한 컬렉션 라이브러리와 컬렉션 간 데이터를 조작하기 위한 종합적인 API(ex. filter, aggregate, order, transfer 등)가 포함된다. 

컬렉션을 조작하는 모든 연산이 인라인 함수이기 때문에 이런 연산을 사용해도 함수 호출이나 람다 호출에 따른 부가 비용이 들지 않는다.

코틀린의 중요한 특징은 불변 컬렉션과 가변 컬렉션을 구분한다는 것이다.

  • 불변 컬렉션 : 생성한 다음에  컬렉션의 내용을 바꿀 수 없음
  • 가변 컬렉션 : 생성한 다음에  컬렉션의 내용을 바꿀 수 있음
  • 불변 변수 : 변수가 가리키는 참조 값을 바꿀 수 없음
  • 가변 변수 : 변수가 가리키는 참조 값을 바꿀 수 있음 

불변 컬렉션 타입에서는 공변성이라는 유용한 특징을 가지고 있다. 공변성이란 T가 U의 하위 타입인 경우 Iterable도 Iterable의 하위 타입이라는 뜻이다. 이터레이터를 예로 들면 아래와 같은 코드가 동작한다. 

fun processCollection(c: Iterable<Any>) {...}

fun main() {
    val list = listOf("a", "b", "c") // List<String>   
    processCollection(list)  // OK: List<String>을 List<Any>로 전달, Iterable <- Collection <- List 불변 컬렉션 상속관계
}

7.1.1 컬렉션 타입

코틀린 컬렉션 타입은 기본적으로 네 가지(배열,이터러블, 시퀀스, 맵)로 분류할 수 있다. 컬렉션 타입은 제네릭 타입이며, 구체적으로 타입을 지정하려면 원소의 타입을 지정해야한다. 

 

배열 - 2장에서 다룸

 

이터러블
Iterable<T> 타입으로 표현되며, 일반적으로 즉시 계산되는 상태가 있는 컬렉션을 표현 한다.

  • 즉시 계산 : 나중에 어느 필요한 시점에 원소가 초기화되지 않고 컬렉션을 최초로 생성할 때 초기화된다는 뜻
  • 상태가 있음 : 컬렉션이 원소를 필요할 때 생성하는 제너레이터 함수를 유지하지 않고 원소를 저장한다는 뜻

내부에는 이터레이터가 컬렉션의 끝에 도달했는지 판단하는 hasNext()와 컬렉션의 다음 원소를 반환하는 next() 메서드가 들어있다. 자바와 유사하며 유일한 차이는 remove()가 없다는 것인데 해당 메서드는 MutableIterator로 옮겨졌다. MutableIterator는 가변 이터러블의 기본 타입이다.

이터러블의 하위 분류 중에는 Collection 인터페이스로 표현되는 타입과 MutableCollection(Collection 타입의 하위 타입) 인터페이스로 표현되는 타입들이 있다. 이터러블에 대한 표준적인 구현을 위한 기본 클래스이며, Collection을 상속한 클래스는 아래와 같이 분류할 수 있다.

  • 리스트
    • ArrayList : 인덱스를 통한 임의 접근이 가능한 리스트
    • LinkedList : 원소의 추가/제거에 용이하지만 인덱스로 원소 접근 시 선형 시간이 걸리는 리스트 
  • 집합 - 유일한 원소들로 이뤄진 컬렉션
    • HashSet : 해시 테이블 기반의 구현으로 원소의 해시 코드에 따라 원소 순서가 정해진다. 일반적으로 원소 타입의 hashCode() 메서드 구현에 따라 다르므로 순서가 예측이 어려움
    • LinkedHashSet : 해시 테이블이 기반이지만 삽입 순서를 유지하여 삽입된 순서대로 원소 순회 가능
    • TreeSet : 이진 검색 트리 기반으로, 비교 규칙에 따라 일관성 잇는 원소 순서를 제공한다. 원소 타입에 비교 규칙을 정의할 수 도 있고, 별도의 Comparator 객체를 통해 비교 규칙 제공 가능

 

시퀀스

이터러블과 비슷하게 시퀀스도 iterator() 메서드를 제공한다. 시퀀스는 지연 계산을 가정하기 때문에 iterator()의 의도가 이터러블과 다르다. 시퀀스 객체 초기화시 원소를 초기화 하지 않고, 지연 계산한 컬렉션 원소 중에 정해진 개수의 원소만 저장한다. 시퀀스 구현은 내부적이므로 외부에서 직접 사용할 수 없다.

 

맵은 키(key)와 값(value) 쌍으로 이뤄진 집합이다. 키는 유일해야하며, 맵 자체는 Collection의 하위 타입은 아니지만 맵에 들어있는 원소들을 컬렉션처럼 사용할 수 있다. 맵에는 두 가지 종류의 원소(키,값)가 들어있기 때문에 맵의 타입은 두 가지 타입을 파라미터로 받는 제네릭 타입이다. 맵의 표현 구현에는 HashMap, LinkedHashMap, TreeMap 등이 있으며 각 대응은 이터러블의 집합(set)과 유사하다.

 

7.1.2 Comparable과 Comparator

코틀린에서도 자바와 같이 Comparable과 Comparator 타입을 지원한다. 

  • Comparable 클래스
    제네릭 클래스로 compareTo() 메서드를 정의하여 동일한 타입의 다른 인스턴스와 순서를 비교할 때 사용
    compareTo() 메서드는 자바와 유사하며, 현재 인스턴스(수신 객체)가 상대방 인스턴스(인자로 받은 값)보다 더 크면 양수, 더 작으면 음수, 같으면 0을 반환한다. 같음을 판별하기 때문에 equals() 구현과 의미가 일치해야한다. 
class Person(
    val firstName: String,
    val familyName: String,
    val age: Int
): Comparable<Person> {
    val fullName get() = "$firstName $familyName"
    override fun compareTo(other: Person) = fullName.compareTo(other.fullName)
}
  • Comparator 클래스
    제네릭 클래스로 Compator<T> 클래스는 타입 T의 인스턴스 객체를 두 개 인자로 받는 compareTo() 함수를 제공한다. 여러 프로퍼티의 조합을 기준으로 정렬할 때 사용한다. 람다 비교 함수를 통해 간결하게 작성 가능하다.
// Comparator 클래스 사용
val AGE_COMPARATOR = Comparator<Person>{ p1, p2 -> 
    p1.age.compareTo(p2,age) 
}

// compareBy() 함수 사용
val AGE_COMPARATOR = compareBy<Person> { it.age }

// compareByDescending() 함수 사용
val REVERSE_AGE_COMPARATOR = compareByDescending<Person>{ it.age }

 

7.1.3 컬렉션 생성하기

컬렉션은 생성자를 통해 생성하거나 표준 함수를 통해 생성할 수 있다. 이와 더불어 배열과 비슷하게 크기를 지정하고 인덱스로부터 값을 만들어주는 함수를 제공함으로써 새 리스트를 만들 수 도 있다. 

아래 예시에서  위쪽은 list/set에 대한 코드, 아래쪽은 map에 대한 코드이다. 

import java.util.TreeMap

fun main() {
    // 생성자를 통한 인스턴스 생성
    val list = ArrayList<String>()
    list.add("red")
    list.add("green")

    val set = HashSet<Int>()
    set.add(12)
    set.add(21)
    set.add(12)

    val map = TreeMap<Int, String>()
    map[20] = "Twenty"
    map[10] = "Ten"

    // emptyList()/emptySet() : 불변인 빈 리스트/집합 인스턴스를 생성
    val emptyList = emptyList<String>()
    //Syntax Error : Unresolved reference: add
    emptyList.add("abc")

    // listOf()/setOf() : 인자로 제공한 배열에 기반한 불변 리스트/집합 인스턴스를 생성
    val singletonSet = setOf("abc")
    //Syntax Error : Unresolved reference: remove
    singletonSet.remove("abc")

    // mutableListOf()/mutableSetOf() : 가변 리스트/집합의 디폴트 구현 인스턴스를 생성
    val mutableList = mutableListOf("abc")
    mutableList.add("def")
    mutableList[0] = "xyz"

    // arrayListOf()/hashSetOf()/linkedSetOf()/sortedSetOf() : 칵 메서드에 맞는 새 인스턴스를 생성
    val sortedSet = sortedSetOf(8, 5, 7, 1, 4)
    sortedSet.add(2)
    
    
    val emptyMap = emptyMap<Int, String>()
    // Syntax Error : No set method providing array access
    emptyMap[10] = "Ten"

    val singletonMap = mapOf(10 to "Ten")
    // Syntax Error : Unresolved reference.
    singletonMap.remove("abc")

    val mutableMap = mutableMapOf(10 to "Ten")
    mutableMap[20] = "Twenty"
    mutableMap[100] = "Hundred"
    mutableMap.remove(10)

    val sortedMap = sortedMapOf(3 to "three", 1 to "one", 2 to "two")
    sortedMap[0] = "zero"
}

 

배열과 비슷하게 크기를 지정하고 인덱스로부터 값을 만들어주는 함수를 제공함으로써 새 리스트를 만들 수 도 있다. 

 

시퀀스를 만드는 가장 단순한 방법은 sequenceOf() 함수를 사용하는 것이며, 함수는 가변 인자를 받는다. 혹은 기존의 컬렉션 객체를 sequence로 바꾸는 asSequence() 함수를 호출해서 시퀀스를 얻을 수도 있다. 맵에 호출하면 맵 엔트리(키, 값 쌍을 감싼 타입)의 시퀀스를 얻는다. 

제너레이터 함수를 바탕으로 시퀀스를 만드는 방법도 있다. 두 제너레이터 함수 모두 다음 값으로 널을 반환하면 시퀀스가 종료된다.

  • 다음 원소를 생성해주는 파라미터가 없는 함수를 인자로 받아 생성
  • 초깃값과 파라미터가 하나인 함수를 인자로 받아 이전 값으로부터 다음 값을 생성 
sequenceOf(1,2,3)
listOf(10,20,30).asSequence()
mapOf(1 to "One", 2 to "Two", 3 to "Three").asSequence()

// 입력 값 시퀀스에 저장
val numbers = generateSequence{ readLine()?.toIntOrNull() }

// 초깃값 10부터 2씩 감소
val evens = generateSequence(10) { if (it >=2) it - 2 else null }

 

코틀린 1.3부터 특별한 빌더를 사용해 시퀀스르 만드는 방법이 추가되었다. SequenceScope가 수신 객체 타입인 확장 람다를 받는 sequence() 함수를 통해 빌더를 구현할 수 있다. SequenceScope 타입은 확장 람다 본문 안에서 시퀀스 뒤에 값을 추가할 수 있는 함수를 제공한다. 

  • yield() : 원소를 하나 시퀀스에 추가
  • yieldAll() : 지정한 이터레이터, 이터러블, 시퀀스에 들어있는 모든 원소를 시퀀스에 추가

시퀀스에서 각 부분에 속한 원소에 접근하는 경우에만 yield(), yieldAll()이 호출되는 지연 계산을 한다는 점을 유의하라.

위와 같은 기능은 다중 스레드 환경에서 아주 유용하며 13장 동시성에서 다룰 예정이다.

 

컬력센 사이의 변환을 처리하는 함수도 있다. 코틀린 공식 홈페이지에서 확인할 수 있으며, 변환 함수에는 몇가지 관례가 있다. to로 시작하는 함수는 원본 컬렉션의 복사본을 생성하며, as로 시작하는 함수는 원본 컬렉션이 변경되는 경우 이를 반영해주는 뷰를 만들어준다.

 

7.1.4 기본 컬렉션 연산

코틀린 컬렉션 타입이 지원하는 몇 가지 기본 연산을 다룬다.

연산 지원 컬렉션
(배열, 이터러블, 시퀀스, 맵)
설명 예시
iterator()  모든 컬렉션 함수가 반환하는 Iterator 객체를 사용해 원소를 순회
맵 타입의 경우 이터레이터는 Map.Entry 타입을 반환
(구조 분해로 사용 가능)
listOf(1,2,3).iterator()
forEach()  모든 컬렉션 컬렉션의 각 원소를 제공하면서 인자로 받은 람다를 실행 intArrayOf(1,2,3).forEach { println(it*it) }
forEachIndexed() 모든 컬렉션 원소 인덱스를 참조하여 사용 listOf(10,20,30).forEachIndexed { i,n -> println("$i: ${n*n}") }
size 배열, 맵, Collection 원소의 개수를 반환 listOf(1,2,3).size
isEmpty() 모든 컬렉션 컬렉션 원소가 없는지 검사 listOf(1,2,3).isEmpty()
contains()
containsAll()
모든 컬렉션 인자로 지정한 원소나 컬렉션의 모든 원소가 수신 객체 컬렉션에
들어있는지 검사
equals() 메서드를 제대로 구현해야 정상 동작
list.contains(4)
list.containsAll(listOf(1,2))
add() MutableCollection을
상속받은 컬렉션
원소 하나를 추가 list.add(4)
remove() MutableCollection을
상속받은 컬렉션
원소 하나를 제거 list.remove(3)
addAll() MutableCollection을
상속받은 컬렉션
두 컬렉션의 합집합 list.addAll(setOf(5,6))
removeAll() MutableCollection을
상속받은 컬렉션
두 컬렉션의 차집합 list.removeAll(listOf(5,6))
retainAll() MutableCollection을
상속받은 컬렉션
두 컬렉션의 교집합 list.retainAll(listOf(5,6,7))
+= 모든 컬렉션 입력 받은 값을 더한 새로운 컬렉션을 생성
가변 변수에 대해서만 사용 가능
var lst1 = listOf(1,2,3)
lst1 += 4

var lst2 = listOf(1,2)
lst2 += listOf(3,4)
-= 모든 컬렉션 입력 받은 값을 빼고 새로운 컬렉션을 생성
가변 변수에 대해서만 사용 가능
var lst1 = listOf(1,2,3)
lst1 -= 4

var lst2 = listOf(1,2)
lst2 -= listOf(3,4)
[] 리스트, 배열 인덱스를 기반으로 메서드에 접근
가변 컬렉션인 경우 원소 변경 가능
list[10]
subList() 리스트 시작 인덱스(포함)과 끝 인덱스(포함하지 않음)로 지정한
리스트의 일부분에 대한 래퍼를 생성

뷰로 생성되며 원본 컬렉션의 데이터를 공유
(원본의 변화가 반영됨)
list.subList(2,5)
getOrDefault() 기본값을 지정하여 키에 대응하는 맵 값을 확인 map.getOrDefault(100,"?"))
getOrElse() 키에 대응하는 맵이 없는 경우 람다식을 사용 map.getOrDefault(100, {"?"})
containsKey() 입력 받은 키 값을 사용하는지 확인 map.containsKey(10)
containsValue() 입력받은 value 값을 사용하는지 확인 map.containsValue("V")
keys 맵의 모든 키 값을 반환 map.keys
values 맵의 모든 value 값을 반환 map.values
entries 맵의 모든 값을 Map.Entry 타입으로 반환 map.entries

일반적으로 +=/-= 연산자는 대입이 일어날 때마다 암시적으로 새로운 컬렉션을 만들기 때문에 프로그램 성능에 영향을 미칠 수 있으니 유의해서 사용해야 한다. 집합은 Collection에 있는 공통 메서드만 지원하며 추가된 연산을 지원하지는 않는다. 맵에서 += 연산자는 키-값 쌍을 인자로 받지만, - 연산자는 키만 받는다는 점도 기억해야한다. 

 

 

7.1.5 컬렉션 원소에 접근하기

코틀린 표준 라이브러리에는 개별 컬렉션 원소에 대한 접근을 편하게 해주는 확장 함수들이 포함되어있다.

연산 설명 예시
first() 주어진 컬렉션의 첫 번째 원소를 반환 listOf(1,2,3).first()
firstOrNull() first() 함수에서 원소가 없으면 null을 반환하는 안전한 버전 함수 emptyArray<String>().firstOrNull()
last() 주어진 컬렉션의 마지막 번째 원소를 반환 listOf(1,2,3).last()
lastOrNull() last() 함수에서 원소가 없으면 null을 반환하는 안전한 버전 함수 emptyArray().lastOrNull()
single() 싱글턴 컬렉션의 원소를 반환
비어있거나 원소가 두 개 이상인 경우 예외 발생
listOf(1).single()
singleOrNull() single() 함수에서 예외 발생 대신 null 반환 emptyArray().singleOrNull()
elementAt() 인덱스를 사용해 컬렉션의 원소 확인 listOf(1,2,3).elementAt(2)
elementAtOrNull 인덱스 값을 벗어날 때 null을 반환하며,
아닌 경우 elementAt()과 동일하게 동작
listOf(1,2,3).elementAtOrNull(4)
elementAtOrElse 인덱스 값을 벗어날 때 기본 값을 반환하며,
아닌 경우 elementAt()과 동일하게 동작
listOf(1,2,3).elementAtOrElse(1) {"???"}

first/last 함수(orNull 함수 포함)의 경우 주어진 조건을 만족하는 값을 찾기 위해 술어를 넘길 수도 있다. 임의 접근 컬렉션이 아닌 컬렉션에서 elementAt() 함수 사용 시 인덱스 값에 비례하여 실행 시간이 걸린다. 배열이나 리스트에 대한 구조 분해를 통해 앞에서부터 최대 다섯 개의 원소를 추출할 수 있다는 점도 유의하자.

listOf(1,2,3).first { it >2 }

 

 

7.1.6 컬렉션에 대한 조건 검사

코틀린 라이브러리는 컬렉션 원소에 대해 주어진 술어를 테스트하는 등의 기본적인 검사를 구현하는 함수를 제공한다. 

연산 설명 예시
all() 컬렉션의 모든 원소가 주어진 술어를 만족하면 true 반환 listOf(1,2,3,4).all { it < 0 }
none() 컬렉션에 주어진 조건을 만족하는 원소가 하나도 없으면 true 반환 listOf(1,2,3,4).none { it > 5 }
any() 컬렉션 원소 중 적어도 하나가 주어진 술어를 만족할 때 true 반환 listOf(1,2,3,4).any { it < 0 }

빈 컬렉션의 경우 all()과 none() 함수는 true, any() 함수는 false를 반환한다. any()와 none() 함수에서는 파라미터를 전혀 받지 않는 오버로딩된 버전이 있으며 이는 컬렉션 객체가 비어있는지를 검사한다. 

 

 

7.1.7 집계

컬렉션 내용으로부터 한 값을 계산해내는 경우를 집계라고 부른다. 집계 함수는 크게 세 가지 기본 그룹으로 나눌 수 있다.

  1. 합계, 최솟값, 최댓값 등 자주 쓰이는 집계값
  2. 컬렉션 원소를 문자열로 엮는 집계값
  3. 임의의 집계 방식으로 구현하게 해주는 함수

합계, 최솟값, 최댓값 등 자주 쓰이는 집계값

연산 지원 컬렉션 설명 예시
count() 모든 컬렉션 원소의 개수를 반환
원소의 개수가 Int.MAX_VALUE 보다 크면 예외 발생
조건을 적용하는 오버로딩된 버전도 활용 가능
listOf(1,2,3,4).count 
listOf(1,2,3,4).count { it < 0 }
sum() 수로 이뤄진 배열, 이터러블, 시퀀스 원소의 합을 반환
조건을 적용하는 오버로딩된 버전도 활용 가능
doubleArrayOf(1.2, 2.3, 3.4).sum())
sumOf() 원소 타입을 수로 변환할 수 있는 컬렉션 원소의 합을 반환
조건을 적용하는 오버로딩된 버전도 활용 가능
arrayOf("1","2","3").sumOf{ it.toInt() })
average() 수로 이뤄진 배열, 이터러블, 시퀀스 원소의 산출 평균을 반환하며 결과는 Double 타입
컬렉션이 비어있으면 Double.NaN을 반환 
원소의 개수가 Int.MAX_VALUE 보다 크면 예외 발생
doubleArrayOf(1.2, 2.3, 3.4).average())
minOrNull() 비교 가능한 타입의 값이 들어있는 배열,이터러블, 시퀀스 컬렉션의 최솟값을 반환 intArrayOf(5,8,1,4,2).minOrNull()
maxOrNull() 비교 가능한 타입의 값이 들어있는 배열,이터러블, 시퀀스 컬렉션의 최댓값을 반환 intArrayOf(5,8,1,4,2).maxOrNull()
minWithOrNull() 모든 컬렉션 변환 함수 대신 Comparetor 인스턴스를 전달받아 최솟값을 반환 person.minByOrNull { it.firstName } 
maxWithOrNull() 모든 컬렉션 변환 함수 대신 Comparetor 인스턴스를 전달받아 최댓값을 반환 person.maxByOrNull { it.firstName }

 

컬렉션 원소를 문자열로 엮는 집계값

문자열을 받아 구분 문자열로 컬렉션의 원소를 엮에서 집계하는 함수이다.

연산 설명 예시
joinToString() 각 원소의 toString() 메서드를 이용해 문자열로 변환한 후에
구분 문자열로 콤마와 공백을 사용
커스텀 변환 함수를 제공해 다른 형태인 문자열로 변환할 수 있음(람다)

아래의 파라미터를 지원
- separator : 인접한 원소 사이에 들어간 구분 문자열 (디폴트 : ", ")
- prefix & postfix : 결과 문자열의 맨 앞 or 맨 뒤에 들어갈 문자열
- limit : 최대로 보여줄 수 있는 원소의 개수 ( 디폴트 : -1 / 무제한)
- truncated : limit가 양수인 경우, 원소를 표현하지 못할 때 표현하는 값 
list.joinToString()
list.joinToString(prefix= "[", postfix= "]")
list.joinToString(seperator = "|")
list.joinToString(limit = 2)

joinTo() Appendable 객체를 파라미터로 받아, 해당 객체에 결과를 붙여 반환 val builder = StringBuilder("joinTo: ")
val list = listOf(1,2,3)

list.joinTo(builder)

 

임의의 집계 방식으로 구현하게 해주는 함수

두 값을 조합하는 함수를 활용해 원하는 임의의 집계 방식을 구현하게 해주는 함수이다. 

 

reduce

intArrayOf(1,2,3,4,5).reduce { acc, n -> acc*n }
intArrayOf(1,2,3,4,5).reduceIndexed { i, acc, n -> if (i%2 == 1) acc*n else acc }

reduce() 함수는 파라미터가 두 개이다. 첫 번재 인자는 누적된 값이고, 두 번째 인자는 컬렉션의 현재 값이다. 집계 과정은 아래와 같다.

  1. 누적값은 최초에 컬렉션의 첫 번째 원소로 초기화됨
  2. 컬렉션의 매 원소에(두 번째 원소부터) 대해 현재 누적값과 현재 원소를 파라미터로 받은 함수에 적용하고, 적용 결과를 누적값에 대입
  3. 누적의 결과를 반환

컬력션이 비어있으면 누적값을 초기화 할 수 없기 때문에 예외가 발생한다. 

집계 규칙이 원소의 인덱스에 따라 달라진다면 reduceIndexed()를 사용하면 된다. reduce() 함수의 파라미터에서 첫 번째 파라미터로 인덱스 값을 추가하여 사용한다. 만약에 누적값의 초깃값을 지정하고 싶다면 fold 관련 함수를 사용하면 된다.

 

fold

intArrayOf(1,2,3,4,5).fold("") { acc, n -> acc + ('a' + n - 1) }

listOf(1,2,3,4).foldIndexed("") } i, acc, n ->
    if ( i%2 == 1 ) acc + ('a' + n - 1) else acc
}

fold()/foldIndexed()를 사용하면 누적값을 컬렉션 원소의 타입과 다른 타입의 값으로 만들 수 있다. 항상 초깃값을 지정하기 때문에 리스트가 비어있어도 예외가 발생하지 않는다. 

arrayOf("a","b","c","d").reduceRight { s, acc -> acc + s }

listOf("a","b","c","d").reduceRightIndexed { i, s, acc -> 
    if (i%2 == 0) acc + s else acc
}

intArrayOf(1,2,3,4).foldRight("") { n, acc -> acc + ('a' + n - 1) }

listOf(1,2,3,4).foldRightIndexed("") { i, n, acc ->
    if (i%2==0) acc + ('a'+n-1) else acc
}

앞서 네 함수(reduce(), reduceIndexed(), fold(), foldIndexed())에 대해서 마지막 원소부터 반대 방향으로 계산해주는 함수도 있으며, 이런 함수의 이름 뒤에는 Right 키워드가 붙는다. 전달하는 파라미터도 누적값과 현재값의 파라미터 순서가 다르다는 것을 유의해라.

 

 

7.1.8 걸러내기

코틀린 표준 라이브러리는 컬렉션에서 조건을 만족하는 원소만 남기는 여러 확장 함수를 제공한다. 걸러내기 연산은 원본 컬렉션을 변경하지 않으며, 새로운 컬렉션을 만들거나 원본 컬렉션과 구별되는 기존 가변 컬렉션에 선택된 원소를 추가한다. 맵의 경우 술어 파라미터가 Map.Entrty을 인자로 받는다.

연산 지원 컬렉션 설명 예시
filter() 모든 컬렉션 전달되는 술어를 기반으로 현재 원소를 인자로 받아 유지하는 경우 true 반환, 버려야하는 경우 false 반환 listOf("red","green", "blue", "green").filter { it.length >3 }
filterKeys() 키를 걸러내고 싶은 경우 사용 map.filterKeys { it != "L" }
filterValue() 값을 걸러내고 싶은 경우 사용 map.filterValues{ it >=10 }
filterNot() 모든 컬렉션 조건을 부정해 걸러냄 map.filterNot { it.value > 5 }
filterIndexed() 배열, 이터러블, 시퀀스 인덱스와 관련있는 경우 사용하는 필터 list.filterIndexed { i, v -> v.length>3 && i <list.lastIndex }
filterNotNull() 모든 컬렉션 널인 원소를 걸러냄 list.filterNotNull()
filterIsInstance() 모든 컬렉션 타입 인자로 넘긴 타입과 동일한 타입의 원소만 필터 list.filterIsInstance<Int>()
partition() 모든 컬렉션 술어를 만족하는 부분과 그렇지 않은 부분으로 부분 컬렉션 쌍을 만들어주는 함수 val (evens, odds) = listOf(1,2,3,4,5).partition { it %2 == 0 }

filter()의 경우 입력 타입이 Array<T>나 Iterable<T>인 경우 반환 타입은 List<T>로 지정된다. 

걸러낸 결과를 이미 존재하는 가변 컬렉션에 집어 넣고 싶은 경우 남겨야할 원소를 추가하는 대상 컬렉션을 추가 인자로 받는 이름 끝에 To가 붙은 특별한 함수가 존재한다. filter(), filterNot(), filterIndexed(), filterIsInstance(), filterNotNull() 함수에 대해서 To가 붙은 버전이 존재한다. 일반적으로 우너본 컬렉션을 대상 컬렉션으로 지정하면 순회 도중 컬렉션 내용이 변경되어 에러가 발생한다. 

 

 

7.1.9 변환

코틀린에서는 컬렉션의 모든 원소를 주어진 규칙에 따라 변경한 다음, 이를 정해진 방식으로 조합해서 새로운 컬렉션을 만들어내는 변환 함수를 사용할 수 있다. 이런 함수는 mapping(매핑), flatterning(평탄화), association(연관짓기)라는 세 가지 유형으로 나눌 수 있다.

 

mapping

매핑 변환은 주어진 함수를 원본컬렉션의 각 원소에 적용하며, 결과는 새 컬렉션의 원소가 된다. 모든 컬렉션이 사용 가능하다. 시퀀스에 적용한 경우 시퀀스로 반환되지만, 나머지는 리스트로 반환된다. 

연산 설명 예시
map()
mapNotNull()
각 컬렉션 원소에 적용된 결과를 새로운 컬렉션 원소로 사용 setOf("red", "green", "blue").map { it.length }

mapIndexed()
mappIndexedNotNull()
map() 함수 사용을 하며, 변환 시 인덱스를 고려하는 경우 사용 List(6) { it * it }.mapIndexed { i, n -> i to n }
mapNotNull() null이 아닌 값만 입력 받아 매핑 함수를 사용 array.mappNotNull { it.toIntOrNull() }
mapKeys() 맵 켈력션의 키만 변경하는 함수 map.mapKeys{ it.key.lowercase() }
mapValues() 맵 컬렉션의 값만 변경하는 함수 map.mapValues{ it.value.toString(16) }

각 mapxxx() 함수에는 새 컬렉션을 만들지 않고 기존 컬렉션에 원소를 추가하는 함수가 있으며, 필터와 마찬가지로 이름 끝에 To가 붙는다. 

 

Flatterning

Flatterning의 술어는 컬렉션으로 만들어져야한다. 시퀀스에 적용한 경우 시퀀스로 반환되지만, 나머지는 리스트로 반환된다. 

연산 설명 예시
flatMap() 원래 컬렉션의 각 원소를 컬렉션으로 변환한 다음,
각 컬렉션을 차례로 이어 붙여서 한 컬렉션으로 합침
listOf(1,2,3,4).flatMapp { listOf(it) }
flatten() 원소가 컬렉션인 모든 컬렉션에 적용할 수 있고, 각각의 컬렉션을 이어 붙인 한 컬렉션을 반환 arrayOf("a", "b").flattern()

map()과 마찬가지로 마지막에  To 키워드가 붙은 함수를 통해 기존 컬렉션에 추가할 수 있다.

 

Association

주어진 변환 함수를 바탕으로 원본 컬렉션 원소를 맵의 키나 맵의 값으로 만들 수 있다.

연산 설명 예시
associateWith() 원래 컬렉션의 키를 기준으로 새로운 맵을 만듬
배열에는 적용할 수 없음
listOf("red", "green", "blue").associateWith { it.length }
// {red=3, green=5, blue=4}
associateBy() 원래 컬렉션을 값으로 취급하고 변환 함수를 통해 키를 얻음 listOf("red", "green", "blue").associateBy { it.length }
// {3=red, 5=green, 4=blue}
associate() 컬렉션의 원소를 사용해서 키와 값을 만듬 listOf("red", "green", "blue").associate { it.uppercase() to it.length }

 

7.1.10 하위 컬렉션 추출

컬렉션의 일부를 추출하는 함수에 대해서 살펴보자. 

연산 적용 가능 컬렉션 설명 예시
slice() 리스트, 배열 정수 범위를 사용해 컬렉션 일부에 대한 뷰를 반환 listOf(0,1,4,9,16,25,36).slice(2..4)
// [4,9,16]
sliceArray() 배열 slice() 함수에서 입력 값이 Array타입일 때,
출력값도 Array 타입으로 반환하는 경우에 사용
arrayOf(0,1,4,9,16,25,36).sliceArray(2..4)
take() 리스트, 이터러블, 배열 컬렉션의 첫 부분 부터 원하는 개수 만큼 원소를 추출 listOf(0,1,4,9,16,25,36).take(2)
// [0,1]
takeLast() 리스트, 이터러블, 배열 컬렉션의 마지막 부분 부터 원하는 개수 만큼 원소를 추출 listOf(0,1,4,9,16,25,36).takeLast(3)
// [16,25,36]
drop() 리스트, 이터러블, 배열 컬렉션의 첫 부분 부터 원하는 개수 만큼 원소를 제거하고 반환 listOf(0,1,4,9,16,25,36).drop(2)
// [9,16,25,36]
dropLast() 리스트, 이터러블, 배열 컬렉션의 마지막 부분 부터 원하는 개수 만큼 원소를 제거하고 반환 listOf(0,1,4,9,16,25,36).dropLast(3)
// [0,1,4,9]
takewhile() 리스트, 이터러블, 배열 주어진 조건을 만족하지 못하는 첫 번째 원소를 발견할 때까지 take() 연산을 수행 list.takeWhile { it <10 }
takeLastWhile() 리스트, 이터러블, 배열 주어진 조건을 만족하지 못하는 첫 번째 원소를 발견할 때까지 takeLast() 연산을 수행 list.takeLastWhile { it <10 }
dropwhile() 리스트, 이터러블, 배열 주어진 조건을 만족하지 못하는 첫 번째 원소를 발견할 때까지 drop() 연산을 수행 list.dropWhile { it <10 }
dropLastWhile() 리스트, 이터러블, 배열 주어진 조건을 만족하지 못하는 첫 번째 원소를 발견할 때까지 dropLast() 연산을 수행 list.dropLastWhile { it <10 }
chunked() 이터러블, 시퀀스 주어진 개수를 넘지 않는 작은 리스들로 나눈 리스트를 반환 listOf(0,1,4,9,16,25,36).chunked(3)
// [ [0,1,4], [9,16,25], [36] ]
windowed() 이터러블, 시퀀스 일정 간격으로 청크를 연속적으로 얻어낸 슬라이딩 윈도우를 반환
아래 파라미터 지정 가능
- step : 서로 인접한 윈도우의 첫 번째 원소 사이의 거리(기본값 1)
- partialWindows : 컬렉션의 마지막 부분에 지정한 윈도우보다 작은 크기의 윈도우를 포함할지 여부 (기본값 false)
listOf(0,1,4,9,16,25,36).windowed(3)
// [ [0,1,4], [1,4,9], [4,9,16], ..,[16,25,36] ]
zipWithNext() 리스트, 시퀀스 원소가 두 개뿐인 윈도우를 만들어 반환 listOf(0,1,4,9,16,25,36).zipWithNext()
// [(0,1), (1,4), (4,9), .. , (16,25) ]

 

 

7.1.11 순서

순서와 관련된 정렬 함수에 대해서 알아보자.

연산 설명 예시
sorted() 원소 타입이 비교 가능한 배열/이터러블/시퀀스에 대해서 오름차순으로 원소를 정렬된 컬렉션을 반환
시퀀스는 시퀀스로, 그외는 list로 반환
listOf("abc","w","xyz","def", "hij").sorted()
// [abc, def, hij, w, xyz]
sortedDescending() sorted() 함수의 오름차순을 내림차순으로 바꾼 함수  listOf("abc","w","xyz","def", "hij").sortedDescending()
// [xyz, w, hij, def, abc]
sortedArray() 배열에 대해서 sorted() 결과를 배열로 반환 intArrayOf(5,8,1,4,2).sortedArray()
// [ 1,2,4,5,8]
sortedArrayDescending() 배열에 대해서 sortedDescending() 결과를 배열로 반환 intArrayOf(5,8,1,4,2).sortedArrayDescending()
// [8,5,4,2,1]
sortedBy() 컬렉션 원소를 비교 가능한 타입의 값으로 변환하는 함수를 인자로 받음 persons.sortedBy { it.age }
sortedByDescending() sortedBy() 함수의 오름차순을 내림차순으로 바꾼 함수 persons.sortedByDescending { it.age }
sortedWith() Comparator 인스턴스를 인자로 받음 persons.sortedWith(Comparator{

    p1, p2 -> p1.fullName.compareTo(p2.full)
}
reversed() 이터러블이나 배열을 역순으로 뒤집어주는 함수 intArrayOf(1,2,3,4,5).reversed()
// [5,4,3,2,1]
reversedArray() 배열에 대해서 reversed() 결과를 배열로 반환 intArrayOf(1,2,3,4,5).reversed()
// [5,4,3,2,1]
asReversed() reversed() 결과와 동일한 원본에 대한 래퍼를 반환
원본이나 래퍼 중 어느 쪽을 변경해도 다른 쪽에 변경 내용이 반영
intArrayOf(1,2,3,4,5).asReversed()
// [5,4,3,2,1]
shuffled() 이터러블에 적용하면 임의의 순서로 재배치한 새 리스트를 반환 arrayListOf(1,2,3,4,5).shuffled()

위의 함수들은 새로운 컬렉션을 만드는 함수들이다. 새로운 컬렉션을 사용하지않고, 가변 컬렉션을 활용하여 원본 컬렉션을 변경하는 함수들도 사용할 수 있다.

연산 설명 예시
sort() 원소 타입이 비교 가능한 배열/이터러블/시퀀스에 대해서 오름차순으로 입력 컬렉션을 정렬 listOf("abc","w","xyz","def", "hij").sort()
// [abc, def, hij, w, xyz]
sortDescending() 원소 타입이 비교 가능한 배열/이터러블/시퀀스에 대해서 내림차순으로 입력 컬렉션을 정렬 listOf("abc","w","xyz","def", "hij").sortDescending()
// [xyz, w, hij, def, abc]
reverse() 이터러블이나 배열을 역순으로 입력 컬렉션을 뒤집어주는 함수 intArrayOf(1,2,3,4,5).reverse()
// [5,4,3,2,1]
shuffle() 임의의 순서로 입력 컬렉션을 재배치하는 함수 arrayListOf(1,2,3,4,5).shuffle()

 

 

7.2 파일과 I/O 스트림

입력/출력 연산을 처리하는 코틀린 표준 라이브러리 기능을 다룬다. 해당 기능은 자바에 있는 파일과 I/O 스트림, URL 관련 API를 기반으로 만들어졌다.