최근 수정 시각 : 2025-08-29 23:03:50

Kotlin/문법

파일:상위 문서 아이콘.svg   상위 문서: Kotlin
프로그래밍 언어 문법
{{{#!folding [ 펼치기 · 접기 ]
{{{#!wiki style="margin: 0 -10px -5px; word-break: keep-all"
프로그래밍 언어 문법
C(포인터 · 구조체 · size_t) · C++(이름공간 · 클래스 · 특성 · 상수 표현식 · 람다 표현식 · 템플릿/제약조건/메타 프로그래밍) · C# · Java · Python(함수 · 모듈) · Kotlin · MATLAB · SQL · PHP · JavaScript(표준 내장 객체, this) · Haskell(모나드) ·
마크업 언어 문법
HTML · CSS
개념과 용어
함수(인라인 함수 · 고차 함수 · 콜백 함수 · 람다식) · 리터럴 · 문자열 · 식별자(예약어) · 상속 · 예외 · 조건문 · 반복문 · 비트 연산 · 참조에 의한 호출 · eval · 네임스페이스 · 호이스팅
기타
#! · == · === · deprecated · GOTO · NaN · null · undefined · S-표현식 · 배커스-나우르 표기법
}}}}}}
프로그래밍 언어 목록 · 분류 · 문법 · 예제

1. 개요2. 기본3. 파일 구조4. 메인 함수5. 제어자
5.1. 접근 제어자5.2. 기타 제어자
6. 람다식
6.1. 기본6.2. 고차함수
7. 함수
7.1. 확장 함수7.2. infix 함수
8. 클래스
8.1. Getter, Setter
9. 타입
9.1. 배열(반복 가능한 객체)
9.1.1. Array9.1.2. 컬렉션
10. 변수 선언
10.1. 특이한 경우: Nullable10.2. 특이한 경우: 형변환110.3. 특이한 경우: 형변환2
11. 제네릭
11.1. where11.2. reified
12. 스코프 함수
12.1. let12.2. run12.3. also12.4. with12.5. apply12.6. takeIf12.7. takeUnless
13. 입력 받기
13.1. readln / readlnOrNull13.2. Scanner
14. 출력하기15. 반복문
15.1. for문15.2. while15.3. do-while
16. 제어문
16.1. if[else]16.2. when
17. 상속

1. 개요

Kotlin(코틀린)의 문법을 설명하는 문서다.

==# 편집 지침 #==
소스 코드로 예시를 들 때 아래와 같이 문법을 활용하여 소스 코드를 써 주시기 바랍니다.
\#!syntax kotlin (소스코드)

또한 메인함수에서 매개변수가 필요한 경우 통일을 위해 args를 사용하시기 바랍니다.

예시 코드는 아래와 같습니다.
#!syntax kotlin
fun main(args: Array<String>) {
    println("Hello, World!")
}

2. 기본

코틀린의 주목적은 현재 자바가 사용되고 있는 모든 용도에 적합하면서도 더 간결하고 생산적이며 안전한 대체 언어를 제공하는 것이다.
1장 코틀린이란 무엇이며, 왜 필요한가?

코틀린은 개발자를 더 행복하게 하기 위해 고안된 모던하면서도 성숙한 프로그램언어 입니다. 간결하고 안전하며 Java 및 기타 언어와 상호 운용 가능하며 생산적인 프로그래밍을 위해 여러 플랫폼 간에 코드를 재사용할 수 있는 다양한 방법을 제공합니다.
원문: Kotlin is a modern but already mature programming language designed to make developers happier. It's concise, safe, interoperable with Java and other languages, and provides many ways to reuse code between multiple platforms for productive programming.
Get started with Kotlin

코틀린의 코드는 객체 지향을 원칙으로 하며, 자바100% 연계되는 문법을 사용하고 있다.[1]

.kt, 스크립트의 경우 .kts의 확장자를 가진다.

상술했듯 자바와 굉장히 비슷한 문법 구조를 가지고 있어 이 문서를 읽기 전에 Java/문법 문서를 읽으면 큰 도움이 된다.

3. 파일 구조

[package 이름 명시]
[import할 패키지 명시]
[클래스 구현, 변수 및 함수 선언]

Java와 다른점은 다음과 같다.
  • 패키지를 실제 파일의 경로와 동일하게 명시할 필요가 없다. 다만, 일관성을 위해 동일하게 명시하는 것을 추천한다.
  • 하나의 파일이 여러개의 public class를 가질 수 있다. 비슷하게 파일 이름에 제한이 없다.[2]
  • top-level에 변수와 함수를 선언할 수 있다.

4. 메인 함수

#!syntax kotlin
fun main(args: Array<String>) {
    println("Hello, World!")
}

Java와 비교하면 메인 함수의 길이가 상당히 짧다.


#!syntax kotlin
fun main() {
    println("Hello, World!")
}

Kotlin 1.3 버전부터는 매개변수를 생략할 수 있다.

5. 제어자

5.1. 접근 제어자

자바와 다르게 기본값이 public이다.
  • public: 공개. 어디에서나 접근 가능하다.
  • Internal: 내부. 코틀린에 새로 추가된 제어자로, 동일 모듈[3]에서만 접근 가능하다.
  • protected: 보호. 해당 클래스나 파일에서만 접근 가능하지만, 상속 관계라면 외부에서도 접근 가능하다.
  • private: 비공개. 해당 클래스나 파일에서만 접근 가능하다.

자바의 접근 제어자와 차이가 있어, 자바와 코틀린을 함께 사용할 때 접근 범위에 차이가 생긴다.
  • 코틀린에서 자바 코드 사용
    • default: 코틀린에서는 private-package 개념이 없어. private로 취급된다.
    • protected: 코틀린의 접근 범위가 적용되어 상속 관계가 아니면 접근할 수 없다.
  • 자바에서 코틀린 코드 사용
    • internal: public으로 취급하지만 권장되지 않는다. 경고를 하는 IDE도 있다.
    • protected: 자바의 접근 범위가 적용되어 동일 패키지라면 사용 가능하지만, 권장되지 않는다. 이 역시 경고를 하는 IDE도 있다.

5.2. 기타 제어자

  • final, open: 수정 가능. 기본값은 final이며 기본값이 다르다는걸 제외하면 자바와 동일하다.
  • abstract: 추상. 자바와 동일하다. 추상 클래스는 open이 자동으로 적용된다.
  • override: 재정의. 자바에서는 동일한 이름, 매개변수를 가지면 재정의되었고, 어노테이션을 사용해 명시했다면 코틀린에서는 제어자를 사용해야 한다.
  • lateinit: 초기화 지연. 변수의 초기화 시점을 지연한다. 상수와 자바의 원시 타입에 대응되는 타입에는 사용할 수 없다.
  • const: 상수. 원시 타입 또는 String 타입인 val 키워드로 선언된 상수에 사용 가능하며, 자바의 static final로 컴파일된다.
  • data: 데이터. 클래스에 사용 가능하며, 필드에 대해 equals, hashCode, toString 등의 메서드를 자동으로 생성한다.
  • infix: 객체 a에 대해 하나의 매개 변수 m을 받는 메서드에 사용 가능하다. 메서드 이름을 method라고 할 때 호출을 a.method(b) 대신 a method b로 호출할 수 있다.
  • operator: 해당 메서드가 연산자를 정의하는 메서드임을 알린다. 이는 자바에서 원시 타입과 String에서만 사용 가능한 연산자를 다른 객체에서 사용할 수 있게 된다. 예를 들어 + 연산자는 plus()메서드를 호출하고, 이 메서드를 정의하면 다른 객체에서 + 연산자를 쓸 수 있다

6. 람다식

6.1. 기본

다음과 같이 활용한다.
  • 함수형
    {{{#!syntax kotlin

    • package Kotlin
fun main(){
fun fn(a:Int, b:Int):Int {return a+b}
println(fn(1,2))
}
}}}
  • 람다식
    {{{#!syntax kotlin

    • package Kotlin
fun main(){
val fn = {a:Int, b:Int -> a+b}
println(fn(1,2))
}
}}}

6.2. 고차함수

코틀린은 인터페이스를 통해서 함수를 인자로 넘겨주던 Java와 다르게 아주 간편하게 함수를 주고받거나 반환할 수 있다.
  • 함수를 인자로 받기
    {{{#!syntax kotlin

    • package Kotlin
fun printer(a: Int, b: Int, f: (Int, Int) -> Int) {
println("$a 와 $b 를 함수에 -> ${f(a, b)}")
}
}}}
* 함수를 반환하기
{{{#!syntax kotlin
package Kotlin

fun getPrintingFunction(str: String): () -> Unit {
return { println(str) }
}

}}}

또한, 함수(람다식)을 인자로 넘겨주는 함수를 사용할 경우, 함수의 내용을 소괄호 밖에 표시하는 것이 권장된다.

#!syntax kotlin

printer(3, 5, {i, j -> i + j}) // 작동
printer(3, 5) {i, j ->
    i + j
} // 작동



이와 앞서 언급한 클래스 내 함수 삽입을 이용하면 context를 만들 수 있다.

#!syntax kotlin
package HelloWorld

object NamuWiki {
    fun getDocument(name: String): String {
        // TODO
        return document
    }
}

fun <T> namuWiki(lambda: NamuWiki.() -> T) : T { // lambda는 일종의 확장 함수
    return NamuWiki.lambda()
}

fun main() {
    val kotlin: String = namuWiki { // namuWiki 함수 호출 및 결과
        getDocument("Kotlin/문법") // 문서 얻기 (확장 함수로 가능한 this 생략)
    }

    getDocument("Kotlin/문법") // ERROR: function getDocument doesn't exist

    println(kotlin)
}

7. 함수

자바와 달리 모든 함수가 반환값을 가진다.

함수를 선언하려면 다음과 같이 작성한다.
#!syntax kotlin
// fun 키워드와 함수이름을 쓰고 뒤에 소괄호와 매개변수를 쓴다. 반환값은 마지막에 쓴다.
fun sum(a: Int, b: Int): Int {
    return a + b
}


앞에 제어자를 쓰거나, 자바의 void 메서드와 동일한 함수를 선언할 수 있다.
#!syntax kotlin
// 문자열을 출력하는 함수
private fun printHelloWorld() { // 반환타입 Unit이 생략되었다.
    println("Hello, World!")
    // 자바와 마찬가지로 return 또는 return Unit이 생략되었다.
}


함수 내용이 한줄이라면 더 간단히 표현할 수 있다.
#!syntax kotlin
fun sum(a: Int, b: Int) = a + b // 이 경우 타입 추론이 가능하다.

fun printHelloWorld() = println("Hello, World!)

7.1. 확장 함수

이미 선언되어있는 클래스의 메서드를 재정의하거나 새로 선언할 수 있다.

#!syntax kotlin
fun String.sayHello() {
    println("Hello, $this") // this는 객체 자기 자신을 가리킴
}

fun main() {
    "NamuWiki".sayHello() // "Hello, NamuWiki" 출력
}

7.2. infix 함수

infix 키워드를 이용해서 .과 ()를 쓰지 않아도 되는 함수를 만들 수 있다.

infix 함수의 조건으로는
  • 확장 함수 또는 메서드여야 한다
  • 매개 변수 1개여야 한다
  • 매개 변수는 기본 값이 없으면서 vararg 매개변수가 없어야 한다

#!syntax kotlin
class Human(var name: String, var age: Int, var location: String) {
    fun travel(location: String) {
        this.location = location
    }
    override fun toString(): String {
        return "${name}, ${age}세, ${location} 거주"
    }
    infix fun eat(food: String) = println("${this.name}님이 ${food}를 먹었습니다.")
}

fun main() {
    Human("홍길동", 30, "서울") eat "피자" // 홍길동님이 피자를 먹었습니다.
    Human("홍길동", 30, "서울") browse URL("https://namu.wiki/") // 홍길동님이 https://namu.wiki/를 검색했습니다.
}

infix fun Human.browse(url: URL) = println("${this.name}님이 ${url}를 검색했습니다.")

8. 클래스

자바의 정적 메서드를 모아둔 유틸리티 클래스는 코틀린에서는 객체[4]라고 따로 있다.

"사람"이라는 클래스는 다음과 같다.[5]
#!syntax kotlin
class Person(name: String) {
    companion object { // 이 블록 안의 함수와 변수는 static이 붙은 것처럼 호출해야 한다.
        const val SPECIES = "Homo sapiens"
    }

    var name = name
        private set
}

fun main() {
    val myPerson = Person("John")
    println(myPerson.name)
    println(myPerson.SPECIES)
}

8.1. Getter, Setter

자바와 달리, 단순히 변수 선언 만으로 getter와 setter가 자동 생성되며, object.field처럼 직접 field에 접근하는것 처럼 보여도 내부적으로 getter를 호출한다.

예시
#!syntax kotlin
class Car {
    var speed = 0
        set(speed: Int) {
            if (speed < 0) throw IllegalArgumentException("속력은 음수가 될 수 없음")
            field = speed // field는 필드 자기 자신을 가리킨다.
        }
    private var gear = 0
    val trunk = mutableListOf<Any>()
        get() = field.toMutableList()
    var licensePlate = "12가 3456"
        protected set
}

9. 타입

Kotlin에는 원시 타입[6]과 그 래퍼 클래스[7]가 구분되지 않고 모든 타입이 객체로 취급되고, 컴파일 시 가능한 경우 원시 타입으로 변환된다.

아래 타입들은 Java의 원시 타입과 대응된다. 소괄호는 대응되는 자바 타입이다.
  • 부호 있는 정수: Long(long) > Int(int) > Short(short) > Byte(byte)
  • 실수: Double(double) > Float(float)
  • 논리: Boolean(boolean)
  • 문자: Char(char)

다음 타입들은 Java에는 없는 타입이다. 코틀린에서는 내부적으로 각 사이즈에 맞는 부호있는 정수로 저장하고, toString() 메서드가 부호 없는 정수로 반환한다.
ULong[8] > UInt[9] > UShort[10] > UByte[11]

아래 타입들은 특수한 타입이다. Unit과 Nothing은 Java에는 없는 Kotlin 오리지널 타입이다.[12]
  • 객체: Any, 최상위 클래스로, Java의 Object에 대응된다.[13]
  • Unit: 단 하나의 인스턴스를 가진 싱글톤 객체로, 모든 함수가 값을 반환하는 Kotlin의 특징을 유지하기 위해 존재하는 의미없는 값이다. 때문에 Unit을 직접 다루는 경우는 거의 없고[14] Java의 void 메서드를 구현하기 위해 사용된다.
  • Nothing: Any와 반대인 최하위 클래스로 값을 가지지 않는 타입이다.[15] 함수가 반환을 하지 않음을 알릴 때 사용된다. 여기서 반환하지 않는 함수는 종료되지 않거나[16] 예외를 던지는 경우를 말한다. Nothing 함수 이후의 코드는 IDE에서 죽은 코드임을 알 수 있게 된다.

9.1. 배열(반복 가능한 객체)

JVM에서 특별하게 관리되는 객체인 Java와 달리, Kotlin은 제네릭을 사용하여 일반적인 객체로 다룬다.

단, 원시 타입의 배열은 따로 존재한다.[17]
  • 원시 타입 배열[18]
    • 부호 있는 정수의 배열: LongArray, IntArray, ShortArray, ByteArray
    • 부호 없는 정수의 배열: ULongArray, UIntArray, UShortArray, UByteArray
    • 실수 배열: DoubleArray, FloatArray
    • 논리 배열: BooleanArray
    • 문자 배열: CharArray
  • 참조 타입 배열: Array<T>[19]

9.1.1. Array

Array를 만들 때는 이와 같이 하면 된다
#!syntax kotlin package HelloWorld

fun main() {
    val doubleArray: Array<Double> = arrayOf(1.0, 1.5, 2.0, 3.0)
} 


위의 경우 arrayOf 안에 있는 값을 가져가서 Array가 그 값들의 type을 가지게 된다.

9.1.2. 컬렉션

코틀린이 자바에서 비롯됐기 때문에 이러한 부분은 자바에 의존한다 볼 수 있다. 자바의 컬렉션을 보고 오자.
컬렉션은 변경 불가한 Collection와 변경 가능한 MutableCollection으로 나누어져 있으며, 컬렉션을 List, Set, Map 처럼 인터페이스로 다룬다.
Array와 비슷하게 listOf(), mutableSetOf(), mapOf() 와 같은 함수로 객체를 만들 수 있으며, 이 때 만들어지는 객체의 타입은 각각 ArrayList, LinkedHashSet, LinkedHashMap이며, 함수의 반환 타입은 각 객체의 상위 인터페이스인 (Mutable)List, (Mutable)Set, (Mutable)Map이다.
Java의 클래스를 직접 다루지 않기 때문에 임포트가 필요 없다.

10. 변수 선언

#!syntax kotlin
package HelloWorld

fun main() {
    var a1: Int = 1
    var a2 = 1
    var b: String = "1"
    val c: Double = 3.141592

    println(a1) // OK
    println(b) // OK
    println(c) // OK

    a1 = a1 ++ // OK
    b = b + "2" // OK
    c = c ++ // ERROR

}


타입을 적어줘도, 적어주지 않아도 된다. var이 아니라 val로 쓰게 된다면 c = c ++과 같이 그 값을 바꾸지 못한다. 기본적으로는 val을 쓰는게 좋은 습관이며 권장된다. 하지만 자바에서 final을 잘 쓰지 않듯이 어차피 대부분 var로 바꿀거 코틀린에서도 var로 코딩 하는 사람도 적지 않다. 다만 val을 블록 안에 쓰면 블록 범위 안에서만 동작하므로 run {} 같은 블록 안에 val을 써서 val의 동작 범위를 정해줄 수도 있다.

10.1. 특이한 경우: Nullable

#!syntax kotlin
package HelloWorld

fun main() {
    val a: Int? = null // OK
    val b: Int = null // ERROR

    val c: String? = " Hello, World! "
    //trim()은 앞 뒤 공백을 제거하는 메서드다.
    c.trim() // ERROR
    c?.trim() // OK. 만약 c가 null이라면 호출 대신 null을 반환한다.

    val d: Int? = getIntOrNull() // 정수 또는 null을 반환하는 임의의 함수
    println(d!!) // d가 null이라면 NPE를 발생시키며, 아니면 그대로 사용한다. 이후에 사용되는 d는 자동으로 non-nullable로 캐스팅된다.
    
    val e: Int = getIntOrNull() ?: 0 // null이 반환되면 뒤에 있는 0을 사용한다.
    
    val f = getIntOrNull()
    if (f != null) println(f + 1) // 스마트 캐스팅. if문에서 f가 null이 아님을 확인했고, 불변 변수인 val이므로 자동으로 non-nullalbe로 캐스팅한다.
}

기본적으로 모든 타입은 non-nullalbe로 null을 사용할 수 없으며, 타입 뒤에 ?를 붙여 nullable 타입으로 선언해 사용해야 한다. 하지만 nullable의 경우, NullPointerExceptionNPE가 발생할 수 있어 바로 메서드를 호출할 수 없다.[20]

nullalbe 변수의 메서드를 호출하려면 safe call 연산자인 ?.를 사용한다. 이 연산자는 이름 그대로 "안전한 호출"이며, null이 아니면 메서드를 호출, null이면 호출하지 않고 대신에 null을 반환한다.

만약 null이 아님을 확신했거나 의도적으로 NPE를 발생시키려면 변수 뒤에 Non-null Assertion 연산자인 !!를 붙이면 된다. null이라면 NPE로 실행이 끊기므로 불변 변수에 한에 스마트 캐스팅이 된다.[21]

null일 경우 기본값 개념으로 대체할 수 있다. Elvis 연산자인 ?:는 앞의 값이 null이라면 뒤의 값을 사용한다.

!!, if문, 스코프 함수 등 null이 아님을 검사했다면 그 이후에서는 non-nullable로 스마트 캐스팅된다.
스마트 캐스팅은 자동으로 항변환해주는 기능으로, nullalbe 변수가 null일 경우 도달할 수 없는 위치에서는 null이 아님이 확정되어서 non-nullable 변수 처럼 다룰 수 있다. 단, var로 선언한 가변 변수는 다른 스레드에서 변경 가능성이 있어 스마트 캐스팅이 안되는 경우도 있다.
여담으로 스마트 캐스팅은 형변환이 가능한지 검사하는 is 연산자에 의해서도 발생할 수 있다.

10.2. 특이한 경우: 형변환1

#!syntax kotlin
package HelloWorld

fun main() {
    val a: Int = 1
    val b: String = "1"

    println(a + b) // ERROR
    println(a + b.toInt()) // OK

    val c: Int = 1
    val d: String = "1"

    println(c + d!!.toInt()) // OK

}

String과 Int를 더하려면 오류가 나기 때문에 .to타입()을 붙여 타입을 변경할 수 있다. null check를 하는 경우 d!!.toInt()로 해주면 된다.

10.3. 특이한 경우: 형변환2

A 클래스 밑에 B 클래스가 있는 관계가 있다고 하자.

#!syntax kotlin
package HelloWorld

fun main() {
    val a: A = B()  // OK
    val b: B = A()  // ERROR
}


하위 클래스가 상위 클래스로 형변환 되면서 저장되는 것은 가능하다. 하지만 상위 클래스가 하위 클래스로 형변환 되면서 저장되는 것은 불가능하다. 하위 클래스는 당연히 상위 클래스보다 더 많은 기능을 가지고 있다. 하위 클래스가 상위 클래스로 형변환 되면서 저장될 때 하위 클래스에는 있지만 상위 클래스에는 없는 기능은 잘려서 저장된다. 하지만 상위 클래스가 하위 클래스로 저장될 때는 기능을 새로 만들어야 한다. 인자가 필요 없을 때는 가능할 수도 있겠으나 만약 인자가 복잡하고 하위 클래스에 있는 인자가 상위 클래스에는 없다면 만들 수 없다. 그래서 자바에서는 하위 클래스였지만 기능이 잘린 상위 클래스를 다시 기능을 붙여서 다시 하위 클래스로 만드는 방법을 쓴다.

이럴 때 쓰는 것이 캐스팅이라는 것이다. JavaC언어 등에서는 값 앞에 소괄호 안에 타입을 넣어서 캐스팅을 했으나 코틀린에서는 값 as (하위 클래스 타입) 이렇게 쓴다. 근데 만약 캐스팅에 실패했다면,
에러가 발생한다.

#!syntax kotlin
package HelloWorld

fun main() {
    val a: A = A()
    val b: A = B()
    
    a as B // ERROR
    b as B // OK
}


그리고 그 에러를 발생시키지 않기 위해 코틀린은 캐스팅에서 as? 연산자를 준다. 사용법은 같지만 캐스팅에서 실패할 경우 에러가 아닌 그 식의 값이 null이 되어버린다.

#!syntax kotlin
package HelloWorld

fun main() {
    val a: A = A()
    val b: A = B()
    
    a as? B // null
    b as? B // OK
}

11. 제네릭

자바에서도 제네릭이라 부르고, C++에서는 템플릿이라 부르는 문법이다. 제네릭은 많은 곳에서 존재한다. 예를 들어 반복 가능한 객체의 타입인 Iterable 클래스가 제네릭을 명시하고 있으므로 제네릭을 없앤 일부 클래스를 제외한 나머니 컬렉션을 쓸 수 없다. 다시 말해서 List, Set, Map 등을 쓸 수 없다는 것이다.

#!syntax kotlin
package HelloWorld

fun <T> a(args: Array<T>) {      // 기본 제네릭
    // TODO
}
fun <T> b(args: Array<out T>) {  // 공변성 제네릭
    // TODO
}
fun <T> c(args: Array<in T>) {   // 반공변성 제네릭
    // TODO
}
fun d(args: Array<*>) {          // star-projections
    // TODO
}


기본 제네릭은 자바와 거의 동일하므로 자바의 제네릭을 보도록 하자.

공변성 제네릭은 읽기 전용이다. b 함수 안에서는 args의 값을 바꿀 수 없다는 소리다. 그리고 T를 상속[22]한 클래스도 인자[23]의 값으로 들어갈 수 있다.
예를 들어서, 매개변수의 타입이 Array<Any>이라고 하면 인자로 들어갈 수 있는 값의 타입은 Any를 상속한 모든 클래스, Array<Int>, Array<Double>, Array<String> 등은 불가능하고 오직 Array<Any>만 들어갈 수 있다. 하지만 매개변수의 타입이 Array<out Any>라고 한다면 들어갈 수 있는 인자의 값으로는 Array<Any>와 더불어서 Array<Int>, Array<Double>, Array<String>도 같이 들어갈 수 있다.

반공변성 제네릭은 쓰기 전용이다.[24] c 함수 안에서는 args의 값을 바꿀 수 있다는 소리다. 그리고 이름부터 공변성의 반대로, 인자의 값으로 들어갈 수 있는 값도 T가 상속을 하는 것이다.
예를 들어서, 매개변수의 타입이 Array<Int>이라고 하면 인자로 들어갈 수 있는 값의 타입은 Any를 상속한 모든 클래스, Array<Any>, Array<Number> 등은 불가능하고 오직 Array<Int>만 들어갈 수 있다. 하지만 매개변수의 타입이 Array<in Any>라고 한다면 들어갈 수 있는 인자의 값으로는 Array<Int>와 더불어서 Array<Any>, Array<Number>도 같이 들어갈 수 있다.[25]

star-projections(스타 프로젝션)은 그냥 <out Any>와 같은 제네릭이다. 다시 말해서, 인자로 들어온 값은 변경이 불가하다는 것이다.

11.1. where

where은 뒤에 인터페이스 등이 붙어서 인터페이스를 구현한 타입이 필요하다는 것을 말해주는 제어자이다. 함수 제일 뒤, 중괄호 앞에 붙어서 이러한 인터페이스를 구현한 타입이 필요하다는 것을 말해준다.

#!syntax kotlin
package HelloWorld

fun <T> a(arg: T) where T: AutoCloseable {
    // TODO
}
fun <T> b(arg: T): Any where T: AutoCloseable {
    // TODO
}
fun <T> c(arg: T) where T: AutoCloseable, T: Iterable<*> {
    // TODO
}
fun <T> d(arg: T): Any where T: AutoCloseable, T: Iterable<*> {
    // TODO
}

11.2. reified

만약에 inline 함수에서 제네릭 타입 검사 또는 강제 형변환이 필요하다면 어떻게 해야 할까?

#!syntax kotlin
package HelloWorld

inline fun <T, E> a(args: List<E>, clazz: Class<T>): T {
    // TODO
    if (clazz.isInstance(args[0])) {
        // TODO
    }
}


보통은 클래스를 따로 인자에 넣을 것이다. 하지만 오직 타입 하나만을 위해서 매개변수를 추가해야 되는가? 이를 해결하기 위해서 reified 키워드가 등장했다.
#!syntax kotlin
package HelloWorld

inline fun <reified T, E> a(args: List<E>): T {
    // TODO
    if (args[0] is T) {
        // TODO
    }
}


inline 함수에서만 reified 키워드를 쓰면은 T를 그저 제네릭이 아닌 그냥 일반 타입으로 취급할 수 있게 한다.

12. 스코프 함수

12.1. let

객체에 대해서 람다식 수행하고, 블록의 마지막 문장 실행 결과를 반환한다. 자기 자신을 참조할 때에는 it을 기본으로 사용하며, 이는 변경이 가능하다. (이는 it을 사용하는 다른 스코프 함수에도 동일하다.)

let이 있는 버전
#!syntax kotlin
package HelloWorld

class Human(var name: String, var age: Int, var location: String) {
    fun travel(location: String) {
        this.location = location
    }
    override fun toString(): String {
        return "${name}, ${age}세, ${location} 거주"
    }
}

fun main() {
    Human("홍길동", 30, "서울").let {
        println(it)
        it.travel("부산")
        println(it)
    } // 마지막 println()의 결과인 Unit이 반환됨
    /*
        실행결과
        홍길동, 30세, 서울 거주
        홍길동, 30세, 부산 거주
    */
}

let이 없는 버전
#!syntax kotlin
package HelloWorld

class Human(var name: String, var age: Int, var location: String) {
    fun travel(location: String) {
        this.location = location
    }
    override fun toString(): String {
        return "${name}, ${age}세, ${location} 거주"
    }
}

fun main() {
    val human = Human("홍길동", 30, "서울")
    println(human)
    human.travel("부산")
    println(human)
}

let을 사용하는 경우
  • null이 가능한 오브젝트가 null이 아닐 때 코드를 실행하게 할 때
    {{{#!folding [ 예시 ]
#!syntax kotlin
package HelloWorld
val humans = ArrayList<Human>()


class Human(var name: String, var age: Int, var location: String) {
    fun travel(location: String) {
        this.location = location
    }
    override fun toString(): String {
        return "${name}, ${age}세, ${location} 거주"
    }
}

fun main() {
    
    Human("홍길동", 30, "서울").let {
        humans.add(it)
    }
    // ?. 연산자를 사용, getHuman()의 결과가 null이라면 람다식이 무시되고 null이 반환된다.
    getHuman("홍길동")?.let {
        println(it.name)
    }
}

fun getHuman(name: String): Human? {
    return humans.firstOrNull {
        it.name == name
    }
}

}}}
  • 특정 변수를 제한적인 블록에서만 접근하게 만들 때

12.2. run

runlet과 비슷하나 자신을 참조할 때 it 대신 this를 쓴다는 점이 다르다. let과 마찬가지로 마지막 실행 결과가 반환된다.

#!syntax kotlin
fun main() {
    Human("홍길동", 30, "서울").run {
        println(this)
        this.travel("부산")
        println(this)
    } // 마지막 println()의 결과인 Unit이 반환됨
}


run는 Top-level에서 사용할 수도 있다. 객체 내에서 run을 사용할 때 객체 자신을 가리키지 않은 것과 동일하다. 객체 안에 있지 않으므로 객체 자신을 가리키는 this는 사용할 수 없다.

#!syntax kotlin
fun main() {
    run { // Top-level이므로 this 키워드 사용 불가
        val human = Human("홍길동", 30, "서울")
        println(human)
        human.travel("부산")
        println(human)
    } // 마지막 println()의 결과인 Unit이 반환됨
}

12.3. also

alsolet과 달리 자기 자신을 반환한다. let의 마지막 문장으로 it을 쓴 것과 동일하다.

#!syntax kotlin
fun main() {
    val type = Human("홍길동", 30, "서울").also {
        println(it)
        it.travel("부산")
        println(it)
    } // Human 객체가 그대로 반환됨

    println(type::class.simpleName) //Human 출력
}



해당 코드는 아래와 동치다. let 사용에 유의하자.


#!syntax kotlin
fun main() {
    val type = Human("홍길동", 30, "서울").let {
        println(it)
        it.travel("부산")
        println(it)
        it
    } // Human 객체가 그대로 반환됨

    println(type::class.simpleName) //Human 출력
}

12.4. with

withrun의 일반 함수 버전으로, with(receiver, transform)receiver.run(transform)과 동일한 효력을 갖는다. Top-level에서 사용할 수 있으며, 자기 자신 참조는 this를 사용한다.

#!syntax kotlin
fun main() {
    with(Human("홍길동", 30, "서울")) {
        println(this)
        this.travel("부산")
        println(this)
    } // 마지막 println()의 결과인 가 그대로 반환됨
}

12.5. apply

applyrun과 달리 자기 자신을 반환한다. run의 마지막 문장으로 this를 쓴 것과 동일하다.

#!syntax kotlin
fun main() {
    Human("홍길동", 30, "서울").also {
        println(this)
        this.travel("부산")
        println(this)
    } // Human 객체가 그대로 반환됨
}

12.6. takeIf

람다식에 명세된 조건을 만족하는 경우 자기 자신을, 그렇지 않으면 null을 반환한다. 자기 자신은 it으로 가리킨다.

#!syntax kotlin
fun main() {
    val human = Human("홍길동", 30, "서울").also {
        this.travel("부산")
    }.takeIf {
        it.location == "서울"
    }
    println(human) // it.location == "서울"이 false이므로 null 출력
}

12.7. takeUnless

람다식에 명세된 조건을 만족하는 경우 null을, 그렇지 않으면 자기 자신을 반환한다. 자기 자신은 it으로 가리킨다.

#!syntax kotlin
fun main() {
    val human = Human("홍길동", 30, "서울").also {
        this.travel("부산")
    }.takeUnless {
        it.location == "서울"
    }
    println(human) // it.location == "서울"이 false이므로 "홍길동, 30세, 부산 거주" 출력
}

13. 입력 받기

13.1. readln / readlnOrNull


#!syntax kotlin
package HelloWorld

fun main() {
    println("이름을 입력하세요")
    var name: String = readln()
    // 또는
    // var name: String? = readlnOrNull()
    println("${name}님 안녕하세요!")
}

readln 함수는 읽는 곳[26]에서 한 줄을 읽는다. 그리고 파일 등에서 EOF(End Of File)을 만나면 RuntimeException이 발생한다.
readlnOrNull 함수는 읽는 곳[27]에서 한 줄을 읽는다. 그리고 파일 등에서 EOF(End Of File)을 만나면 null을 반환한다.

13.2. Scanner


#!syntax kotlin
package HelloWorld

import java.util.*

fun main() {
    val scanner = Scanner(System.`in`)
    println("이름을 입력하세요")
    var name: String = scanner.nextLine()
    println("나이를 입력하세요")
    var age: Int = scanner.nextInt()
    println("이름 : ${name}, 나이 : ${age}")
}

자바와 다른점이 System.`in`인데, Kotlin에서 in은 예약어이기 자바에서 쓰는 in은 ``으로 감싸서 사용한다.
next타입()으로 다른 타입들도 사용할 수 있다.

14. 출력하기


#!syntax kotlin
package HelloWorld

 fun main() {
     print("안녕") // 1
     println("하세요") // 2
     var Hello: String = "안녕하세요"
     println(Hello) // 3
     var World1 : String = "안녕"
     var World2 : String = "하세요"
     println("${World1 + World2}")
     println(World1 + World2) // 4
 }

네 가지 경우 모두 안녕하세요가 출력된다. print은 줄 바꿈을 하지 않는다. 반대로 println은 줄 바꿈을 한다. print로 출력하고 또 \\n을 출력한 것과 println이 같다는 것이다. 물론 println에서 \\n 사용이 불가능 한 것은 아니다. 다만 println과 \\n도 같이 쓰인다면 두줄 줄 바꿈이 된다.

15. 반복문

15.1. for문

파이썬을 배워봤다면은 되게 익숙할 것이다. 실제로도 같은 원리라고 볼 수 있다.
#!syntax kotlin
package HelloWorld

fun main () {
    for (i in 0..9) {
        println("안녕하세요")
    }
    for (i in 0 until 10) {
        println("안녕하세요")
    }
    for (i in 9 downTo 0) {
        println("안녕하세요")
    }
    for (i in 0 until 10 step 2) {
        println("안녕하세요")
    }
    for (i in 10 downTo 1 step 3) {
        println("안녕하세요")
    }
    for (i in arrayOf("0", "1", "2", "3", "4", "5", "6", "7", "8", "9")) {
        println("안녕하세요")
    }
}

기존 Java와 절차지향 언어인 C언어에서 쓰였던 var i = 0; i < 10; i++ 같은 문법보다 더 잘 이해되는 문법이다.

until와 downTo, step은 모두 확장 함수로, until은 뒤의 수가 포함되지 않게, downTo은 포함되게 범위를 만드는 확장 함수이고 step은 범위에서 증가폭을 나타내는 확장 함수이다.

사실 for문은 in 앞에 나오는 변수에 in 뒤에 나오는 반복가능한 객체를 하나씩 갖고 와서 본체를 실행시키는 것이다. 그래서 제일 아래 예시도 가능한 것이다.

이중 반복문을 사용하려는 경우 첫번째 반복문에 변수로 i를 지정해주었다면 두번째 반복문에는 i를 사용하면 안된다. 코드 안에서 내부적으로 i를 여러번 반복하고 그 반복 안에 새로운 i가 있는 형식이기 때문이다. 여러 IDE에서 실행을 해보면 빨간색 밑줄 또는 노란색 밑줄이 그어지면서 this variable is already defined같은 오류가 뜬다.

15.2. while

C언어랑 다를 게 없다. C언어의 while

15.3. do-while

while문과 마찬가지로 C언어랑 다를 게 없다. C언어의 do-while

16. 제어문

16.1. if[else]

if (condition1) statement1 [if (condition2) statement2]* [else statement3]

() 속의 조건식(condition1)이 참이 되면 statement1을, () 속의 조건식(condition1)이 거짓이고 두 번째 () 속의 조건식(condition2)이 참이면 statement2을, 거짓이면 statement3를 실행하는 구조로 되어 있다. else if 이하는 생략 가능하다.

#!syntax kotlin
package HelloWorld

fun main() {
    var a = 1
    var b = 1
    if(a == b) {
        // TODO
    } else {
        // TODO
    }
}

16.2. when

자바switch 문이랑 비슷하지만 더 많은 기능을 가졌다.
또한 sealed class나 enum처럼 컴파일 단계에서 모두 평가가 가능할 경우 마지막 else를 생략할 수 있다.

#!syntax kotlin
package HelloWorld

fun main() {
    var a = 1

    when(a) {
        1 -> println("a는 1입니다")
        5 -> println("a는 5입니다")
        7, 9 -> println("a는 7 아니면 9입니다")
        in 10..100 -> println("a는 10 이상 100 이하입니다")
        else -> println("a는 그 외입니다")
    }
    // "a는 1입니다" 가 출력됨.
}

또한 when 자체를 값으로 사용할 수 있다. 밑에 있는 코드는 위랑 같은 결과를 보여준다.
#!syntax kotlin
package HelloWorld

fun main() {
    var a = 1

    println(when(a) {
        1 -> "a는 1입니다"
        5 -> "a는 5입니다"
        7, 9 -> "a는 7 아니면 9입니다"
        in 10..100 -> "a는 10 이상 100 이하입니다"
        else -> "a는 그 외입니다"
    })

}

17. 상속

한 객체가 다른 객체를 상속할 때에는 다음과 같은 형태를 띈다.
#!syntax kotlin
open class ParentClass()

class ChildClass: ParentClass()


코틀린은 다른 프로그래밍 언어나 자바와 달리 더 이상 상속하지 못하는 final이 클래스의 기본값이다

따라서 이 클래스를 상속가능하게 하고싶다면 open(상속 선택)이나 abstract(상속 필수)를 붙여서 다른 클래스가 해당 클래스를 상속할 수 있도록 해주어야 한다

한 객체가 인터페이스를 구현할 때에는 다음과 같은 형태를 띈다.
#!syntax kotlin
interface SampleInterface()

class ChildClass: SampleInterface


[1] 실제로 IntelliJ IDEA안드로이드 스튜디오 등에서 자바 코드를 넣으면 자동으로 코틀린으로 변환해 준다.[2] Java에서는 파일 이름과 주요 클래스의 이름이 동일해야한다.[3] 한번에 컴파일되는 단위.[4] 인스턴스가 아니다.[5] Java/문법의 코드와 동일하다.[6] Java의 int, double, char 등 소문자로 시작하는 타입[7] Integer, Double, Character 등 원시 타입을 참조 타입으로 변환한 클래스[8] 8바이트[9] 4바이트[10] 2바이트[11] 1바이트[12] 때문에 자바에서 그냥 쓸 수 없다. 단, Unit이나 Nothing을 반환하는 함수는 void로 처리되어 사용 가능하다.[13] 단, KClass 등 일부 제너릭에서는 AnyObject를 명확하게 구분짓기도 한다. 이 때는 <T: Any> 등으로 명시해야 한다.[14] 어차피 가질 수 있는 값이 단 하나밖에 존재하지 않는다.[15] Nullable로 설정하면 null만을 가질 수 있다.[16] while (true) 같은 무한루프[17] IntArray와 Array<Int>는 각각 int[\]와 Integer[\]에 대응된다.[18] 부호 없는 정수는 내부적으로 부호 있는 정수 타입을 사용하므로 UIntArray, Array<UInt>는 각각 int[\]을 활용, 그 값 자체를 배열로 처리한다.[19] 후술할 제네릭이라는 것이다.[20] 연산자도 메서드로 정의되므로 사용할 수 없다. 단, equals 메서드는 연산자로 호출하면 컴파일 시 null 체크를 하므로 사용 가능하다.[21] 스마트 캐스팅에 관한건 후술한다.[22] 후술할 내용이다. 간단히 T가 더 상위라고 하자.[23] 함수 호출할 때 들어가는 값이다. 이와 비슷하게 함수 선언할 때 들어가는 값은 매개변수라고 부른다.[24] 읽기도 되더라[25] Any -> Number -> Int이다.[26] 파일 또는 콘솔창 등[27] 파일 또는 콘솔창 등