코틀린 인 액션 - Chapter2. 코틀린 기초
by Gunju Ko
코틀린 인 액션 - 코틀린 기초
기본 요소 : 함수와 변수
fun main(args: Array<String>) {
println("Hello World!")
}
- 함수를 선언할 때 fun 키워드를 사용한다.
- 함수를 최상위 수준에 정의할 수 있다. 꼭 클래스 안에 함수를 넣어야 할 필요가 없다.
fun max(a: Int, b: Int): Int {
return if (a > b) a else b
}
- 코틀린 if는 문장이 아니고 결과를 만드는 식이다.
문(statement)과 식(expression) 구분
- 식은 값을 만들어 내며 다른 식의 하위 요소로 계산에 참여할 수 있는 반면 문은 자신이 둘러싸고 있는 가장 안쪽 블록의 최상위 요소로 존재하며 아무런 값을 만들어내지 않는다.
- 자바에서는 모든 제어 구조가 문인 반면 코틀린에서는 루프를 제외한 대부분의 제어 구조가 식이다.
- 반면 대입문은 자바에서는 식이였으나 코틀린에서는 문이 됐다.
식이 본문인 함수
fun max(a: Int, b: Int): Int = if (a > b) a else b
- 본문이 중괄호로 둘러싸인 함수를 블록이 본문이 함수라 부르고, 등호와 식으로 이뤄진 함수를 식이 본문이 함수라고 부른다.
- 위의 코드에서 반환 타입을 생략할 수 있다.
- 컴파일러가 함수 본문 식을 분석해 식의 결과 타입을 함수 반환 타입으로 정해준다. (타입 추론)
- 식이 본문이 함수의 반환 타입만 생략 가능하다.
- 블록이 본문인 함수는 반환 타입을 지정해야 하며, return문을 사용해 반환 값을 명시해야 한다.
변수
- 타입을 지정하지 않으며 컴파일러가 초기화 식을 분석해 초기화 식의 타입을 변수 타입으로 지정한다.
- 초기화 식을 사용하지 않고 변수를 선언하려면 변수 타입을 반드시 명시해야한다.
- val : 변경 불가능한 참조를 저장하는 변수
- val 변수는 블록을 실행할 때 정확히 한번만 초기화돼야 한다. 하지만 어떤 블록이 실행될 때 오직 한 초기화 문장만 실행됨을 컴파일러가 확인할 수 있다면 조건에 따라 val 값을 다른 여러 값으로 초기화할 수도 있다.
- var : 변경 가능한 참조
클래스와 프로퍼티
class Person(val name: String)
- 위와 같은 유형의 클래스를 값 객체라 부른다. (코드가 없이 데이터만 저장하는 클래스)
- 코틀린의 기본 가시성은
public
이다. - ㄴ자바에서는 필드와 접근자를 한데 묶어 프로퍼티라고 부른다. 코틀린은 프로퍼티를 언어 기본 기능으로 제공하며, 코틀린 프로퍼티는 자바의 필드와 접근자 메소드를 완전히 대신한다.
class Person(
val name: String,
var isMarried: Boolean
)
- val : 읽기 전용 프로퍼티로, 코틀린은 필드(비공개)와, getter(공개)를 만들어낸다.
- var : 쓸 수 있는 프로퍼티로, 코틀린은 필드(비공개)와, getter, setter(공개)를 만들어낸다.
- getter와 setter의 이름을 정하는 규칙에는 예외가 있다. 이름이 is로 시작하는 프로퍼티의 getter에는 get이 붙지 않고 원래 이름을 그대로 사용하며, setter에는 is를 set으로 바꾼 이름을 사용한다.
val person = Person("Bob", true)
println(person.name)
println(person.isMarried) // 프로퍼티 이름을 직접 사용해도 자동으로 게터를 호출한다.
- 자바에서 선언한 클래스에 대해 코틀린 문법을 사용할 수 있다. 예를 들어 setName과 getName이라는 접근자를 제공하는 자바 클래스를 코틀린에서 사용할 때는 name이라는 프로퍼티를 사용할 수 있다.
- 대부분이 프로퍼티에는 그 프로퍼티의 값을 저장하기 위한 필드가 있다. 이를 프로퍼티를 뒷받침하는 필드(backing field)라고 부른다. 하지만 원한다면 프로퍼티 값을 그때그때 계산할 수도 있다. 커스텀 게터를 작성하면 그런 프로퍼티를 만들 수 있다.
class Rectangle(val height: Int, val width: Int) {
val isSquare: Boolean
get() {
return height == width
}
}
디렉터리와 패키지
- 같은 패키지에 속해 있다면 다른 파일에서 정의한 성언일지라도 직접 사용할 수 있다. 반면 다른 패키지에 정의한 선언을 사용하려면 임포트를 통해 선언을 불러와야한다.
- 코틀린에서는 클래스 임포트와 함수 임포트에 차이가 없다.
- 패지키 이름 뒤에
.*
를 추가하면 패키지 안의 모든 선언을 임포트할 수 있다. 이런 스타 임포트를 사용하면 패키지 안에 있는 모든 클래스뿐 아니라 최상위에 정의된 함수나 프로퍼티까지 모두 불러온다. - 코틀린은 여러 클래스를 한 파일에 넣을 수 있고, 파일의 이름도 마음대로 정할 수 있다.
- 코틀린은 디스크상의 어느 디렉터리에 소스코드 파일을 위치시키든 관계없다. 하지만 대부분이 경우 자바와 같이 패키지별로 디렉터리를 구성하는 편이 낫다.
선택 표현과 처리: enum과 when
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);
fun rgb() = (r * 256 + g) * 256 + b
}
- enum에서도 생성자와 프로퍼티를 선언한다.
- enum 클래스 안에 메소드를 정의하는 경우 반드시 enum 상수 목록과 메소드 정의 사이에 세미콜론을 넣어야한다.
when
- when도 값을 만들어내는 식이다. 따라서 식이 본문인 함수에 when을 바로 사용할 수 있다.
fun getMnemonic(color: Color) =
when (color) {
Color.RED -> "Richard"
Color.ORANGE -> "Of"
Color.YELLOW -> "York"
Color.GREEN -> "Gave"
Color.BLUE -> "Battle"
}
- 분기 끝에 break를 넣지 않아도 된다.
fun getWarmth(color: Color) = when (color) {
Color.RED, Color.ORANGE, Color.YELLOW -> "Warm"
else -> "Not Warm"
}
- 한 분기 안에서 여러 값을 매치 패턴으로 사용 가능하다.
when과 임의의 객체를 함께 사용
- 분기조건에 상수만 사용할 수 있는 switch와는 달리 코틀린 when의 분기 조건은 임의의 객체를 허용한다.
fun mix(c1: Color, c2: Color) =
when (setOf(c1, c2)) {
setOf(RED, YELLOW) -> ORANGE
setOf(YELLOW, BLUE) -> GREEN
else -> throw Exception("Dirty Color")
}
- when은 인자로 받은 객체가 각 분기 조건에 있는 객체와 같은지 테스트한다.
- setOf(c1, c2)와 분기 조건에 있는 객체 사이를 매치할 때 동등성을 사용한다.
- when의 분기 조건 부분에 식을 넣을 수 있기 때문에 많은 경우 코드가 더 간결하고 아름답게 작성할 수 있다.
인자가 없는 when 사용
- when에 아무 인자도 없으려면 각 분기의 조건이 불리언 결과를 계산하는 식이어야 한다.
스마트 캐스트 : 타입 검수와 타입 캐스트를 조합
interface Expr
class Num(val value: Int): Expr
class Sum(val left: Expr, val right: Expr): Expr
fun eval(e: Expr) : Int {
if (e is Num) {
return n.value
}
if (e is Sum) {
return eval(e.right) + eval(e.left)
}
throw IllegalArgumentException("")
}
- 어떤 변수가 원하는 타입인지 일단 is로 검사하고 나면 굳이 변수를 원하는 타입으로 캐스팅하지 않아도 마치 처음부터 그 변수가 원하는 타입으로 선언된 것처럼 사용할 수 있다. 하지만 실제로는 컴파일러가 캐스팅을 수행해준다. 이를 스마트 캐스트라고 한다.
- 스마트 캐스트는 is로 변수에 든 값의 타입을 검사한 다음에 그 값이 바뀔 수 없는 경우에만 작동한다.
- 위 예제처럼 클래스의 프로퍼티에 대해 스마트캐스트를 사용한다면 그 프로퍼티는 반드시 val이어야 하며 커스텀 접근자를 사용한 것이어도 안된다.
- 원하는 타입으로 명시적으로 캐스팅하려면 as 키워드를 사용한다.
리팩토링 : if를 when으로 변경
fun eval(e: Expr): Int =
when (e) {
is Num -> e.value
is Sum -> eval(e.right) + eval(e.left)
else -> throw IllegalArgumentException("")
}
- 타입을 검사하면 스마트캐스트가 적용된다.
- if나 when 모두 분기에 블록을 사용할 수 있다. 그런 경우 블록의 마지막 문장이 블록 전체의 결과가 된다.
- 블록의 마지막 식이 블록의 결과라는 규칙은 블록이 값을 만들어내야 하는 경우 항상 성립한다.
- 이 규칙은 함수에 대해서는 성립하지 않는다. 식이 본문인 함수는 블록을 본문으로 가질 수 없고 블록이 본문인 함수는 내부에 return문이 반드시 있어야 한다.
대상을 이터레이션 : while과 for 루프
while 루프
- 자바와 동일하다.
while (조건) {
}
do {
} while (조건)
수에 대한 이터레이션
- 자바에 for루프에 해당하는 요소가 없다. => 코틀린은 이를 대신하기 위해 범위(range)를 사용한다.
- 범위 :
..
연산자로 시작 값과 끝 값을 연결해서 범위를 만든다.- 숫자 타입의 값뿐만 아니라 문자 타입의 값에도 적용할 수 있다.
val oneToTen = 1..10
val aToF = 'A'..'F'
for (i in 1..100) {
println(i)
}
- 코틀린이 범위는 폐구간(닫힌 구간) 또는 양끝을 포함하는 구간이다. => 두 번째 값이 항상 범위에 포함된다는 뜻이다.
- 정수 범위로 수행할 수 있는 가장 단순한 작업은 범위에 속한 모든 값에 대한 이터레이션이다. 이런 식으로 어떤 범위에 속한 값을 일정한 순서로 이터레이션하는 경우를 수열이라고 부른다.
범위와 수열은 코틀린에서 같은 문법을 사용하며, for 루프에 대해 같은 추상화를 제공한다.
step
을 이용해서 증가값을 가질 수 있다.downTo
는 역방향 수열을 만든다.
val nums = 100 downTo 1 step 2
until
끝 값을 포함하지 않는 반만 닫힌 범위를 만들때 사용한다.
맵에 대한 이터레이션
val maps = TreeMap<Char, String>()
for ((letter, binary) in maps) {
println("$letter = $binary")
}
- letter에는 키가 들어가고 binary에는 값이 들어간다.
- 코틀린에서는
map[key]
나map[key] = value
를 사용해 값을 가져오고 설정할 수 있다.
val list = arrayListOf("10", "11", "12")
for ((index, element) in list.withIndex()) {
println("$index: $element")
}
in으로 컬렉션이나 범위의 원소 검사
- in 연산자를 사용해 어떤 값이 범위에 속하는지 검사할 수 있다.
fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
fun isNotDigit(c: Char) = c !in '0'..'9'
- c in ‘a’..’z’ : ‘a’ <= c && c <= ‘z’로 변환된다.
- 범위는 비교가 가능한 클래스(Comparable 인터페이스를 구현한 클래스라면) 그 클래스의 인스턴스 객체를 사용해 범위를 만들 수 있다.
- “Kotlin” in “Java”..”Scala” : “Java” <= “Kotlin” && “Kotlin” <= “Scala”와 같다.
코틀린의 예외 처리
- 코틀린의 throw는 식이므로 다른 식에 포함될 수 있다.
try, catch, finally
- 자바와 비슷하다.
- 코틀린은 체크 예외와 언체크 예외를 구별하지 않는다.
try를 식으로 사용
- try는 if나 when과 마찬가지로 식이다. 따라서 try의 값을 변수에 대입할 수 있다.
- if와 달리 try의 본문은 반드시 중괄호로 둘러싸야 한다. 다른 문장과 마찬가지로 try의 본문도 내부에 여러 문장이 있으면 마지막 식의 값이 전체 결과 값이다.
- catch 블록도 그 안의 마지막 식이 블록 전체의 값이 된다.
fun readNumber(reader: BufferedReader): Int {
val number = try {
Integer.parseInt(reader.readLine())
} catch (e: NumberFormatException) {
null
}
return number
}