기본 구문
핵심 개념
- 패키지와 가져오기: 코드를 네임스페이스로 organizes.
main함수: Kotlin 애플리케이션의 진입점입니다. 문자열 배열을 인자로 받을 수 있습니다.- 표준 출력으로 인쇄:
print()와println()은 정보를 표시하는 데 사용됩니다.println()은 출력 후 줄바꿈 문자를 추가합니다. - 표준 입력에서 읽기:
readln()은 콘솔에서 한 줄의 텍스트를 읽습니다. - 함수:
fun키워드로 정의합니다. 표현식 본문을 사용할 수 있으며 반환 타입을 추론할 수 있습니다. 의미 있는 값을 반환하지 않는 함수에는Unit반환 타입을 사용합니다. - 변수:
val: 변경할 수 없는(읽기 전용) 변수입니다.var: 변경할 수 있는 변수입니다.- 타입 추론: Kotlin은 일반적으로 타입을 자동으로 결정할 수 있습니다.
- 클래스와 인스턴스:
class키워드로 정의합니다. 속성은 선언부 또는 본문에서 정의할 수 있습니다. 상속은 콜론:으로 표시합니다. 클래스는 기본적으로final이며,open을 사용하면 상속할 수 있습니다. - 주석: 한 줄 주석(
//)과 여러 줄 주석(/* ... */)이 있습니다. 여러 줄 주석은 중첩할 수 있습니다. - 문자열 템플릿:
$variable또는${expression}을 사용해 변수와 표현식을 문자열에 직접 삽입합니다. - 조건식:
if문이며,if를 표현식으로 사용할 수도 있습니다. - 반복문:
for반복문(컬렉션 또는 범위 순회)과while반복문입니다. when표현식: 더 강력한switch문입니다.- 범위:
in연산자로 값이 범위 안에 있는지 확인합니다.for로 범위를 순회합니다. - 컬렉션:
for로 순회하고,in으로 멤버 여부를 확인합니다. 필터링, 매핑 및 기타 작업에는 람다 표현식을 사용합니다. - 널 가능한 값과 null 검사:
?로 타입을 널 가능하게 표시합니다. 널 가능한 값을 사용하기 전에null인지 확인합니다. - 타입 검사와 자동 변환:
is연산자로 변수의 타입을 검사합니다. Kotlin은if블록에서 변수를 검사한 타입으로 자동 변환합니다.
일반적인 구문
DTO(POJO/POCO) 생성
data class Customer(val name: String, val email: String)
data class는getter(var의setter포함),equals(),hashCode(),toString(),copy()및componentN()함수(해석용)를 자동으로 생성합니다. 데이터 클래스 생성이 크게 단순해집니다.
함수 파라미터의 기본값
fun foo(a: Int = 0, b: String = "") { ... }
- 함수 정의에서 파라미터에 기본값을 지정할 수 있습니다. 함수를 호출할 때 기본값이 있는 파라미터는 생략할 수 있습니다.
목록 필터링
val positives = list.filter { x -> x > 0 }
val positives = list.filter { it > 0 } // 더 간결함
filter함수와 람다 표현식을 사용해 특정 조건을 만족하는 요소만 포함하는 새 목록을 만듭니다.it은 람다 표현식에 매개변수가 하나일 때 이를 가리키는 암묵적 이름입니다.
컬렉션에 특정 요소가 있는지 확인
if ("john@example.com" in emailsList) { ... }
if ("jane@example.com" !in emailsList) { ... }
in과!in연산자를 사용하면 컬렉션에 특정 요소가 있는지 쉽게 확인할 수 있습니다.
문자열 보간
println("Name $name")
$기호를 사용해 문자열 연결 없이 변수를 문자열에 직접 삽입합니다.
표준 입력을 안전하게 읽기
toIntOrNull()은 문자열을 정수로 변환을 시도하고, 변환에 실패하면null을 반환하므로NumberFormatException을 피할 수 있습니다.
인스턴스 검사
when (x) {
is Foo -> ...
is Bar -> ...
else -> ...
}
is연산자로 객체의 타입을 검사합니다.when표현식에서는 검사에 성공하면 객체가 해당 타입으로 자동 변환됩니다.
읽기 전용 목록
val list = listOf("a", "b", "c")
listOf()는 변경할 수 없는 목록을 만듭니다.
읽기 전용 매핑
val map = mapOf("a" to 1, "b" to 2, "c" to 3)
mapOf()는 변경할 수 없는 매핑을 만듭니다.
매핑 항목 접근
println(map["key"])
map["key"] = value // 对于可变映射
- 대괄호
[]를 사용해 매핑의 값에 접근합니다.
매핑 또는 쌍 목록 순회
for ((k, v) in map) {
println("$k -> $v")
}
- 구조 분해 선언을 사용해 매핑 또는
Pair목록을 순회합니다.k와v는 편의에 맞는 어떤 이름이어도 됩니다.
범위 순회
..로 닫힌 구간을,..<로 열린 구간을 만듭니다.step은 간격을 지정하고,downTo는 역순 범위를 만듭니다.
지연 속성
val p: String by lazy { // 仅在第一次访问时计算该值
// 计算字符串
}
lazy위임은 속성이 처음 접근될 때만 초기화되도록 합니다. 특히 계산 비용이 높은 속성에서 성능을 높일 수 있습니다.
확장 함수
fun String.spaceToCamelCase() { ... }
"Convert this to camelcase".spaceToCamelCase()
- 기존 클래스를 상속하거나 수정하지 않고도 새 함수를 추가할 수 있습니다.
싱글턴 생성
object Resource {
val name = "Name"
}
object선언은 싱글턴 객체를 만듭니다. Kotlin이 싱글턴의 생성과 관리를 자동으로 처리합니다.
타입 안전한 값을 위한 인라인 값 클래스
@JvmInline
value class EmployeeId(private val id: String)
@JvmInline
value class CustomerId(private val id: String)
value class는 JVM에서@JvmInline이 필요하며, 기존 타입을 감싸는 클래스를 만들어 컴파일 타임의 타입 안전성을 제공합니다.CustomerId가 필요한 함수에EmployeeId를 잘못 전달하는 일을 방지합니다.
추상 클래스 인스턴스화
- 익명 객체로 추상 클래스를 인스턴스화하려면 모든 추상 메서드를 구현해야 합니다.
null이 아닐 때의 간단한 표현
val files = File("Test").listFiles()
println(files?.size) // 如果 files 不为空,则打印 size
- 안전 호출 연산자
?.를 사용하면files가null일 때 식의 결과가null이 되어NullPointerException을 발생시키지 않습니다.
null이 아닐 때 실행하고, 그렇지 않으면 다른 작업 수행하기
- Elvis 연산자
?:를 사용하면 왼쪽 식이null일 때 반환할 기본값을 제공할 수 있습니다.run을 사용하면 기본값을 계산할 코드 블록을 실행할 수 있습니다.
값이 비어 있으면 문 실행하기
val values = ...
val email = values["email"] ?: throw IllegalStateException("Email is missing!")
values["email"]이null이면 Elvis 연산자?:를 사용해 예외를 발생시킵니다.
널일 수도 있는 컬렉션의 첫 항목 가져오기
val emails = ... // 可能为空
val mainEmail = emails.firstOrNull() ?: ""
firstOrNull()은 컬렉션의 첫 요소를 반환하고, 컬렉션이 비어 있으면null을 반환합니다.firstOrNull()이null을 반환하면 Elvis 연산자가 기본값을 제공합니다.
null이 아닐 때 실행하기
val value = ...
value?.let {
... // 如果 value 不为空,则执行此代码块
}
let함수는 값이null이 아닐 때만 실행됩니다.it은let블록에서 해당 값을 가리키는 암묵적 이름입니다.
null이 아닐 때 널 가능한 값 매핑하기
val value = ...
val mapped = value?.let { transformValue(it) } ?: defaultValue
// 如果 value 或 transform 结果为空,则返回 defaultValue。
- 안전 호출 연산자
?.,let함수 및 Elvis 연산자?:를 함께 사용해 널 가능한 값을 간결하게 변환하고 값이나 변환 결과가 null일 때 기본값을 제공합니다.
when 문에서 값 반환하기
when표현식은 값을 반환할 수 있어 코드를 더 간결하게 만들 수 있습니다.
try-catch 표현식
fun test() {
val result = try {
count()
} catch (e: ArithmeticException) {
throw IllegalStateException(e)
}
// 使用 result
}
try-catch블록은 값을 반환할 수 있으므로 예외 처리 로직을 표현식에 포함할 수 있습니다.
if 표현식
val y = if (x == 1) {
"one"
} else if (x == 2) {
"two"
} else {
"other"
}
if문은 값을 반환할 수 있으므로 조건에 따라 서로 다른 값을 선택할 수 있습니다.
Builder 스타일로 Unit을 반환하는 메서드 사용하기
fun arrayOfMinusOnes(size: Int): IntArray {
return IntArray(size).apply { fill(-1) }
}
apply함수를 사용하면 객체에서 여러 메서드를 호출하고 그 객체 자체를 반환할 수 있습니다. 따라서 Builder 스타일로 객체를 구성할 수 있습니다.
단일 표현식 함수
- 함수에 표현식이 하나만 있으면 중괄호와
return키워드를 생략할 수 있습니다.
객체 인스턴스에서 여러 메서드 호출하기 (with)
with함수를 사용하면 객체 이름을 반복하지 않고 하나의 객체에서 여러 메서드를 호출할 수 있습니다.
객체 속성 구성하기 (apply)
val myRectangle = Rectangle().apply {
length = 4
breadth = 5
color = 0xFAFAFA
}
apply함수를 사용하면 객체의 속성을 구성하고 그 객체 자체를 반환할 수 있습니다. 생성자에 없는 속성을 구성할 때 유용합니다.
Java 7의 try-with-resources
val stream = Files.newInputStream(Paths.get("/some/file.txt"))
stream.buffered().reader().use { reader ->
println(reader.readText())
}
use함수는 예외가 발생하더라도 스트림과 같은 리소스를 사용 후 올바르게 닫도록 보장합니다.
제네릭 타입 정보가 필요한 일반 함수
inline fun <reified T: Any> Gson.fromJson(json: JsonElement): T = this.fromJson(json, T::class.java)
reified키워드를 사용하면 런타임에 제네릭 타입 정보에 접근할 수 있습니다. 제네릭 타입을 알아야 하는 함수에서 유용합니다.
두 변수 교환하기
var a = 1
var b = 2
a = b.also { b = a }
also함수를 사용해 대입 전에 작업을 수행할 수 있습니다. 이 경우also함수는a의 값을b에 할당한 다음b의 값을a에 할당합니다.
데이터 타입
타입 목록
- 숫자:
Byte(8비트)Short(16비트)Int(32비트)Long(64비트)Float(32비트)Double(64비트)- 부호 없는 타입:
UByte,UShort,UInt,ULong
- 불리언:
Boolean(true/false) - 문자:
Char(유니코드) - 문자열:
String(변경 불가) - 배열:
Array<T>(변경 가능), 특정 타입의 배열:IntArray,ByteArray등
특수 타입
Any: 모든 비어 있지 않은 타입의 상위 타입(Java의Object와 유사)Nothing: "절대로 반환하지 않는" 타입(예: 예외를 발생시키는 함수)Unit: "의미 있는 반환값이 없는" 타입(Java의void와 유사)
타입 검사 및 변환
- 타입 검사:
is - 안전한 변환:
as?(실패하면null반환) - 안전하지 않은 변환:
as(예외가 발생할 수 있음)
핵심 사항
- Kotlin에는 원시 타입이 없습니다.
- 타입 추론이 강력하므로 일반적으로 타입을 명시적으로 선언할 필요가 없습니다.
예제
val age: Int = 30
val name = "Alice" // 类型推断为 String
val pi = 3.14 // 类型推断为 Double
제어문
if 표현식
if (조건) 결과 else 결과(결과가Unit이 아닌 한 반드시else가 있어야 함)- 삼항 연산자와 유사하게 값을 반환할 수 있습니다.
val max = if (a > b) a else b // 返回 a 或 b 中较大的值
when 문/표현식
switch를 대체합니다.when (변수) { 값 -> 결과 ... else -> 결과 }- 표현식으로 사용할 때는 모든 경우를 처리하거나
else가 있어야 합니다. - 타입(
is)과 범위(in)를 검사할 수 있습니다.
for 반복문
for (item in 컬렉션) { ... }- 컬렉션의 요소를 순회합니다.
for (i in 범위) { ... }는 숫자 범위를 순회합니다.
val numbers = listOf(1, 2, 3)
for (number in numbers) {
println(number)
}
for (i in 1..5) { // 1 到 5 (包含 5)
println(i)
}
while 반복문
while (조건) { ... }는 조건을 먼저 검사한 후 실행합니다.do { ... } while (조건)는 먼저 실행한 후 조건을 검사하므로 최소 한 번은 실행됩니다.
반복 제어
break: 반복문을 빠져나갑니다.continue: 현재 반복의 나머지 부분을 건너뜁니다.
for (i in 1..10) {
if (i == 3) continue // 跳过 i=3 的迭代
if (i == 7) break // 循环在 i=7 时终止
println(i)
}
점프 표현식
return
- 기본적으로 가장 가까운 enclosing 함수 또는 익명 함수에서 반환합니다.
break
- 가장 가까운 enclosing 반복문을 종료합니다.
continue
- 가장 가까운 enclosing 반복문의 다음 반복으로 이동합니다.
이 표현식의 타입은 Nothing입니다.
레이블
Kotlin에서는 identifier@ 형식의 레이블로 표현식을 표시할 수 있습니다. 레이블을 사용하면 break와 continue의 동작을 더 정확하게 제어할 수 있습니다.
loop@ for (i in 1..100) {
for (j in 1..100) {
if (...) break@loop // 退出标记为 loop 的循环
}
}
break@label:label레이블이 지정한 반복문 다음 실행 지점으로 이동합니다.continue@label:label레이블이 지정한 반복문의 다음 반복으로 이동합니다.
레이블로 반환하기
중첩 함수(함수 리터럴, 지역 함수, 객체 표현식)에서는 한정된 return 문을 사용해 외부 함수에서 반환할 수 있습니다. 주로 Lambda 표현식에서 반환할 때 사용합니다.
Lambda 표현식에서 반환하기
명시적 레이블
CodeBlock Loading...이 예제에서
return@lit은lit@레이블이 지정된 Lambda 표현식에서 반환합니다.암묵적 레이블: 더 간결하며, 레이블 이름은 Lambda 표현식이 전달된 함수 이름과 같습니다.
CodeBlock Loading...이 예제에서
return@forEach는forEach에 전달된 Lambda 표현식에서 반환합니다.익명 함수: Lambda 표현식 대신 익명 함수를 사용할 수 있습니다.
return문은 익명 함수 자체에서 반환합니다.CodeBlock Loading...익명 함수를 사용한 효과는
continue와 유사합니다.
break 동작 모방하기
중첩된 Lambda 표현식과 비지역 반환을 사용해 break의 동작을 모방할 수 있습니다.
주의 사항
값을 반환할 때 파서는 레이블이 지정된 return 문을 우선합니다. 예를 들어 return@a 1은 "레이블 @a에서 1을 반환"으로 해석되며, "레이블이 @a인 표현식 (1)을 반환"으로 해석되지 않습니다.
클래스와 상속
기본 상위 클래스
- Kotlin의 모든 클래스는 공통 상위 클래스인
Any를 가집니다. - 클래스가 상위 타입을 선언하지 않으면
Any가 기본 상위 클래스입니다.
class Example // 隐式继承自 Any
Any에는equals(),hashCode(),toString()의 세 메서드가 있습니다. 이 메서드는 모든 Kotlin 클래스에 정의되어 있습니다.
열린 클래스
- 기본적으로 Kotlin 클래스는
final이므로 상속할 수 없습니다. - 클래스를 상속할 수 있게 하려면
open키워드로 표시합니다.
open class Base // 类可以被继承
명시적 상위 타입
- 클래스 헤더에서 명시적인 상위 타입을 선언하려면 콜론 뒤에 타입을 작성합니다.
open class Base(p: Int)
class Derived(p: Int) : Base(p)
- 주 생성자: 파생 클래스에 주 생성자가 있으면 해당 파라미터에 따라 주 생성자에서 기본 클래스를 초기화해야 합니다.
- 보조 생성자: 파생 클래스에 주 생성자가 없으면 각 보조 생성자는
super키워드로 기본 타입을 초기화하거나, 그 작업을 수행하는 다른 생성자에 위임해야 합니다. 서로 다른 보조 생성자는 기본 타입의 서로 다른 생성자를 호출할 수 있습니다.
class MyView : View {
constructor(ctx: Context) : super(ctx)
constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}
메서드 재정의
- Kotlin에서는 재정의할 수 있는 멤버와 재정의에 명시적修饰자를 사용해야 합니다.
open class Shape {
open fun draw() { /*...*/ }
fun fill() { /*...*/ }
}
class Circle() : Shape() {
override fun draw() { /*...*/ }
}
Circle.draw()에는override修饰자가 필요합니다. 생략하면 컴파일러에서 오류가 발생합니다.open修饰자가 없는 함수(예:Shape.fill())는override의 유무와 관계없이 하위 클래스에 같은 시그니처의 메서드를 선언할 수 없습니다.final클래스에 추가한 멤버에는open修饰자가 적용되지 않습니다.override로 표시한 멤버 자체는open이므로 하위 클래스에서 다시 재정의할 수 있습니다.- 다시 재정의하는 것을 금지하려면
final을 사용합니다.
open class Rectangle() : Shape() {
final override fun draw() { /*...*/ }
}
속성 재정의
- 속성 재정의 메커니즘은 메서드와 같은 방식으로 동작합니다.
- 상위 클래스에 선언한 뒤 파생 클래스에서 다시 선언한 속성은
override로 시작해야 하며 호환되는 타입이어야 합니다. - 선언된 각 속성은 초기자가 있는 속성이나
get메서드가 있는 속성으로 재정의할 수 있습니다.
open class Shape {
open val vertexCount: Int = 0
}
class Rectangle : Shape() {
override val vertexCount = 4
}
val속성을var속성으로 재정의할 수 있지만 반대는 불가능합니다.val속성은 본질적으로get메서드를 선언하며, 이를var로 재정의하면 파생 클래스에set메서드도 선언되기 때문입니다.- 주 생성자에서
override키워드를 속성 선언의 일부로 사용할 수 있습니다.
파생 클래스 초기화 순서
- 파생 클래스의 새 인스턴스를 생성할 때 기본 클래스 초기화가 첫 단계로 수행됩니다(기본 클래스 생성자의 인수를 계산하기 전에만). 따라서 파생 클래스의 초기화 로직이 실행되기 전에 일어납니다.
- 기본 클래스 생성자가 실행될 때 파생 클래스에 선언하거나 재정의한 속성은 아직 초기화되지 않았습니다. 기본 클래스 초기화 로직에서 이러한 속성을 사용하거나 다른 재정의된 열린 멤버 구현을 통해 간접적으로 사용하면 잘못된 동작이나 런타임 오류가 발생할 수 있습니다.
- 기본 클래스를 설계할 때는 생성자, 속성 초기자 또는
init블록에서open멤버를 사용하지 않는 것이 좋습니다.
상위 클래스 구현 호출하기
- 파생 클래스의 코드는
super키워드를 사용해 상위 클래스의 함수와 속성 접근자 구현을 호출할 수 있습니다.
- 내부 클래스에서 외부 클래스의 상위 클래스에 접근할 때는 외부 클래스 이름으로 한정한
super키워드인super@Outer를 사용합니다.
재정의 규칙
- Kotlin에서 상속 구현에는 다음 규칙이 적용됩니다. 클래스가 직접 상위 클래스에서 동일한 멤버의 여러 구현을 상속받는 경우, 그 멤버를 재정의해 자체 구현을 제공해야 합니다(상속된 구현 중 하나를 사용할 수도 있음).
- 상속 구현을 가져올 상위 타입을 나타내려면
super를 사용하고,尖괄호로 묶은 상위 타입 이름으로 한정합니다. 예를 들어super<Base>입니다.
Rectangle와Polygon을 상속할 수 있지만 둘 다 자체draw()구현이 있으므로Square에서draw()를 재정의하고 별도의 구현을 제공해 모호성을 제거해야 합니다.
제네릭: in, out, where
제네릭 클래스
- 선언:尖괄호
<>를 사용해 타입 매개변수를 선언합니다.
class Box<T>(t: T) {
var value = t
}
- 인스턴스화: 타입 매개변수를 제공하거나 컴파일러가 추론하도록 합니다.
val box: Box<Int> = Box<Int>(1) // 显式指定类型
val box = Box(1) // 编译器推断类型为 Box<Int>
변형
- 불변성: 기본적으로 Kotlin의 제네릭 타입은 불변입니다. 즉
List<String>은List<Object>의 하위 타입이 아닙니다. - 공변성:
out키워드로 공변 타입 매개변수를 선언합니다. 공변 타입 매개변수는out위치에만 나타날 수 있으며(반환값으로만 사용할 수 있고 매개변수로 사용할 수 없음), 반환 타입으로만 사용할 수 있습니다. 클래스C의 타입 매개변수T를out으로 선언하면C<Base>는C<Derived>의 상위 타입이 될 수 있습니다. 즉C는T의 생산자이지 소비자가 아닙니다. - 반변성:
in키워드로 반변 타입 매개변수를 선언합니다. 반변 타입 매개변수는in위치에만 나타날 수 있으며(매개변수로만 사용할 수 있고 반환값으로 사용할 수 없음), 매개변수로만 사용할 수 있습니다.List<? super String>의String을 매개변수로 받는 메서드를 호출할 수 있으며,T를 반환하는 메서드를 호출하면String이 아니라Object가 반환됩니다. - 선언 지점 변형: 타입 매개변수를 선언할 때
in과out키워드를 사용해 변형을 지정합니다. - 사용 지점 변형: 타입을 사용할 때 타입 투영을 통해 변형을 지정합니다.
- PECS 원칙(Producer Extends, Consumer Super):
- 객체가 생산자라면(데이터를 읽기만 하므로)
out을 사용합니다(Java의extends에 해당). - 객체가 소비자라면(데이터를 쓰기만 하므로)
in을 사용합니다(Java의super에 해당).
- 객체가 생산자라면(데이터를 읽기만 하므로)
out
T만 반환
in
T만 받음
타입 투영
- 개념: 타입 안전성을 보장하기 위해 제네릭 타입의 사용을 제한합니다.
out투영: 쓰기를 금지하고 읽기만 허용합니다(Array<? extends Object>과 유사).in투영: 읽기를 금지하고 쓰기만 허용합니다(Array<? super String>와 유사).
별표 투영
- 개념: 타입 매개변수에 대해 전혀 알지 못하지만 안전한 방식으로 사용할 수 있음을 나타냅니다.
Foo<out T : TUpper>는Foo<out TUpper>와 같습니다(TUpper타입의 값을 안전하게 읽을 수 있음).Foo<in T>는Foo<in Nothing>와 같습니다(안전하게 쓸 수 없음).Foo<T : TUpper>는 읽을 때는Foo<out TUpper>, 쓸 때는Foo<in Nothing>와 같습니다.- Java의 원시 타입과 유사하지만 더 안전합니다.
제네릭 함수
- 선언: 타입 매개변수를 함수 이름 앞에 둡니다.
fun <T> singletonList(item: T): List<T> { ... }
fun <T> T.basicToString(): String { ... } // 확장 함수
- 호출: 타입 매개변수를 명시하거나 컴파일러가 추론하도록 합니다.
val l = singletonList<Int>(1) // 显式指定类型
val l = singletonList(1) // 编译器推断类型
제네릭 제약
- 개념: 타입 매개변수를 대체할 수 있는 타입의 범위를 제한합니다.
- 상한: 콜론
:을 사용해 Java의extends와 유사하게 상한을 지정합니다.
fun <T : Comparable<T>> sort(list: List<T>) { ... } // T 必须是 Comparable<T> 的子类型
where절: 여러 상한이 필요하면where절을 사용합니다.
명시적으로 널 불가능한 타입
T & Any를 사용해 타입 매개변수를 명시적으로 널 불가능한 타입으로 선언합니다.- 일반적으로
@NotNull注解이 지정된 Java 메서드와 상호 운용할 때 사용합니다.
타입 소거
- Kotlin은 컴파일 타임에 제네릭 타입 안전성 검사를 수행합니다.
- 런타임에 제네릭 타입의 인스턴스에는 실제 타입 매개변수에 관한 정보가 포함되지 않습니다.
Foo<Bar>와Foo<Baz?>의 인스턴스는 런타임에 모두Foo<*>로 소거됩니다.
제네릭 타입 검사 및 변환
- 타입 소거 때문에 런타임에 제네릭 타입 인스턴스가 특정 타입 매개변수로 생성되었는지 검사할 수 없습니다(예:
ints is List<Int>검사 불가). - 별표 투영 타입에 대해서는 인스턴스 검사를 할 수 있습니다(예:
something is List<*>). - 타입 매개변수가 정적으로 검사된 경우 타입 매개변수를 포함하지 않는 비제네릭 부분에 대해 타입 검사 또는 변환을 수행할 수 있습니다(예:
list as ArrayList). - 제네릭 함수 호출의 타입 매개변수는 컴파일 타임에만 검사됩니다. 함수 본문 내부에서는 타입 매개변수를 타입 검사에 사용할 수 없으며, 타입 매개변수로의 변환은 검사되지 않은 변환입니다. 유일한 예외는 구체화된 타입 매개변수를 가진 인라인 함수로, 호출 지점마다 인라인된 실제 타입 매개변수를 갖습니다.
검사되지 않은 변환
- 런타임에 구체적인 타입 매개변수를 가진 제네릭 타입으로의 변환을 검사할 수 없습니다(예:
foo as List<String>). - 이러한 검사되지 않은 변환은 타입 안전성이 상위 프로그램 로직에서 암시되지만 컴파일러가 직접 추론할 수 없을 때 사용할 수 있습니다.
@Suppress("UNCHECKED_CAST")를 사용해 문 또는 선언에 검사되지 않은 변환 경고를 표시하지 않도록 할 수 있습니다.
타입 매개변수에 대한 밑줄 연산자
- 다른 타입을 명시적으로 지정하면 타입 매개변수의 타입을 자동으로 추론하는 데 밑줄 연산자
_를 사용할 수 있습니다.
데이터 클래스
핵심 개념
- Kotlin의 데이터 클래스는 주로 데이터를 저장하는 데 사용됩니다. 각 데이터 클래스에 대해 컴파일러는 인스턴스를 읽기 쉬운 형식으로 출력하거나, 인스턴스를 비교하거나, 인스턴스를 복사하는 등의 추가 멤버 함수를 자동으로 생성합니다. 데이터 클래스에는
data가 표시됩니다.
data class User(val name: String, val age: Int)
자동 생성 멤버
- 주 생성자에 선언된 모든 속성에 대해 컴파일러는 다음 멤버를 자동으로 생성합니다:
equals()/hashCode()쌍.User(name=John, age=42)형식의toString().- 속성 선언 순서에 대응하는
componentN()함수. copy()함수.
데이터 클래스의 요구 사항
- 생성되는 코드의 일관성과 의미 있는 동작을 보장하려면 데이터 클래스가 다음 요구 사항을 충족해야 합니다:
- 주 생성자에 매개변수가 하나 이상 있어야 합니다.
- 모든 주 생성자 매개변수는
val또는var로 표시해야 합니다. - 데이터 클래스는
abstract,open,sealed또는inner일 수 없습니다.
멤버 상속 규칙
- 데이터 클래스 본문에서
equals(),hashCode()또는toString()을 명시적으로 구현했거나 상위 클래스에 최종 구현이 명시되어 있으면 해당 함수가 생성되지 않으며 기존 구현이 사용됩니다. - 상위 타입에
componentN()함수가 있는 경우(open이며 호환되는 타입을 반환함), 데이터 클래스에 해당 함수가 생성되고 상위 타입의 함수를 재정의합니다. 시그니처가 호환되지 않거나 함수가final이어서 상위 타입의 함수를 재정의할 수 없으면 오류가 보고됩니다. componentN()및copy()함수의 명시적 구현은 허용되지 않습니다.- 데이터 클래스는 다른 클래스를 확장할 수 있습니다.
JVM에서 인수가 없는 생성자
- JVM에서 생성된 클래스에 인수가 없는 생성자가 필요한 경우 속성에 기본값을 지정해야 합니다:
data class User(val name: String = "", val age: Int = 0)
클래스 본문에서 선언한 속성
- 컴파일러는 자동 생성을 위해 주 생성자 내부에 정의된 속성만 사용합니다. 속성을 생성된 구현에서 제외하려면 클래스 본문에서 선언합니다:
data class Person(val name: String) {
var age: Int = 0
}
- 이 예제에서는 기본적으로
name속성만toString(),equals(),hashCode()및copy()구현에 사용되며, 컴포넌트 함수도component1()하나만 있습니다.age속성은 클래스 본문에서 선언되었으므로 제외됩니다. 따라서name은 같지만age값이 다른 두Person객체는 같은 것으로 간주됩니다.equals()가 주 생성자의 속성만 평가하기 때문입니다:
복사
copy()함수로 객체를 복사하면서 일부 속성만 변경하고 나머지는 그대로 유지할 수 있습니다. 위User클래스의 경우 이 함수의 구현은 다음과 같습니다:
fun copy(name: String = this.name, age: Int = this.age) = User(name, age)
val jack = User(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)
데이터 클래스와 구조 분해 선언
- 데이터 클래스에 생성되는 컴포넌트 함수를 사용하면 구조 분해 선언에서 사용할 수 있습니다:
val jane = User("Jane", 35)
val (name, age) = jane
println("$name, $age years of age")
// Jane, 35 years of age
표준 데이터 클래스
- 표준 라이브러리는
Pair와Triple클래스를 제공합니다. 하지만 대부분의 경우 속성에 의미 있는 이름을 부여해 코드를 읽기 쉽게 만드는 이름 있는 데이터 클래스가 더 좋은 설계 선택입니다.
열거형 클래스
핵심 개념
- Kotlin의 열거형 클래스는 서로 다른 유한한 값의 집합을 나타내는 데 사용되며, 각 값은 열거형 클래스의 인스턴스입니다.
기본 사용법
- 선언:
enum class키워드로 열거형 클래스를 선언합니다.
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
- 상수: 열거형 상수는 클래스의 객체이며 쉼표로 구분합니다.
초기화
- 생성자: 열거형 상수는 클래스처럼 생성자를 사용해 인수를 전달하며 초기화할 수 있습니다.
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
익명 클래스
- 정의: 열거형 상수는 각자의 메서드와 기본 클래스 메서드의 재구현을 포함하는 자체 익명 클래스를 선언할 수 있습니다.
- 멤버 정의: 열거형 클래스가 멤버를 정의하면 세미콜론을 사용해 상수 정의와 멤버 정의를 구분합니다.
인터페이스 구현
- 인터페이스 구현: 열거형 클래스는 인터페이스를 구현할 수 있지만 클래스를 상속할 수는 없습니다. 모든 항목에 인터페이스 멤버의 공통 구현을 제공하거나 각 항목의 익명 클래스에 개별 구현을 제공할 수 있습니다.
- 기본적으로 모든 열거형 클래스는
Comparable인터페이스를 구현합니다. 열거형 클래스의 상수는 자연스러운 순서로 정의되어 비교할 수 있습니다.
열거형 상수 사용하기
Kotlin의 열거형 클래스에는 정의된 열거형 상수를 나열하고 이름으로 열거형 상수를 가져오기 위한 합성 속성과 메서드가 있습니다.
EnumClass.valueOf(value: String): EnumClass: 이름에 따라 열거형 상수를 반환합니다. 지정한 이름이 클래스에 정의된 열거형 상수와 일치하지 않으면valueOf()메서드가IllegalArgumentException을 발생시킵니다.EnumClass.entries: EnumEntries<EnumClass>: Kotlin 1.9에서 도입되어 특별한List<EnumClass>타입의 열거형 항목을 반환합니다. Kotlin 2.0에서는 배열을 반환하는enumValues<T>()를 대체합니다.CodeBlock Loading...
각 열거형 상수에는 다음 속성도 있습니다:
name: 열거형 상수의 이름을 가져옵니다.ordinal: 열거형 클래스 선언에서 열거형 상수의 위치를 가져옵니다(0부터 시작).KOTLINprintln(RGB.RED.name) // prints RED println(RGB.RED.ordinal) // prints 0
제네릭 방식으로 열거형 상수 접근하기
enumEntries<T>(): Kotlin 2.0에서 도입되어 지정된 열거형 타입T의 모든 열거형 항목 목록을 반환합니다. 권장되며 성능이 좋습니다.
인라인 값 클래스
핵심 개념
- 인라인 값 클래스는 단일 값을 감싸지만 런타임에 실제 클래스 인스턴스를 생성하지 않는 특수한 클래스입니다. 이를 통해 메모리 사용량을 줄이고 성능을 높입니다.
선언 및 사용
- 선언:
value class키워드로 인라인 값 클래스를 선언합니다. JVM에서는@JvmInline注解이 필요합니다.
@JvmInline
value class Password(val value: String)
- 특징:
- 속성이 하나만 있어야 하며 생성자에서 초기화해야 합니다.
- 성능 최적화: 런타임에 인라인 값 클래스의 인스턴스는 기본 값을 직접 사용하며 새 객체를 만들지 않습니다.
- 타입 안전성: 기본 타입이
String이더라도Password타입을String으로 직접 사용할 수 없으며 그 반대도 불가능합니다. 이는typealias와 다릅니다.typealias는 타입에 별칭만 부여하므로 서로 바꿔 사용할 수 있습니다.
사용 사례
- 새로운 타입을 만들어야 하지만 성능을 희생하고 싶지 않을 때.
- 기존 타입에 더 강한 제약을 적용하고 싶을 때.
예제
object 키워드
싱글턴 객체
- 정의:
object키워드는 인스턴스가 하나뿐인 클래스인 싱글턴 클래스를 선언하는 데 사용합니다. - 특징:
- 싱글턴 객체는 처음 접근할 때 생성되며 애플리케이션 수명 내내 존재합니다.
- 싱글턴 객체에는 생성자가 있을 수 없습니다.
- 클래스를 상속하거나 인터페이스를 구현할 수 있습니다.
동반 객체
- 정의:
companion object키워드는 클래스 안에 동반 객체를 선언하는 데 사용합니다. - 특징:
- 동반 객체는 클래스와 연결되어 있지만 클래스의 인스턴스는 아닙니다.
- 동반 객체는 클래스의 private 멤버에 접근할 수 있습니다.
- 일반적으로 클래스의 팩토리 메서드를 만들거나 클래스의 정적 멤버를 저장하는 데 사용합니다.
익명 객체
- 정의:
object : ClassName()은 이름이 없는 익명 객체를 만드는 데 사용합니다. - 특징:
- 익명 객체는 보통 인터페이스를 구현하거나 클래스를 상속하면서 일회성 구현을 제공할 때 사용합니다.
- 외부 범위의 변수에 접근할 수 있습니다.
- 즉시 실행되고 초기화됩니다.
객체 선언 대 객체 표현식
| 특성 | 객체 선언(싱글턴) | 객체 표현식(익명 객체) |
|---|---|---|
| 용도 | 싱글턴 생성 | 일회성 사용 |
| 초기화 시점 | 최초 접근 시 지연 로드 | 즉시 초기화 |
| 접근 방식 | 이름을 통해 직접 접근 | 표현식 결과(변수 참조) |
| 이름 지정 | 이름이 필수 | 이름을 지정할 필요 없음 |
패키지와 import
핵심 개념
- 패키지: Kotlin 코드를 정리하는 데 사용되며 폴더와 유사합니다. 각 소스 파일은 속할 패키지를 선언할 수 있습니다.
- 가져오기: 다른 패키지의 클래스, 함수 등을 코드에서 사용할 수 있게 해 전체 패키지 이름을 쓰지 않아도 됩니다.
핵심 정보
패키지 선언
- 소스 파일 맨 앞에서
package키워드로 패키지 이름을 선언합니다:package org.example - 소스 파일의 모든 내용(클래스, 함수 등)이 해당 패키지에 속합니다.
- 예:
org.example.printMessage및org.example.Message - 패키지를 지정하지 않으면 기본 패키지(이름 없음)에 속합니다.
- 소스 파일 맨 앞에서
기본 import
- Kotlin은 일반적으로 사용하는 패키지를 기본으로 가져오므로手动으로 가져올 필요가 없습니다.
kotlin.*,kotlin.annotation.*,kotlin.collections.*등이 포함됩니다.- 대상 플랫폼(JVM, JS)에 따라
java.lang.*(JVM) 또는kotlin.js.*(JS)와 같은 추가 패키지도 가져옵니다.
사용자 정의 import
import키워드로 개별 이름이나 전체 패키지를 가져올 수 있습니다.- 개별 이름 가져오기:
import org.example.Message(Message를 직접 사용할 수 있음) - 전체 패키지 가져오기:
import org.example.*(org.example의 모든 내용 사용 가능)
별칭
- 가져온 이름이 충돌하면
as키워드로 가져온 항목을 다시 이름 지정해 충돌을 해결할 수 있습니다. 예:
KOTLINimport org.example.Message import org.test.Message as TestMessage // 现在可以用 TestMessage 代替 org.test.Message
- 가져온 이름이 충돌하면
가져올 수 있는 항목
- 최상위 함수와 속성
- 객체 선언의 함수와 속성
- 열거형 상수
최상위 선언의 가시성
- 최상위 선언(예: 최상위 함수, 최상위 변수)이
private로 표시되면 해당 선언이 있는 파일에서만 접근할 수 있습니다.
- 최상위 선언(예: 최상위 함수, 최상위 변수)이
인터페이스
핵심 개념
- Kotlin의 인터페이스는 Java 8 이상의 인터페이스와 유사하며 추상 메서드 선언과 메서드 구현을 포함할 수 있습니다. 핵심 차이는 인터페이스가 상태를 저장할 수 없다는 것입니다. 속성은 가질 수 있지만 해당 속성은 추상적이어야 하거나 접근자 구현을 제공해야 합니다.
인터페이스 정의
interface키워드로 인터페이스를 정의합니다:
인터페이스 구현
- 클래스나 객체는 하나 이상의 인터페이스를 구현할 수 있습니다:
class Child : MyInterface {
override fun bar() { // 必须实现 MyInterface 中的抽象方法 bar()
// 方法体
}
}
- 콜론(
:)으로 상속 또는 구현 관계를 나타냅니다. - 인터페이스의 메서드를 재정의할 때는 반드시
override키워드를 사용해야 합니다.
인터페이스의 속성
- 인터페이스는 속성을 선언할 수 있습니다.
- 인터페이스의 속성에는 백킹 필드가 없습니다.
- 인터페이스의 속성은 추상적이거나 접근자 구현을 제공할 수 있습니다:
인터페이스 상속
- 인터페이스는 다른 인터페이스를 상속할 수 있습니다.
- 상속된 인터페이스는 멤버 구현을 제공하거나 새 함수와 속성을 선언할 수 있습니다.
- 구현 클래스는 누락된 구현만 정의하면 됩니다:
재정의 충돌 해결
- 클래스가 여러 인터페이스를 구현할 때 동일한 메서드의 여러 구현을 상속받을 수 있습니다. 이 경우 해당 메서드를 어떻게 구현할지 명시적으로 지정해야 합니다:
- 같은 시그니처를 가진 여러 메서드를 상속받고 특정 인터페이스의 구현을 호출하려면
super<인터페이스 이름>.메서드 이름()을 사용할 수 있습니다. - 구현을 하나만 상속받았더라도(예:
bar()) 클래스의 동작을 명확히 하기 위해 이를 재정의하고 구현을 지정해야 합니다. 컴파일러가 어떤 인터페이스의 기본 구현을 사용해야 하는지 자동으로 결정할 수 없기 때문입니다. 재정의하지 않으면 컴파일 오류가 발생합니다.
함수형(SAM) 인터페이스
핵심 개념
- 함수형 인터페이스(Functional Interface)라고도 하는 단일 추상 메서드(Single Abstract Method, SAM) 인터페이스는 추상 멤버 함수가 하나뿐인 인터페이스입니다. 여러 비추상 멤버 함수가 있을 수 있지만 추상 멤버 함수는 하나만 있어야 합니다. Kotlin은 함수형 인터페이스를 선언하기 위한
fun interface키워드를 제공하고 SAM 변환을 지원합니다. 이를 통해 클래스를 직접 구현하는 대신 Lambda 표현식을 사용해 코드를 더 간결하게 만들 수 있습니다.
함수형 인터페이스 선언
fun interface키워드로 선언합니다:
fun interface KRunnable {
fun invoke() // 唯一的抽象方法
}
SAM 변환
- 개념: SAM 변환을 사용하면 함수형 인터페이스를 구현하는 클래스를 직접 만드는 대신 Lambda 표현식을 사용할 수 있습니다. Kotlin은 인터페이스의 단일 추상 메서드 시그니처와 일치하는 Lambda 표현식을 인터페이스의 인스턴스로 자동 변환합니다.
- 장점: 코드를 단순화하고 가독성을 높입니다.
- 예제:
이 예제에서 Lambda 표현식
{ it % 2 == 0 }은IntPredicate인터페이스의 구현으로 자동 변환됩니다.Java 인터페이스도 SAM 변환을 지원합니다
생성자가 있는 인터페이스에서 함수형 인터페이스로 마이그레이션
Kotlin 1.6.20 이상에서는 함수형 인터페이스 생성자의 호출 참조(callable reference)를 사용할 수 있습니다. 이를 통해 생성자 함수가 있는 인터페이스에서 함수형 인터페이스로 소스 호환 방식으로 마이그레이션할 수 있습니다.
예제:
- 이진 호환성 유지:
@Deprecated注解를 사용해 기존 함수Printer를 숨김(DeprecationLevel.HIDDEN)으로 표시합니다.
@Deprecated(
message = "使用函数式接口 Printer 代替",
level = DeprecationLevel.HIDDEN
)
fun Printer(...) {...}
함수형 인터페이스 대 타입 별칭
타입 별칭도 함수 타입 사용을 단순화할 수 있지만, 함수형 인터페이스와 타입 별칭의 목적은 서로 다릅니다:
타입 별칭: 기존 타입의 별칭일 뿐 새 타입을 만들지 않습니다.
KOTLINtypealias IntPredicate = (i: Int) -> Boolean // IntPredicate 仅仅是函数类型 (i: Int) -> Boolean 的别名함수형 인터페이스: 새 타입을 만듭니다.
KOTLINfun interface IntPredicate { fun accept(i: Int): Boolean // IntPredicate 是一个真实的接口类型 }
차이 요약
| 특성 | 타입 별칭 | 함수형 인터페이스 |
|---|---|---|
| 타입 생성 | 새 타입을 만들지 않음 | 새 타입을 생성함 |
| 멤버 | 함수 타입 하나만 가질 수 있음 | 여러 비추상 멤버 함수와 추상 멤버 함수 하나를 가질 수 있음 |
| 인터페이스 확장 | 다른 인터페이스의 구현/상속을 지원하지 않음 | 다른 인터페이스의 구현/상속을 지원함 |
| 확장 함수 | 동일한 시그니처를 가진 모든 함수 타입에 적용되며 특정 별칭으로 제한되지 않음 | 해당 함수형 인터페이스의 인스턴스에만 적용되며 다른 함수 타입에는 영향을 주지 않음 |
| 사용 사례 | API가 특정 매개변수 및 반환 타입을 가진 함수를 받아야 할 때, 즉 모든 함수를 받아야 할 때 함수 타입을 단순화하는 데 사용 | 함수 타입의 시그니처로 표현할 수 없는 비자명한 규칙이나 동작을 가진 더 복잡한 엔티티를 정의할 때 사용 |
| 성능 | 오버헤드가 더 작음 | 특정 인터페이스로 변환하는 경우 약간의 오버헤드가 있을 수 있음 |
선택 방법
- 타입 별칭: API가 특정 매개변수 및 반환 타입을 가진 함수, 즉 어떤 함수든 받아야 할 때 타입 별칭이나 단순한 함수 타입을 사용합니다.
- 함수형 인터페이스: API가 함수보다 더 복잡한 엔티티를 받아야 할 때, 예를 들어 함수 타입의 시그니처로 표현할 수 없는 비자명한 규칙이나 동작이 있는 경우 함수형 인터페이스를 사용합니다.
가시성修饰자
핵심 개념
- 가시성 수정자는 클래스, 객체, 인터페이스, 생성자, 함수, 속성 및 setter의 가시 범위를 제어합니다. Getter의 가시성은 항상 해당 속성과 같습니다. Kotlin은
private,protected,internal,public의 네 가지 가시성 수정자를 제공합니다. 기본 가시성은public입니다.
가시성 수정자 개요
| 수정자 | 설명 |
|---|---|
public | (기본) 어디에서든可见합니다. |
private | 선언한 파일 또는 클래스 안에서만可见합니다. |
protected | 선언한 클래스와 그 하위 클래스 안에서만可见합니다. |
internal | 같은 모듈 안에서만可见합니다. 모듈은 함께 컴파일되는 Kotlin 파일들의 모음입니다. |
패키지 수준 선언
패키지 안에서 직접 선언한 함수, 속성, 클래스, 객체 및 인터페이스를 최상위 선언이라고 합니다.
public(기본): 어디에서든可见합니다.private: 해당 선언을 포함한 파일 안에서만可见합니다.internal: 같은 모듈 안의 어디에서든可见합니다.protected: 최상위 선언에는 적용되지 않습니다.
- 다른 패키지의 가시 최상위 선언을 사용하려면 이를 가져와야 합니다.
클래스 멤버
클래스 내부에서 선언한 멤버에는 다음 규칙이 적용됩니다:
private: 해당 클래스 내부에서만可见합니다(모든 멤버 포함).protected:private와 가시성이 같지만 하위 클래스에서도可见합니다.internal: 같은 모듈에서 선언 클래스를 볼 수 있는 모든 클라이언트가 해당internal멤버를 볼 수 있습니다.public: 선언 클래스를 볼 수 있는 모든 클라이언트가 해당public멤버를 볼 수 있습니다.
- 외부 클래스는 내부 클래스의
private멤버를 볼 수 없습니다. protected또는internal멤버를 재정의할 때 가시성을 명시하지 않으면 재정의된 멤버는 원래 멤버와 같은 가시성을 갖습니다.
생성자
- 다음 구문을 사용해 클래스 주 생성자의 가시성을 지정할 수 있습니다:
class C private constructor(a: Int) { ... } // 构造函数是 private
- 기본적으로 모든 생성자는
public이며, 이는 클래스가可见되는 모든 곳에서 생성자를 사용할 수 있다는 뜻입니다. 따라서internal클래스의 생성자는 같은 모듈 안에서만可见합니다. - sealed 클래스의 생성자는 기본적으로
protected입니다.
지역 선언
- 지역 변수, 함수 및 클래스에는 가시성 수정자를 사용할 수 없습니다.
모듈
internal가시성 수정자는 멤버가 같은 모듈 안에서可见됨을 의미합니다. 구체적으로 모듈은 함께 컴파일되는 Kotlin 파일들의 모음입니다. 예를 들면 다음과 같습니다:- IntelliJ IDEA 모듈.
- Maven 프로젝트.
- Gradle 소스 세트(테스트 소스 세트에서
main의internal선언에 접근할 수 있는 예외 있음). - 단일
<kotlinc>Ant 작업을 호출해 컴파일한 파일들의 모음.
위임 속성
핵심 개념
- 위임 속성은 속성의
get()및set()메서드를 다른 객체(위임 객체)에 위임할 수 있게 하는 강력한 Kotlin 기능입니다. 이를 통해 코드 재사용, 공통 동작 및 속성 구현 논리의 단순화가 가능합니다. 지연 초기화, 속성 변경 감지, 데이터 저장소 등의 일반적인 프로그래밍 패턴을 구현하는 데 매우 적합합니다.
기본 구문
kotlin
복사
var <속성 이름>: <타입> by <표현식>
<속성 이름>: 선언하려는 속성의 이름입니다.<타입>: 속성의 타입이며 위임 객체의getValue()메서드 반환 타입과 일치해야 합니다.<표현식>: 위임 객체를 반환하는 표현식입니다. 이 객체가 실제 속성 접근 및 수정 논리를 담당합니다.by: Kotlin이 위임 메커니즘을 사용하도록 지시하는 키워드입니다.위임 객체의 요구 사항
위임 객체는 특정 시그니처의
getValue()함수(val및var속성 모두에 필요)와setValue()함수(var속성에만 필요)를 제공해야 합니다. 이 함수는operator로 선언해야 합니다.getValue() 함수(필수):
kotlin
복사
KOTLINoperator fun getValue(thisRef: Any?, property: KProperty<*>): 반환값 타입 { ... }thisRef: 속성을 포함하는 객체에 대한 참조(Any?)입니다. 속성이 최상위 속성이면 이 값은null입니다. 이 매개변수를 사용하면 위임 객체가 자신이 속한 객체의 상태에 접근할 수 있지만 일반적으로 권장되지는 않습니다.property: 위임된 속성을 나타내는KProperty인스턴스(KProperty<*>)입니다.property.name같은 속성 메타데이터를 가져오는 데 사용할 수 있습니다.반환값 타입: 위임 속성의 타입입니다.getValue()는 해당 타입 또는 그 하위 타입의 값을 반환해야 합니다.
setValue() 함수(var 속성에만 사용):
KOTLINoperator fun setValue(thisRef: Any?, property: KProperty<*>, value: 새 값 타입) { ... }thisRef: 속성을 포함하는 객체에 대한 참조입니다(getValue()와 동일).property: 위임된 속성을 나타내는KProperty인스턴스입니다(getValue()와 동일).value: 속성에 할당할 새 값입니다(속성과 동일한 타입).setValue()는 이 값을 지원 저장소에 저장하는 작업을 수행해야 합니다.새 값 타입: 위임 속성의 타입입니다.setValue()는 해당 타입 또는 상위 타입의 인수를 받아야 합니다.
표준 위임자
Kotlin 표준 라이브러리는 kotlin.properties.Delegates와 kotlin 패키지에서 접근할 수 있는 유용한 사전 정의 위임자들을 제공합니다:
lazy(): 속성을 지연 초기화합니다. 속성의 값은 처음 접근할 때만 제공된 Lambda 표현식으로 계산됩니다. 이후 접근에서는 캐시된 값을 반환합니다.CodeBlock Loading...LazyThreadSafetyMode: 지연 초기화 중 여러 스레드의 접근을 처리하는 방식을 제어합니다.SYNCHRONIZED(기본): 값이 하나의 스레드에서만 초기화되도록 보장합니다. 스레드 안전하지만 성능 오버헤드가 발생할 수 있습니다.PUBLICATION: 여러 스레드가 값 초기화를 시도할 수 있지만 계산 결과 하나만 사용되도록 보장합니다. 초기화 Lambda 표현식이 멱등인 경우, 즉 여러 번 실행해도 같은 결과가 나오는 경우 이 옵션을 사용할 수 있습니다.NONE: 스레드 안전성을 보장하지 않습니다. 속성이 단일 스레드에서만 접근되는 경우 더 높은 성능을 위해 이 옵션을 사용할 수 있습니다.
Delegates.observable(): 관찰 가능한 속성을 만듭니다. 속성 값에 새 값이 할당될 때마다 제공된 핸들러가 호출됩니다.CodeBlock Loading...- 핸들러 함수의 시그니처는
(KProperty<*>, oldValue, newValue) -> Unit입니다.
- 핸들러 함수의 시그니처는
Delegates.vetoable(): 승인 또는 거절할 수 있는 속성을 만듭니다. 속성 값에 새 값을 할당하려고 할 때마다 제공된 핸들러가 할당 전에 호출됩니다. 핸들러가true를 반환하면 할당이 이루어지고,false를 반환하면 할당이 차단됩니다.CodeBlock Loading...- 핸들러 함수의 시그니처는
(KProperty<*>, oldValue, newValue) -> Boolean입니다.
- 핸들러 함수의 시그니처는
다른 속성에 위임하기
Kotlin에서는 속성의 getter와 setter를 같은 클래스 또는 다른 클래스의 다른 속성에 위임할 수 있습니다. 이는 이전 버전과 호환되는 이름 변경을 구현하거나 여러 속성을 동기화하는 데 유용합니다.
- 이 예제에서
delegatedToMember의 getter와 setter는 모두memberInt에 위임됩니다.delegatedToMember를 수정하면memberInt에도 영향을 주고 그 반대도 마찬가지입니다. @Deprecated: 사용자에게oldName대신newName을 사용하도록 안내합니다.
속성을 Map에 저장하기
Map 또는 MutableMap을 위임 객체로 사용해 문자열 키로 속성 값을 동적으로 가져올 수 있습니다. JSON이나 다른 동적 데이터 구조를 처리할 때 유용합니다.
- 이 예제에서
User클래스의 생성자는Map<String, Any?>를 받습니다.name및age속성의 값은 이 map에서"name"과"age"문자열 키를 사용해 가져옵니다.
지역 위임 속성
지역 위임 속성을 사용하면 함수나 코드 블록 내부에서 위임 속성을 선언할 수 있습니다. 계산 결과를 캐시하거나 특정 조건에서만 필요한 지역 변수를 지연 초기화하는 데 유용합니다.
- 이 예제에서
processedData는data가null이 아니고 빈 문자열도 아닐 때만 초기화됩니다.data가null이거나 비어 있으면 초기화 Lambda 표현식이 실행되지 않습니다.
provideDelegate 함수
provideDelegate 함수를 사용하면 위임 속성이 생성되는 과정을 가로챌 수 있습니다. 속성이 올바르게 사용되는지 검증하거나 사용자 정의 위임 인스턴스를 설정하거나 다른 초기화 논리를 실행하는 데 유용합니다.
operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): PropertyDelegate { ... }
thisRef: 속성을 포함하는 객체에 대한 참조입니다(getValue()와 동일).property: 위임된 속성을 나타내는KProperty인스턴스입니다(getValue()와 동일).- 반환값: 속성 위임에 실제로 사용되는 객체입니다. 원래 위임 객체와 다를 수 있으며, 사용자 정의 Delegate를 사용해 사용자가 제공한 Delegate를 대체할 수 있습니다.
provideDelegate함수는 속성이 생성될 때 호출되어 실제 위임 객체를 사용하기 전에 논리를 실행할 수 있게 합니다. 예제에서는 속성과 ID가 바인딩되었음을 나타내는 메시지를 출력한 뒤 리소스를 지연 로드하는 실제ResourceDelegate를 만듭니다.
위임 속성의 변환 규칙
Kotlin 컴파일러가 위임 속성을 만나면 다음 보조 코드를 생성합니다:
- 숨겨진
$delegate속성:var prop: Type by MyDelegate()의 경우 컴파일러는prop$delegate같은 숨겨진 속성을 생성하며 타입은MyDelegate입니다. 이 속성이 실제 위임 객체를 저장합니다. Getter 및 setter 메서드: 컴파일러는
prop의 getter와 setter 메서드를 생성합니다. 이 메서드는 위임 객체의getValue()및setValue()메서드를 호출합니다.CodeBlock Loading...최적화 시나리오: 다른 속성에 직접 위임하는 경우처럼 일부 상황에서는 컴파일러가 성능 최적화를 위해
$delegate속성 생성을 생략하고 위임 속성의 백킹 필드에 직접 접근합니다.
확장
핵심 개념
- Kotlin 확장은 클래스를 상속하거나 데코레이터 같은 디자인 패턴을 사용하지 않고 클래스나 인터페이스에 새로운 기능을 추가할 수 있게 합니다. 이를 특수한 선언인 확장으로 구현합니다. 서드파티 라이브러리의 클래스나 인터페이스를 수정하지 않고도 새 함수를 작성할 수 있습니다.
확장 함수
- 선언: 함수 이름 앞에 수신자 타입을 붙입니다.
확장 함수에서
this키워드는 수신자 객체, 즉 점(.) 앞에 전달된 객체를 가리킵니다.호출: 일반 메서드와 같은 방식으로 확장 함수를 호출합니다.
val list = mutableListOf(1, 2, 3)
list.swap(0, 2) // 'this' 在 'swap()' 内部将保存 'list' 的值
확장의 정적 해석
- 확장은 실제로 확장 대상 클래스를 수정하지 않습니다. 확장을 정의해도 클래스에 새 멤버가 삽입되는 것이 아니라, 점 표기법으로 변수에서 새 함수를 호출할 수 있게 될 뿐입니다.
- 확장 함수는 정적으로 분배됩니다. 컴파일 타임에 수신자 타입을 기준으로 호출할 확장 함수가 결정됩니다.
- 멤버 우선: 클래스에 멤버 함수가 있고, 같은 수신자 타입과 이름을 가지며 주어진 인자에 적용되는 확장 함수도 정의되어 있다면 멤버 함수가 항상 우선합니다.
- 이름은 같지만 시그니처가 다른 멤버 함수를 오버로드할 수 있습니다.
널 가능한 수신자
- 널 가능한 수신자 타입을 가진 확장을 정의할 수 있습니다. 이러한 확장은 객체 변수의 값이
null일 때도 호출할 수 있습니다. 수신자가null이면this도null입니다. 컴파일 오류를 피하려면 함수 본문 안에서this == null검사를 수행하는 것이 좋습니다.
확장 속성
- Kotlin은 함수와 유사한 확장 속성을 지원합니다.
val <T> List<T>.lastIndex: Int
get() = size - 1
- 확장은 실제로 클래스에 멤버를 삽입하지 않으므로 확장 속성에는 백킹 필드를 사용할 수 없습니다. 확장 속성에 초기자를 사용하는 것이 허용되지 않는 이유입니다. 확장 속성의 동작은 getter나 setter를 명시적으로 제공할 때만 정의할 수 있습니다.
class House
val House.number = 1 // 错误:不允许对扩展属性进行初始化
동반 객체 확장
- 클래스가 동반 객체를 정의했다면 해당 동반 객체에 확장 함수와 확장 속성을 정의할 수 있습니다. 동반 객체의 일반 멤버와 마찬가지로 클래스 이름만 한정자로 사용해 호출할 수 있습니다.
확장의 범위
- 대부분의 경우 확장은 최상위에서, 즉 패키지 바로 아래에서 정의합니다:
package org.example.declarations
fun List<String>.getLongestString() { /*...*/ }
- 선언된 패키지 밖에서 확장을 사용하려면 사용 지점에서 가져와야 합니다:
확장을 멤버로 선언하기
- 클래스 내부에서 다른 클래스의 확장을 선언할 수 있습니다. 이러한 확장은 여러 암묵적 수신자를 가지므로 한정자 없이 멤버에 접근할 수 있는 객체가 존재합니다. 확장이 선언된 클래스의 인스턴스를 분배 수신자라고 하고, 확장 메서드의 수신자 타입 인스턴스를 확장 수신자라고 합니다.
- 분배 수신자의 멤버와 확장 수신자의 멤버 사이에 이름 충돌이 있으면 확장 수신자가 우선합니다. 분배 수신자의 멤버를 참조하려면 한정된
this구문을 사용합니다.
- 멤버로 선언된 확장은
open으로 선언하고 하위 클래스에서 재정의할 수 있습니다. 이는 이러한 함수의 분배가 분배 수신자 타입에 대해서는 가상이고 확장 수신자 타입에 대해서는 정적이라는 뜻입니다.
가시성 주의 사항
- 확장은 같은 범위에서 선언된 일반 함수와 동일한 가시성 수정자를 사용합니다. 예를 들면 다음과 같습니다:
- 파일 최상위에서 선언된 확장은 같은 파일의 다른
private최상위 선언에 접근할 수 있습니다. - 확장이 수신자 타입 외부에서 선언되었다면 수신자의
private또는protected멤버에 접근할 수 없습니다.
- 파일 최상위에서 선언된 확장은 같은 파일의 다른
타입 별칭
핵심 개념
- 타입 별칭은 타입에 "별명"을 붙이는 것과 같습니다. 별명과 진짜 이름을 사용하는 효과는 완전히 같지만, 별명이 더 짧고 기억하기 쉬울 수 있습니다.
용도
긴 타입 이름 단순화:
Set<Network.Node>처럼 긴 타입에NodeSet같은 간단한 이름을 붙일 수 있습니다. 복잡한 제네릭 타입을 다룰 때 특히 유용합니다.함수 타입에 별칭 지정:
(Int, String, Any) -> Unit같은 함수 타입에MyHandler라는 별칭을 붙여 읽고 사용하기 쉽게 할 수 있습니다.내부 클래스/중첩 클래스에 별칭 지정: 여러 클래스에 내부 클래스가 있다면
A.Inner와B.Inner를 각각AInner와BInner로 바꿔 쉽게 구분할 수 있습니다.
예제
중요: 타입 별칭은 새 타입이 아닙니다!
typealias Predicate<T> = (T) -> Boolean은 단순히(T) -> Boolean에 별칭을 붙인 것입니다.- 컴파일러는 컴파일 타임에 모든 타입 별칭을 대응하는 원래 타입으로 바꿉니다.
- 따라서 별칭 타입과 원래 타입을 자유롭게 서로 바꿔 사용할 수 있으며, Kotlin 컴파일러는 둘을 완전히 동일하게 취급합니다.
예제 설명
함수
핵심 개념
- 함수는 특정 작업을 수행하기 위한 코드의 기본 구성 요소입니다. Kotlin은 함수를 정의하고 사용하는 유연한 방법을 제공합니다.
함수 선언
- Kotlin 함수는
fun키워드로 선언합니다. - 선언 형식:
fun 함수 이름(매개변수 이름: 매개변수 타입): 반환 타입 { 함수 본문 }
fun double(x: Int): Int {
return 2 * x
}
함수 사용(호출)
- 표준 방식으로 함수를 호출합니다:
함수 이름(매개변수) - 멤버 함수(클래스나 객체의 함수)는 점 표기법으로 호출합니다:
인스턴스.함수 이름(매개변수)
val result = double(2) // 调用 double 函数
Stream().read() // 创建 Stream 类的实例并调用 read 函数
매개변수
- 함수 매개변수는 Pascal 표기법으로 정의합니다:
매개변수 이름: 매개변수 타입 - 여러 매개변수는 쉼표로 구분합니다.
- 모든 매개변수에 타입을 명시적으로 지정해야 합니다.
fun powerOf(number: Int, exponent: Int): Int { /* 함수 본문 */ }
- 함수 매개변수를 선언할 때 후행 쉼표를 사용할 수 있습니다:
fun powerOf(
number: Int,
exponent: Int, // 후행 쉼표
) { /* 함수 본문 */ }
기본 매개변수
- 함수 매개변수에는 기본값을 지정할 수 있으며, 함수를 호출할 때 해당 매개변수를 생략하면 기본값이 사용됩니다. 이를 통해 함수 오버로드의 수를 줄일 수 있습니다.
- 타입 뒤에
=를 추가해 기본값을 설정합니다. 예:매개변수 이름: 매개변수 타입 = 기본값
fun read(
b: ByteArray,
off: Int = 0,
len: Int = b.size,
) { /* 함수 본문 */ }
- 재정의된 메서드는 항상 기본 클래스의 메서드에 지정된 기본값을 사용합니다. 기본값이 있는 메서드를 재정의할 때 기본값은 시그니처에서 생략해야 합니다:
open class A {
open fun foo(i: Int = 10) { /*...*/ }
}
class B : A() {
override fun foo(i: Int) { /*...*/ } // 不允许设置默认值.
}
- 기본값이 있는 매개변수 뒤에 기본값이 없는 매개변수가 있다면, 기본값을 사용하려면 이름이 지정된 매개변수로 함수를 호출해야 합니다:
fun foo(
bar: Int = 0,
baz: Int,
) { /*...*/ }
foo(baz = 1) // 使用默认值 bar = 0
- 기본 매개변수 뒤의 마지막 매개변수가 Lambda 표현식이라면 이름이 지정된 매개변수로 전달하거나 괄호 밖에 둘 수 있습니다:
이름이 지정된 매개변수
- 함수를 호출할 때 하나 이상의 매개변수에 이름을 지정할 수 있습니다. 특히 매개변수가 많거나 매개변수와 값을 연결하기 어려울 때 유용합니다. 부울 값이나 null 값인 매개변수에 특히 그렇습니다.
- 함수 호출에서 이름이 지정된 매개변수를 사용하면 목록에 나열된 순서를 자유롭게 바꿀 수 있습니다. 기본값을 사용하려면 해당 매개변수를 완전히 생략할 수 있습니다.
- 기본값이 있는 4개의 매개변수를 가진
reformat()함수를 생각해 봅시다.
- 이 함수를 호출할 때 모든 매개변수의 이름을 지정할 필요는 없습니다:
reformat(
"String!",
false,
upperCaseFirstLetter = false,
divideByCamelHumps = true,
'_'
)
- 기본값이 있는 모든 매개변수를 생략할 수도 있습니다:
reformat("This is a long String!")
- 기본값이 있는 모든 매개변수를 생략하는 대신 특정 매개변수만 건너뛸 수도 있습니다. 하지만 건너뛴 첫 매개변수 이후에는 모든 후속 매개변수에 이름을 지정해야 합니다:
reformat("This is a short String!", upperCaseFirstLetter = false, wordSeparator = '_')
- spread 연산자(
*를 배열 접두사로 사용)를 사용해 이름이 지정된 가변 개수의 매개변수(vararg)를 전달할 수 있습니다:
fun foo(vararg strings: String) { /*...*/ }
foo(strings = *arrayOf("a", "b", "c"))
- JVM에서 Java 함수를 호출할 때는 Java 바이트코드가 함수 매개변수 이름을 항상 보존하지 않으므로 이름이 지정된 매개변수 구문을 사용할 수 없습니다.
Unit 반환 함수
- 함수가 유용한 값을 반환하지 않으면 반환 타입은
Unit입니다.Unit은 값이 하나뿐인 타입이며 그 값은Unit입니다. 이 값을 명시적으로 반환할 필요는 없습니다:
Unit반환 타입 선언도 선택 사항입니다. 위 코드는 다음과 같습니다:
fun printHello(name: String?) { ... }
단일 표현식 함수
- 함수 본문이 단일 표현식으로 구성되면 중괄호를 생략하고
=기호 뒤에 함수 본문을 지정할 수 있습니다:
fun double(x: Int): Int = x * 2
- 컴파일러가 반환 타입을 추론할 수 있다면 반환 타입을 명시적으로 선언하지 않아도 됩니다:
fun double(x: Int) = x * 2
명시적 반환 타입
- 블록 형태의 함수 본문을 가진 함수는
Unit을 반환하려는 경우가 아니라면 항상 반환 타입을 명시적으로 지정해야 합니다. - Kotlin은 블록 형태의 함수 본문에 대해 반환 타입을 추론하지 않습니다. 이러한 함수의 본문에는 복잡한 제어 흐름이 있을 수 있으며, 반환 타입이 독자에게 때로는 컴파일러에게도 명확하지 않을 수 있기 때문입니다.
가변 개수의 매개변수
vararg수정자를 사용해 함수 매개변수(보통 마지막 매개변수)를 표시할 수 있습니다:
- 이 경우 함수에 가변 개수의 인자를 전달할 수 있습니다:
val list = asList(1, 2, 3)
- 함수 내부에서
T타입의vararg매개변수는T의 배열로 나타납니다. 위 예시에서ts변수의 타입은Array<out T>입니다. - 하나의 매개변수만
vararg로 표시할 수 있습니다.vararg매개변수가 목록의 마지막 매개변수가 아니라면, 이름이 지정된 매개변수 구문을 사용해 후속 매개변수 값을 전달해야 합니다. 후속 매개변수에 함수 타입이 있다면 Lambda 표현식을 괄호 밖에 두어 전달할 수 있습니다. vararg함수를 호출할 때 인자를 개별적으로 전달할 수 있습니다. 예를 들어asList(1, 2, 3)입니다. 이미 배열이 있고 그 내용을 함수에 전달하려면 spread 연산자(*를 배열 접두사로 사용)를 사용합니다:
val a = arrayOf(1, 2, 3)
val list = asList(-1, 0, *a, 4)
- 원시 타입 배열을
vararg에 전달하려면toTypedArray()함수를 사용해 일반적인 타입 지정 배열로 변환해야 합니다:
val a = intArrayOf(1, 2, 3) // IntArray는 원시 타입 배열입니다
val list = asList(-1, 0, *a.toTypedArray(), 4)
중위 표기법
infix키워드로 표시된 함수는 호출에서 점과 괄호를 생략한 중위 표기법으로도 호출할 수 있습니다. 중위 함수는 다음 조건을 충족해야 합니다:- 멤버 함수나 확장 함수여야 합니다.
- 매개변수가 하나뿐이어야 합니다.
- 해당 매개변수는 가변 개수 인자를 받을 수 없어야 하며 기본값도 없어야 합니다.
infix fun Int.shl(x: Int): Int { /* 함수 본문 */ }
// 중위 표기법으로 함수 호출
1 shl 2
// 다음과 동일
1.shl(2)
중위 함수 호출의 우선순위는 산술 연산자, 타입 변환 및
rangeTo연산자보다 낮습니다. 다음 표현식은 서로 동등합니다:1 shl 2 + 3은1 shl (2 + 3)과 같음0 until n * 2는0 until (n * 2)와 같음xs union ys as Set<*>는xs union (ys as Set<*>)와 같음
반대로 중위 함수 호출의 우선순위는 불리언 연산자
&&와||,is및in검사, 그 밖의 일부 연산자보다 높습니다. 다음 표현식도 서로 동등합니다:a && b xor c는a && (b xor c)와 같음a xor b in c는(a xor b) in c와 같음
중위 함수는 항상 수신자와 인수를 명시해야 합니다. 현재 수신자에서 메서드를 중위 표기법으로 호출할 때는
this를 명시적으로 사용하세요. 이는 명확한 해석을 보장하기 위함입니다.
함수 범위
- Kotlin 함수는 파일 최상위에서 선언할 수 있으므로 함수를 보관할 클래스를 만들 필요가 없습니다. 이는 Java, C#, Scala 같은 언어에서는 필수였습니다(Scala 3부터는 최상위 정의를 사용할 수 있음). 최상위 함수 외에도 Kotlin 함수는 멤버 함수와 확장 함수로 로컬 선언할 수 있습니다.
지역 함수
- Kotlin은 다른 함수 내부에 정의되는 지역 함수를 지원합니다:
- 지역 함수는 외부 함수의 지역 변수에 접근할 수 있습니다(클로저). 위 예시에서
visited는 지역 변수일 수 있습니다:
멤버 함수
- 멤버 함수는 클래스나 객체 내부에서 정의되는 함수입니다:
class Sample {
fun foo() { print("Foo") }
}
- 멤버 함수는 점 표기법으로 호출합니다:
Sample().foo() // Sample 클래스의 인스턴스를 만들고 foo를 호출
제네릭 함수
- 함수에는 제네릭 매개변수를 둘 수 있으며,尖괄호로 함수 이름 앞에 지정합니다:
fun <T> singletonList(item: T): List<T> { /*...*/ }
꼬리 재귀 함수
- Kotlin은 꼬리 재귀라고 부르는 함수형 프로그래밍 스타일을 지원합니다. 일반적으로 반복문을 사용하는 일부 알고리즘은 스택 오버플로 위험 없이 재귀 함수로 대체할 수 있습니다. 함수가
tailrec수정자로 표시되고 필요한 공식 조건을 충족하면 컴파일러가 재귀를 최적화해 빠른 효율적인 루프 기반 버전을 남깁니다:
tailrec수정자 조건을 충족하려면 함수가 자체 호출을 마지막으로 수행하는 작업으로 삼아야 합니다. 재귀 호출 뒤에 코드가 더 있거나, 재귀 호출이try/catch/finally블록 안에 있거나,open함수이면 꼬리 재귀를 사용할 수 없습니다. 현재 Kotlin for the JVM과 Kotlin/Native는 꼬리 재귀를 지원합니다.
고차 함수와 Lambda 표현식
핵심 개념
- 함수도 값입니다: Kotlin에서는 함수를 숫자나 문자열처럼 변수에 할당하고, 데이터 구조에 저장하고, 다른 함수의 인자로 전달하거나, 함수에서 반환할 수 있습니다.
- 고차 함수: 간단히 말해 함수를 인자로 받거나 함수를 반환하는 함수입니다.
- Lambda 표현식: 간단히 말해 익명 함수로, 함수를 빠르게 정의하고 사용할 수 있습니다.
고차 함수란?
- 정의: 함수를 인자로 받거나 함수를 반환하는 함수입니다.
예:
fold함수는 대표적인 고차 함수입니다.fold함수는 초기값과 조합 함수를 받습니다. 컬렉션의 각 요소를 순회하면서 조합 함수로 현재 요소와 누적 값을 결합한 뒤 최종 결과를 반환합니다.코드 예제(단순화):
CodeBlock Loading...combine인수는 두 인자(R과 T)를 받고 R 타입의 값을 반환하는 함수 타입입니다.
fold호출 방법: Lambda 표현식이나 함수 참조를 사용합니다.예:
CodeBlock Loading...
함수 타입
- 역할: Kotlin은 함수 타입으로 함수를 나타냅니다. 예를 들어
(Int) -> String은Int인자를 받고String을 반환하는 함수를 나타냅니다. - 구문:
(매개변수 타입 1, 매개변수 타입 2, ...) -> 반환 값 타입.()는 인자가 없음을 나타내며() -> Unit과 같습니다.Unit은 Java의void에 해당합니다.A.(B) -> C는 수신자가 있는 함수 타입입니다. A 타입의 확장 함수로 볼 수 있습니다.
- 널 가능한 함수 타입:
((Int, Int) -> Int)?는 이 함수 타입이null일 수 있음을 나타냅니다. - 타입 별칭:
typealias를 사용해 함수 타입에 별칭을 지정해 사용하기 쉽게 할 수 있습니다.
typealias MyHandler = (String, Int) -> Unit
함수 타입의 인스턴스는 어떻게 만드는가?
다음 여러 가지 방법이 있습니다:
- Lambda 표현식:
{ a, b -> a + b } - 익명 함수:
fun(s: String): Int { return s.toIntOrNull() ?: 0 } - 함수 참조:
::isOdd(최상위 함수),String::toInt(멤버 함수) - 함수 타입 인터페이스를 구현한 클래스:
class IntTransformer: (Int) -> Int {
override operator fun invoke(x: Int): Int = TODO() // invoke 메서드 구현
}
함수 타입 인스턴스 호출하기
invoke(...)를 사용합니다:f.invoke(x)- 직접 호출합니다:
f(x) - 함수 타입에 수신자가 있다면 확장 함수처럼 호출할 수 있습니다:
1.foo(2).
클로저
- 정의: Lambda 표현식이나 익명 함수는 외부 범위에서 정의된 변수에 접근할 수 있습니다.
- 외부 변수 수정: Lambda 표현식은 외부 범위의 변수를 수정할 수 있습니다.
var sum = 0
numbers.forEach { sum += it }
println(sum) // 출력: numbers의 모든 원소의 합
수신자가 있는 함수 리터럴
- 타입:
A.(B) -> C - 역할: Lambda 표현식에서 수신자 객체의 메서드를 확장 함수처럼 호출할 수 있습니다.
this키워드: Lambda 표현식에서this키워드로 수신자 객체를 참조할 수 있습니다.- 예:
val sum: Int.(Int) -> Int = { other -> this + other }
println(1.sum(2)) // 출력: 3
타입 안전 빌더
- 수신자가 있는 Lambda 표현식은 타입 안전 DSL(도메인별 언어)을 구축할 때 자주 사용됩니다.
Lambda 표현식
- 구문:
{ 매개변수 -> 함수 본문 } - 매개변수 타입 추론: 컴파일러가 매개변수 타입을 추론할 수 있으면 매개변수 타입을 생략할 수 있습니다.
- 후행 Lambda: 함수의 마지막 매개변수가 함수 타입이면 Lambda 표현식을 괄호 밖에 둘 수 있습니다.
numbers.filter { it > 0 }
it키워드: Lambda 표현식에 매개변수가 하나만 있으면it키워드로 해당 매개변수를 참조할 수 있습니다.numbers.map { it * 2 }
- 반환값: Lambda 표현식의 마지막 표현식 값이 반환값입니다.
return@label로 명시적으로 반환할 수도 있습니다. - 밑줄
_: Lambda 표현식의 매개변수를 사용하지 않으면 밑줄_를 매개변수 이름 대신 사용할 수 있습니다.map.forEach { (_, value) -> println(value) }
익명 함수
- 구문:
fun(매개변수: 타입): 반환 값 타입 { 함수 본문 } - 특징:
- 반환 타입을 명시적으로 지정할 수 있습니다.
return문은 이를 포함하는 함수가 아니라 익명 함수 자체에서 반환합니다(Lambda 표현식과 다름).
인라인 함수
핵심 개념
- 인라인 함수는 일반 함수처럼 호출하는 대신 컴파일러가 호출 지점에 함수 본문을 직접 삽입할 수 있게 하는 특수한 함수입니다. 이를 통해 함수 호출 오버헤드를 줄여, 특히 함수 호출이 매우 빈번한 경우 성능을 높일 수 있습니다.
인라인 함수가 필요한 이유는?
- 성능 최적화: 경우에 따라 함수 호출 오버헤드가 클 수 있으며, 특히 반복문에서 함수를 자주 호출할 때 그렇습니다. 인라인 함수는 이 오버헤드를 피해 코드 실행 효율을 높일 수 있습니다.
- 비지역 반환: 인라인 함수를 사용하면 Lambda 표현식에서
return문을 사용해 이를 포함하는 함수에서 직접 반환할 수 있어 매우 유용합니다.
인라인 함수 사용 방법
- 함수 정의 앞에
inline키워드를 추가합니다:
inline fun <T> lock(lock: Lock, body: () -> T): T { ... }
- 컴파일러가
lock()함수의 코드를 호출 지점에 직접 삽입하도록 시도합니다.
인라인 함수의 제한
- 성능 문제: 인라인 함수는 성능을 높일 수 있지만 너무 많이 사용하면 생성된 코드 크기가 커질 수 있습니다. 따라서大型 함수의 인lining을 피하고 적당히 사용해야 합니다.
- 비지역 반환: 인라인 함수에서는 Lambda 표현식이
return문을 사용해 이를 포함하는 함수에서 직접 반환할 수 있습니다. 매우 유용하지만 코드 논리가 복잡해질 수도 있습니다.
noinline 키워드
- Lambda 표현식 인자 중 일부만 인라인하고 싶다면
noinline키워드를 사용할 수 있습니다:
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { ... }
inlined인자는 인라인되지만notInlined는 인라인되지 않습니다.noinline타입의 Lambda 표현식은 일반 함수처럼 사용할 수 있으며 변수에 저장하거나 다른 함수에 전달할 수 있습니다.
비지역 반환
- 일반 Lambda 표현식에서는 Lambda 표현식이 외부 함수를 반환시킬 수 없으므로
return을 직접 사용할 수 없습니다. - 하지만 Lambda 표현식이 인라인되었다면
return을 사용해 이를 포함하는 Lambda 표현식의 외부 함수에서 직접 반환할 수 있습니다. 이를 비지역 반환이라고 합니다.
fun foo() {
inlined {
return // OK: the lambda is inlined, foo()에서 직접 반환
}
}
crossinline 키워드
- 인라인 함수가 다른 실행 컨텍스트(예: 지역 객체나 중첩 함수)에서 Lambda 표현식을 호출하면 해당 Lambda 표현식은 비지역 반환을 사용할 수 없습니다.
crossinline키워드로 이를 강제합니다:
break와 continue
- 인라인 함수에서는 Lambda 표현식이
break와continue를 사용해 이를 포함하는 반복문의 제어 흐름을 제어할 수 있습니다.
reified 키워드
- Kotlin에서는 제네릭이 런타임에 소거됩니다. 즉 함수 내부에서 구체적인 제네릭 타입에 접근할 수 없습니다.
reified키워드를 사용하면 인라인 함수에서 일반 클래스인 것처럼 제네릭 타입에 접근할 수 있습니다. 예를 들면 다음과 같습니다:
inline fun <reified T> TreeNode.findParentOfType(): T? { ... }
- 이를 사용하면
T::class,T is Type등의 작업을 반사 없이 사용할 수 있습니다. - 주의:
reified키워드는 인라인 함수에서만 사용할 수 있습니다.
인라인 속성
inline키워드는 속성의get()및set()메서드에 사용할 수 있습니다. 속성 전체를 인라인하거나 getter나 setter만 따로 인라인할 수 있습니다.
공개 API 인라인 함수의 제한
- 인라인 함수가 공개 API의 일부(
public또는protected)라면 다른 모듈에서 호출되고 그 안에서도 인라인됩니다. - 잠재적인 이진 호환성 문제를 피하기 위해 공개 API 인라인 함수는 공개되지 않은 API 선언(
private또는internal)을 사용할 수 없습니다. @PublishedApi注解를 사용하면 공개 API 인라인 함수에서internal선언을 사용할 수 있습니다.
연산자 오버로딩
핵심 개념
- 연산자 오버로딩을 사용하면 사용자 정의 타입에 연산자의 동작을 정의할 수 있습니다. 그러면 기본 타입을 다루는 것처럼
+,-,*,/같은 표준 연산자로 사용자 정의 타입을 다룰 수 있습니다.
연산자 오버로딩을 구현하는 방법
- Kotlin에서는 클래스에 특정 함수를 정의해 연산자 오버로딩을 구현합니다. 이 함수들은
operator키워드로 표시합니다.
단항 연산자
| 연산자 | 대응 함수 | 예 | 설명 |
|---|---|---|---|
+ | unaryPlus() | +a | 양수 부호(보통 아무것도 바꾸지 않음) |
- | unaryMinus() | -a | 부호 반전 |
! | not() | !a | 논리 부정(반전) |
예제
증가/감소 연산자(++, --)
a++(후위 증가): 먼저a의 원래 값을 반환한 다음a를 증가시킵니다.++a(전위 증가): 먼저a를 증가시킨 다음 새 값을 반환합니다.a--와--a도 같은 방식입니다.핵심:
inc()및dec()함수는 원래 객체를 직접 수정하지 말고 새 값을 반환해 변수에 할당해야 합니다.
이항 연산자
| 연산자 | 대응 함수 | 예 | 설명 |
|---|---|---|---|
+ | plus(b) | a + b | 덧셈 |
- | minus(b) | a - b | 뺄셈 |
* | times(b) | a * b | 곱셈 |
/ | div(b) | a / b | 나눗셈 |
% | rem(b) | a % b | 나머지(모듈러) |
.. | rangeTo(b) | a..b | 범위 생성(예: 1..10) |
..< | rangeUntil(b) | a..<b | 범위 생성(마지막 요소 제외) |
예제
data class Counter(val dayIndex: Int) {
operator fun plus(increment: Int): Counter {
return Counter(dayIndex + increment)
}
}
in 연산자
a in b:a가b에 있는지 검사합니다. 대응 함수:b.contains(a)a !in b:a가b에 없는지 검사합니다. 대응 함수:!b.contains(a)
인덱스 접근 연산자([])
a[i]: 배열/컬렉션의 요소에 접근합니다. 대응 함수:a.get(i)a[i] = b: 배열/컬렉션 요소의 값을 설정합니다. 대응 함수:a.set(i, b)
invoke 연산자(())
a(): 객체를 함수처럼 호출합니다. 대응 함수:a.invoke()
복합 대입 연산자(+=, -=, *=, /=, %=)
a += b:plusAssign함수가 없으면a = a + b와 같습니다.a.plusAssign(b)를 우선 사용합니다.- 다른 연산자도 비슷합니다.
같음/다름 연산자(==, !=)
a == b:a와b가 같은지 검사합니다. 대응 함수:a?.equals(b) ?: (b === null)a != b:a와b가 다른지 검사합니다. 대응 함수:!(a?.equals(b) ?: (b === null))
비교 연산자(>, <, >=, <=)
a > b:a와b의 크기를 비교합니다. 대응 함수:a.compareTo(b) > 0- 다른 연산자도 비슷합니다.
주의
===와!==(같은 객체인지 확인)는 오버로딩할 수 없습니다.
this 표현식
핵심 개념
this는 "현재 객체"를 뜻합니다. 다른 사람에게 말할 때 "나"로 자신을 가리키는 것과 같습니다.
this 사용법
- 클래스 안에서:
this는 현재 클래스의 객체를 뜻합니다. 예를 들어Person클래스에서this는 현재Person객체를 가리킵니다. - 확장 함수나 수신자가 있는 함수 리터럴에서:
this는 수신자 매개변수, 즉 점(.) 왼쪽에 있는 객체를 뜻합니다.
this의 대상
this가 하나뿐이면 일반적으로 가장 안쪽 범위를 가리킵니다. 하지만 외부 범위의 this에 접근하려면 한정된 this를 사용해야 합니다.
한정된 this
외부 범위(예: 외부 클래스, 확장 함수, 레이블이 지정된 함수 리터럴)에서 this에 접근하려면 this@label 형식을 사용할 수 있습니다.
@label은 범위의 레이블입니다.
예제
암묵적 this
this 객체에서 멤버 함수를 호출할 때 this. 부분을 생략할 수 있습니다.
주의
동일한 이름의 비멤버 함수가 있다면 this를 사용할 때 주의해야 합니다.某些 경우 비멤버 함수가 호출될 수 있기 때문입니다.
예제
注解
핵심 개념
-注解는 코드에 추가 정보를 붙이는 방법으로, 코드에 라벨을 붙이는 것과 같습니다. 이 정보는 컴파일러, 도구 또는 런타임 환경에서 사용할 수 있습니다.
注解를 선언하는 방법
class키워드 앞에annotation키워드를 추가합니다.
annotation class Fancy
메타注解
메타注解는注解의 동작을 지정하기 위해注解를 수정하는注解입니다.
@Target:注解를 사용할 수 있는 코드 요소(클래스, 함수, 속성 등)를 지정합니다.@Retention:注解가 컴파일된 class 파일에 저장되는지, 런타임에 반사로 접근할 수 있는지를 지정합니다. 기본적으로 둘 다true입니다.@Repeatable: 같은 코드 요소에 동일한注解를 여러 번 사용할 수 있게 합니다.@MustBeDocumented:注解가 공개 API의 일부이며 생성된 API 문서에 포함되어야 함을 지정합니다.
예제
注解 사용 방법
@Fancy class Foo {
@Fancy fun baz(@Fancy foo: Int): Int {
return (@Fancy 1)
}
}
注解 생성자
-注解에는 인수를 받는 생성자를 둘 수 있습니다.
annotation class Special(val why: String)
@Special("example") class Foo {}
허용되는 매개변수 타입
- Java 원시 타입(Int, Long 등)
- 문자열(String)
- 클래스(Foo::class)
- 열거형(Enum)
- 다른注解
- 위 타입들의 배열
주의
-注解 매개변수는 널 가능한 타입일 수 없습니다. JVM이 null을注解 속성 값으로 지원하지 않기 때문입니다.
注解를 인자로 사용하기
-注解를 다른注解의 인자로 사용하면 이름 앞에 @ 기호를 붙일 필요가 없습니다.
클래스를 인자로 사용하기
- 클래스를注解의 인자로 사용해야 한다면 Kotlin의
KClass를 사용합니다. Kotlin 컴파일러는 이를 자동으로 Java 클래스로 변환해 Java 코드가注解와 인자에 정상적으로 접근할 수 있게 합니다.
import kotlin.reflect.KClass
annotation class Ann(val arg1: KClass<*>, val arg2: KClass<out Any>)
@Ann(String::class, Int::class) class MyClass
注解 클래스 인스턴스화
- Kotlin에서는 일반 클래스의 생성자를 호출하는 것처럼注解 클래스의 생성자를 호출해 생성된 인스턴스를 사용할 수 있습니다.
Lambda 표현식에注解 사용
- Lambda 표현식에注解를 사용할 수 있습니다. 이注解는 Lambda 표현식에 생성된
invoke()메서드에 적용됩니다.
annotation class Suspendable
val f = @Suspendable { Fiber.sleep(10) }
사용 지점 대상
- 속성이나 주 생성자 매개변수에注解를 적용하면 여러 Java 요소가 생성되므로注解를 서로 다른 위치에 놓을 수 있습니다. 사용 지점 대상을 사용하면注解가 어떤 Java 요소에 생성되어야 하는지 정확히 지정할 수 있습니다.
일반적인 사용 지점 대상
file: 전체 파일property: 속성(Java에서는 볼 수 없음)field: 필드get: 속성 getterset: 속성 setterreceiver: 확장 함수나 속성의 수신자 매개변수param: 생성자 매개변수setparam: 속성 setter 매개변수delegate: 위임 속성의 위임 인스턴스 필드
동일한 대상을 가진 여러注解
- 여러注解이 동일한 사용 지점 대상을 가진다면 대괄호로 묶어 대상을 반복해서 지정하지 않아도 됩니다.
class Example {
@set:[Inject VisibleForTesting]
var collaborator: Collaborator
}
Java注解과의 호환성
- Kotlin은 Java注解과 100% 호환됩니다.
Java注解의 인자
- Java注解의 인자 순서는 정의되어 있지 않으므로 일반 함수 호출 구문으로 인자를 전달할 수 없습니다. 이름이 지정된 매개변수 구문을 사용해야 합니다.
// Java
public @interface Ann {
int intValue();
String stringValue();
}
// Kotlin
@Ann(intValue = 1, stringValue = "abc") class C
value 인자의 특수한 경우
- Java에서注解에
value라는 인자가 하나만 있으면 Kotlin에서는 인자 이름을 생략할 수 있습니다.
// Java
public @interface AnnWithValue {
String value();
}
// Kotlin
@AnnWithValue("abc") class C
배열을注解 인자로 사용
- Java에서注解의 인자 타입이 배열이면 Kotlin에서는
vararg매개변수가 됩니다.
// Java
public @interface AnnWithArrayValue {
String[] value();
}
// Kotlin
@AnnWithArrayValue("abc", "foo", "bar") class C
- 다른 배열 타입의 인자에는 배열 리터럴 구문이나
arrayOf(...)를 사용해야 합니다.
// Java
public @interface AnnWithArrayMethod {
String[] names();
}
@AnnWithArrayMethod(names = ["abc", "foo", "bar"])
class C
注解 인스턴스의 속성 접근
-注解 인스턴스의 값은 Kotlin에서 속성으로 노출됩니다.
// Java
public @interface Ann {
int value();
}
// Kotlin
fun foo(ann: Ann) {
val i = ann.value // value 속성에 직접 접근
}
반복 가능한注解
- Kotlin은 반복 가능한注解를 지원하므로 같은 코드 요소에 동일한注解를 여러 번 사용할 수 있습니다.注解 선언에
@kotlin.annotation.Repeatable메타注解를 표시해 Kotlin과 Java 모두에서 반복 가능하게 만듭니다.
@Repeatable
annotation class Tag(val name: String)
// 编译器会自动生成包含注解 @Tag.Container
@kotlin.jvm.JvmRepeatable메타注解를 사용해 명시적으로 선언한 포함注解 클래스를 인자로 전달함으로써 포함注解의 이름을 사용자 정의할 수 있습니다.
@JvmRepeatable(Tags::class)
annotation class Tag(val name: String)
annotation class Tags(val value: Array<Tag>)
- Kotlin 또는 Java의 반복 가능한注解를 반사로 추출하려면
KAnnotatedElement.findAnnotations()함수를 사용합니다.
구조 분해
핵심 개념
- 구조 분해 선언은 객체를 여러 변수로 분해해 쉽게 사용할 수 있게 하는 편리한 구문입니다.
구문
원리: componentN() 함수
- Kotlin은 일련의
componentN()함수를 호출해 구조 분해를 구현합니다. component1()은 첫 번째 변수에 대응합니다.component2()는 두 번째 변수에 대응합니다.- 같은 방식으로
component3(),component4()등이 이어집니다. - 따라서 객체에
componentN()함수가 있으면 구조 분해 선언을 사용할 수 있습니다.
val name = person.component1() // person의 첫 번째 속성과 같음
val age = person.component2() // person의 두 번째 속성과 같음
operator 키워드
componentN()함수는 구조 분해 선언에 사용할 수 있도록operator키워드로 표시해야 합니다.
활용 사례
for반복문: 컬렉션에서 요소를 편리하게 추출할 수 있습니다.
for ((a, b) in collection) {
// a와 b는 각각 컬렉션의 각 요소에 대한 component1() 및 component2()의 반환값
}
- 함수에서 여러 값 반환하기:
data class를 사용하면 여러 값을 편리하게 반환하고 직접 구조 분해할 수 있습니다.
- Map 순회:
for반복문에서 Map의 키-값 쌍을 직접 구조 분해할 수 있습니다.
필요 없는 변수 무시하기: 밑줄 _
- 구조 분해할 때 어떤 변수가 필요하지 않으면 밑줄
_를 변수 이름 대신 사용할 수 있습니다. Kotlin은 해당componentN()함수를 호출하지 않으므로 resources를 절약할 수 있습니다.
val (_, status) = getResult() // status만 필요하며 첫 번째 반환값은 무시
Lambda 표현식에서 구조 분해
- Lambda 표현식의 매개변수에서 구조 분해 선언을 사용해 코드를 더 간결하게 만들 수 있습니다.
Lambda에서 타입 선언
- 전체 구조 분해 매개변수나 개별 구성 요소에 타입을 지정할 수 있습니다.
클래스 참조, 함수 참조, 속성 참조, 생성자 참조 및 바인딩 참조
Kotlin은 여러 종류의 참조를 제공해 런타임에 프로그램 구조를 조작할 수 있게 합니다. 이러한 참조 유형을 이해하는 것은 더 유연하고 강력한 코드를 작성하는 데 중요합니다.
클래스 참조: ::class
- 역할: Kotlin 클래스의 런타임
KClass객체를 가져옵니다. - 구문:
ClassName::class - 타입:
KClass<ClassName> - 용도:
- 타입 검사(
is연산자) - 클래스 정보 가져오기(예: 이름, 생성자, 속성 등)
- Java 반사와 상호 운용(
.java속성으로java.lang.Class객체 가져오기)
- 타입 검사(
- 예제:
함수 참조: ::
- 역할: 함수의 참조를 가져와 함수 타입의 값처럼 사용할 수 있습니다.
- 구문:
::functionName - 타입:
(ParameterTypes) -> ReturnType또는KFunction<out R> - 용도:
- 함수를 고차 함수에 인자로 전달(예:
filter,map,reduce) - 함수 조합 생성
- 함수 동적 호출
- 함수를 고차 함수에 인자로 전달(예:
- 예제:
속성 참조: ::
- 역할: 속성의 참조를 가져와 속성 값과 메타데이터에 접근할 수 있습니다.
- 구문:
::propertyName - 타입:
KProperty0<Type>(최상위 속성 또는 수신자가 없는 속성)KProperty1<Receiver, Type>(수신자가 있는 속성)KMutableProperty...(변경 가능한 속성)
- 용도:
- 속성 값 동적으로 가져오기 및 설정
- 속성의 이름과 타입 접근
- 고차 함수에 사용(예:
map은 객체의 속성 값을 추출할 수 있음)
- 예제:
생성자 참조: ::ClassName
- 역할: 생성자의 참조를 가져와 클래스의 인스턴스를 동적으로 만들 수 있습니다.
- 구문:
::ClassName - 타입:
(ConstructorParameterTypes) -> ClassName또는KFunction<out ClassName> - 용도:
- 의존성 주입
- 객체 동적 생성
- 팩토리 패턴
- 예제:
바인딩 참조: instance::member
- 역할: 함수나 속성을 특정 객체 인스턴스에 바인딩합니다.
- 구문:
instance::memberName(memberName은 함수나 속성일 수 있음) - 타입: 바인딩된 멤버와 인스턴스에 맞게 조정되며 수신자 매개변수를 포함하지 않습니다.
- 용도:
- 객체 인스턴스를 매번 전달하지 않아 코드를 단순화합니다.
- 상태와 관련된 함수 참조를 생성합니다.
- 함수형 프로그래밍에서 객체의 특정 동작을 전달합니다.
- 예제:
핵심 차이 요약
| 특성 | 클래스 참조(::class) | 함수 참조(::) | 속성 참조(::) | 생성자 참조(::ClassName) | 바인딩 참조(instance::member) |
|---|---|---|---|---|---|
| 참조 대상 | 클래스 | 함수 | 속성 | 생성자 | 인스턴스와 멤버(함수 또는 속성) |
| 주요 용도 | 타입 정보, 상호 운용 | 함수형 프로그래밍 | 동적 접근 | 객체 동적 생성 | 코드 단순화, 상태 관련 |
| 바인딩 | 바인딩하지 않음 | 바인딩하지 않음 | 바인딩하지 않음 | 바인딩하지 않음 | 특정 객체 인스턴스에 바인딩 |
적절한 참조 유형 선택하기
- 클래스 자체의 정보를 조작해야 한다면 클래스 참조(
::class)를 사용합니다. - 함수를 인자로 전달하거나 동적으로 호출해야 한다면 함수 참조(
::functionName)를 사용합니다. - 속성 값을 동적으로 accessed하거나 속성 메타데이터를 가져와야 한다면 속성 참조(
::propertyName)를 사용합니다. - 클래스의 인스턴스를 동적으로 생성해야 한다면 생성자 참조(
::ClassName)를 사용합니다. - 함수나 속성을 특정 객체 인스턴스에 바인딩해야 한다면 바인딩 참조(
instance::member)를 사용합니다.