Kotlin / 컬렉션 알아보기
1. 리스트, 세트, 맵 알아보기
컬렉션의 가변과 불변
- 가변(mutable) : 보통 컬렉션 객체는 추가, 수정, 삭제를 할 수 있다
- 불변(immutable) : 컬렉션 객체 중에 한 번 만들어지면 추가, 수정, 삭제를 할 수 없다.
리스트 클래스
- ArrayList : 고정 길이로 리스트를 만든다. listOf, mutableListOf로 객체 생성
- LinkedList : 가변 길이로 리스트를 만든다. 각 원소가 가지는 주소를 별도로 보관. LinkedList 클래스로 리스트 객체 생성
val list1 = listOf(1,2,3) // 불변 리스트 객체 생성
val list2 = mutableListOf(1,2,3) // 가변 리스트 객체 만들기
list2.add(10) // 원소 추가
list2.removeAt(1) // 원소 삭제
list2[0] = 0 // 원소 대체
가변리스트는 원소를 추가하거나 삭제 가능하다.
변수에 자료형을 지정하지 않으면 타입추론으로 자료형을 확장한다.
val linkedList = LinkedList<String>()
linkedList.addAll(listOf("지구","금성","화성")) // 링크드리스트에 원소 추가
println(linkedList) // [지구, 금성, 화성]
println("First planet = ${linkedList.first}") // First planet = 지구
val joinedPlanets = linkedList.plus(linkedList)
println(joinedPlanets) // [지구, 금성, 화성, 지구, 금성, 화성]
빈 링크드리스트를 객체로 만들고 리스트 객체를 addAll 메서드로 추가했다.
링크드 연산자나 메서드로 객체를 결합할 수 있다.
불변 리스트처럼 원본을 바꾸지 않고 새로운 리스트 객체를 만들어 반환할 수 있다.
val emptyList: List<String> = emptyList<String>() // 아무것도 없는 리스트 생성
val nonNullsList = listOfNotNull(2, 45, null, 5, null) // null 제거 리스트 생성
println(emptyList) // []
println(nonNullsList) // [2, 45, 5]
아무것도 없는 리스트는 emptyList 함수로 만든다.
널 값이 없는 리스트를 만들때는 listOfNotNull 함수를 사용한다.
집합(Set) 클래스
순서가 없고 모든 원소는 유일한 값을 가진다. (집합의 종류도 해시를 기반으로 처리)
- HashSet : HashMap을 이용하여 구현되고, value는 dummy object로 사용된다.
- LinkedHashSet : LinkedHashMap을 이용하여 구현되고, value는 dummy object로 사용된다.
- TreeSet : TreeMap으로 구현되고, value는 dummy object로 사용된다.
val intHSet: HashSet<Int> = hashSetOf(1, 2, 6, 3) // 해시 집합 생성
intHSet.add(5) // 원소 추가
intHSet.remove(1)
val intMSet: MutableSet<Int> = mutableSetOf(3, 5, 6, 2, 0) // 가변집합 생성
intMSet.add(8) // 원소 추가
intMSet.remove(3) // 원소 삭제
val intLSet: LinkedHashSet<Int> = linkedSetOf(5,2,7,2,5)
intLSet.add(4) // 원소 추가
intLSet.remove(2) // 원소 삭제
intLSet.clear() // 원소 전체 삭제
val treeSet = TreeSet<String>()
treeSet.addAll(listOf("화성","금성"))
val treeSet2 = TreeSet<String>()
treeSet2.addAll(listOf("화성","지구"))
treeSet.retainAll(treeSet2) // 교집합된 원소만 유지
println(treeSet) // [화성]
가변 집합 (hashSetOf, mutableSetOf, linkedSetOf)를 생성하고 add, remove 등의 메서드로 원소를 추가,삭제할 수 있다.
clear는 전체 삭제 메서드
TreeSet은 클래스로 빈 집합을 만든다. addAll 메서드로 집합을 추가할 수 있다.
retainAll 메서드는 두 집합의 교집합으로만 현재 집합을 갱신한다.
맵 클래스
유일 값을 갖는 키와 이 키에 대응하는 값으로 구성된다.
Key는 문자열 등의 불변 객체만 올 수 있고, Value는 모든 객체를 다 저장할 수 있다.
val tomcat = "Tom" to "cat" // 두 개의 원소를 가진 튜플 만들기
val m = hashMapOf( // 튜플로 맵 생성
Pair("Tom", "cat"),
Pair("Jerry", "Mouse")
)
val map1 = linkedMapOf("1" to "one") // 튜플 연산자로 맵 생성
맵의 원소는 키와 값으로 구성해야 한다. -> to 메서드로 키와 값을 한 쌍으로 묵는다. (Pair 클래스의 객체)
val map1 = linkedMapOf("1" to "one") // 튜플 연산자로 맵 생성
val callingMap: Map<Int, String> = mapOf(82 to "한국", 1 to "USA")
for ((key, value) in callingMap) {
println("$key 코드는 어느나라 $value")
}
val currencyMap: MutableMap<String, String> = mutableMapOf("원" to "한국")
println("국가 = ${currencyMap.values}") // 국가 = [한국]
currencyMap["엔"] = "일본"
println(currencyMap.remove("원")) // 한국
println(currencyMap.values) // [일본]
mapOf 함수로 불변인 맵을 생성할 수 있다.
변경 가능한 맵은 mutableMapOf 함수로 객체를 만든다.
스택(Stack)
하나의 출입구만 있으므로 차례로 스택에 쌓이고 마지막에 쌓인 것을 꺼낸다.
- pupsh : 마지막 스택에 데이터를 가져온다. (스택에 쌓음)
- pop : 마지막 스택에 쌓인 데이터를 꺼내온다. (스택에서 제거)
- peek : 스택의 상태를 알려준다. (스택의 최상단에 있는 데이터를 알려줌)
코틀린에서는 별도의 스택을 제공하지 않지만, 자바에 있는 스택을 활용할 수 있다.
큐(queue)
입구와 출구가 있는 자료구조. 데이터를 저장하는 입구와 데이터를 꺼내는 출구가 다르다.
FIFO(First In First Out) 선입선출 방식이다.
자바의 큐 인터페이스와 링크드리스트로 큐를 만들 수 있다.
2. 컬렉션 메서드 알아보기
컬렉션 상속구조 알아보기
코틀린의 컬렉션의 메서드는 상속관계에 따라서 제공된다.
모두 kotlin.collections 패키지 내부에 존재하며, Iterable을 기본적으로 상속받는다.
https://kotlinlang.org/docs/collections-overview.html#collection-types
불변 컬렉션의 경우 Iterable > Collection > List 순으로 구조화
가변 컬렉션의 경우 Iterable > MutableIterable > MutableCollection > MutableList 순으로 구조화
검색과 조건검사
컬렉션 상태 확인
val list = listOf("a","b","c") //리스트 객체 생성
val set1 = setOf(1,2,3)
val map = mapOf("a" to 100, "b" to 300)
println(list.size) // 원소 개수
println(list.isEmpty()) // 원소가 있는지 확인
println(list.contains("B")) // 포함 여부
println(list.containsAll(list)) // 전체가 다 포함되었는지
// 3
// false
// false
// true
컬렉션 내부 순환
- forEach 메서드로 처리. 인자로 람다 표현식을 받는다.
- 인덱스와 값 가져와서 처리 -> forEachIndexed메서드
list.forEach { print("$it,") } // 순환 처리
println()
list.forEachIndexed { index, value -> print("$index = $value, ") } // 인덱스 값과 같이 순환
println()
map.forEach{(key,value) -> print("$key = $value")} // 키와 밸류에 대한 순환처리
// a,b,c,
// 0 = a, 1 = b, 2 = c,
// a = 100b = 300
조건 검사
- 람다 표현식으로 인자를 받아서 리스트나 맵의 모든 원소를 비교해서 논리값으로 반환한다.
println("list any : "+ list.any{it.length > 3}) // list any : false
println("list all : "+ list.all{it>"a"}) // list all : false
println("list any : "+ list.none{it > "a"}) // list any : false
println("list any : "+ map.any(){it.key.length>3}) // list any : false
정렬
- sorted, reversed : 정렬 후 새로운 객체로 반환
- sort, reverse : 정렬 -> 내부 변경
val mList = mutableListOf("나","가","다")
println("mList : $mList")
println("정렬해서 새객체 : ${mList.sorted()}")
println("반대로 정렬 새객체 : ${mList.reversed()}")
println("mList : $mList")
/*--------------*/
val mCopyList = mList.toMutableList() // 객체를 다시 처리하면서 복사
println("mCopyList : $mCopyList")
mCopyList.sort()
println("정렬 후 내부 변경 : $mCopyList")
mCopyList.reverse()
println("반대로 정렬 후 내부 변경 : $mCopyList")
// mList : [나, 가, 다]
// 정렬해서 새객체 : [가, 나, 다]
// 반대로 정렬 새객체 : [다, 가, 나]
// mList : [나, 가, 다]
// mCopyList : [나, 가, 다]
// 정렬 후 내부 변경 : [가, 나, 다]
// 반대로 정렬 후 내부 변경 : [다, 나, 가]
맵 리듀스 처리
컬렉션은 여러 원소를 가진다. 원소를 처리하기 위해 순환 등의 기능을 하는 메서들르 만들고 실제 특정 처리는 별도의 함수를 전달받아서 한다.
- 맵(map) : 컬렉션을 순환하면서 모든 원소를 변환하고 컬렉션을 반환한다. 변환하는 로직을 람다표현식으로 받는다.
- 필터(filter) : 컬렉션을 순환하면서 람다표현식으로 전달된 조건을 점검해서 true인 원소를 추출해서 반환한다.
- 리듀스(reduce) : 컬렉션을 순환하면서 모든 원소를 람다표현식으로 연산해서 최종 결과를 일반적인 자료형 값으로 반환한다.
- 폴드(fold) : 리듀스와 동일하지만 초깃값 인자를 더 받아서 초깃값부터 람다표현식으로 연산한다.
val cities = listOf("Seoul","Tokyo")
cities.map { str: String -> str.uppercase() }.forEach{print(it)} // SEOULTOKYO
println()
cities.filter{it.contains("S")}.forEach{println(it)} // Seoul
val amounts = listOf(1,2,3,4,5)
println(amounts.reduce{total,x -> total+x}) // 15
그룹 연산
컬렉션을 처리할 때 특정 원소를 기준으로 동일한 범주로 묶어서 처리할 수 있다.
val cities1 = listOf("제주", "서울", "상하이")
val keySelector = { city: String ->
if (city.length == 3) "A" else "B"
}
cities1.groupBy(keySelector) // 함수 전달해서 그룹화 처리
.forEach { (key, value) -> // 맵으로 구성된 결과를 출력
println("$key : $value")
}
cities1.groupBy { if (it.length == 3) "A" else "B" } // 람다를 바로 전달해서 처리
.forEach { (key, value) ->
println("$key : $value")
}
// B : [제주, 서울]
// A : [상하이]
// B : [제주, 서울]
// A : [상하이]
시퀀스(Sequence)
컬렉션은 두 가지 방식으로 처리한다. (즉시 실행 / 지연 실행)
지연 처리하는 방식인 시퀀스를 알아보자!
지연 처리의 기준은 특정 액션을 만나면 그때 모든 것을 처리한다.
val seq = sequenceOf(1,2,3,4,5) // 시퀀스 생성
println("seq 원소 개수 : ${seq.count()}") // 실행 연산을 해야 처리된 결과를 보여준다
seq.forEach{print("$it , ")} // 실행해서 원소 출력
println()
val numList = listOf(1,2,3,4,5) // 리스트 생성
val seq2 = numList.asSequence() // 시퀀스로 변환
println("두 시퀀스 객체 비교 : ${seq == seq2}") // 두 객체 비교
println("seq2 원소개수 : ${seq2.count()}")
// seq 원소 개수 : 5
// 1 , 2 , 3 , 4 , 5 ,
// 두 시퀀스 객체 비교 : false
// seq2 원소개수 : 5
시퀀스는 최종 처리할 수 있는 메서드를 만나지 못하면 시퀀스 객체 상태를 유지한다. -> 실행 메서드를 호출하면 최종 결과를 반환
val words = "가나 다라마바 사아자차".split(" ")
iterableCheck(words)
val wordsSeq = words.asSequence()
seqCheck(wordsSeq)
fun iterableCheck(words: List<String>) {
val lengthsList = words
.filter {
println("iterable filter : $it")
it.length > 3
}
.map {
println("iterable 길이 확인 : ${it.length}")
it.length
}
println("----- iterable 처리된 결과 출력 -----")
println(lengthsList)
}
fun seqCheck(words: Sequence<String>) {
val lengthsSequence = words
.filter {
println("seq filter : $it")
it.length > 3
}
.map {
println("seq 길이 확인 : ${it.length}")
it.length
}
println("----- seq 처리된 결과 출력 -----")
println(lengthsSequence.toList())
}
// iterable filter : 가나
// iterable filter : 다라마바
// iterable filter : 사아자차
// iterable 길이 확인 : 4
// iterable 길이 확인 : 4
// ----- iterable 처리된 결과 출력 -----
// [4, 4]
// ----- seq 처리된 결과 출력 -----
// seq filter : 가나
// seq filter : 다라마바
// seq 길이 확인 : 4
// seq filter : 사아자차
// seq 길이 확인 : 4
// [4, 4]
- 일반 리스트 처리 : 메서드 단위를 모두 처리 후 다음 메서드 처리됨
- 시퀀스 처리 : toList 메서드가 실행될 때까지 처리하지 않는다. toList 메서드가 실행될 때 전체 로직이 실행된다.