Hello, World!
fun main(args: Array<String>) {
println("Hello, world!")
}
간단한 코드에서 코틀린의 여러가지 특성을 볼수 있다.
함수
fun max(a : Int, b : Int) : Int {
return if (a > b) a else b
}
함수의 선언은 fun 키워드로 시작한다. fun 다음에는 함수 이름이 온다. 예제는 max라는 이름의 함수다. 함수 이름 뒤에는 괄호 안에 파라미터 목록이 온다. 함수의 반환타입은 파라미터 목록의 닫는 괄호 다음에 오는데, 괄호와 반환 타입 사이를 콜론(:)으로 구분해야 한다.
함수를 더 간결하게 표현할 수 있다.
fun max(a : Int, b : Int) : Int = if(a > b) a else b
중괄호를 없애고 return을 제거하면서 등호를 식 앞에 붙이면 더 간결하게 함수를 표현할 수 있다.
본문인 중괄호로 둘러싸인 함수를 블록이 본문인 함수라 부르고, 등호와 식으로 이뤄진 함수를 식이 본문인 함수라고 부른다.
코틀린에서는 식이 본문인 함수가 자주 쓰인다. 그런 함수의 본문 식에는 단순한 산술식이나 함수 호출 식뿐 아니라, if, when, try 등의 더 복잡한 식도 자주 쓰인다. 반환 타입을 생략하면 max 함수를 더 간략하게 만들 수 있다.
fun max(a : Int, b : Int) = if(a > b) a else b
코틀린은 정적 타입 지정 언어이므로 컴파일 시점에 모든 식의 타입을 지정해야 하지 않는가? 실제로 모든 변수나 모든식에는 타입이 있으며, 모든 함수는 반환 타입이 정해져야 한다. 하지만 식이 본문인 함수의 경우 굳이 사용자가 반환 타입을 적지 않아도 컴파일러가 함수 본문 식을 분석해서 식의 결과 타입을 함수 반환 타입으로 정해준다.
실전 프로그램에는 아주 긴 함수에 return문이 여럿 들어있는 경우가 자주 있다. 그런 경우 반환 타입을 꼭 명시하고 return을 반드시 사용한다면 함수가 어떤 타입의 값을 반환하고 어디서 그런 값을 반환하는지 더 쉽게 알아볼 수 있다.
변수
val question = "삶, 우주, 그리고 모든 것에 대한 궁극적인 질문"
val answer = 42 // 타입 생략
val answer: Int = 42 // 타입 명시
식이 본문인 함수에서와 마찬가지로 타입을 지정하지 않으면 컴파일러가 초기화 식을 분석해서 초기화 식의 타입을 변수 타입으로 지정한다. 여기서 초기화 식은 42로 Int 타입이다. 변수도 Int 타입이 된다.
val yearsToCompute = 7.5e6 // 부동소수점 상수를 사용한다면 변수 타입은 Double이 된다.
초기화 식을 사용하지 않고 변수를 선언하려면 변수 타입을 반드시 명시해야 한다.
val answer : Int
answer = 42
변경 가능한 변수와 변경 불가능한 변수
val 변수는 블록을 실행할 때 정확히 한 번만 초기화돼야 한다. 하지만 어떤 블록이 실행될 때 오직 한 초기화 문장만 실행됨을 컴파일러가 확일할 수 있다면 조건에 따라 val 값을 다른 여러 값으로 초기화할 수도 있다.
val message : String
if(canPerformOperation()) {
message = "Success" // ...연산을 수행한다.
} else {
message = "Failed"
}
val 참조 자체는 불변일지라도 그 참조가 가리키는 객체의 내부 값은 변경될 수 있다.
val languages = arrayListOf("Java")
languages.add("Kotlin")
var 키워드를 사용하면 변수의 값을 변경할 수 있지만 변수의 타입은 고정돼 바뀌지 않는다.
var answer = 42
answer = "no answer" // 컴파일 오류 발생
어떤 타입의 변수에 다른 타입의 값을 저장하고 싶다면 변환 함수를 써서 값을 변수의 타입으로 변환하거나, 값을 변수에 대입할 수 있는 타입으로 강제 형 변환해야한다.
fun main(args: Array<String>) {
val name = if (args.size > 0) args[0] else "Kotlin"
println("Hello, $name")
}
문자열 리터럴의 필요한 곳에 변수를 넣되 변수 앞에 $를 추가해야 한다.
$ 문자를 문자열에 넣고 싶으면 println("$x")와 같이 \를 사용해 $를 이스케이프시켜야 한다. 그렇다면 x의 값을 출력하지 않고 $x를 출력한다.
// Java
public class Person {
private final String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
자바-코틀린 변환기를 사용하여 Person클래스를 코틀린으로 변환해보자.
class Person(val name: String)
자바를 코틀린으로 변환한 결과, public 가시성 변경자가 사라졌음을 확인했다. 코틀린의 기본 가시성은 public이므로 이런 경우 변경자를 생략해도 된다.
자바에서는 필드와 접근자를 한데 묶어 프로퍼티라고 부르며, 프로퍼티라는 개념을 활용하는 프레임워크가 많다. 코틀린은 프로퍼티를 언어 기본 기능으로 제공하며, 코틀린 프로퍼티는 자바의 필돠 접근자 메서드를 완전히 대신한다.
class Person(
val name: String, // 읽기 전용 프로퍼티로, 코틀린은 (비공개) 필드와 필드를 읽는 단순한 (공개) 게터를 만들어 낸다.
var isMarried: Boolean // 쓸 수 있는 프로퍼티로, 코틀린은 (비공개)필드, (공개) 게터, (공개) 세터를 만들어 낸다.
)
코틀린은 값을 저장하기 위한 비공개 필드와 그 필드에 값을 저장하기 위한 세터, 필드의 값을 읽기 위한 게터로 이뤄진 간단한 디폴트 접근자 구현을 제공한다.
코틀린에서는 클래스 임포트와 함수 임포트에 차이가 없으며, 모든 선언을 import키워드로 가져올 수 있다. 최상위 함수는 그 이름을 써서 임포트할 수 있다.
package geometry.example
import geometry.shapes.createRandomRectangle // 이름으로 함수 임포트하기
fun main(args: Array<String>) {
println(createRandomRectangle().isSquare) // "true"가 아주 드물게 출력된다.
}
when은 자바의 switch를 대치하되 훨씬 더 강력하며, 앞으로 더 자주 사용할 프로그래밍 요소라고 생각할 수 있다.
코틀린에서 enum은 소프트 키워드라 부르는 존재다. 자바와 마찬가지로 enum은 단순히 값만 열거하는 존재가 아니다. 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 // enum 클래스 안에서 메서드를 정의한다.
}
enum클래스 안에 메서드를 정의하는 경우 반드시 enum 상수 목록과 메서드 정의 사이에 세미콜론을 넣어야한다.
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"
}
자바의 Switch와 달리 when에서는 break를 넣지 않아도 된다. 한 분기안에서 여러 값을 매치 패턴으로 사용할 수도 있다. 그럴 경우 값 사이를 콤마(,)로 분리한다.
코틀린에서 when은 자바의 switch보다 훨씬 더 강력하다. 분기 조건에 상수만을 사용할 수 있는 자바 switch와 달리 코틀린 when의 분기 조건은 임의 객체를 허용한다.
fun mix(c1: Color, c2: Color) =
when (setOf(c1, c2)) {
// when 식의 인자로 아무 객체나 사용할 수 있다. when은 이렇게 인자로 받은 객체가 각 분기 조건에 있는
// 객체와 같은지 테스트한다.
setOf(RED, YELLOW) -> ORANGE
setOf(YELLOW, BLUE) -> GREEN
setOf(BLUE, VIOLET) -> INDIGO
else -> throw Exception("Dirty color") // 매치되는 분기 조건이 없으면 이 문장을 실행한다.
}
코틀린 표준 라이브러리에는 인자로 전달받은 여러 객체를 그 객체들을 포함하는 집합인 Set 객체로 만드는 setof라는 함수가 있다. 집함은 원소가 모여 있는 컬렉션으로, 각 원소의 순서는 중요하지 않다.
이 함수는 호출될 때마다 함수 인자로 주어진 두 색이 when의 분기 조건에 있는 다른 두 색과 같은지 비교하기 위해 여러 Set 인스턴스를 생성한다. 보통은 이런 비효율성이 크게 문제가 되지 않는다. 하지만 이 함수가 아주 자주 호출된다면 불필요한 가비지 객체가 늘어나는 것을 방지하기 위해 함수를 고쳐 쓰는 편이 낫다. 인자가 없는 when 식을 사용하면 불필요한 객체 생성을 막을 수 있다.
fun mixOptimized(c1: Color, c2: Color) =
when { // 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")
}
추가 객체를 만들지 않는다는 장점이 있지만 가독성은 더 떨어진다.
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")
}
코틀린에서는 is를 사용해 변수 타입을 검사한다. is 검사는 자바의 instanceof와 비슷하다. 하지만 자바에서 어떤 변수의 타입을 instanceof로 확인한 다음에 그 타입에 속한 멤버에 접근하기 위해서는 명시적으로 변수 타입을 캐스팅해야 한다. 코틀린에서는 프로그래머 대신 컴파일러가 캐스팅을 해준다. 어떤 변수가 원하는 타입인지 일단 is로 검사하고 나면 굳이 변수를 원하는 타입으로 캐스팅하지 않아도 마치 처음부터 그 변수가 원하는 타입으로 선언된 것처럼 사용할 수 있다. 이를 스마트 캐스트라고 부른다.
코틀린에서는 if가 값을 만들어 내기 때문에 자바와 달리 3항 연산자가 따로 없다.
fun eval(e: Expr): Int =
if (e is Num) {
e.value
} else if (e is Sum) {
eval(e.right) + eval(e.left)
} else {
throw IllegalArgumentException("Unknown expression")
}
when 식을 앞에서 살펴본 값 동등성 검사가 아닌 다른 기능에도 쓸 수 있다.
fun eval(e: Expr): Int =
when (e) {
is Num ->
e.value
is Sum ->
eval(e.right) + eval(e.left)
else ->
throw IllegalArgumentException("Unknown expression")
}
코틀린 while루프는 자바와 동일하다. for는 자바의 for-each루프에 해당하는 형태만 존재한다.
fun main(args: Array<String>) {
for (i in 100 downTo 1 step 2) {
print(fizzBuzz(i))
}
}
여기서 증가 값 step을 갖는 수열에 대해 이터레이션한다. 증가 값을 사용하면 수를 건너 뛸 수 있다. 증가 값을 음수로 만들면 정방향 수열이 아닌 역방향 수열을 만들 수 있다. 이 예제에서 100 downTo 1 은 역방향 수열을 만든다.
범위를 만들고 싶다면 until 함수를 사용하면된다. for(x in 0 until size)라는 루프는 for(x in 0..size-1)과 같지만 좀 더 명확하게 개념을 표현한다.
import java.util.TreeMap
fun main() {
val binaryReps = TreeMap<Char, String>()
for (c in 'A'..'F') {
val binary = Integer.toBinaryString(c.toInt())
binaryReps[c] = binary // c를 키로 c의 2진 표현을 맵에 넣는다.
}
for ((letter, binary) in binaryReps) {
println("$letter = $binary")
}
}
fun main() {
val list = arrayListOf("10", "11", "1001")
for ((index, element) in list.withIndex()) {
println("$index: $element")
}
}
인덱스를 저장하기 위한 변수를 별도로 선언하고 루프에서 매번 그 변수를 증가시킬 필요가 없다.
in 연사자를 사용해 어떤 값이 범위에 속하는지 검사할 수 있다. 반대로 !in을 사용하면 어떤 값이 범위에 속하지 않는지 검사할 수 있다.
fun recognize(c: Char) = when (c) {
in '0'..'9' -> "It's a digit!"
in 'a'..'z', in 'A'..'Z' -> "It's a letter!"
else -> "I don't know…"
}
코틀린의 예외 처리는 자바나 다른 언어의 예외 처리와 비슷하다. 함수는 정상적으로 종료할 수 있지만 오류가 발생하면 예외를 던질 수 있다.
val percentage =
if (number in 0..100)
number
else
throw IllegalArgumentException(
"A percentage value must be between 0 and 100: $number")
자바와 마찬가지로 예외를 처리하려면 try와 catch, finally 절을 함께 사용한다. 자바 코드와 가장 큰 차이는 throws절이 코드에 없다는 점이다. 자바에서는 함수를 작성할 때 함수 선언 뒤에 throws IOException을 붙여야 한다. 이유는 IOException이 체크 예외이기 때문이다. 자바에서는 체크 예외를 명시적으로 처리해야 한다.
fun readNumber(reader : BufferedReader) {
val number = try {
Integer.parseInt(reader.readLine())
} catch (e: NumberFormatException) {
return
}
println(number)
}
코틀린의 try 키워드는 if나 when과 마찬가지로 식이다. 따라서 try의 값을 변수에 대입할 수 있다.