< 스칼라 컬렉션 특징 >

- 사용하기 쉬움 : 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. 잎(단말 노드)에 정수 원소를 저장하는 이진 트리 클래스 계층


sealed abstract class Tree
case class Branch(left: Tree, right: Tree) extends Tree
case class Node(elem: Int) extends Tree

" foreach는 균형 잡힌 트리를 순회하는 데 트리 안의 노드 개수에 비례하는 시간이 걸린다. "

" N개의 잎이 있는 균현 잡힌 트리의 경우 Branch 타입의 내부 노드는 N-1 => N + N-1
sealed abstract class Tree extends Traversable[Int] {
  def foreach[U](f: Int => U) = this match {
    case Node(elem) => f(elem)
    case Branch(l, r) => l foreach f; r foreach f
  }
}

" ++ 연산의 효율성 문제로 트리에서 가져온 잎을 모을 때 log(N)번의 간접 경로 발생 => Nlog(N)
sealed abstract class Tree extends Iterable[Int] {
  def iterator: Iterator[Int] = this match {
    case Node(elem) => Iterator.single(elem)
    case Branch(l, r) => l.iterator ++ r.iterator
  }
}

 

< 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")
fruit: scala.collection.immutable.Set[java.lang.String] =
Set(apple, orange, peach, banana)

scala> fruit("peach")
res7: Boolean = true

scala> fruit("potato")
res8: Boolean = false

: 집합 연산으로 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) = {
  | println("taking my time."); Thread.sleep(100)
  | x.reverse }
f: (x: String)String

 

=> f에 부수 효과가 없다면, 같은 인자로 다시 f를 호출하면 항상 같은 결과가 나온다.

=> f를 사용해 계산한 인자와 결과 값을 맵에 담아두고 f의 인자를 맵의 키에서 찾을 수 없을 경우에만 f를 호출

=> 맵이 함수 f의 계산에 대한 캐시

 


scala> val cache = collection.mutable.Map[String, String]()
cache: scala.collection.mutable.Map[String,String] = Map()


scala> def cachedF(s: String) = cache.getOrElseUpdate(s, f(s))    // 두 번째 인자는 이름에 의한 호출
cachedF: (s: String)String

scala> cachedF("abc")
taking my time.
  res15: String = cba

scala> cachedF("abc")
res16: String = cba

 

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
vec: scala.collection.immutable.Vector[Nothing] = Vector()

scala> val vec2 = vec :+ 1 :+ 2
vec2: scala.collection.immutable.Vector[Int] = Vector(1, 2)

scala> val vec3 = 100 +: vec2
vec3: scala.collection.immutable.Vector[Int]
= Vector(100, 1, 2)

scala> vec3(0)
res23: Int = 100

: 벡터는 트리로 표현하고 모든 트리 노드는 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)
vec: scala.collection.immutable.Vector[Int] = Vector(1, 2, 3)

scala> vec updated (2, 4)      
res24: scala.collection.immutable.Vector[Int] = Vector(1, 2, 4)

scala> vec
res25: scala.collection.immutable.Vector[Int] = 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]()
empty: scala.collection.immutable.Queue[Int] = Queue()

scala> val has1 = empty.enqueue(1)
has1: scala.collection.immutable.Queue[Int] = Queue(1)

scala> val has123 = has1.enqueue(List(2, 3))
has123: scala.collection.immutable.Queue[Int]
= Queue(1, 2, 3)

scala> val (element, has23) = has123.dequeue
element: Int = 1
has23: scala.collection.immutable.Queue[Int] = Queue(2, 3)

 

< 범위 >

: 간격이 일정한 정수를 순서대로 나열한 시퀀스

scala> 1 to 3
res30: scala.collection.immutable.Range.Inclusive
  with scala.collection.immutable.Range.ByOne
= Range(1, 2, 3)

scala> 5 to 14 by 3
res31: scala.collection.immutable.Range
= Range(5, 8, 11, 14)


scala> 1 until 3
res32: scala.collection.immutable.Range
  with scala.collection.immutable.Range.ByOne = Range(1, 2)

: 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을 반환한다.

: 순차적으로 원소를 접근할 때 가장 좋고 연결 리스트에 원소를 추가하거나 다른 연결 리스트를 추가하기가 쉽다.

 

+ Recent posts