< 스칼라 컬렉션 특징 >
- 사용하기 쉬움 : 20~50개 메소드만 알아두면 대부분의 컬렉션 문제를 몇 가지 연산만 조합해 해결 가능하다.
- 간결함 : 하나 이상의 루프를 한 단어로 대체한다. => 쉽게 함수적 연산을 기술할 수 있고 연산을 조합할 수 있다. 이렇게 작성한 코드는 대수식 같아 보인다.
- 안전함 : 정적 타입 검사를 통과해야 해 대부분의 실수를 컴파일 시간에 걸러낼 수 있다.
* 걸러낼 수 있는 이유
1. 컬렉션 연산 자체가 널리 쓰이며 잘 검증되어 있다.
2. 컬렉션 연산의 입력과 출력을 함수 파라미터와 결과 값으로 명시해야 한다.
3. 2번 항목의 명시적인 입출력이 정적 타입 검사를 통과해야 한다.
- 빠름 : 컬렉션 연산은 튜닝과 최적화가 되어 있어 매우 효율적이다.
* 컬렉션을 다중 코어에서 병렬 실행할 수 있다. 병렬 컬렉션은 컬렉션과 동일한 연산을 제공하기 때문에 사용이 쉽다.
- 보편적임 : 각 컬렉션은 같은 연산을 제공한다. ( 문자의 시퀀스인 문자열도 모든 시퀀스 연산을 제공한다. )
24.1 변경 가능, 변경 불가능 컬렉션
: 스칼라 컬렉션은 변경 불가능한 것과 변경 가능한 것으로 구분한다.
- 변경 가능 컬렉션 : 메모리상에서 변경하거나 확장할 수 있는 컬렉션, 부수 효과를 사용해 원소를 변경, 추가, 삭제할 수 있다.
- 변경 불가능한 컬렉션 : 결코 변하지 않는다. 추가, 삭제, 변경하는 연산이 있지만 새 컬렉션을 반환하며 원래 컬렉션은 변하지 않고 그대로 남는다.
: 모든 컬렉션 클래스는 scala.collection 패키지나 그 하위 패키지인 muable, immutable, generic 패키지 안에 있다.
: 컬렉션 클래스는 3가지 변형이 존재하며 각각은 변경 가능성 측면에서 차이가 있다.(scala.collection, scala.collection.immutable, scala.collection.mutable 패키지에 위치)
- scala.collection.immutable 안 컬렉션 : 변경 불가능
- scala.collection.mutable : 변경 가능
- scala.collection : 변경 불가능할 수도, 변경 가능할 수도 있다.
Ex. scala.collection.IndexedSeq[T]는 scala.collection.immutable.IndexedSeq[T]와 scala.collection.mutable.IndexedSeq[T]의 슈퍼 트레이트이다.
: 일반적으로 scala.collection에 있는 루트 컬렉션들은 변경 불가능한 컬렉션과 동일한 인터페이스를 정의한다. 그리고 scala.collection.mutable에 변경 불가능한 인터페이스에 부수 효과가 있는 변경 연산을 추가한다.
: 루트 컬렉션과 변경 불가능한 컬렉션의 차이는 변경 불가능한 것을 사용하는 클라이언트에게는 컬렉션이 변경되지 않는다는 것을 보장하는 반면, 루트 컬렉션은 루트 컬렉션 타입을 통해서 컬렉션을 변경할 수 없다는 점이다. 단, 실행 시점에 루트 컬렉션이 변경 가능한 컬렉션일 수 있기 때문에 그 컬렉션이 변경될 수도 있다.
: 기본적으로 스칼라는 항상 변경 불가능한 컬렉션을 선택한다.
: collection.generic은 컬렉션 클래스들의 연산에 대한 구현을 generic에 있는 클래스에 위임한다.
24.2 컬렉션 일관성
: 어떤 종류의 컬렉션이라도 일관되고 동일한 문법으로 생성할 수 있다.
: 모든 컬렉션은 컬렉션에서 지원하는 API에 대해 구체적인 개별 클래스를 반환한다.
Ex. List에 대해 map을 호출하면 List가 나오고 Set에 대해 map을 호출하면 Set을 반환한다.
: 대부분의 컬렉션은 루트, 변경 가능, 변경 불가능한 세 가지 버전이 존재한다. 유일한 예외는 Buffer 트레이트로 변경 가능한 컬렉션만 있다.
24.3 Traversable 트레이트
: Traversable 트레이트에는 foreach 유일한 추상 멤버가 존재해 클래스가 Traversable을 구현하려면 foreach만 정의하면 된다.
* def foreach[U](f:Elem => U) : 모든 원소를 순회하면서 f 연산을 각 원소에 적용
: foreach가 Traversable의 모든 연산을 만드는 기반
: Traversable은 아래의 구체적인 메소드를 포함한다.
* 크기 연산 : 순회 가능한 컬렉션은 유한할 수도 있고 무한할 수도 있다. 무한한 순회 가능 컬렉션의 예로 자연수의 스트림인 Stream.from(0) => hasDefiniteSize가 false라면 size는 오류를 발생시키거나 아무 값도 반환하지 않는다.
* 원소를 가져오는 연산 : 해시 집합은 순서를 가지고 있지 않기 때문에 해시 집합의 첫 원소는 프로그램을 실행할 때마다 바뀔 수 있다.(순서를 포기하는 대신 효율성을 선택)
: 어떤 컬렉션이 매번 원소를 같은 순서로 반환하는 경우 이를 순서가 있는 컬렉션이라 한다.
: 순서가 꼭 필요한 경우가 있기 때문에 몯느 컬렉션에 대해 순서가 정해진 대안을 제공한다.(순서가 있는 HashSet은 LinkedHashSet)
* view는 필요에 따라 나중에 계산이 이뤄지는 컬렉션
24.4 Iterable 트레이트
: iterable의 모든 메소드는 추상 메소드 iterator을 기반으로 한다. iterator는 컬렉션의 원소를 하나씩 돌려준다.
: Traversable에서 상속한 추상 foreach 메소드를 iterator를 사용해 정의한다.
def foreach[U](f: Elem => U): Unit = { val it = iterator while (it.hasNext) f(it.next()) } |
: 효율적인 구현을 제공하기 위해서 Iterable 서브 클래스 중 상당수는 Iterable에 있는 표준 foreach 구현을 오버라이드한다.
: 이터레이터를 반환하는 메소드로 grouped와 sliding이 있다.
scala> val xs = List(1, 2, 3, 4, 5) xs: List[Int] = List(1, 2, 3, 4, 5) scala> val git = xs grouped 3 git: Iterator[List[Int]] = non-empty iterator scala> git.next() res2: List[Int] = List(1, 2, 3) scala> git.next() res3: List[Int] = List(4, 5) scala> val sit = xs sliding 3 sit: Iterator[List[Int]] = non-empty iterator scala> sit.next() res4: List[Int] = List(1, 2, 3) scala> sit.next() res5: List[Int] = List(2, 3, 4) scala> sit.next() res6: List[Int] = List(3, 4, 5) |
< Trabersable 과 Iterable이 각각 존재하는 이유 >
: 모든 것을 iterator로 하지 않고 모든 메소드를 iterator 대신 foreach를 가지고 만들기 위해 한 단께 더 추상적인 트레이트를 두는 이유는 때때로 iterator을 구현하는 것보다 foreach 메소드의 구현을 효율적으로 하는 편이 쉬운 경우가 있기 때문
Ex. 잎(단말 노드)에 정수 원소를 저장하는 이진 트리 클래스 계층
" N개의 잎이 있는 균현 잡힌 트리의 경우 Branch 타입의 내부 노드는 N-1 => N + N-1 |
< Iterable의 하위 분류 >
: Iterable 아래에는 Seq, Set, Map 하위 트레이트가 위치
: Set, Map, Seq의 공통점은 PartialFunction을 구현해서 apply와 isDefinedAt 메소드를 제공한다.
- Seq의 apply는 위치 인덱스로 사용 => Seq(1,2,3)(1) == 2
- Set의 apply는 원소인지 검사 => Set(1,2,3)(1) == true , Set()(1) == false
- Map의 apply는 키로 값 선택 => Map('a'->1, 'b'->10)('b') == 10
24.5 시퀀스 트레이트 : Seq, IndexedSeq, LinearSeq
: 시퀀스는 길이가 정해져 있고, 각 원소의 위치를 0부터 시작하는 고정된 인덱스로 지정할 수 있는 iterable의 일종
: Seq의 apply는 인덱스로 원소를 찾는 것으로 Seq[T] 타입의 시퀀스는 Int 인자(인덱스)를 받아서 T 타입의 원소를 돌려주는 부분 함수다.(Seq[T]는 PartialFunction[Int, T]를 확장한다.)
: Seq 트레이트에는 두 가지 하위 트레이트 LinearSeq와 IndexedSeq가 있다. 각각 성능이 다르다.
: 선형 시퀀스는 더 효율적인 head와 tail 연산을 제공한다. List나 Stream
: 인덱스 시퀀스는 효율적인 apply와 length, update연산을 제공한다. Array와 ArrayBuffer
: Vector는 선형 시퀀스와 인덱스 시퀀스 사이에 절충을 제공한다. 사실상 인덱스 시간과 선형 접근 시간이 상수
< 버퍼 >
: 기존 원소의 변경을 허용하고 원소의 삽입, 제거도 지원하며 버퍼의 맨 뒤에 효율적으로 원소를 추가할 수 있다.
: 버퍼 구현으로 List를 배경으로 하고 원소들을 효율적으로 리스트로 바꿀 수 있는 ListBuffer, Array를 배경으로 하고 원소들을 빠르게 배열로 바꿀수 있는 ArrayBuffer가 있다.
24.6 집합
: 원소 중복을 허용하지 않는 Iterable
: contain는 어떤 집합에 주어진 원소가 들어 있는지 표시한다. 집합에 대한 apply 메소드는 contains와 동일하다. => set(elem)은 set contains elem과 같다.
scala> val fruit = Set("apple", "orange", "peach", "banana") |
: 집합 연산으로 intersect, union, diff가 있고 그에 대응하는 기호 이름은 각각 &, |, &~이다.
: Traversable에서 상속한 ++는 union이나 |의 별명으로 볼 수 있지만 union이나 |는 집합만을 취하는 반면, ++는 Traversable 인자를 취할 수 있다.
: +, ++, -, --는 변경 불가능/변경 가능 집합에 원소 추가/제거를 위해 존재하지만 연산이 진행될 때 집합을 복사하기 때문에 변경 가능 집합에서는 잘 사용하지 않는다.
: 변경 가능 집합에서는 효율적인 대안으로 +=와 -=을 사용한다.
: 변경 불가능한 집합에서도 +=와 -= 사용 가능 => 변경 불가능한 집합에서 s+=4는 s = s +4를 짧게 쓴 것
scala> var s = Set(1, 2, 3) s: scala.collection.immutable.Set[Int] = Set(1, 2, 3) scala> s += 4; s -= 2 scala> s res9: scala.collection.immutable.Set[Int] = Set(1, 3, 4) scala> val s = collection.mutable.Set(1, 2, 3) s: scala.collection.mutable.Set[Int] = Set(1, 2, 3) scala> s += 4 res10: s.type = Set(1, 4, 2, 3) scala> s -= 2 res11: s.type = Set(1, 4, 3) |
=> 변경 가능 컬렉션을 val로 저장하는 것은 변경 불가능한 컬렉션을 var에 저장하는 것으로 바꾸거나 그 반대로 바꿀 수 있다.
: 변경 가능 집합은 +=와 -= 다른 버전으로 add와 remove를 제공하며 차이점은 연산이 집합에 대해 효과가 있었는지 알려주는 불리언 값을 반환한다.
: 변경 가능한 집합은 해시 테이블을 사용해 원소를 저장한다.
: 변경 불가능한 집합은 원소의 개수에 따라 가변적인 방식(빈 집합 - 싱글톤, 4개 이하는 모든 원소를 필드로 저장하는 객체 하나, 5개 이후는 해시 트라이를 사용해 구현) => 원소가 4개 이하인 변경 불가능한 집합은 변경 가능 집합보다 훨씬 작고 효율적
24.7 맵
: 키와 값 쌍 Iterable
: 스칼라의 Predef 클래스에 key->value를 (key, value) 대신 사용할 수 있는 암시적 변환이 들어있다. => Map("x"->24)은 Map(("x", 24))와 같다.
: Map의 get 연산은 키에 대한 값이 있다면 값을 Some으로 묶어서 반환한다. 키가 없다면 None를 반환한다. apply 메소드는 Option에 값을 감싸지 않고 바로 반환한다.
: 변경 가능한 맵도 +, -, updated와 같은 연산을 제공하지만 복사해야 하기 때문에 자주 사용하지는 않는다. 대신 변경 가능한 맵은 보통 m(key) = value 나 m+= (key->value)를 사용해 그 자리에서 변경한다.
: 맵을 캐시로 사용하는 경우 getOrElseUpdate가 유용
" 비용이 많이 드는 f " scala> def f(x: String) = {
=> f에 부수 효과가 없다면, 같은 인자로 다시 f를 호출하면 항상 같은 결과가 나온다. => f를 사용해 계산한 인자와 결과 값을 맵에 담아두고 f의 인자를 맵의 키에서 찾을 수 없을 경우에만 f를 호출 => 맵이 함수 f의 계산에 대한 캐시
|
24.8 변경 불가능한 구체적인 컬렉션 클래스
< 리스트 >
: 유한한 변경 불가능한 시퀀스, head와 tail은 상수 시간이 걸린다. 다른 많은 연산은 선형 시간이 걸린다.
< 스트림 >
: 리스트와 비슷하지만 원소를 지연 계산한다.
: 스트림은 무한할 수가 있고 외부에서 요청하는 원소만 계산한다.
: 리스트는 "::"연산자로 구성하는 반면 스트림은 "#::" 을 사용해 구성한다.
scala> val str = 1 #:: 2 #:: 3 #:: Stream.empty str: scala.collection.immutable.Stream[Int] = Stream(1, ?) |
: 인터프리터가 tail 부분을 표시하지 않았는 데 이는 아직 계산이 이뤄지지 않았기 때문이다. 스트림은 요청을 받으면 그 때 계산한다. toString 메소드는 스트림의 추가 계산을 요구하지 않도록 한다.
Ex. 피보나치
scala> def fibFrom(a: Int, b: Int): Stream[Int] = a #:: fibFrom(b, a + b) fibFrom: (a: Int,b: Int)Stream[Int] |
: "#::" 대신 "::"을 사용했따면 이 함수에 대한 호출이 다른 호출을 다시 만들어 내서 무한 재귀가 일어날 것이다. 하지만 "#::"을 사용했기 때문에 "#::"의 오른쪽 부분은 요청이 있기 전까지 계산하지 않는다.
scala> val fibs = fibFrom(1, 1).take(7) fibs: scala.collection.immutable.Stream[Int] = Stream(1, ?) scala> fibs.toList res22: List[Int] = List(1, 1, 2, 3, 5, 8, 13) |
< 벡터 >
: 리스트는 헤드에 접근, 추가, 제거하는 데 상수 시간이 걸려 헤드만을 처리한다면 매우 효율적이다. 하지만 리스트 뒤쪽에 있는 우너소에 접근하거나 이를 변경하는 시간은 리스트의 길이에 선형으로 비례해 길어진다.
: 벡터는 헤드가 아닌 원소도 효율적으로 접근할 수 있는 컬렉션 타입이다. 임의의 원소에 접근하기 위해서 사실상 상수 시간이 걸린다.
scala> val vec = scala.collection.immutable.Vector.empty |
: 벡터는 트리로 표현하고 모든 트리 노드는 32개의 벡터 원소를 넣거나 32개의 다른 트리 노드를 넣을 수 있다.
: 32개 이하인 벡터는 노드 하나로 표현할 수 있다. 원소가 1024(32*32)개가 될 때까지는 한 번만 간접 노드를 거치면 접근이 가능하다.
: 2개 노드를 거치면 2^15, 3개 노드를 거치면 2^20, 4개 노드를 거치면 2^25, 5개 노드를 거치면 2^30 원소를 다룬다.
=> 일반적인 원소 선택은 최대 5단계 정도의 기본 배열 선택으로 가능하다. => 사실상 상수 시간
: 벡터는 변경 불가능
scala> val vec = Vector(1, 2, 3) |
: updated도 사실상 상수 시간이다. 백터의 중간에 있는 원소를 변경하려면 그 원소가 있는 노드를 복사하고 트리의 루트부터 시작해서 원래의 노드를 가리키던 모든 노드를 복사한다. => 전체 벡터를 모두 복사하는 경우보다는 비용이 훨씬 덜 든다.
: 벡터가 빠른 임의 접근 읽기와 빠른 임의 접근 변경 사이에서 균형을 잘 잡고 있기 때문에 변경 불가능한 인덱스가 있는 시퀀스의 기본 구현은 벡터이다.
scala> collection.immutable.IndexedSeq(1, 2, 3) res26: scala.collection.immutable.IndexedSeq[Int] = Vector(1, 2, 3) |
< 변경 불가능한 스택 >
: 후입선출 시퀀스, push/pop/peek 모두 상수 시간
: 변경 불가능한 스택에 push하는 것은 리스트의 ::와 같고 스택의 pop은 tail과 같기 때문에 사용하는 경우는 드물다.
scala> val stack = scala.collection.immutable.Stack.empty stack: scala.collection.immutable.Stack[Nothing] = Stack() scala> val hasOne = stack.push(1) hasOne: scala.collection.immutable.Stack[Int] = Stack(1) scala> stack res27: scala.collection.immutable.Stack[Nothing] = Stack() scala> hasOne.top res28: Int = 1 scala> hasOne.pop res29: scala.collection.immutable.Stack[Int] = Stack() |
< 변경 불가능한 큐 >
: 선입선출 시퀀스
scala> val empty = scala.collection.immutable.Queue[Int]() |
< 범위 >
: 간격이 일정한 정수를 순서대로 나열한 시퀀스
scala> 1 to 3 |
: 3개의 수인 시작, 끝, 간격만 가지고 모든 범위를 표현할 수 있기 때문에 범위를 표현하는 공간 복잡도는 상수 => 이런 표현 때문에 범위에 대한 대부분의 연산이 매우 빠르다
< 해시 트라이 >
: 변경 불가능한 집합이나 맵을 효율적으로 구현하는 표준적인 방법이다.
: 벡터와 비슷하게 32개의 원소나 32개의 하위 트리를 포함하는 노드를 사용하는 트리다.하지만 선택 시 해시 코드를 활용한다.
: 맵에서 어떤 키를 찾을 때 키의 해시 코드의 최하위 5비트를 루트의 하위 트리를 선택하기 위해 사용하고 그 다음 5비트는 다음 단계 트리를 선택하는 데 사용된다.
: 노드 안에 들어 있는 모든 원소의 해시 코드가 지금까지 트리를 순회하는 데 사용한 비트들을 제외하고 모두 다 달라지면 검색을 중단한다.
: 해시 트라이는 충분히 빠른 검색과 충분히 효율적인 함수형 추가(+), 제거(-) 사이의 균형을 잘 잡고 있다. 그래서 스칼라의 맵과 집합의 기본 구현에 사용된다.
< 적흑 트리 >
: 일부 노드는 빨간색, 일부 노드는 검은색으로 표시된 균형 이진 트리
: TreeSet과 TreeMap, SortedSet이 적흑 트리를 사용한다.(적흑 트리를 사용하면 모든 원소를 정렬 순서대로 방문할 수 있는 효율적인 이터레이터를 만들 수 있기 때문)
< 변경 불가능한 비트 집합 >
: 비트 집합은 작은 정수의 컬렉션을 그보다 더 큰 정수의 비트로 표현된다. ( Ex. 3,2,0을 포함하는 비트 집합은 이진수 1101, 십진수 13)
: 내부적으로 64비트 Long 배열 사용, 첫 번째 Long은 0부터 63까지의 비트를 표현하고 두 번째 Long은 64부터 127까지 표현한다.
: 원소 포함 여부 검사는 상수 시간이 걸린다. => Long 배열에서 특정 Long을 고른 다음, 그 Long 안에서 비트 연산을 수행
: 원소를 추가하는 데는 Long의 개수에 선형으로 비례하는 시간이 걸린다.
* 원소를 추가할 때 해당 원소가 비트 집합의 현재 Long 배열이 표현할 수 있는 범위 안에 있다면 상수시간에 원소 추가 가능
scala> val bits = scala.collection.immutable.BitSet.empty bits: scala.collection.immutable.BitSet = BitSet() scala> val moreBits = bits + 3 + 4 + 4 moreBits: scala.collection.immutable.BitSet = BitSet(3, 4) scala> moreBits(3) res34: Boolean = true scala> moreBits(0) res35: Boolean = false |
< 리스트 맵 >
: 키-값 쌍의 연결 리스트로 맵을 표현한다.
: 리스트 맵에 대한 연산은 전체 리스트를 이터레이션해야 한다. 따라서 맵의 크기에 선형으로 비례하는 시간이 걸린다.
: 변경 불가능 맵이 더 빠르기 때문에 사용할 일이 거의 없으나 맵의 첫 원소를 다른 원소들보다 더 자주 선택해야 하는 경우 사용한다.
24.9 변경 가능한 구체적인 컬렉션 클래스
< 배열 버퍼 >
: 배열과 크기를 저장한다. 대부분의 연산은 배열과 속도가 같고 데이터를 배열 끝에 효율적으로 추가할 수 있다.(상수 분할 상환 시간이 걸린다.)
< 리스트 버퍼 >
: 배열 버퍼와 비슷하나 내부에서 배열 대신 연결 리스트를 사용한다.
: 버퍼를 다 구성한 다음에 리스트로 변환할 예정이라면 배열 버퍼 대신에 리스트 버퍼를 사용
< 문자열 빌더 >
: 문자열을 만들 때 유용하다.
: 문자열 빌더는 기본 네임스페이스에 이미 들어가 있어 바로 사용 가능
scala> val buf = new StringBuilder buf: StringBuilder = StringBuilder() scala> buf += 'a' res43: buf.type = StringBuilder(a) scala> buf ++= "bcdef" res44: buf.type = StringBuilder(a, b, c, d, e, f) scala> buf.toString res45: String = abcdef |
< 연결 리스트 >
: 다음 노드를 가리키는 next 포인터로 서로 연결된 노드들로 이뤄진 변경 가능 시퀀스
: 대부분 언어에서 빈 연결 리스트를 표현하는 데 스칼라는 next 포인터가 자기 자신을 가리키는 방법을 사용 => 빈 연결리스트.isEmpty는 NullPointerException 대신 true을 반환한다.
: 순차적으로 원소를 접근할 때 가장 좋고 연결 리스트에 원소를 추가하거나 다른 연결 리스트를 추가하기가 쉽다.
'스칼라' 카테고리의 다른 글
스칼라 26장 익스트랙터(Programming in Scala, 3rd) (0) | 2019.06.24 |
---|---|
스칼라 25장 스칼라 컬렉션의 아키텍처(Programming in Scala, 3rd) (0) | 2019.06.24 |
스칼라 23장 for 표현식 다시 보기(Programming in Scala, 3rd) (0) | 2019.06.24 |
스칼라 22장 리스트 구현(Programming in Scala, 3rd) (0) | 2019.06.23 |
스칼라 21장 암시적 변환과 암시적 파라미터(Programming in Scala, 3rd) (0) | 2019.06.23 |