Hello, World 출력 함수를 보며 Kotlin의 특성을 간단히 스캔해보자.
fun main(args: Array<String>) {
println("Hello, World!")
}
fun (파라미터 이름: 파라미터 타입)
이 때 fun은 함수 선언 키워드
함수를 최상위 수준에 정의할 수 있다.
Java의 경우 하나의 메소드를 호출하기 위해서는 클래스 안에 넣어줘야 했지만 Kotlin은 그럴 필요 없이 그냥 파일 내 최상위 수준에 위치해도 된다.
배열도 일반적인 클래스와 마찬가지다.
Kotlin에는 Java와 달리 배열 처리를 위한 문법이 따로 존재하지 않는다.
Kotlin의 배열과 관련해서는 6장에서 더 자세히 다루도록 하겠다. (6장 바로가기)
System.out.println
대신에 println
이라고 쓴다.
Kotlin 표준 라이브러리에서 Java 표준 라이브러리 함수를 간결하게 쓸 수 있도록 wrapper를 제공한다. 개이득!
라인 끝에 세미콜론 ;
을 붙이지 않아도 된다. 개이득! 22
fun max(a: Int, b: Int): Int {
return if(a > b) a else b
}
함수 형태는 fun (파라미터 이름: 파라미터 타입): 반환 타입 { 함수 본문 }
와 같다.
이 때 if
가 statement(문)가 아니고 expression(식)이라는 점을 눈여겨 봐두자.
따라서 위 예제의 if
식은 마치 Java의 3항 연산자로 작성한 (a > b) ? a : b
라는 식과 비슷한 느낌을 준다.
위 형태에서 발전하여 식이 본문인 함수로 표현하면 더욱 간결한 표현이 가능하다. (Kotlin에서는 식이 본문인 함수가 자주 쓰인다.)
fun max(a: Int, b: Int): Int = if (a > b) a else b
식이 본문인 함수에서 더 나아가 반환 타입을 생략하면 max 함수는 더 간략해진다.
fun max(a: Int, b: Int) = if (a > b) a else b
블록이 본문인 함수에서는 반드시 반환 타입을 지정하고 return문을 이용해 반환 값을 명시해야 함을 유의하자.
Kotlin에서는 변수 이름 뒤에 타입을 명시하거나 생략을 허용한다.
Kotlin의 경우 타입 지정을 생략하는 경우가 흔하다.
식이 본문인 함수에서와 마찬가지로 타입을 지정해주지 않으면 컴파일러가 알아서 초기화 식의 타입을 변수 타입으로 지정해주기 때문이다.
그렇기에 Java 처럼 타입을 먼저 쓰고 변수명을 쓰게 되면 식과 변수 선언을 구별할 수 없게 된다.
val answer = 42
여기서 초기화 식은 42
로 Int에 해당한다. 따라서 변수 answer
는 Int 타입이 된다.
val answer: Int
...
answer = 42
만약 초기화 식을 사용하지 않고 변수를 선언하려면 변수 타입을 반드시 명시해야 한다.
또한 Kotlin에는 변경 가능한 변수 var
와 변경 불가능한 변수 val
이 있다.
기본적으로는 모든 변수를 val 키워드를 사용해 불변 변수로 선언하고, 나중에 꼭 필요할 때에만 var로 변경하라.
var
(from variable): 변경 불가능한(immutable) 참조를 저장하는 변수.
var로 선언된 변수는 일단 초기화하고 나면 재대입이 불가능하다.
Java로 말하자면 final
변수에 해당한다.
val
(from value): 변경 가능한(mutable) 참조
Java의 일반 변수들에 해당한다.
val
변수는 블록을 실행할 때 정확히 한 번만 초기화돼야 하지만 어떤 블록이 실행될 때 오직 한 초기화 문장만 실행됨을 컴파일러가 확인할 수 있다면 조건에 따라 val
값을 여러 다른 값으로 초기화할 수도 있다.
val massage: String
if (canPerformOperation()){
message = "Success" //message 값 Success로 초기화
...
}
else{
message = "Failed" //message 값 Failed로 초기화
}
위 예시의 경우 if/else에 따라 하나의 초기화 문장만 실행되므로 여러 값으로 초기화할 수 있다.
또한 val
참조 자체는 불변일지라도 그 참조가 가리키는 객체의 내부 값은 변경될 수 있다.
이와 관련해서는 6장에서 더 자세히 다뤄보자. (6장 바로가기)
fun main(args: Array<String>) {
val name = if (args.size > 0) args[0] else "Kotlin"
println("Hello, $name!")
}
위 코드는 문자열 템플릿이라는 기능을 보여준다.
name이라는 변수를 문자열 리터럴 안에서 사용한 것을 볼 수 있는데, Java의 문자열 접합 연산 ("Hello," + name + "!")
처럼 복잡하게 쓸 필요가 없다.
그냥 문자열 리터럴의 필요한 곳에 변수를 넣되 변수 앞에
$
를 추가하면 된다.
($
문자 자체를 문자열에 넣고 싶다면\
를 붙여 이스케이프 시켜주면 된다.)
또한 문자열 템플릿에는 위 예시에 있는 간단한 변수 이름 뿐만 아니라 복잡한 식도 { }
로 둘러싸서 넣어줄 수 있다.
fun main(args: Array<String>) {
if (args.size > 0) {
println("Hello, ${args[0]}!")
}
클래스 선언 기본 문법에 대해서 간단하게만 알아보자. 자세한 내용은 4장에서 다룬다. (4장 바로가기)
// Java 클래스 Person
public class Person {
private final String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
위는 Java로 작성한 Person 클래스이다. 이를 Kotlin으로 변환해보면 아래와 같다.
// Kotlin 클래스 Person
clss Person(val name: String)
엄청나다! 당장 모두가 Kotlin을 배워야 한다!
이처럼 코드 없이 데이터만 저장하는 클래스를 value object(값 객체)라고 부른다.
클래스의 목적은 데이터를 캡슐화하고, 캠슐화한 데이터를 다루는 코드를 한 주체 아래 가두는 것이다.
Java에서는 필드와 해당 필드의 접근자를 묶어 프로퍼티라고 부른다.
Kotlin은 프로퍼티를 언어 기본 기능으로 제공하여 Java의 필드와 접근자 메서드를 완전히 대신한다.
Kotlin에서 프로퍼티를 선언하는 기본적인 방식은 프로퍼티와 관련 있는 접근자를 선언하는 것이다.
클래스에서 프로퍼티를 선언할 때는 변수와 마찬가지로 val
과 var
를 사용한다.
val
: 읽기 전용 프로퍼티 -> 게터만 선언함var
: 변경 가능한 프로퍼티 -> 게터와 세터를 모두 선언함class Person(
val name: String,
var isMarried: Boolean
)
위 코드에서 name은 읽기 전용 프로퍼티로 (비공개)필드와 필드를 읽는 (공개)게터를 만들어낸다.
isMarried는 쓸 수 있는 프로퍼티로 (비공개)필드, (공개)게터, (공개)세터를 만들어낸다.
Person클래스를 Kotlin 코드에서 사용하면 다음과 같다.
val person = Person("Bob", false)
println(person.name) //Bob 출력
println(person.ismarred) //false 출력
이후 변경 사항을 적용할 때는 person.isMarried = true
와 같이 직관적으로 쓸 수 있다.
프로퍼티의 접근자를 직접 작성할 수도 있다. 파라미터가 없는 함수를 정의하는 방식과 커스텀 게터를 정의하는 방식 중 뭐가 더 낫다고 말할 수는 없다. 구현이나 성능상 차이는 없고 단지 가독성에서 차이가 있을 뿐이다.
4장에서 클래스와 프로퍼티에 대한 예제를 통해 생성자를 명시적으로 선언하는 문법에 대해 자세히 다뤄보도록 하겠다. (4장 바로가기)
Kotlin에도 Java와 비슷한 개념의 패키지가 있다. 모든 코틀린 파일의 맨 앞에 package
문을 넣을 수 있으며, 그러면 그 파일 안의 모든 클래스, 함수, 프로퍼티 등의 선언이 해당 패키지에 들어가게 된다.
import
를 통해 선언을 불러와야 한다.Kotlin에서는 클래스 임포트와 함수 임포트에 차이가 없다. 모든 선언을
import
키워드로 가져올 수 있으며, 최상위 함수는 그 이름을 써서 임포트할 수 있다.
Kotlin에서 enum은 enum class
의 형태로 쓰이는 soft keyword이다.
class
앞에서는 특별한 의미를 갖지만 그렇지 않은 경우에는 아무렇게나 이름처럼 사용할 수 있다. (반면 class
는 keyword로 그 어디에서도 이름으로 쓸 수 없다.)
enum은 열거형을 말한다. 그러나 Java와 마찬가지로 Kotlin에서 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
클래스 역시 일반적인 클래스와 마찬가지로 생성자와 프로퍼티를 선언한다.
특히 이 코드에는 Kotlin에서 유일하게 세미콜론이 필수인 부분을 볼 수 있다.
enum
클래스 안에 메서드를 정의하는 경우 반드시enum
상수 목록과 메서드 정의 사이에 세미콜론을 넣어야 한다.
Kotlin에는 when
이라는 구성요소가 있다. Java의 switch
를 대치하되 훨씬 강력하며, 더 자주 사용된다.
if
와 마찬가지로 when
역시 값을 만들어내는 식이다. 따라서 식이 본문인 함수에 when
을 바로 사용할 수 있다.
fun getMnemonic(color: Color) = //함수의 반환 값으로 when식을 직접 사용
when (color) {
Color.RED -> "Richard"
Color.ORANGE -> "Of"
Color.YELLOW -> "York"
Color.GREEN -> "Gave"
Color.BLUE -> "Battle"
Color.INDIGO -> "In"
Color.VIOLET -> "Vain"
}
위의 when
식은 색이 특정 enum 상수와 같을 때 그 상수에 대응하는 문자열을 돌려준다.
Java와 달리 각 분기의 끝에 break
를 넣지 않아도 된다.
앞서 말했듯이 Kotlin의 when
은 Java의 switch
보다 훨씬 더 강력하다.
분기 조건에 상수나 숫자 리터럴만을 사용할 수 있는 Java의
switch
와 달리 Kotlin의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
의 분기 조건 부분에 식을 넣을 수 있기 때문에 많은 경우 코드를 더 간결하고 아름답게 작성할 수 있다.
그러나 위 함수는 여전히 어느정도 비효율적인 면이 있다.
호출될 때 마다 함수 인자로 주어진 두 색이 when
의 분기 조건에 있는 다른 두 색과 같은지 비교하기 위해 여러 Set
인스턴스를 생성한다.
이러한 함수가 자주 호출된다면 불필요한 가비지 객체가 늘어날 수 있으므로 리팩터링이 필요하다.
이 때 인자가 없는
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")
}
코드는 약간 읽기 어려워지지만 성능을 더 향상시키기 위해(추가 객체를 만들지 않음) 이를 감수하는 경우에 해당한다.
Kotlin에서는 is
를 사용해 변수 타입을 검사한다.
Java의 경우 변수의 타입을 검사한 후에 그 타입에 속한 멤버에 접근하기 위해서는 명시적으로 변수 타입을 캐스팅해야 하지만 Kotlin은 프로그래머 대신 컴파일러가 캐스팅을 해준다 (!)
스마트 캐스트 : 어떤 변수가 원하는 타입인지 일단
is
로 검사하고 나면 굳이 변수를 원하는 타입으로 캐스팅하지 않아도 마치 처음부터 그 변수가 원하는 타입으로 선언된 것 처럼 사용할 수 있다. (컴파일러가 캐스팅을 수행해주기 때문이다.)
스마트 캐스트는 is
로 변수에 든 값의 타입을 검사한 다음에 그 값이 바뀔 수 없는 경우에만 작동한다.
또한 IDE를 사용하면 스마트 캐스트 부분의 배경색을 달리 표시해주므로 변환이 자동으로 이뤄졌음을 쉽게 알 수 있다.
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) //변수 e에 대해 스마트 캐스트를 사용
}
throw IllegalArgumentException("Unknown expression")
}
원하는 타입으로 명시적으로 타입 캐스팅 하려면 as
키워드를 사용한다.
val n = e as Num
앞서 Kotlin에서는 if
가 값을 만들어내기 때문에 Java와 달리 3항 연산자가 따로 없다는 특성을 살펴보았다.
이런 특성을 사용하면 위의 eval 함수에서 return
문과 중괄호를 없애고 if
식을 본문으로 사용해서 더 간단하게 만들 수 있다.
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")
}
if
분기에 블록을 사용하는 경우 그 분기의 마지막 식이 그 분기의 결과 값이다.
위 코드를 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")
}
Kotlin의 while
루프는 Java와 동일하다.
Kotlin에서 for
의 경우 Java의 for-each
루프에 해당하는 형태만 존재한다.
일반적인 루프에서 가장 흔한 용례인 초깃값, 증가 값, 최종 값을 사용한 루프를 대신하기 위해 코틀린에서는 범위(range)를 사용한다.
범위는 기본적으로 두 값으로 이루어진 구간이다. 보통 그 두 값은 정수 등의 숫자 타입의 값이며 ..
연산자로 시작 값과 끝 값을 연결해서 범위를 만든다.
Kotlin의 범위는 두 번째 값(끝 값)이 항상 범위에 포함된다.
fun main(args: Array<String>) {
for (i in 100 downTo 1 step 2) {
print(fizzBuzz(i))
}
}
위 코드에서는 증가 값 step
을 갖는 수열에 대해 이터레이션한다.
증가 값을 사용하면 수를 건너 뛸 수 있으며 증가 값이 음수일 경우 정방향 수열이 아닌 역방향 수열을 만들 수 있다.
위에서 언급한 대로 ..
는 항상 범위의 끝 값을 포함한다.
하지만 끝 값을 포함하지 않는 반만 닫힌 범위에 대해 이터레이션 하면 편할 때가 있다.
반만 닫힌 범위를 만들고 싶다면
until
함수를 사용하면 된다.
나중에 3장에서 이와 관련하여 더 자세히 다루도록 하겠다. (3장 바로가기)
앞에서 다룬대로 컬렉션에 대한 이터레이션을 위해서는 for .. in
루프를 자주 사용한다.
이러한 for
루프는 Java와 마찬가지로 작동하므로 깊게 살펴볼 것들은 많이 없다.
여기서는 map
에 대한 이터레이션을 살펴보도록 하겠다.
val binaryReps = TreeMap<Char, String>() //키에대해 정렬하기 위해 TreeMap을 사용
for (c in ‘A’ .. ‘F’) { //A부터 F까지 문자의 범위를 사용해 이터레이션
val binary = Integer.toBinaryString(c.toInt()) //ASCII 코드를 2진 표현으로 바꿈
binaryReps[c] = binary
}
for ((letter, binary) in binaryReps) { //맵에 대해 이터레이션 (맵의 키와 값을 두 변수에 각각 대임)
println(“$letter = $binary”)
}
위 코드는 맵을 만들고, 몇 글자에 대한 2진 표현으로 맵을 채운 다음, 그 맵의 내용을 출력하는 내용이다.
코드를 보면 ..
연산자를 숫자 타입의 값뿐 아니라 문자 타입의 값에도 적용할 수 있다.
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…"
}
나중에 7장에서 범위나 수열, 직접 만든 데이터 타입을 함께 사용하는 방법에 대해 살펴보고 in
검사를 적용할 수 있는 객체에 대한 일반 규칙을 더 자세히 다뤄보도록 하겠다. (7장 바로가기)
Kotlin의 예외처리는 Java나 다른 언어의 예외 처리와 비슷하다.
fun readNumber (reader: BufferedReader): Int? { //함수가 던질 수 있는 예외를 명시할 필요가 없음
try {
val line = reader.readLine()
return Integer.parseInt(line)
}
catch (e: NumberFormatException) { /./예외 타입을 :의 오른쪽에 씀
return null
}
finally {
reader.close()
}
}
Java 코드와 가장 큰 차이는 throws
절이 코드에 없다는 점이다.
finally
절을 없애고 파일에서 읽은 수를 추가하는 코드를 추가해보면 Java와 Kotlin의 중요한 차이를 살펴볼 수 있다.
fun readNumber (reader: BufferedReader) {
val number = try {
Integer.parseInt(reader.readLine()) // 이 식의 값이 “try” 식의 값이 됨
} catch (e: NumberFormatException) {
return
}
println(number)
}