ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 7-1 컬렉션과 I/O 자세히 알아보기 : 컬렉션 타입(1)
    Study(종료)/Kotlin 22.09.13 ~ 12.18 2022. 10. 28. 22:18

    7장

    7-1. 컬렉션과 I/O 자세히 알아보기 : 컬렉션 타입 (1) (7-1.1~ 7-1.4)

    7-1. 컬렉션과 I/O 자세히 알아보기 : 컬렉션 타입 (2) ( 7-1.5~ 7-1.7 )

    7-1. 컬렉션과 I/O 자세히 알아보기 : 컬렉션 타입 (3) ( 7-1.8~ 7-1.11 )

    7-2. 컬렉션과 I/O 자세히 알아보기 : 파일과 I/O 스트림


    컬렉션은 정말 자주쓰고 정말 중요한 내용이라고 생각한다.

    사실 나는 이미 코딩테스트 준비를 하고, 실제 코드를 작성할때

    Hashmap, ArrayList, List 같은 컬렉션은 상당히 많이 쓰고 있지만,

    내부적으로 어떻게 구현되어 있는지, 어떤 내용을 상속받는지 등에 대해서는 잘 모르고 있다.

    이 단원에서 이러한 내용들에 대해 좀 더 자세히 살펴봐야겠다.

     

    7-1. 컬렉션

    컬렉션은 앨리먼트들로 이루어진 그룹을 저장하기 위해 설계된 객체다.

    같은 타입의 속하는 개수가 정해진 여러 원소를 함계 저장하는 배열이 바로 이런 컬렉션 객체였다.

     

    컬렉션 객체를 조작하는 모든 연산이 인라인 함수이므로,

    컬렉션이 제공하는 연산을 사용해도 함수 호출이나 람다 호출에 따른 부가 비용이 들지 않는다.

     

    1) 컬렉션 타입

    코틀린 컬렉션 타입은 크게 내 가지로 분류할 수 있다.

    배열, 이터러블(iterable), 시퀸스(sequence), 맵(map)이다.

    배열과 비슷하게 컬렉션 타입도 제네릭 타입이므로,

    컬렉션의 타입을 구체적으로 지정하려면 원소의 타입을 지정해야 한다

    예를들어 List<String> 은 문자열로 이루어진 리스트를, Set<Int>는 Int 타입의 값들로 이뤄진 집합을 의미한다

    컬렉션 타입들

    이터러블(Iterable)

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

    상태가 있다는 말은 컬렉션이 원소를 필요할 때 생성하는 제너레이터 함수를 유지하지 않고 원소를 저장한다는 뜻 이다.

    즉시 계산이라는 말은 나중에 어느 필요한 시점에 원소를 초기화하는게 아닌 컬렉션 최소 생성 시 초기화된다는 뜻 이다.

     

    이터러블 타입은 원소를 순회(traverse)할 수 있는 iterator() 메서드를 제공한다.

    코틀린 for 루프에서는 이 메서드를 통해 모든 이터러블 객체를 활용할 수 있다.

    val list = listOf("red", "green", "blue") // Create new List
    
    for(item in list){
        print("$item ") //red green blue
    }

    코틀린 Iterator 타입은 기본적으로 자바와 동일하다.

    내부에는 이터레이터가 컬렉션에 끝에 도달했는지 판단하는 hasNext()와

    컬렉션의 다음 원소를 반환하는 next() 메서드가 들어있다.

    유일한 차이는 remove()가 없다는 점인데, 이 메서드는 MuteableIterator로 옮겨졌다.

     

    자바와 비교할 때 코틀린 이터러블의 중요한 특징은 가변/불변 컬렉션을 구분한다는 점이다.

    불변 컬렉션은 생성한 뒤 내용을 바꿀 수 없지만, 가변 컬렉션은 언제든 원소를 추가/삭제가 가능하다

     

    컬렉션의 변경 가능성은 컬렉션 인스턴스에 대한 참조를 저장하는 변수의 변경 가능성과 아무런 관계가 없다는 점에 유의해라.

    fun foo(arr: List<String>) {...} 
    
    fun main(){
        val list1 = listOf("red", "green", "blue") 
        var list2 = listOf("red", "green", "blue") 
        foo(list1)
        foo(list2)
    }

    가변 변수가 뜻하는 것은 변수가 가리키는 참조를 다른 참조로 바꿀 수 있다는 점 뿐이다.

    예를들어 가변 컬렉션을 불편 변수에 저장할 수 있다. 이 경우 변수가 다른 컬렉션을 가리키게 변경할 수는 없지만,

    변수가 가리키는컬렉션에 원소를 추가하거나 삭제할 수 있다.

    var list2 = listOf("red", "green", "blue")
    val list1 = listOf("red", "green", "blue")
    list2 = list1 // OK
    list1 = list2 // Error : Val cannot be reassigned

    가변 이터러블의 기본 타입은 MutableIterable로 표현된다. 

    이 인터페이스의 객체는 MutableIterator 인스턴스를 생성할 수 있다.


    불변 컬렉션 타입에서 유용한 특징으로는 공변성(covariance)이 있다.

    공변성은 T가 U의 하위 타입인 경우 Iterable<T>도 Iterable<U>의 하위 타입이라는 의미이다.

    Iterator, Collection, List, Set, Map 등과 같은 컬렉션 관련 타입의 경우에도 이런 공변성이 성립한다.

    이를 이용해 아래와 같은 코드를 작성할 수 있다.

    fun processCollection(c: Iterable<Any>) {...}
    
    fun main(){
        var list = listOf("red", "green", "blue") //<List<String> type
        processCollection(list) // OK : List<String>을 List<Any>로 전달
    }

    하지만 가변 컬렉션의 경우 위와 같은 코드가 동작하지 않는다.

    가변 컬렉션에 대해 위 코드를 작성할 수 있따면 정수를 문자열 리스트에 추가하는 것과 같은 일이 벌어질 수 있다.

    fun processCollection(c: MutableCollection<Any>) {c.add(123)}
    
    fun main(){
        var list = listOf("red", "green", "blue") //<List<String> type
        processCollection(list) 
        // ERROR : Type mismatch. 
        // Required: MutableCollection<Any>
        // Found: List<String>
    }

     

    컬렉션, 리스트, 집합(Set)

    이터러블의 하위 분류 중에는 Collection 인터페이스로 표현되는 타입들과

    Collection 타입의 하위 타입인 MutableCollection 인터페이스로 표현되는 타입들이 있다.

    이들은 이터러블에 대해 여러 표준적인 구현을 위한 기본 클래스다.

    Collection을 상속한 클래스는 크게 아래와 같이 분류 가능하다.

     

    1. 리스트

      리스트(List, MutableList)는 인덱스를 통한 원소 접근이 가능한 순서가 정해진 원소의 컬렉션이다.

      일반적인 리스트 구현은 인덱스를 통한 임의 접근이 가능한 ArrayList와

      원소를 (끝쪽에) 빠르게 추가하거나 삭제할 수 있지만, 

      인덱스로 원소에 접근할 때는 선형 시간(O(n))이 걸리는 LinkedList가 있다.

     

    2. 집합(Set)

      1) HashSet

        HashSet은 해시 테이블이 기반인 구현이며 원소의 해시 코드에 따라 원소 순서가 정해진다.

        일반적으로 이 순서는 원소 타입의 hashCode() 메서드 구현에 따라 달라지므로 예측하기 어렵다.

      2) LinkedHashSet

        LinkedHashSet은 해시 테이블이 기반이지만, 삽입 순서를 유지하기 때문에 원소를 이터레이션하면

        집합에 삽입된 순서대로 원소를 순회할 수 있다.

      3) TreeSet

        TreeSet은 이진 검색 트리(binary search tree) 기반이며, 어떤 비교 규칙에 따라 일관성 있는 원소 순서를 제공한다.

        원소가 Comparable 인터페이스를 상속한 경우 원소 타입에 대한 this 비교 규칙을 정의할 수 있고

        별도의 Comparator 객체를 통해 비교 규칙을 제공할 수도 있다.


    시퀸스(Sequence)

    이터러블과 비슷하게 시퀸스도 iterator() 메서드를 제공한다. 이 메서드를 통해 시퀸스 내용을 순해할 수 있다.

    하지만 시퀸스는 지연 계산을 가정하기 때문에 iterator()의 의도가 이터러블과 다르다.

    대부분의 시퀸스 구현은 객체 초기화 시 원소를 초기화하지 않고 요청에 따라 원소를 계산한다.

    대부분의 시퀸스 구현은 상태가 없다. 이 말은 지연 계산한 컬렉션 원소 중에 정해진 개수의 원소만 저장한다는 뜻 이다.

    반면 이터러블은 원소 개수에 비례해 메모리를 사용한다.


    맵(Map)

    맵은 키(Key)와 값(Value) 쌍으로 이루어진 집합이다(Key Value Pair). 여기서 키는 유일해야 한다. 

    맵 자체는 Collection의 하위 타입이 아니지만 맵에 들어있는 원소들을 컬렉션처럼 사용할 수 있다.

    구체적인 예를 들면, 맵으로부터 모든 키로 이뤄진 집합을 얻을 수 있고 모든 키-값 쌍으로

    이뤄진 집합이나 모든 값으로 이뤄진 컬렉션을 얻을 수도 있다.키-값 쌍은 Map.Entry와

    MutableMap.MutableEntry 인터페이스로 표현된다.

     

    맵에는 두 종류의 원소(키, 값)가 들어있기 떄문에 맵의 타입은 두 가지 타입을 파라미터로 받는 제네릭 타입이다.

    예를 들어 Map<Int, String>은 Int 키를 String 값에 연관시켜주는 맵이다.

     

    맵의 표준 구현에는 HashMap, LinkedHashMap, TreeMap 등이 있다.


    2) Comparable과 Comparator

    자봐와 동일하게 코틀린도 Comparable, Comparator 타입을 지원하며 몇몇 컬렉션 연산에 이를 활용한다.

    비교가능(comparable) 인스턴스는 자연적인 순서(natural order)를 지원하며

    이런 타입의 인스턴스들은 모두 동일한 다입의 다른 인스턴스와 순서를 비교할 때 쓸 수 있는 CompareTo() 메서드를 포함한다.

     

    사용하는 타입이 Comparable을 사용하면 자동으로 < > 등의 연산을 사용할 수 있고

    원소 타입이 비교 가능한 타입인 컬렉션의 순서 연산에도 이런 연산이 쓰인다.

    Person 클래스에 전체 이름을 바탕으로 자연스러운 순서를 부여하고 싶을 경우를 생각해보자

    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)
    }

    CompareTo() 함수의 협약(Convention)은 자바와 동일하다.

    현재 (수신 객체) 인스턴스가 (인자로 받은) 상대방 인스턴스보다 더 크면 양수, 더 작으면 음수, 같으면 0을 반환한다.CompareTo() 구현은 equals() 함수 구현과 서로 의미가 일치해야 한다.

     

    어떤 클래스를 여러 가지 방법으로 비교해야 하는 경우도 많다.

    예를들어 Person의 컬렉션을 성, 이름, 나이 중 하나를 기준으로 정렬하거나,

    여러 프로퍼티의 조합을 기준으로 정렬할 수도 있다.이 경우 코틀린 라이브러리에

    있는 비교기(Comparator) 개념을 사용하면 된다.자바와 마찬가지로 Comparator<T> 클래스는

    타입 T의 인스턴스 객체를 두 개 인자로 받는 compare() 함수를 제공한다.

    이 함수는 compareTo() 와 동일한 협약을 따라 인자로 받은 두 인스턴스를 비교한 결과를 반환한다.

    코틀린에서는 람다 비교 함수를 통해 comparator를 간결하게 작성할 수 있다.

    val AGE_COMPARATOR = Comparator<Person>{p1, p2 -> p1.age.compareTo(p2.age)}

    compareBy()나 compareByDescending() 함수를 통해 대상 객체 대신 사용할 수 있는 비교 가능 객체를 제공함으로써comparator를 만들 수 도 있다.

    val AGE_COMPARATOR = compareBy<Person>{it.age}
    val REVERSE_AGE_COMPARATOR = compareByDescending<Person>{it.age}

    또한 comparator 인스턴스를 sorted()나 max()처럼 순서를 이용하는 함수에 넘길 수 있다.


    3) 컬렉션 생성하기

    배열 인스턴스를 생성자를 통해 생성하거나, arrayOf() 같은 표준 함수를 통해 생성할 수 있다.

    또한 자바와 마찬가지로 ArrayList나 LinkedHashSet 같은 클래스의 인스턴스를 생성자를 호출해 생성할 수 있다.

    val list = ArrayList<String>()
    list.add("red")
    list.add("green")
    println(list) // [red, green]
    
    val set = HashSet<Int>()
    set.add(12)
    set.add(20)
    set.add(12)
    println(set) // [12, 20]
    
    val map = TreeMap<Int, String>()
    map[20] = "Twenty"
    map[10] = "Ten"
    println(map) // {10=Ten, 20=Twenty}

    arrayOf()와 비슷하게 가변 길이 인자를 받는 함수를 사용해 몇몇 컬렉션 클래스의 인스턴스를 생성할 수 있다.


    •  ArrayList / LinkedHashSet / TreeSet 인스턴스를 생성하는 방법
      • emptyList() / emptySet() : 불변인 빈 리스트 / Set 인스턴스 생성
      • listOf() / setOf() : 인자로 제공한 배열에 기반한 불변 리스트 / Set 인스턴스 생성
      • listOfNotNull() : 널인 값을 걸러내고 남은 원소들로 이뤄진 새 불변 리스트 생성
      • mutableListOf() / mutableSetOf() : 가변 리스트/Set의 디폴트 구현 인스턴스 생성
      • arrayListOf() : 새로운 ArrayList 생성 (내부적으로 ArrayList, LinkedHashSet 사용)
      • hashSetOf() / linkedSetOf() / sortedSetOf() : HashSet / LinkedHashSet / TreeSet의 새 인스턴스 생성
    • 맵 인스턴스를 생성하는 방법
      • emptyMap() : 빈 불변 맵 생성
      • mapOf() : 새 불변 맵 생성 (내부적으로 LinkedHashMap을 만듬)
      • mutableMapOf() : 가변 맵의 디폴트 구현 인스턴스 생성 (내부적으로 LinkedHashMap을 만듬)
      • hashMapOf() / linkedMapOf() / sortedMapOf() : HashMap / LinkedHashMap / TreeMap의 새 인스턴스를 만듬

     

    맵 함수들은 Pair 객체들로 이뤄진 가변 인자를 받는다. to 중위 연산자를 사용하면 Pair 객체를 쉽게 만들 수 있다.

    또는 가변 맵을 만들고 set() 메서드나 인덱스 연산자( [ ] )를 사용해 맵에 원소를 추가할 수도 있다. 

    val emptyMap = emptyMap<Int, String>()
    println(emptyMap) // {}
    // emptyMap[10] = "ten" // Error : No set method providing array access
    
    val singletonMap = mapOf(10 to "ten")
    println(singletonMap) // {10=Ten}
    
    val mutableMap = mutableMapOf(10 to "Ten")
    println(mutableMap) // {10=Ten}
    mutableMap[20] = "Twenty"
    mutableMap[100] = "Hundred"
    mutableMap.remove(10)
    println(mutableMap) // {20=Twenty, 100=Hundred}
    
    val sortedMap = sortedMapOf(3 to "three", 1 to "one", 2 to "two")
    println(sortedMap) // {1=one, 2=two, 3=three}
    sortedMap.set(20, "Twenty");
    sortedMap[0] = "zero"
    println(sortedMap) // {0=zero, 1=one, 2=two, 3=three, 20=Twenty}

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

    println(List(5) {it + it}) // [0, 2, 4, 6, 8]

    원소를 알고 있는 시퀸스를 만드는 가장 단순한 방법은 sequenceOf() 함수를 사용하는 것이다.

    이 함수를 가변 인자를 받거나 배열, 이터러블, 맵 등의 기존 컬렉션 객체에 대해

    asSequence() 함수를 호출해 시퀸스를 얻을 수 있다.

    맵에 대해 asSequence()를 호출하면 맵 앤트리(Entry : Key, Value를 감싼 타입)의 시퀸스를 얻는다

    println(sequenceOf(1, 2, 3).iterator().next()) // 1
    println(listOf(10, 20, 30).asSequence().iterator().next()) // 10
    println(mapOf(1 to "One", 2 to "Two").asSequence().iterator().next()) // 1 = One

    제네레이터 함수를 바탕으로 시퀸스를 만드는 방법도 있다.

    generateSequence() 함수를 사용해 시퀸스를 만드는 두 가지 방법이 있다.

     

    1) generateSequence() 함수는 시퀸스의 다음 원소를 생성해주는 파라미터가 없는 함수를 인자로 받는다.

    이 함수가 널을 반환할 때 까지 시퀸스 원소 생성이 계속된다.

    val numbers = generateSequence{(readLine()?.toIntOrNull())}
    // 숫자가 아닌 입력을 받거나 null이 입력될 때 까지 입력을 받는 시퀸스
    // 숫자인 입력값을 받는 경우 다시 그 값을 반환

    2) generateSequence() 함수는 초깃값과 파라미터가 하나인 함수를 인자로 받는다.

    이 함수는 이전 값으로부터 다음 값을 만들어낸다. 

    첫 번째 방법과 동일하게 null을 반환할 때 까지 입력값을 계속 받는다.

     

    yield(), tieldAll() 빌더를 사용해 시퀸스를 만드는 방법도 있다.

    yield()는 원소를 하나 시퀸스에 추가하는 것 이고,

    yieldAll()은 지정한 이터레이터, 이터러블, 시퀸스에 들어 있는 모든 원소를 시퀸스에 추가한다.

    이 때 원소가 지연(lazy) 계산된다. 즉 시퀸스에서 각 부분에 속한 원소에 접근하는 경우에만 yield(), yiedAll()이 호출된다.

    val numbers = sequence{
        yield(20)
        yieldAll(listOf(22, 33, 44))
        yieldAll(intArrayOf(111, 222, 333).iterator())
        yieldAll(generateSequence(10) {if (it < 50) it * 3 else null})
    }
    println(numbers.toList()) // [20, 22, 33, 44, 111, 222, 333, 10, 30, 90]

    sequence() / yield() / yieldAll() 로 만들어진 시퀀스 빌더는

    실제로 유예 가능 계산(suspendable computation)이라는 강력한 코트린 기능의 예시이다.

    다중 스레드 환경에서 이 기능이 아주 유용하다.


    컬렉션 사이의 변환을 처리하는 함수 역시 존재한다.

    println(listOf(22, 33, 44).toSet())
    println(listOf(22, 33, 44).toList())
    println(sequenceOf(1 to "one", 2 to "two").toMap())

    4) 기본 컬렉션 연산

    모든 코틀린 컬렉션 타입이 지원하는 공통 연산으로는 이터레이션이 있다.

    배열, 이터러블, 시퀸스, 맵은 iterator() 함수를 지원한다. 이 함수가 반환하는 Iterator 객체를 사용해

    컬렉션 원소를 순회할 수 있지만 더 간결한 방법이 있으므로 실무에서 이터레이터를 사용하는 경우는 드물다.

     

    iterator()함수가 있다는 사실은 모든 컬렉션에 대해 for루프를 쓸 수 있다는 뜻 이다.

    다만 map의 iterator() 함수가 반환하는 이터레이터는 Map.Entry 타입의 값을 돌려준다는 사실을 알아둬야 한다.

    코틀린 맵 원소는 다음과 같이 맵 이터레이션(iteration : 순회, 반복, 되풀이)을 사용할 수 있다.

    val map = mapOf(1 to "one", 2 to "two", 3 to "three")
        for((key, value) in map){
            println("$key -> $value")
    //        1 -> one
    //        2 -> two
    //        3 -> three
        }

    for 루프의 대안으로 forEach() 확장 함수가 있다. 이 함수는 컬렉션의 각 원소를 제공하며 인자로 받은 람다를 실행해준다.

    intArrayOf(11, 22, 33).forEach { println(it * it) } // 121 484 1089
    listOf("a", "b", "c").forEach { println("$it") } // a b c

    원소의 인덱스를 사용하고 싶을 경우 forEachIndexed() 함수를 쓰면 된다.

    intArrayOf(11, 22, 33).forEachIndexed{ index, i -> println(i* i)} // 121 484 1089

    컬렉션 타입이 제공하는 기본 기능은 크게 3가지가 있다.

    • Size 프로퍼티는 원소 개수를 돌려준다.
    • isEmpty() 함수는 컬렉션에 원소가 없는지 검사한다
    • contains() / containsAll()
      • 인자로 지정한 원소나 인자로 지정한 컬렉션의 모든 원소가 수신 객체 컬랙션에 들어있는지 검사한다
      • contains() 함수를 in 연산자로 대체할 수 있다.
    val list = listOf("a", "b", "c")
    println(list.isEmpty()) // false
    println(list.size) // 3
    println("a" in list) // true
    println(list.containsAll(listOf("a", "d"))) // false
    println(list.containsAll(listOf("a", "b", "c"))) // true

    만약 직접 작성한 클래스의 인스턴스를 컬렉션 원소로 사용하는경우

    필요에 따라 내용을 기반으로 동등성 비교를 하는 equals()를 반드시 작성해야 contains() / containsAll()이 재대로 작동한다.

    리스트도 배열처럼 원소를 인덱스로 접근할 수 있는 메서드를 제공한다.

    val list = listOf("a", "b", "c")
    println(list[0]) // a
    println(list.get(2)) // c
    println(list.indexOf("b")) // 1

    MutableCollection 타입은 원소를 추가 / 제거할 수 있는 메서드를 제공한다.

    add() / remove() / addAll() / removeAll() 대신 += -=같은 복합 연산도 사용할 수 있다.

    val list = arrayListOf("a", "b", "c")
    list.add("z")
    list.remove("c")

    Map 인스턴스는 키를 사용해 값을 얻는 메서드와 모든 키나 값의 컬렉션을 돌려주는 메서드 등을 지원한다.

    val map = mapOf(1 to "one", 2 to "two", 3 to "three", 4 to "four")
    println(map.isEmpty()) // false
    println(map.size) // 4
    println(map.get(3)) // three
    println(map[1]) // one

    MutableMap 인스턴스는 기본적인 변경 연산과 + - 연산자를 지원한다.

    val map = sortedMapOf(2 to "two", 1 to "one", 4 to "four", 3 to "three")
    map.put(20, "twenty")
    map[100] = "hundred"
    map += 200 to "twoHundred"
    map -= 2

    다음글

    7-1. 컬렉션과 I/O 자세히 알아보기 : 컬렉션 타입 (2) ( 7-1.5~ 7-1.7 )

     

    반응형

    댓글

Designed by Tistory.