2장에서 다루는 내용
- 함수, 변수, 클래스, enum, 프로퍼티를 선언하는 방법
- 제어 구조
- 스마트 캐스트
- 예외 던지기와 예외 잡기
코틀린은 fun
키워드를 이용해서 함수를 나타내고, 파라미터 이름 뒤에 타입을 명시한다. 세미콜론을 붙이지 않아도 된다는 특징이 있다.
fun max(a: Int, b: Int) : Int {
return if (a > b) a else b
}
1️⃣ max
: 함수 이름
2️⃣ (a: Int, b: Int)
: 파라미터 목록
3️⃣ : Int
: 반환 타입
4️⃣ return if (a > b) a else b
: 함수 본문
으로 이루어져 있다.
문(statement)와 식(expression)의 차이
식: 값을 만들어 내며 다른 식의 하위 요소로 계산에 참여할 수 있음
문: 자신을 둘러싼 가장 안쪽 블록의 최상위 요소로 존재
자바에서는 모든 제어 구조가 문이지만 코틀린에서는 루프를 제외한 대부분의 제어 구조가 식이다. 대입문은 자바에서는 식이지만 코틀린에서는 문이다.
코틀린에서는 변수를 선언할 때 타입이 뒤에 온다. 타입 지정을 생략(컴파일러의 타입 추론에 의존)하는 경우가 많은데, 책에서는 '타입으로 변수 선언을 시작하면 타입을 생략할 경우 식과 변수 선언을 구별할 수 없다' 라고 표현했다.
그래서 변수의 타입이 선언문에서의 끝에 위치한다.
var foo = 5
var bar: Int = 3
코틀린은 위과 같이 변수 키워드 + 변수명 + (타입)의 형태로 변수를 선언한다.
위처럼 타입 명시를 생략하는 경우도 있는데, int foo = 5
와 같은 형태의 경우 타입을 생략하는 foo = 5
처럼 되는데, 이는 기존의 변수에 값을 대입한 것인지 새로 선언하는 변수에 타입을 생략한 것인지 구분되지 않기 때문이다.
또한 초기화 식을 사용하지 않고 변수를 선언하려면 반드시 변수의 타입을 명시해야 한다.
fun main() {
val foo: Int
foo = 5
var bar
bar = 3
}
bar는 타입을 명시하지 않아 에러가 발생한다. 코틀린은 자바와 달리 함수를 꼭 클래스 안에 넣어줄 필요가 없고, 코틀린 REPL도 활용할 수 있는데 위의 foo 선언을 해당 환경에서 진행하면 오류가 발생한다. 왜인지는 아직 모르겠다.
val(value): Immutable 참조를 저장하는 변수. 초기화하면 재대입이 불가능
var(variable): Mutable 참조. 변수의 값은 바뀔 수 있다.
fun foo(arr: Array<Int>) {
arr[1] = 0
}
fun main() {
val arr = arrayOf(1, 2, 3)
foo(arr)
for(num in arr) {
print(num)
}
}
// 103
val의 참조 자체는 불변이지만 가리키는 내부 객체의 값은 바뀔 수 있다. var 키워드를 사용했을 때도 값은 변경할 수 있지만 타입은 변경할 수 없다.
문자열 템플릿 println("Hello $name")
을 활용해서 문자열 리터럴 안에서 변수를 활용할 수 있다. 복잡한 식도 중괄호로 둘러싸서 표현할 수 있다.
class Person(val name: String) {
val name: String,
var isMarried: Boolean
}
위와 같이 클래스를 표현한다. 기본 가시성은 public으로 설정된다.
클래스는 데이터를 캡슐화 하고 데이터를 다루는 코드를 포함한다. 외부에서 필드 및 메서드에 접근할 수 있는 범위를 나타내기 위해 접근 제어자(access modifier)를 사용하고, 일반적으로 필드에 접근할 수 있는 getter/setter 메서드를 제공한다.
클래스에서 val로 선언한 프로퍼티(자바에서의 final)는 읽기 전용이며, var로 선언한 프로퍼티는 변경이 가능하다.
위의 코드에서 name
은 val로 선언되어 비공개 필드와 필드를 읽을 수 있는 단순(공개) getter를 생성하고, isMarried
는 이에 추가로 공개 setter를 만든다.
외부에서 접근할 때도 자바처럼 getter/setter 메서드를 호출하는 것이 아니라 프로퍼티를 직접 접근하는 문법으로 사용 가능하다(내부에서는 게터세터가 호출됨).
당연히 커스텀 getter/setter를 선언할 수 있다는 것도 알아두자
코틀린에서 enum을 사용할 때는 enum class 키워드를 사용한다. 자바와 마찬가지로 프로퍼티와 메서드도 사용할 수 있다.
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
은 자바의 switch에 해당하는 구성 요소이지만 더 강력한 기능을 제공한다.
분기 조건에 상수만이 아닌 임의의 객체를 할당할 수 있으며, 끝에 break
를 작성하지 않아도 괜찮다. 한 분기가 여러 조건에 매치될 경우 ,
를 사용해서 구분한다.
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("No Specific Color")
}
인자 값과 매치되는 조건을 찾기 위해 equals
메서드를 사용한다. equals
가 정의되지 않은 객체의 경우는 Object의 것을 상속받는 것을 유의하자.
Java Object.equals() 👉
https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html#equals(java.lang.Object)
class Person(val name: String, val age: Int)
fun main() {
val p = Person("binary", 25)
when(p) {
Person("binary", 25) -> print("Hi")
Person("yun", 30) -> print("Hello")
else -> print("World")
}
}
// 결과: World
class Person(val name: String, val age: Int) {
override fun equals(other: Any?)
= (other is Person)
&& name == other.name
&& age == other.age
}
fun main() {
val p = Person("binary", 25)
when(p) {
Person("binary", 25) -> print("Hi")
Person("yun", 30) -> print("Hello")
else -> print("World")
}
}
// 결과: Hi
위의 예제는 커스텀 클래스가 equals를 정의하지 않아 when의 아무 분기에도 매치되지 않았고, else 분기로 빠진다. 반면 아래에서 equals를 정의해주었을 때는 첫 번째 분기에 매치된 것을 볼 수 있다.
when (e) {
is Num -> {
println("num: ${e.value}")
e.value // e.value 반환
}
}
if와 when의 경우 분기의 가장 마지막 문장이 블록의 결과가 된다.
코틀린에서는 is
를 사용해서 변수 타입을 검사한다(자바의 instanceof
와 비슷하다).
자바에서는 해당 타입에 속한 멤버에 접근하기 위해서는 하위 타입으로 캐스팅해줘야 했는데, 코틀린에서는 일단 is로 검사하면 캐스팅하지 않아도 그 변수가 원하는 타입으로 캐스팅된 것 처럼 사용할 수 있다.
이것을 코틀린의 스마트 캐스트라고 한다.
if (e is Sum) {
return eval(e.right) + eval(e.left)
}
이 때, 변수에 든 값의 타입을 검사해서 Immutable한 경우에만 작동하기 때문에 프로퍼티는 반드시 val
이어야 한다. 원하는 타입으로 명시적 타입캐스팅을 하려면 as
키워드를 사용하면 된다.
코틀린도 while, do-while, for를 사용해서 이터레이션을 표현할 수 있다.
while, do-while은 자바에서의 문법과 거의 유사하며, for문은 초기/증감/조건문이 없고 범위로만 표현하기 때문에 파이썬과 유사하다고 느꼈다.
fun fizzBuzz(i: Int) = when {
i % 15 == 0 -> "FizzBuzz"
i % 3 == 0 -> "Fizz"
i % 5 == 0 -> "Buzz"
}
for (i in 1..100) {
print(fizzBuzz(i))
}
for (i in 1 until 100) {
// 위의 코드와 동일하게 동작
}
for (i in 100 downTo 1 step 2) {
print(fizzBuzz(i))
}
특이한 건 in
을 사용하면 코틀린의 범위는 항상 양끝을 포함하는 폐구간이며, until
을 사용하면 끝을 포함하지 않는 반폐구간으로 만들 수 있다.
step
키워드를 사용해서 증감 값을 지정해줄 수 있다(절대값으로 표현).
val binaryReps = TreeMap<Char, String>()
for (c in 'A'..'F') {
val binary = Integer.toBinaryString(c.toInt())
binaryReps[c] = binary
}
for ((letter, binary) in binaryReps) {
println("$letter = $binary")
}
맵에 대한 이터레이션도 간결하게 표현할 수 있다. 코드에서 볼 수 있듯 get/put이 아니라 일반 배열에 접근하는 것 처럼 맵에 접근 가능하다.
fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
fun isNotDigit(c: Char) = c !in '0'..'9'
println("Kotlin" in "Java".."Scala") // lexical order
println("Kotlin" in setOf("Java", "Scala")) // element of set
코틀린에서는 예외 인스턴스를 만들 때 new
키워드를 붙일 필요가 없다.
fun readNumber(reader: BufferedReader): Int? {
try {
val line = reader.readLine()
return Integer.parseInt(line)
}
catch (e: NumberFormatException) {
return null
} finally {
reader.close()
}
}
fun main(args: Array<String>) {
val reader = BufferedReader(StringReader("239"))
println(readNumber(reader))
}
throws
절이 코드에 없다. 코틀린은 checked/unchecked exception을 구별하지 않으며, 던져지는 예외를 처리해주는 것은 개발자의 선택이다.
try
키워드는 if, when과 마찬가지로 식이기 때문에 try의 값을 변수에 대입할 수 있다.
(답변은 조만간 달겠습니다 🥲)