출처: https://www.boostcourse.org/mo234/lecture/154310?isDesc=false
String s1 = "Cat"
에서 "Cat"과 같은 String 리터럴은 Heap 영역 내의 String Pool 이라는 곳에 생성되며, 객체 s1은 스택 영역에서 해당 문자열을 가리키고 있다. 그리고 String 리터럴로 생성한 객체는, 내용이 같으면 동일한 메모리 주소를 가리킨다.
반면에, 자바에서 new 연산자로 생성한 문자열 객체는 같은 값이 String Pool에 이미 존재하더라도, Heap 영역 내에 매번 새로운 객체로 생성된다.
cf) 자바에서 == 연산자는 값과 참조를 모두 비교하지만, 코틀린에서 == 연산자는 값만 비교하고 참조를 비교할 때는 === 연산자를 사용한다.
package chap04.section3
fun main() {
val hello = "Hello World!" // val은 변경 불가
println(hello[0]) // H
//hello[0] = 'K' // error
var s = "abcdef" // var은 변경 가능
s = "xyz" // 새로운 메모리 공간에 할당됨. (기존의 문자열은 가비지 컬렉터에 의해 제거됨.)
println(s)
}
그리고 코틀린의 String 리터럴은 불변(immutable) 값으로 생성된다. 즉, 한번 생성된 문자열의 메모리 주소는 변경될 수 없다. 따라서, 문자열 객체에 새로운 값을 할당하면 원본 자체가 변경되는 게 아니라 아예 새로운 메모리 공간에 할당된다. 위의 예제에서 s는 "abcdef"가 아니라 "xyz" 문자열을 가리키게 된다.
String.substring(인덱스 범위 지정): String
CharSequence.subSequence(인덱스 범위 지정): CharSequence
package chap04.section3
fun main() {
var s = "abcdef"
s = s.substring(0..1) + "x" +
s.substring(3 until s.length)
println(s) // abxdef
}
a.compareTo(b)
- a가 b보다 작으면 양수, 같으면 0, 그렇지 않으면 음수 반환
- 각 문자를 아스키 코드로 변환하여 두 문자열의 크기 비교
- 다른 문자를 하나라도 발견하면, 두 문자의 크기 차이 반환하고 종료
package chap04.section3
fun main() {
val s1 = "Hello Kotlin"
val s2 = "Hello KOTLIN"
println(s1.compareTo(s2)) // o(111), O(79) -> 32 반환
println(s1.compareTo(s2, true)) // 대소문자 무시하면, 동일 크기여서 0 반환
}
앞서 말했듯이 String은 불변 객체여서, 메모리에 한번 할당된 값을 바꿀 수 없다.
문자열을 변경하고 싶을 때는 새 String 객체를 생성하고, 참조 값을 바꾸는 방식으로 동작한다. (기존의 문자열 객체는 GC가 수거해간다.)
이러한 방식으로는 문자열이 변경될 때마다 메모리의 할당 및 해제가 반복되어서 성능이 좋지 않다.
StringBuilder는 초기화 된 값보다 더 크게 일정한 버퍼 공간을 사용한다.
따라서, 문자열의 변경이 자주 일어날 때, 추가 메모리 공간을 할당하지 않고 기존의 버퍼 공간을 활용할 수 있어서 메모리 효율적이다.
package chap04.section3
fun main() {
val s = java.lang.StringBuilder("Hello")
s[2] = 'x' // 요소의 변경이 가능해짐.
println(s) // Hexlo
}
단, 문자열을 변경할 때 버퍼 공간의 문자를 일종의 포인터로 가리키는 원리여서, 기존의 String보다 처리 속도가 느린 편이다.
단어를 변경하지 않을 때는 StringBuilder의 버퍼 공간으로 인해, 불필요하게 메모리를 낭비할 수도 있다.
따라서, 상황에 따라 String, StringBuilder 중에 적합한 것을 선택해서 사용하자.
참고로 멀티 스레드 환경에서 thread-safe 하게 연산을 수행하려면, StringBuilder 대신에 StringBuffer를 사용해야 한다. StringBuffer는 문자열 조작 함수에 synchronized 키워드가 붙어있어서 동시성 제어가 가능하기 때문이다. 멀티 스레드 환경이 아니라면, String Builder를 사용하는 것이 성능이 더 좋다.
package chap04.section3
fun main() {
val sb = StringBuilder("Hello")
// 문자열 추가
sb.append(", Kotlin!")
println("After append: $sb") // Hello, Kotlin!
// 문자열 삽입
sb.insert(7, "beautiful ")
println("After insert: $sb") // Hello, beautiful Kotlin!
// 문자열 삭제
sb.delete(7, 17)
println("After delete: $sb") // Hello, Kotlin!
// 문자열 교체
sb.replace(0, 5, "Hi")
println("After replace: $sb") // Hi, Kotlin!
// 문자열 뒤집기
sb.reverse()
println("After reverse: $sb") // !nitloK ,iH
}
- 소문자/대문자 변경: uppercase(), lowercase()
- 특정 문자 단위로 잘라내기: split("분리 문자") → 분리된 내용은 List로 반환
- 앞뒤의 화이트 스페이스 문자 제거: trim()
package chap04.section3
fun main() {
val deli = "Welcome to Kotlin"
val sp = deli.split(" ")
println(sp) // [Welcome, to, Kotlin]
}
cf) newline과 carriage return은 윈도우, 맥, 리눅스 등 사용하는 운영체제에 따라 처리 방식이 약간 다르다.
Window | Mac | Unix |
---|---|---|
\r\n | \r | \n |
개행 그대로 출력하면서 앞의 공백은 제거하고 싶은 경우 trimMargin() 사용
package chap04.section3
fun main() {
val text = """
Tell me and I forget.
|Teach me and I remember.
Involve me and I learn.
|(Benjamin Franklin)
""".trimMargin() // trim 디폴트 문자는 |
println(text)
}
[Tab] Tell me and I forget.
Teach me and I remember.
[Tab] Involve me and I learn.
(Benjamin Franklin)
format()을 사용하여 원하는 형식대로 출력할 수 있다.