Kotlin 주요 문법 이해

문법 포인트

널 안전성: Nullable 형식 수정자

'?'는 변수가 null일 수 있음을 나타냅니다. Kotlin의 '?'와 Elvis 연산자('?:')는 Java보다 큰 발전으로, 많은 NPE 문제를 방지할 수 있습니다.

박싱 타입?

Kotlin은 별도의 박싱 타입이 필요 없습니다. 예를 들어:

val a: Int = 100 // 자동으로 int 사용
val b: Int? = 200 // 자동으로 nullable Int 사용

문자열 보간

String.format나 StringBuilder 대신 ${}를 사용하여 변수나 표현식을 문자열에 직접 삽입할 수 있습니다:

fun getPrjWorkDir(project: Project): String {
    return "${project.basePath}/.backend_checker"
}

반복문에서 문자열 결합은 여전히 StringBuilder를 권장합니다.

필수 override

Java에서는 @Override 어노테이션이 선택 사항이지만, Kotlin에서는 'override' 키워드가 필수입니다.

객체 표현식 == 익명 클래스 인스턴스

다음과 같은 형태는 Java의 익명 클래스 인스턴스와 동일합니다:

object : AbstractListener() {
    override fun func1() { ... }
    override fun func2() { ... }
}

함수형 인터페이스(SAM)의 경우에는 람다 식을 사용한 간단한 구문을 사용할 수 있습니다:

ignoreAllItem.addActionListener { 
    val model = table.model 
    ...
}

쌍느낌표(!!)

참조가 null이 아님을 강제로 보장하며, null일 경우 NPE를 발생시킵니다. 일반적으로 명확히 null이 될 가능성이 없는 상황에서만 사용됩니다.

val + get()

getter와 동일하지만 속성으로 존재하며, 접근 시마다 재계산됩니다. 성능 비용을 고려해야 하므로 특정 범위 내에서 값이 변하지 않는다면 지역 변수를 사용하는 것이 좋습니다.

private fun buildNativeCmd(project: Project): String {
    val exePath = exePath
    LOG.info("exe path:$exePath")

    if (!File(exePath).exists()) {
        LOG.error("exe:$exePath NOT found!")
        throw RuntimeException("No $EXE_NAME found!")
    }

    val fullCmd = "$exePath -d ${project.basePath}"
    LOG.warn("buildNativeCmd cmd:$fullCmd")
    return fullCmd
}

private val exePath: String
    get() = "$pluginRoot${File.separator}bin${File.separator}$EXE_NAME"

이런 패턴은 사용자가 속성 호출이 비용이 들지 않는다고 착각하게 할 수 있으므로 권장되지 않습니다.

static 변수

Kotlin에는 static이 없으며, 동등한 기능은 클래스의 companion object에 정의됩니다:

companion object {
    private val LOG = Logger.getInstance(AnalyzeTask::class.java)
    const val EXE_NAME: String = "XXX"
}

for 루프

Kotlin 예제:

for(i in 1..10)

루프 변수는 미리 선언할 필요가 없습니다.

전역 함수 및 전역 변수

Kotlin은 순수 객체 지향 언어가 아니며 FP+OO 다중 패러다임 언어입니다. 따라서 전역 함수나 변수를 지원하는 것은 자연스럽습니다. 또한 Kotlin에서는 Java와 달리 "단일 클래스 단일 파일" 규칙이 강제되지 않습니다.

속성 할당 == setter

Kotlin의 속성 할당은 setter 함수 호출과 동일합니다. val+get와 유사하게 var+set을 통해 변경 가능한 속성을 구현할 수 있습니다.

리스트와 가변 리스트

Kotlin은 "읽기 전용"과 "가변" 컬렉션을 타입 수준에서 엄격히 구분합니다. List는 읽기 전용이며 add/remove를 지원하지 않고, MutableList는 가변입니다.

Java와의 상호 운용성

Kotlin은 Java와 100% 호환되므로, 기존 코드를 점진적으로 Kotlin으로 전환할 수 있습니다. 작업이 완료되면互通 관련 어노테이션(@JvmStatic 등)을 제거하면 됩니다.

상속

Kotlin의 상속은 모두 콜론(:)을 사용하며, extends와 implements 구문을 구별하지 않습니다.

생성자

주 생성자는 클래스 선언과 init 블록으로 구성됩니다:

class LintResultPanel(private val project: Project, private val errors: MutableList<LintError>) :
    SimpleToolWindowPanel(false, true), DataProvider {
    private val table = JBTable()

    init {
        setContent(ScrollPaneFactory.createScrollPane(table))
        table.model = ErrorsTableModel()
        table.setShowGrid(false)
    }
}

보조 생성자는 constructor로 지정되며 반드시 주 생성자를 호출해야 합니다.

takeIf/taskUnless/let

takeIf와 taskUnless는 필터링과 유사하고, let은 맵핑과 유사합니다.

apply와 let의 차이점

둘 다 객체에 대한 매핑 처리를 수행하지만, apply는 객체 자체를 반환하고 let은 람다 마지막 줄 값을 반환합니다.

apply 예제:

private val table = JBTable().apply {
    setRowSelectionAllowed(true)
    setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
}

apply와 also의 차이점

둘 다 객체 자체를 반환하지만, also는 파라미터를 가지며 apply는 그렇지 않습니다.

also 예제:

private val table = JBTable().also {
    it.setRowSelectionAllowed(true)
    it.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
}

apply는 초기화 체인에 적합하고, also는 부작용이 필요한 경우에 사용됩니다.

객체 체이닝

if-else 대체 예제:

var lineNo = err.lineNo?.let { it - 1 } ?: 0

이는 다음 코드와 동일합니다:

var lineNo = 0
if (err.lineNo != null) {
    lineNo = err.lineNo - 1
}

return은 표현식

Kotlin에서는 return이 문이 아닌 표현식입니다. 이를 통해 elvis 표현식 내에서 사용될 수 있습니다.

override fun actionPerformed(e: AnActionEvent) {
    val project = e.project ?: return
    AnalyzeTask(project).queue()
}

비국소적 반환(NLR)

Kotlin의 return은 기본적으로 가장 가까운 외부 함수를 종료합니다. lambda 내에서만 반환하려면 '@라벨'을 사용해야 합니다.

ignoreItem.addActionListener {
    val selected = table.selectedRows
    if (selected.isEmpty()) {
        Messages.showInfoMessage(
            project,
            "No rows selected",
            "Hint"
        )
        return@addActionListener
    }
}

Unit과 Nothing

Unit은 void 대신 사용하며, Nothing은 모든 타입의 하위 타입입니다.

with 블록

특정 객체의 메서드나 속성을 간략히 호출할 수 있는 블록입니다.

private fun hideColumn(table: JTable, colIndex: Int) {
    with(table.columnModel.getColumn(colIndex)) {
        minWidth = 0
        maxWidth = 0
        width = 0
        preferredWidth = 0
    }
}

when

switch-case 또는 여러 if-else를 대체합니다.

return when (column) {
    0 -> e.filePath
    1 -> e.lineNo
    else -> e.message
}

예외 선언

Kotlin에서는 예외 선언이 필요 없습니다. 검사된 예외는 번거롭기 때문에 Kotlin에서는 이 기능이 폐기되었습니다.

자원 자동 해제

try-with-resource와 유사한 use 함수를 제공합니다.

StringWriter().use { sw ->
    PrintWriter(sw).use {
        e.printStackTrace(it)
        log.info(sw.toString())
    }
}

확장 함수

시스템 클래스에 멤버 함수를 추가할 수 있습니다.

infix fun Int.mod(that: Int) = this % that

바이트코드는 정적 메서드로 변환됩니다.

숫자 타입

Number은 모든 숫자 타입의 기반 클래스입니다.

코루틴

continuation을 통해 코루틴을 구현합니다.

suspend fun co1() {
    println("A: ${Thread.currentThread().name}")
    delay(100)
    println("A done")
}

delay의 내부 구현은 다음과 같습니다:

public suspend fun delay(timeMillis: Long) {
    if (timeMillis <= 0) return
    return suspendCancellableCoroutine { cont ->
        if (timeMillis < Long.MAX_VALUE) {
            cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
        }
    }
}

DSL 특징

infix 함수는 자연어처럼 사용할 수 있습니다.

infix fun Int.mod(that: Int) = this % that

DSL 스타일의 빌더 함수 예제:

fun tasks(block: TaskMgr.() -> Unit) {
    TaskMgr().apply(block)
}

tasks {
    task { "go_to_work" to listOf("drink_coffee", "dress") }
    task { "drink_coffee" to listOf("make_coffee", "wash") }
    task { "dress" to listOf("wash") }
    call("go_to_work")
}

KTS 스크립트에서의 사용자 정의 인터페이스

ScriptEngine을 사용하여 동적 eval 또는 객체 삽입을 통해 사용자 정의 인터페이스를 지원할 수 있습니다.

object ScriptRunner {
    private fun dynInject(engine: ScriptEngine) {
        engine.eval("""
            import kotlin.math.pow
            infix fun Int.mod(that: Int) = this % that
            infix fun Long.mod(that: Long) = this % that
            infix fun Number.power(n: Int) = this.toDouble().pow(n)
        """.trimIndent())
    }

    fun runScript(scriptFile: File) {
        check(scriptFile.exists()) { "script NOT exists: $scriptFile" }
        val engine = ScriptEngineManager().getEngineByExtension("kts")!!
        dynInject(engine)
        val errCollector = ErrCollector()
        engine.put("errCollector", errCollector)
        scriptFile.reader().use {
            val res = engine.eval(it)
            println("kts run result:$res")
        }
    }
}

태그: Kotlin Coroutines DSL

7월 28일 10:19에 게시됨