문법 포인트
널 안전성: 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")
}
}
}