Kotlin - start!

개요

https://kotlinlang.org/docs/command-line.html#install-the-compiler

java21 이 나오고 나서 java 문법도 많이 fluent 해졌지만, 대부분이 kotlin 에서 이미 제공하고 있던 기능인데다가, kotlin 만의 ‘이게 된다고’ 싶을 정도의 문법은 아직 제공되지 않는다.

또한 kotlin 만의 nullsafe 한 디자인 구조는 java 버전이 아무리 올라가도 따라할 수 없을것 같다.

kotlin 또한 컴파일 되면 java 바이트코드로 변환되어 JVM 위에서 동작되다 보니 kotlin 과 java 문법은 일대일 매핑되는 경우가 많다.
하지만 kotlin 에는 java 에 없는 문법들이 대거 등장하는데, 이런경우 새로운 java 문법이 생겨나는것이 아니기 때문에 해당 기능을 구현하기 위한 여러 라인의 java 코드가 등장하게 된다.

그런 면에서 kotlin 의 해괴한 문법이 낯설게 느껴질 수 도 있지만, 그로인해 향상되는 가독성을 생각해보면 충분히 java 에서 kotlin 으로 언어를 변경할 만 하다.

설치 및 실행

kotlin 설치 후 간단히 hello world 예제 출력

brew install kotlin
kotlin -version
Kotlin version 1.6.10-release-923 (JRE 11.0.10+8-jvmci-21.0-b06)
vi hello.kr
fun main() {
    println("Hello, World!")
}
kotlinc-jvm hello.kt
kotlin HelloKt
Hello, World!

kotlinc-jvm 대신에 kotlinc(Kotlin compiler) 사용 가능

kotlinc hello.kt -include-runtime -d hello.jar
java -jar hello.jar

Hello, World!
kotlinc

Welcome to Kotlin version 1.6.10 (JRE 11.0.10+8-jvmci-21.0-b06)
Type :help for help, :quit for quit
>>> println("hello world")
hello world
>>> var name = "test"
>>> println("hello $name")
hello test
>>> :help
Available commands:
:help                   show this help
:quit                   exit the interpreter
:dump bytecode          dump classes to terminal
:load <file>            load script from specified file
>>> :quit

kotlin 스크립트

아래처럼 main 이 없는 코드를 나열하고 JVM 상에서 스크립트 형식으로 실행

import java.time.*

val instant = Instant.now()
val southPole = instant.atZone(ZoneId.of("Antarctica/South_Pole"))
val dst = southPole.zone.rules.isDaylightSavings(instant)
println("It is ${southPole.toLocalTime()} (UTC${southPole.offset}) at the south Pole")
println("The south Pole ${if (dst) "is" else "is not"} on Daylight Savings Time")
kotlinc -script southpole.kts

It is 20:50:43.486781 (UTC+13:00) at the south Pole
The south Pole is on Daylight Savings Time

간단하게 함수형 코드를 테스트하기 좋다.

연산자

동등 연산자(===)

=== 연산자는 참조가 같은지를 확인한다.

equals, hasCode, toString, copy(얕은복사) 가 미리 재정의되는 data class 를 사용하여 동등 비교

data class Product(
    val name: String,
    val price: Double,
    val onSale: Boolean = false
)

fun main() {
    val p1 = Product("product1", 2000.0, true)
    val p2 = p1.copy(price = 2000.0)
    println(p1.equals(p2)) // true
    println(p1 === p2) // false
}

삼항연산자

kotlin 에는 ?, : 를 사용한 삼항연산자가 없고 if, else 로 구성할 수 있다.

fun main() {
    val num = 11
    val result1 = if (num > 10) "it's true" else "it's false"
    val result2 = if (num > 10) {
        5 + 5
    } else {
        100 + 100
    }
    println(result1) // it's true
    println(result2) // 10    
}

is, !is, as, as?

is, !is 연산자는 javainstanceof 와 동일한 역할을 수행하는 연산자.

val anyString: Any = "test"
val anyInt: Any = 1
val isString = anyString is String
val isInt = anyInt is Int
println(isString) // true
println(isInt) // true

as, as? 연산자는 캐스팅 연산자

val other: Any = 100

val asString: String? = other as? String
println(asString?.reversed()) // null

그냥 as 를 사용하면 ClassCastException 발생 가능성이 있음으로 as? 사용을 권장한다.
사실 as, as? 모두 사용을 권장하지 않고 최대한 스마트 캐스트 사용을 권장한다.

when

switch 에 해당하는 연산자, 다른언어의 swtich 와 다르게 단순 코드블럭을 실행하는 것이 아닌 값을 반환한다(단 else 를 강요)

// 값 반환
fun getName(score: Int): String {
    val result: String = when (score) {
        0 -> "zero"
        1 -> "one"
        2, 3 -> "two or three"
        else -> "unknown" // else 생략 불가
    }
    return result
}

in 키워드로 범위를 지정할 수 있다.

fun getName(score: Int): String {
    val result: String = when (score) {
        in 0..10 -> "zero"
        in 10..90 -> "one"
        in 90..100 -> "two or three"
        else -> "unknown"
    }
    return result
}

또한 값을 반환하지 않는 명령문으로서 사용할 수 도 있다.
삼항연산자를 확장해 N항연사자 처럼 사용하고, 반복되는 if, else 문을 사용하지 않아도 된다.

// 값 반환 X, 
fun checkName(score: Any?) {
    when (score) {
        null -> println("null")
        0 -> println("zero")
        1 -> println("one")
        2, 3 -> println("two or three")
        "zero" -> println("zero string")
        in listOf("0", "1", "2") -> println("string number 1, 2, 3")
        in 100..200 -> println("100 to 2000")
        // else -> println("unknown") // else 생략해도 컴파일 가능
    }
}

null 조건검사까지 null point 예외처리 없이 수행가능하다.

for

.. 을 사용하는 Range 클래스와 자주 같이 사용된다.

val oneToFive: IntRange = 1..5
val aToE: CharRange = 'a'..'e'
val list = listOf("a", "b", "c", "d")
for ((index, name) in list.withIndex()) {
    println("${index}:${name}")
    // 0:a
    // 1:b
    // 2:c
    // 3:d
}

val array = arrayOf(1, 2, 3)
for (i in array) {
    print(i) // 123
}
for (i in 1..10 step 2) {
    println(i) // 13579
}
for (i in 10 downTo 1) {
    println(i) // 10987654321
}
for (i in 1 until 10) { // 10 은 포함하지 않음
    println(i) // 123456789
}

val, var 변수선언

val변경할 수 없는 속성(immutable)
할당된 이후 값을 변경하게 되면 컴파일 에러가 발생, val 변수를 생성시 값을 할당하거나 타입을 명시 해줘야 한다.

java 에서는 final과 같다

val num1 = 42
val num2: Int = 45
val num3: Int
// val num4 에러 발생, 타입 지정 필수
// num1 = 1; 재정의 에러 발생

var변경할 수 있는 속성(mutable)
초기화 후 값을 변경이 가능하다(일반적인 변수선언 방식)

모든상황에서 웬만하면 val 사용을 권장한다.

nullable(?) 변수

코틀린은 기본적으로 필드에 null 사용이 불가능한데 nullable(?) 키워드를 사용하면 필드에 null 값 지정이 가능하다.

javaOptional 과 유사하다.
물론 Optional 보다 세련되게 사용 가능하다.

class Person(
    val first: String,
    val middle: String?,
    val last: String
)

String?Stringnull 허용 여부만 결정하는 것 처럼 보이지만
컴파일시 아예 다른 기능의 변수처럼 동작한다.

// 컴파일 된 후 java 코드 변환
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
...

public final class Person {
    @NotNull
    private final String first;
    @Nullable
    private final String middle;
    @NotNull
    private final String last;

    public Person(@NotNull String first, @Nullable String middle, @NotNull String last) {
        ...
    }
    ...
}

안전호출(?.) 연산자

nullable 키워드를 사용하면 아래와 같이 내부 필드나 함수를 호출할 때 안전호출연산자(?.) 를 체인형식으로 사용해야한다.

javaOptional 체인 메서드들과 유사하다.

class Person(
    val first: String,
    val middle: String?,
    val last: String
) {
    fun printMiddleNameLength() {
        val len: Int? = middle?.length
        println(len) // 안전호출 연산자
    }
}

fun main() {
    val p1 = Person("ko", null, "e")
    p1.printMiddleNameLength(); // null
}

컴파일 단계에서 nullpoint 발생 가능한 경우를 원천 차단한다.

스마트 캐스트

스마트 캐스트컴파일 단계에서 nullpoint 이슈가 해결되었다면 더이상 컴파일 에러를 발생시키지 않는다.
예를 들어 아래와 같이 if 문을 통해 해당 변수가 null 이 아님을 확인한다면 if 블럭 내부에선 ? 가 없이 사용 가능하다.

class Person(
    val first: String,
    val middle: String?,
    val last: String
) {
    fun printMiddleNameLength() {
        if (middle != null) {
            val temp: String = middle // smart cast
            println(middle.length) // null 출력
        }
    }
}

fun main() {
    val p1 = Person("ko", null, "e")
    p1.printMiddleNameLength();
}

p1.middlenull 이 아님을 확신할수 있기에 String? 이 아닌 String 타입으로 인식한다.

하지만 val 이 아닌 var 로 선언할경우 이런 스마트 캐스트 가 동작하지 않는다.
언제든 null 로 다시 재정의될 수 있다고 생각하기에 스마트 캐스트 가 동작하지 않는다.

클래스 타입을 확인하는 is 연산자도 스마트 캐스트 할 수 있다.

val other: Any = "is String"
// println(other.reversed()) compile error
if (other is String)
    println(other.reversed())

스마트 캐스트when 연산자 사용시에도 적용된다.

단언 연산자(!!)

아래와 같이 var 로 정의된이상 스마트 캐스트 가 동작하지 않으며 String? 타입에서 벗어날 수 없다.

class Person(
    var first: String,
    var middle: String?,
    var last: String
)

하지만 단언연산자(!!) 를 사용하면 강제로 String 타입으로 변경가능하다.
대신 NullPointException 이 발생할 수 있다.

class Person(
    var first: String,
    var middle: String?,
    var last: String
) {
    fun printMiddleNameLength() {
        println(middle!!.length) // KotlinNullPointerException 예외 발생
    }
}

fun main() {
    val p1 = Person("ko", null, "e")
    p1.printMiddleNameLength();
}

단언 연산자 사용을 권장하지 않는다.
처음부터 val 로 변수로 정의하거나 var 로 정의하고 안전호출 연산자(?.) 를 통해 값을 가져오는것을 권장한다.

엘비스 연산자(?:)

엘비스 연산자(?:) 역시 nullable 변수를 다루기 위한 연산자.

null 체크하는 삼항연산자와 같은 기능

class Person(
    var first: String,
    var middle: String?,
    var last: String
) {
    fun printMiddleNameLength() {
        println(middle?.length ?: 0) // null 이라면 0 반환
    }
}

fun main() {
    val p1 = Person("ko", null, "e")
    p1.printMiddleNameLength(); // 0
}

middle?.lengthInt? 를 반환하고 null 이 아니라면 정상적으로 length 를 반환

null 로 반환되었다면 엘비스 연산자(?:)를 통해 최종적으로 0을 반환한다.

?.let

not null 인 경우동작하는 코드블럭를 정의한다 보면 된다.

val email : String? ="test@test.com"
email?.let {
    println(it) // test@test.com
}

?.let 키워드로 null 이 아닐경우만 let 블록을 실행할 수 있도록 설정
내부에선 it 키워드를 사용함

타입변환

java 와 다르게 Int -> Long 자동승격하지 않는다
toLong, toInt 함수를 사용해야 한다.

fun main() {
    val i: Int = 3
    // val l: Long = i // error 발생
    val l: Long = i.toLong();
}

아래와 같은 타입변환 함수 지원

  • toByte
  • toChar
  • toShort
  • toInt
  • toLong
  • toFloat
  • toDouble

비트연산

Byte, Short, Int, Long 에 한하여 진법출력 함수 toString(radix: Int) 가 있음

fun main() {
    val i: Int = 10
    println(i.toString(2)) // 1010 
}

and, or, xor, inv(not) 비트 연산자를 제공한다.

fun main() {
    var b1: Int = 0b0001_1111
    var b2: Int = 0b0001_0000
    println(b1 and b2)  // 16 
    println(b1 or b2)   // 31
    println(b1 xor b2)  // 15 
    println(b2.inv())   // -17
}

시프트연산으로 아래 3가지 제공

  • shl: signed left shift
  • shr: signed right shift
  • ushr: unsigned left shift

문자열 연산

$, ${} 문자열 템플릿을 사용해 문자열 사이에 변수나 표현식을 삽입할 수 있다.
만약 $ 를 이스케이프 문자로 사용하고 싶으면 백슬레시나 변수명으로 사용할 수 없는 특수문자를 이어서 사용하면 된다.

val price = 12.25
val taxRate = 0.08
// The amount 12.25 after tax comes to $13.23
val output = "The amount $price after tax comes to $${price * (1 + taxRate)}"
// The amount is in US$, that's right in $only
val disclaimer = "The amount is in US$, that's right in \$only"

쌍따옴표를 연속으로 3개 사용하면 멀티라인 문자열 생성이 가능하다.
만약 block 구문안에서 멀티라인 문자열을 정의할 경우 auto indent 로 로 인해 앞에 margin 이 발생하는데,
파이프 특수문자와 trimMargin 을 통해 제거 가능하다.

if (true) {
    val memo = """Test1, a quick reminder about the 
        party we have scheduled next Tuesday at 
        the 'Low Ceremony Cafe' at Noon. | Please plan to..."""
            .trimIndent()
    println(memo)
}
if (true) {
    val memo = """Test2, a quick reminder about the 
        |party we have scheduled next Tuesday at 
        |the 'Low Ceremony Cafe' at Noon. | Please plan to..."""
            .trimMargin()
    println(memo)
}

// Test1, a quick reminder about the 
//             party we have scheduled next Tuesday at 
//             the 'Low Ceremony Cafe' at Noon. | Please plan to...
// Test2, a quick reminder about the 
// party we have scheduled next Tuesday at 
// the 'Low Ceremony Cafe' at Noon. | Please plan to...

typealias

kotlin 1.3 버전에 추가

복잡한 타입을 별칭을 통해 간단하게 표기할 수 있다.

typealias StringDelegate = ReadOnlyProperty<Any?, String>

class CustomDelegate : StringDelegate {
    override fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "Delegate Value for ${property.name}"
    }
}

기타기능

버전출력

버전 출력하기

fun main(args: Array<String>) {
    println("The current Kotlin version is ${KotlinVersion.CURRENT}")
    // The current Kotlin version is 1.3.50
}

repeat

kotlin 내장 repeat 함수

fun main(args: Array<String>) {
    var repeatFunction: (Int) -> (Unit) = {
        println("Countung:$it")
    }
    repeat(5, repeatFunction);
    /*
    Countung:0
    Countung:1
    Countung:2
    Countung:3
    Countung:4
    */
}

TODO

원래는 주석을 통해 TODO 를 작성했지만 kotlin 에선 TODO 함수를 지원하기에 오륲를 강제 발생시킬 수 있다.

fun myCleverFunction() {
    // TODO: 멋진 구현을 찾는 중
}

fun completeThis() {
    TODO() // throw NotImplementedError
}

Random

fun main() {
    println(Random.nextInt()) //in range
    println(Random.nextInt(100)) //0 ~ 99
    println(Random.nextInt(1, 10)) // 1 ~ 9
}

이외에도 여러가지 타입의 Random 함수 제공

println(Random.nextInt())
println(Random.nextLong())
println(Random.nextBoolean())
println(Random.nextDouble())
println(Random.nextFloat())

시드값 전달이 가능하며 반복가능한 난수생성기를 만들 수 있음

val seed: Int = 10
println(Random(seed).nextInt()) // // -129340023
println(Random(10).nextInt()) // -129340023

java 호환

java 호환을 위한 @JvmOverloads 어노테이션에 대해 알아보면

class Person(
    val first: String,
    val middle: String?,
    val last: String
) {
    @JvmOverloads
    fun getDesc(first: String, age: Int = 0, desc: String? = null) =
        "first name $first, desc ${desc ?: "None"}, and age " + NumberFormat.getCurrencyInstance().format(age)
}
fun main() {
    val p1 = Person("hello", null, "world")
    p1.getDesc("test");
    p1.getDesc("test", 10);
    p1.getDesc("test", 10, "world");
}   

@JvmOverloads 어노테이션을 추가하면 컴파일 과정에서 java 에서도 kotlin 처럼 사용할 수 있도록 함수가 자동 셍성됨

// java code
public static void main(String[] args) {
    Person p = new Person("hello", null, "world");
    System.out.println(p.getDesc("hello"));
    System.out.println(p.getDesc("hello", 5));
    System.out.println(p.getDesc("hello", 5, "world"));
}

예외처리

kotlin 의 예외는 모두 unchecked 예외이다.
try, catch, finally 블록이 있긴하지만 강제하지 않는다.

fun throwIoError() {
    throw IOException("File or resource not found")
}

java 의 경우 IOException 의 경우 try, catch 를 요구하지만
kotlin 에선 별도의 처리없이 컴파일 가능하다.

하지만 java 코드에서 kotlin 의 함수를 불러올 때 try, catch 를 사용해 처리하고싶어도 에러가 발생한다.

// java code
public static void main(String[] args) {
    try {
        useThrowsClause();
    } catch (IOException e) { // 예외 'java.io.IOException'은(는) 해당 try 블록에서 한 번도 던져지지 않습니다
        System.out.println("error invoked");
        e.printStackTrace();
    }
}

java 코드에 예외가 발생함을 알리는 어노테이션을 추가하여 호환시킬 수 있다.

@Throws(IOException::class)
fun throwIoError() {
    throw IOException("File or resource not found")
}

카테고리:

업데이트: