
fun 키워드를 사용한다.
fun 함수이름(파라미터 목록) : 함수 반환 타입의 형태로 사용한다.
fun max(a: Int, b: Int) : Int {
return if (a>b) a else b
등호와 식으로 이뤄진 함수를 식이 본문이 함수라고 부른다. 코틀린에서는 식이 본문인 함수가 자주 쓰인다.
함수의 본문 식은 단순한 산술식이나 함수 호출 식뿐만 아니라 if, when, try 등의 더 복잡한 식도 자주 쓰인다. 또한 식이 본문인 함수는 타입 추론을 통해 식의 결과 타입을 함수 반환 타입으로 정해주기 때문에 함수의 반환 타입을 생략할 수 있다.
위의 예시를 식이 본문인 함수로 바꾸면 더 간결해진다.
fun max(a: Int, b: Int) = if (a>b) a else b
블록이 본문인 함수
본문이 중괄호로 둘러싸인 함수를 말한다.
블록이 본문인 함수가 값을 반환한다면 반드시 반환 타입을 지정하고 return 문을 사용해 반환 값을 명시해야 한다.
코틀린에서는 타입 지정을 생략하는 경우가 흔하다. 타입 추론을 통해 초기화 식의 타입을 변수 타입으로 지정한다.
val question = "삶, 우주, 그리고 모든 것에 대한 궁극적인 질문"
val answer = 12
val answer: Int = 12 // 타입을 명시해도 된다.
var yearsToCompute = 7.5e6
초기화 식을 사용하지 않는다면 변수 타입을 반드시 명시해야 한다.
val answer: Int
answer = 42
val (value) val languages = arrayListOf("Java") // 불변 참조를 선언한다.
languages.add("Kotlin") // 참조가 가리키는 객체 내부를 변경한다.
var (variable) var answer = 42
answer = "no answer" // "Error: type mismatch"
기본적으로는 모든 변수를 val 키워드를 사용해 불변 변수로 선언하고, 나중에 꼭 필요할 때에만 var로 변경하라. 변경 불가능한 참조와 변경 불가능한 객체를 부수 효과가 없는 함수와 조합해 사용하면 코드가 함수형 코드에 가까워진다.
어떤 타입의 변수에 다른 타입의 값을 저장하고 싶다면 변환 함수를 써서 값을 변수의 타입으로 변환하거나, 값을 변수에 대입할 수 있는 타입으로 강제 형 변환해야한다.
문자열 안에 변수를 사용할 수 있다. 문자열 리터럴의 필요한 곳에 변수를 넣되 변수 앞에 $를 추가하면 된다.
fun main(args: Array<String>) {
val name = if (args.size>0) args[0] else "Kotlin"
println("Hello, $name")
문자열 템플릿 안에 복잡한 식도 중괄호({})로 둘러싸서 문자열 템플릿 안에 넣을 수 있다.
템플릿 안에서 변수 이름만 사용하는 경우라도 ${name}처럼 중괄호로 변수명을 감싸는 습관을 들이면 더 좋다.
정규식 등을 통해 검색하거나 일괄 변환할 때도 중괄호를 쓴 경우가 처리가 더 쉽고, 코드를 사람이 읽을 때도 문자열 템플릿 안에서 변수가 쓰인 부분을 더 쉽게 식별할 수 있다.
// 자바
public class Person {
private final String name;
public Person(String name) {
this.nane = name;
}
public String getName() {
return name;
}
}
// 코틀린
class Person (val name: String)
훨씬 간결해졌다.
이렇게 코드 없이 데이터만 저장하는 클래스를 값 객체(value object)라 부른다.
자바를 코틀린으로 변환한 결과, public 가시성 변경자(visibility modifier)가 사라졌다. 코틀린의 기본 가시성은 public이므로 이런 경우 변경자를 생략할 수 있다.
코틀린 프로퍼티는 자바의 필드와 접근자 메서드를 완전히 대신한다.
클래스에서 프로퍼티를 선언할 때는 val이나 var를 사용한다.
val로 선언한 프로퍼티는 읽기 전용이며, var로 선언한 프로퍼티는 변경 가능하다.
class Person (
val name: String, // 읽기 전용 프로퍼티. (비공개) 필드, (공개) 게터를 만들어 낸다.
var isMarried: Boolean // 쓸 수 있는 프로퍼티. (비공개) 필드, (공개) 게터, (공개) 세터를 만들어낸다.
)
코틀린은 값을 저장하기 위한 비공개 필드와 그 필드에 값을 저장하기 위한 세터, 필드의 값을 읽기 위한 게터로 이뤄진 간단한 디폴트 접근자 구현을 제공한다.
// 자바에서 Person 클래스를 사용하는 방법
Person person = new Person("Bob", true);
System.out.println(person.getName());
>> Bob
System.out.println(person.isMarried());
>> true
// 코틀린에서 Person 클래스를 사용하는 방법
val person = Person("Bob", true)
println(person.name)
println(person.isMarried)
게터를 호출하는 대신 프로퍼티를 직접 사용했다. 로직은 동일하지만 코드는 더 간결해졌다.
대부분의 프로퍼티에는 그 프로퍼티의 값을 저장하기 위한 필드가 있다. 이를 프로퍼티를 뒷받침하는 필드(backing field)라고 부른다.
원한다면 커스텀 게터를 작성하여 프로퍼티 값을 그때그때 계산할 수도 있다.
직사각형 클래스인 Rectangle을 정의하면서 자신이 정사각형인지 알려주는 기능을 만들어보자. 직사각형이 정사각형인지를 별도의 필드에 저장할 필요가 없다. 사각형의 너비와 높이가 같은지 검사하면 정사각형 여부를 그때그때 알 수 있다.
class Rectangle(val height: Int, val width: Int) {
val isSquare: Boolean
get() { // 프로퍼티 게터 선언
return height == width
}
}
val rectangle = Rectangle(41, 43)
println(rectangle.isSquare)
>> false
클라이언트가 프로퍼티에 접근할 때마다 게터가 프로퍼티 값을 매번 다시 계산한다.
파라미터가 없는 함수를 정의하는 방식과 커스텀 게터를 정의하는 방식은 구현이나 성능상 차이는 없다. 차이가 나는 부분은 가독성뿐이다.
enum class Color {
RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}
코틀린에서 enum은 소프트 키워드(soft keyword)이다. 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 // enum 클래스 안에서 메서드를 정의한다.
}
enum에서도 일반적인 클래스와 마찬가지로 생성자와 프로퍼티를 선언한다.
각 enum 상수를 정의할 때는 그 상수에 해당하는 프로퍼티 값을 지정해야만 한다. 또한 enum 클래스 안에 메서드를 정의하는 경우 반드시 enum 상수 목록과 메서드 정의 사이에 세미콜론을 넣어햐 한다.
if와 마찬가지로 when도 값을 만들어내는 식이다. 따라서 식이 본문인 함수에 when을 바로 사용할 수 있다.
자바의 switch에 해당하는 코틀린 구성 요소는 when이다. switch와 달리 각 분기 끝에 break를 넣지 않아도 된다.
한 분기 안에서 콤마(,)를 사용하여 여러 값을 매치 패턴으로 사용할 수도 있다.
fun getWarmth(color: Color) = when(Color) {
Color.RED, Color.ORANGE, Color.YELLOW -> "warm"
Color.GREEN -> "neutral"
Color.BLUE, Color.INDIGO, Color.VIOLET -> "cold"
}
println(getWarmth(Color.ORANGE))
>> warm
상수 값을 임포트하면 코드를 더 간단하게 만들 수 있다.
import ch02.color.Color
import ch02.color.Color.* // 짧은 이름으로 사용하기 위해 enum 상수를 모두 임포트한다.
fun getWarmth(color: Color) = when(color) {
RED, ORANGE, YELLOW -> "warm" // 임포트한 enum 상수를 이름만으로 사용한다.
GREEN -> "neutral"
BLUE, INDIGO, VIOLET -> "cold"
}
코틀린에서 when은 자바의 switch보다 훨씬 더 강력하다. 분기 조건에 상수만을 사용할 수 있는 자바의 switch와 달리 코틀린의 when의 분기 조건은 임의의 객체를 허용한다.
fun mix(c1: Color, c2: Color) =
when (setOf(c1, c2)) { // when 식의 인자로 아무 객체나 사용할 수 있다.
setOf(RED, YELLOW) -> ORANGE
setOf(YELLOW, BLUE) -> GREEN
setOf(BLUE, VIOLET) -> INDIGO
else -> throw Exception("Dirty Color") // 매치되는 분기 조건이 없으면 이 문장을 실행한다.
}
println(maix(BLUE, YELLOW))
>> GREEN
모든 분기 식에서 만족하는 조건을 찾을 수 없다면 else 분기의 문장을 실행한다.
위 예시에서 함수가 호출될 때마다 함수 인자로 주어진 두 색이 when의 분기 조건에 있는 다른 두 색과 같은지 비교하기 위해 여러 Set 인스턴스를 생성한다.
인자가 없는 when 식을 사용하면 불필요한 객체 생성을 막을 수 있다. 추가 객체를 만들지 않는다는 장점이 있지만 가독성은 더 떨어진다.
성능을 더 향상시키기 위해 이 정도 비용을 감수해야 하는 경우도 자주 있다.
when에 아무 인자도 없으려면 각 분기의 조건이 불리언 결과를 계산하는 식이어야 한다.
fun mixOptimized(c1: Color, c2: Color) =
when {
(c1 == RED && c2 == YELLOW) ||
(c1 == YELLOW && c1 == RED) ->
ORANGE
(c1 == YELLOW && c2 == BLUE) ||
(c1 == BLUE && c2 == YELLOW) ->
GREEN
(c1 == BLUE && c2 == VIOLET) ||
(c1 == VIOLET && c1 == BLUE) ->
INDIGO
else -> throw Exception("Dirty color")
}
println(mizOptimized(BLUE, YELLOW))
>> GREEN
코틀린에서는 프로그래머 대신 컴파일러가 캐스팅을 해준다. 어떤 변수가 원하는 타입인지 일단 is로 검사하고 나면 굳이 변수를 원하는 타입으로 캐스팅하지 않아도 마치 처음부터 그 변수가 원하는 타입으로 선언된 것처럼 사용할 수 있다. 실제로는 컴파일러가 캐스팅을 수행해준다.
이를 스마트 캐스트라고 부른다.
fun eval(e: Expr): Int {
if (e is Num) {
val n = e as Num // 여기서 Num으로 타입을 변환하는데, 이는 불필요한 중복이다.
return n.value
}
if (e is Sum) {
return eval(e.right) + eval(e.left) // 변수 e에 대해 스마트 캐스트를 사용한다.
}
throw IllegalArgumentExceptoin("Unknown expression")
}
println(eval(Sum(Sum(Num(1), Num(2)), Num(4))
스마트 캐스트는 is로 변수에 든 값의 타입을 검사한 다음에 그 값이 바뀔 수 없는 경우에만 작동한다.
예를 들어 앞에서 본 예제처럼 클래스의 프로퍼티에 대해 스마트 캐스트를 사용한다면 그 프로퍼티는 반드시 val이어야 하며 커스텀 접근자를 사용한 것이어도 안된다. val이 아니거나 val이지만 커스텀 접근자를 사용하는 경우에는 해당 프로퍼티에 대한 접근이 항상 같은 값을 내놓는다고 확신할 수 없기 때문이다.
eval 함수를 리팩토링해서 더 코틀린다운 코드로 만들어보자.
코틀린의 if (a>b) a else b는 자바의 a>b ? a : b처럼 작동한다.
코틀린에서는 if가 값을 만들어 내기 때문에 자바와 달리 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")
}
println(eval(Sum(Num(1), Num(2))))
>> 3
when을 사용해 더 다듬어 보자.
fun eval(e: Expr): Int =
when (e) {
isNum ->
e.value // 스마트 캐스트
isSum ->
eval(e.right) + eval(e.left) // 스마트 캐스트
else ->
throw IllegalArgumentException("Unknown expression")
if나 when 모두 분기에 블록을 사용할 수 있다. 그런 경우 블록의 마지막 문장이 블록 전체의 결과가 된다.
fun evalWithLogging(e: Expr): Int =
when(e) {
isNum -> {
println("num: ${e.value}")
e.value // 이 식이 블록의 마지막 식이므로 e의 타입이 Num이면 e.value가 반환된다.
}
is Sum -> {
val left = evalWithLoggin(e.left)
val right = evalWithLogging(e.right)
println("sum: $left + $right")
left + right // e의 타입이 Sum이면 이 식의 값이 반환된다.
}
else -> throw IllegalArgumentException("Unknown expression")
블록의 마지막 식이 블록의 결과라는 규칙은 블록이 값을 만들어내는 경우 항상 성립한다.
식이 본문인 함수는 블록을 본문으로 가질 수 없고 블록이 본문인 함수는 내부에 return문이 반드시 있어야 한다.
코틀린에는 while과 do-while 루프가 있다. 문법은 자바와 같다.
조건이 참인 동안 본문을 반복 실행한다.
while (조건) {
/*...*/
}
맨 처음 무조건 본문을 한 번 실행한 다음, 조건이 참인 동안 본문을 반복 실행한다.
do {
/*...*/
} while (조건)
코틀린에는 자바의 for 루프에 해당하는 요소가 없다. 초깃값, 증가 값, 최종 ㄱ밧을 사용한 루프를 대신하기 위해 코틀린에서는 범위를 사용한다.
범위는 기본적으로 두 값으로 이뤄진 구간이다. .. 연산자로 시작 값과 끝 값을 연결해서 범위를 만들며, 두번째 값은 범위에 포함된다.
val onToTen = 1..10
.. 연산자는 숫자 타입의 값 뿐 아니라 문자 타입의 값에도 적용할 수 있다. 'A'..'F'는 A부터 F에 이르는 문자를 모두 포함하는 범위를 만든다.
증가 값 step을 사용하면 수를 건너 뛸 수 있다. 증가 값을 음수로 만들면 정방향 수열이 아닌 역방향 수열을 만들 수 있다. 또는 downTo를 사용하여 역방향 수열을 만들 수 있다.
다음 예제에서 100 downTo 1은 역방향 수열을 만든다. 그 뒤에 step 2를 붙여 증가 값의 절댓값이 2로 바뀐다.
for (i in 100 downTo 1 step 2) {
print(fizzBuzz(i))
}
끝 값을 포함하지 않는 범위를 만든다.
for (x in 0 until size)
컬렉션에 대한 이터레이션을 위해 for .. in 루프를 자주 쓴다.
맵에 대한 이터레이션을 살펴보자.
val binaryReps = TreeMap<Char, String>() // 키에 대해 정렬하기 위해 TreeMap을 사용한다.
for (c in 'A'..'F') { // A부터 F까지 문자의 범위를 사용해 이터레이션 한다.
val binary = Integer.toBinaryString(c.ToInt()) // 아스키 코드를 2진 표현으로 바꾼다.
binaryReps[c] = binary // c의 키로 c의 2진 표현을 맵에 넣는다.
}
for ((letter, binary) in binaryRepse) { // 맵에 대해 이터레이션 한다. 맵의 키와 값을 두 변수에 각각 대입한다.
println("$letter = $binary")
}
컬렉셕에도 구조 분해 구문을 사용할 수 있다.
val list = arrayListOf("10", "11", "1001")
for((index, element) in list.withIndex()) { // 인덱스와 함께 컬렉션을 이터레이션 한다.
println("$index: $element")
}
컬렉션이나 범위에 대해 in 키워드를 사용했다.
어떤 값이 범위나 컬렉션에 들어있는지 알고 싶을 때도 in을 사용한다.
in 연산자를 사용해 어떤 값이 범위에 속하는지 검사할 수 있다. 반대로 !in을 사용하면 어떤 값이 범위에 속하지 않는지 검사할 수 있다.
fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
fun isNotDigit(c: Char) = c !in '0'..'9'
println(isLetter('q'))
>> true
println(isNotDigit('x'))
>> true
when 식에서 사용할 수도 있다.
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.."
}
println(recognize('8'))
>> It's a digit!
오류가 발생하면 예외를 던질 수 있다. 코틀린의 기본 예외 처리 구문은 자바와 비슷하다. 자바와 달리 new를 붙일 필요가 없으며, throw는 식이므로 다른 식에 포함될 수 있다.
if (percentage !in 0..100) {
throw IllegalArgumentException (
"A percentage value must be between 0 and 100: $percentage")
}
val percentage =
if (number in 0..100)
number
else
throw IllegalArgumentException ( // throw는 식이다.
"A percentage value must be between 0 and 100: $percentage")
}
fun readNumber (reader: BufferedReader): Int? { // 함수가 던질 수 있는 예외를 명시할 필요가 없다.
try {
val line = reader.readLine()
return Integer.parseInt(line)
}
catch (e: NumberForamtException) { // 예외 타입을 :의 오른쪽에 쓴다.
return null
}
finally { // 자바와 똑같이 작동한다.
reader.close()
}
}
val reader = BufferedReader(StringReader("239"))
println(readNumber(reader))
>> 239
코틀린의 try 키워드는 if나 when과 마찬가지로 식이다. 따라서 try의 값을 변수에 대입할 수 있다. if와 달리 try의 본문을 반드시 중괄호 {}로 둘러싸야 한다.
다른 문장과 마찬가지로 try의 본문도 내부에 여러 문장이 있으면 마지막 식의 값이 전체 결과 값이다.
fun readNumber (reader: BufferedReader) {
val numver = try {
Integer.parseInt(reader.readLine()) // 이 식의 값이 "try" 식의 값이 된다.
}
catch (e: NumberForamtException) {
return
}
println(number)
}
코틀린의 간결함
코틀린은 간결한 문법을 제공하여 코드 작성이 훨씬 효율적이고 코드의 가독성과 유지보수성을 개선한다. 함수와 변수 선언 방식부터 클래스와 프로퍼티 정의까지, 코틀린의 구문은 불필요한 코드를 줄이고 더 직관적으로 코드를 작성할 수 있게 해준다. 특히 식이 본문인 함수와 타입 추론 기능은 개발자가 명시적으로 타입을 지정하지 않아도 되므로 코드가 더 깔끔해진다.
불변성과 가변성의 명확한 구분
코틀린은 val과 var 키워드를 통해 불변 변수와 가변 변수를 명확하게 구분한다. 이는 코드의 안정성을 높이는 데 큰 도움을 준다. 기본적으로 모든 변수를 val로 선언하고 필요할 때만 var로 변경하는 접근 방식은 코드의 가독성과 유지보수성을 향상시킨다.
강력한 타입 시스템과 스마트 캐스트
코틀린의 타입 시스템은 강력하다. 스마트 캐스트를 통해 컴파일러가 자동으로 타입을 캐스팅해주므로 개발자는 불필요한 타입 변환 코드를 작성할 필요가 없다. 이는 코드의 가독성을 높일 뿐만 아니라 타입 안전성을 보장한다. 특히 is 연산자와 결합된 스마트 캐스트는 편리한 사용이 가능하다.
풍부한 제어 구조
코틀린의 when 구문은 자바의 switch 구문보다 강력하고 유연하다. when은 단순한 값 비교뿐만 아니라 임의의 객체를 조건으로 사용할 수 있으며, 여러 조건을 한 분기에 묶을 수도 있다. 이는 복잡한 조건문을 더 명확하게 작성할 수 있게 해준다.
예외 처리의 단순화
try를 식으로 사용하여 예외 처리와 값 반환을 한꺼번에 할 수 있는 기능은 코드의 가독성을 높이고 중복을 줄이는 데 도움이 된다.
범위와 이터레이션의 직관성
코틀린의 범위(.., until, step, downTo)와 컬렉션 이터레이션은 직관적이고 사용하기 쉽다. 이는 반복문을 더 간결하고 명확하게 작성할 수 있게 해준다. 특히 구조 분해를 통해 컬렉션을 이터레이션하는 방식은 가독성을 향상시킨다.
📚 드미트리 제메로프 스베트라나 이사코바, Kotlin in Action