Kotlin:Basic
코틀린 기초.
변수
변경 가능한 변수와 변경 불가능한 변수
-
val
- 값을 뜻하는 value에서 유래
- 변경 불가능한(immutable) 참조를 저장하는 변수다. 자바로 말하자면 final 변수에 해당한다.
-
var
- 변수를 뜻하는 variable에서 유래
- 변경 가능한(mutable) 참조다. 자바의 일반 변수에 해당한다.
기본적으로는 모든 변수를 val 키워드를 사용해 불변 변수로 선언하고, 나중에 꼭 필요할 때에만 var로 변경하라.
val 변수는 블록을 실행할 때 정확히 한 번만 초기화돼야 한다. 하지만 조건에 따라 val 값을 다른 여러 값으로 초기화할 수도 있다.
Class
class Person(
val name: String, // 읽기 전용 프로퍼티로, 코틀린은 (비공개) 필드와 필드를 읽는 단순한 (공개) 게터를 만들어 낸다.
var isMarried: Boolean // 쓸 수 있는 프로퍼티로, 코틀린은 (비공개)필드, (공개) 게터/세터를 만들어 낸다.
)
Enum
enum class Color(
val r: Int, val g: Int, val b: Int
) {
RED(255, 0, 0), ORANGE(255, 165, 0),
YELLOW(255, 255, 0), GREEN(0, 255, 0), BLUE(0, 0, 255),
INDIGO(75, 0, 130), VIOLET(238, 130, 238);
fun rgb() = (r * 256 + g) * 256 + b
}
When
fun getMnemonic(color: Color) =
when (color) {
Color.RED -> "Richard"
Color.ORANGE -> "Of"
Color.YELLOW -> "York"
Color.GREEN -> "Gave"
Color.BLUE -> "Battle"
Color.INDIGO -> "In"
Color.VIOLET -> "Vain"
}
코틀린에서 when은 자바의 switch보다 훨씬 더 강력하다.
분기 조건에 상수만을 사용할 수 있는 자바 switch와 달리 코틀린 when의 분기 조건은 임의의 객체를 허용한다.
fun mix(c1: Color, c2: Color) =
when (setOf(c1, c2)) {
setOf(RED, YELLOW) -> ORANGE
setOf(YELLOW, BLUE) -> GREEN
setOf(BLUE, VIOLET) -> INDIGO
else -> throw Exception("Dirty color")
}
인자 없는 When
fun mixOptimized(c1: Color, c2: Color) =
when {
(c1 == RED && c2 == YELLOW) ||
(c1 == YELLOW && c2 == RED) ->
ORANGE
(c1 == YELLOW && c2 == BLUE) ||
(c1 == BLUE && c2 == YELLOW) ->
GREEN
(c1 == BLUE && c2 == VIOLET) ||
(c1 == VIOLET && c2 == BLUE) ->
INDIGO
else -> throw Exception("Dirty color")
}
스마트 캐스트: 타입 검사와 타입 캐스트를 조합
코틀린에서는 is를 사용해 변수 타입을 검사한다. is 검사는 자바의 instanceof와 비슷하다. 하지만 자바에서 어떤 변수의 타입을 instanceof로 확인한 다음에 그 타입에 속한 멤버에 접근하기 위해서는 명시적으로 변수 타입을 캐스팅해야 한다. 코틀린에서는 프로그래머 대신 컴파일러가 캐스팅을 해준다. 이를 스마트캐스트라고 부른다.
fun eval(e: Expr): Int {
if (e is Num) {
val n = e as Num
return n.value
}
if (e is Sum) {
return eval(e.right) + eval(e.left)
}
throw IllegalArgumentException("Unknown expression")
}
Exception
// 조건이 참이면 number의 값이 초기화되고 거짓이면 초기화되지 않고 throw를 호출한다.
val number = try {
Integer.parseInt(reader.readLine())
} catch (e: NumberFormatException) {
return // 예외가 발생한 경우 catch 블록 다음의 코드는 실행되지 않는다.
}
문자열 템플릿
변수 이름 앞에 $를 붙이거나, 식을 ${식}처럼 ${ }로 둘러싸면 변수나 식의 값을 문자열 안에 넣을 수 있다.
함수 정의
fun
을 사용한다.
fun main(args: Array<String>) {
val strings = listOf("first", "second", "fourteenth")
println(strings.last()) // 리스트의 마지막 원소를 가져온다.
val numbers = setOf(1, 14, 2)
println(numbers.max()) // 컬렉션에서 최댓값을 가져온다.
}
제네릭 함수
fun
<T>를 사용한다.
fun <T> joinToString(
collection: Collection<T>,
separator: String = ";",
prefix: String = "(",
postfix: String = ")"
): String {
val result = StringBuilder(prefix)
for ((index, element) in collection.withIndex()) {
if (index > 0) result.append(separator)
result.append(element)
}
result.append(postfix)
return result.toString()
}
fun main(args: Array<String>) {
val list = listOf(1, 2, 3)
println(joinToString(list, "; ", "(", ")"))
println(joinToString(collection = list, separator = ";", prefix = "(", postfix = ")"))
println(joinToString(list))
println(joinToString(list, "; "))
}
확장 함수
개념적으로 확장 함수는 단순하다. 확장 함수는 어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만 그 클래스의 밖에 선언된 함수다. 문자열의 마지막 문자를 돌려주는 확장 메소드를 추가해보자
확장 함수는 오버라이드할 수 없다.
INFORMATION |
어떤 클래스를 확장한 함수와 그 클래스의 멤버 함수의 이름과 시그니처가 같다면 확장 함수가 아니라 멤버 함수가 호출된다(멤버 함수의 우선순위가 더 높다). 클래스의 API를 변경할 경우 항상 이를 염두에 둬야 한다. |
임포트와 확장 함수
확장 함수를 사용하기 위해서는 그 함수를 다른 클래스나 함수와 마찬가지로 임포트해야만 한다. 확장 함수를 임포트 없이 사용한다면 동일한 이름의 확장 함수와 충돌할 수도 있기 때문에 임포트로 어떤 확장함수인지 명시해 주어야 한다.
import strings.lastChar // 명시적으로 사용
import strings.* // * 사용 가능
import strings.lastChar as last // as 키워드를 사용 가능
확장 프로퍼티
확장 프로퍼티를 사용하면 기존 클래스 객체에 대한 프로퍼티 형식의 구문으로 사용할 수 있는 API를 추가할 수 있다. 프로퍼티라는 이름으로 불리기는 하지만 상태를 저장할 적절한 방법이 없기 때문에 실제로 확장 프로퍼티는 아무 상태도 가질 수 없다.
뒷받침하는 필드가 없어서 기본 게터 구현을 제공할 수 없으므로 최소한 게터는 꼭 정의를 해야 한다. 마찬가지로 초기화 코드에서 계산한 값을 담을 장소가 전혀 없으므로 초기화 코드도 쓸 수 없다.
var StringBuilder.lastChar: Char
get() = get(length - 1)
set(value: Char) {
this.setCharAt(length - 1, value)
}
fun main(args: Array<String>) {
println("Kotlin".lastChar)
val sb = StringBuilder("Kotlin?")
sb.lastChar = '!'
println(sb)
}
가변 인자 함수: 인자의 개수가 달라질 수 있는 함수 정의
가변 길이 인자(varargs)는 메소드를 호출할 때 원하는 개수만큼 값을 인자로 넘기면 자바 컴파일러가 배열에 그 값들을 넣어주는 기능이다. 코틀린의 가변 길이 인자도 자바와 비슷하다. 다만 문법이 조금 다르다. 타입 뒤에 ...를 붙이는 대신 코틀린에서는 파라미터 앞에 varag 변경자를 붙인다.
public fun <T> listOf(vararg elements: T): List<T> = if (elements.size > 0) elements.asList() else emptyList()
fun main(args: Array<String>) {
val list = listOf("one", "two", "eight")
}
이미 배열에 들어있는 원소를 가변 길이 인자로 넘길 때도 코틀린과 자바 구문이 다르다. 자바에서는 배열을 그냥 넘기면 되지만 코틀린에서는 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되게 해야 한다. 기술적으로는 스프레드(spread) 연산자가 그런 작업을 해준다.
중위 호출과 구조 분해 선언 (값의 쌍 다루기)
맵을 만들려면 mapOf 함수를 사용한다.
여기서 to라는 단어는 코틀린 키워드가 아니다. 이 코드는 중위 호출이라는 특별한 방식으로 to라는 일반 메소드를 호출한 것이다. 중위 호출 시에는 수신 객체와 유일한 메소드 인자 사이에 메소드 이름을 넣는다.
함수(메소드)를 중위 호출에 사용하게 허용하고 싶으면 infix 변경자를 함수(메소드) 선언 앞에 추가해야 한다.
/**
* Creates a tuple of type [Pair] from this and [that].
*
* This can be useful for creating [Map] literals with less noise, for example:
* @sample samples.collections.Maps.Instantiation.mapFromPairs
*/
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
이런 기능을 구조 분해 선언이라고 부른다. Pair 인스턴스 외 다른 객체에도 구조 분해를 적용할 수 있다. 예를 들어 key와 value라는 두 변수를 맵의 원소를 사용해 초기화할 수 있다.
정규식과 3중 따옴표로 묶은 문자열
fun parsePath(path: String) {
val regex = """(.+)/(.+)\\.(.+)""".toRegex()
val matchResult = regex.matchEntire(path)
if (matchResult != null) {
val (directory, filename, extension) = matchResult.destructured
println("Dir: $directory, name: $filename, ext: $extension")
}
}
로컬 함수와 확장
fun saveUser(user: User) {
fun validate(user: User,
value: String,
fieldName: String) {
if (value.isEmpty()) {
throw IllegalArgumentException(
"Can't save user ${user.id}: empty $fieldName")
}
}
validate(user, user.name, "Name")
validate(user, user.address, "Address")
// Save user to the database
}
Interface
interface Clickable {
fun click()
}
class Button : Clickable {
override fun click() = println("I was clicked")
}
// 결과값
// I was clicked
디폴트 구현
interface Clickable {
fun click() // 일반 메소드 선언
fun showOff() = println("I'm clickable!") // 디폴트 구현이 있는 메소드
}
open, final, abstract 변경자: 기본적으로 final
open class RichButton : Clickable { // 이 클래스는 열려있다. 다른 클래스가 이 클래스를 상속할 수 있다.
fun disable() {} // 이 함수는 파이널이다. 하위 클래스가 이 메소드를 오버라이드할 수 없다.
open fun animate() {} // 이 함수는 열려있다. 하위 클래스에서 이 메소드를 오버라이드해도 된다.
override fun click() {} // 이 함수는 (상위 클래스에서 선언된) 열려있는 메소드를 오버라이드 한다. 오버라이드한 메소드는 기본적으로 열려있다.
}
오버라이드하는 메소드의 구현을 하위 클래스에서 오버라이드하지 못하게 금지하려면 오버라이드하는 메소드 앞에 final을 명시해야 한다.
open class RichButton : Clickable {
// 여기 있는 'final'은 쓸데 없이 붙은 중복이 아니다.
// 'final'이 없는 'override' 메소드나 프로퍼티는 기본적으로 열려있다.
final override fun click() {}
}
abstract
자바처럼 코틀린에서도 클래스를 abstract로 선언할 수 있다. abstract로 선언한 추상 클래스는 인스턴스화할 수 없다. 따라서 추상 멤버 앞에 open 변경자를 명시할 필요가 없다.
abstract class Animated { // 이 클래스는 추상클래스다. 이 클래스의 인스턴스를 만들 수 없다.
abstract fun animate() // 이 함수는 추상 함수다. 이 함수에는 구현이 없다. 하위 클래스에서는 이 함수를 반드시 오버라이드해야 한다.
open fun stopAnimating() { ... } // 추상 클래스에 속했더라도 비추상 함수는 기본적으로 파이널이지만 원한다면 open으로 오버라이드를 허용할 수 있다.
fun animateTwice() { ... } // 추상 클래스에 속했더라도 비추상 함수는 기본적으로 파이널이지만 원한다면 open으로 오버라이드를 허용할 수 있다.
}
가시성 변경자: 기본적으로 공개
internal은 "모듈 내부에서만 볼 수 있음"이라는 뜻이다. 모듈은 한 번에 한꺼번에 컴파일되는 코틀린 파일들을 의미한다.
internal open class TalkativeButton : Focusable {
private fun yell() = println("Hey!")
protected fun whisper() = println("Let's talk!")
}
fun TalkativeButton.giveSpeesh() {
yell()
whisper() // 오류: "whisper"에 접근할 수 없음: "whisper"는 "TalkativeButton"의 "protected" 멤버임
}