// Primary constructor classPersonconstructor(name: String) { // construct 키워드 명시 val name = name funhello() { println("my name is ${name}") } } // Primary constructor classPerson(name: String) { // construct 키워드 생략 가능 val name = name funhello() { println("my name is ${name}") } } ... // Primary constructor classPerson (val name: String) { // val 키워드 사용시 암시적으로 멤버필드로 사용 funhello() { println("my name is ${name}") } }
기본생성자에 initial 데이터를 추가 가능.
자동으로 기본생성자 사용시 initial 데이터로 초기화하도록 생성자를 여러개 생성함.
1 2 3 4 5 6 7 8 9 10 11 12
classPerson (val name: String = "kouzie") { funhello() { println("my name is ${name}") } }
funmain() { var human1 = Person("test") var human2 = Person() human1.hello() // my name is test human2.hello() // my name is kouzie }
아래와 같이 일부는 주 생성자, 일부는 내부 멤버변수로 정의할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13
classPerson(val name: String) { var age = 3 funprint() { println("${this.name}, ${this.age}") } } funmain() { // var person = Person("kouzie") // person.age = 4 // 직관적, 함축적인 이유로 `apply` 블록을 쓰는것을 권장한다. var person = Person("kouzie").apply { age = 4 } person.print() // kouzie, 4 }
보조 생성자: Secondary constructor
보조 생성자는 주 생성자를 호출하거나, 다른 보조 생성자를 호출해야만 한다.
주로 주 생성자 밖에 정의된 멤버변수를 초기화 할 때 같이 사용한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
classPerson(val name: String = "kouzie") {
var location: String = "-"
// Secondary constructor constructor(name: String, age: Int, loc: String) : this(name) { println("my name is ${name} and ${age} years old, loc is ${loc}") location = loc }
funhello() { println("my name is ${name}") } }
funmain() { var human = Person("test", 26) // my name is test and 26 years old }
kotlin은 모든 property 에 public 하게 접근 가능하다, 언뜻보면 객체지향의 캡슐화, 은닉 원칙을 무시하는 것처럼 보이지만 사실 바이트 코드로 변경되면서 모든 필드는 private 에 지원필드인 backing field(get, set) 를 사용하는 코드로 변경된다.
private 키워드로 정의한 경우만 getter 가 생성되지 않는다.
internal 키워드로 정의한 경우 kotlin 만의 가시성 키워드로 같은 모듈에서만 접근가능하다.
어차피 java 도 관례적으로 private 필드 생성후 [getter, setter] 코드를 생성함으로, 이런 보일러 코드를 없에기 위한 kotlin 의 원칙으로 보인다. kotlin 에선 정말 감추고 싶은 필드만 접근제어자를 통해 감추도록 한다.
kotlin 에선 정말 은닉해야할 데이터만 별도로 private 키워드를 사용하고 나머지는 public 하게 사용할 수 있도록 한다.
val message: List<String> get() { if (_message == null) { _message = loadMessage() } return _message!! }
privatefunloadMessage(): List<String>? { return listOf( "Initial contact", "Convinced them to use Kotlin", "Sold training class. Sweet." ).also { println("Loaded messages") } } }
funmain() { val c1 = Customer("kouzie").apply { messages } // Loaded messages, 지연로딩 println(c1.messages) // [Initial contact, Convinced them to use Kotlin, Sold training class. Sweet.] }
private setter 를 설정하고 싶으면 private set 키워드를 사용한다.
1 2 3 4 5 6 7 8 9
classTask(val name: String) { var priority = 3 privateset }
funmain() { var myTask = Task("kouzie") println(myTask.priority) }
엔티티 기능을 수행하는 클래스에서 set 기능을 제한하고 싶을 경우 아래와 같이 priavate 과 backing field 를 사용하면 자연스러움.
@Document(collection = "history") dataclassHistory ( @field:Id val historyId: String? = null, @field:Field("code") privatevar _code: Int = 0, @field:Field("status") privatevar _status: Int = 100, ) { // 읽기 전용 public 프로퍼티 val code: Int get() = _code val status: Int get() = _status
funupdateStatus(status: Int) { if (this.status <= status) this._status = status } }
companion object(동반 객체)
static 형식의 컴파일타임에 정의되는 상수와 메서드를 정의할 때 사용
const 키워드를 사용해 상수를 생성한다.
java 의 static final 과 비슷 kotlin 에선 root 레벨 블록이나 companion object 에서만 사용 가능
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
classBookprivateconstructor(val id: Int, val name: String) { companionobject { constval myBook = "DEFAULT_BOOK" funcreate(): Book = Book(100, "animal farm") }
funmain() { var a = 3; var b = a.pow(2); println(b) // 9 }
확장함수를 통해 클래스에 메서드를 추가한것처럼, 확장속성을 통해 클래스에 마치 속성이 추가된것 처럼 구성할 수 있다.
아래의 경우 area 를 설정하면 radius 를 해당 area 에 맞춰서 변경하도록 설정했다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
dataclassCircle(val cx: Int, val cy: Int, var radius: Double)
// 확장속성 var Circle.area: Double get() = PI * radius * radius set(value) { val radius = sqrt(value / PI) this.radius = radius }
funmain() { val circle = Circle(100, 100, 10.0) println("Area is ${circle.area}") // Area is 314.1592653589793 println("Radius is ${circle.radius}") // Radius is 10.0 circle.area = 10.0 println("Area is ${circle.area}") // Area is 10.000000000000002 println("Radius is ${circle.radius}") // Radius is 1.7841241161527712 }
보다싶이 물리적으로 필드를 추가했다기 보단 추가한것 처럼 보이는 기법이라 할 수 있다.
reflection 을 사용해 toMap 기능을 지원하도록 확장함수 추가.
1 2 3 4 5 6
// 확장 함수 정의 fun Any.toMap(): Map<String, Any?> { returnthis::class.memberProperties.associate { property -> property.name to property.getter.call(this) } }
use
Kotlin 표준 라이브러리에서 제공되는 Closeable 인터페이스 구현클래스의 확장함수이다.
1 2 3
@InlineOnly publicinlinefun<T : Closeable?, R> T.use(block: (T) -> R): R { ... } // T 를 받아 R 을 반환하는 역할 수행
1 2 3 4 5 6 7 8 9 10 11 12 13
funmain() { val result = FileWriter("output.txt").use { writer -> writer.write("Hello, Kotlin!") "success" } // writer.close()가 자동 호출됨 }
funmain() { val content = BufferedReader(FileReader("input.txt")).use { reader -> reader.readLine() // 첫 번째 줄을 읽고 반환 } println(content) }
funmain() { top { println("In outer lambda $this and $length") // In outer lambda hello and 5 nested { println("in inner lambda $this and ${toDouble()}") // in inner lambda -2 and -2.0 println("from inner through receiver of outer: $length") // from inner through receiver of outer: 5 println("from inner to outer receiver ${ this@top }") // from inner to outer receiver hello } } }
정의된 확장함수의 리시버가 어떤 객체를 가리키는지 확인할 수 있다. $length 키워드의 경우 현재 스코프의 리시버에 필드가 없기에 바로 부모 스코프의 리시버를 찾아간다.
코드구조상 스코프의 깊이가 끝없이 깊어질 수 있는데 this@... 키워드를 통해 직접 지정할 수 있다.
클래스 상속
기본적으로 kotlin 에서 class 는 항상 더이상의 상속이 진행되지 않도록 final 클래스로 정의된다.
만약 클래스 상속을 하고싶다면 다음처럼 open 키워드로 상속 가능한 클래스임을 알려야 한다.
// 이후에 더이상 toString 메서드를 override 하지 못하도록 final 설정 finaloverridefuntoString() = "year: $year, Color: $color, KM: $km"
// repain 역시 기본 final 임으로 override 하지 못함 funrepaint(newColor: String) { color = newColor } }
openclassCar(year: Int, color: String) : Vehicle(year, color) { overridevar km: Int = 0 set(value) { if (value < 1) throw RuntimeException("can't set negative value") field = value }
fundrive(distance: Int) { km += distance } }
funmain() { val car = Car(2019, "Blue") car.drive(10) car.repaint("Red") println(car) // year: 2019, Color: Red, KM: 10 }
open 키워드를 property 에도 적용가능한데, 각 클래스에 해당 필드는 각각 존재하지만 [getter, setter] 가 자동으로 override 되면서 필드를 상속한것 처럼 사용할 수 있다.
sealed class 도 open class 클래스처럼 상속가능한 키워드이다. 하지만 동일한 파일에 작성된 클래스에서만 상속을 지원함으로 정확히 어떤 클래스들이 sealed class 를 상속하고 있는지 한눈에 파악할 수 있다.
Any, Unit, Nothing
java 에서 모든 객체의 최상위 부모객체가 Object 인것처럼 kotiln 에선 모든 객체의 최상위 부모객체가 Any 이다.
Any 클래스는 너무 많은 유연성을 제공하다 보니 사용을 자제하는 것이 좋다.
1 2 3 4 5 6 7 8 9
publicopenclassAny { publicopenoperatorfunequals(other: Any?): Boolean publicopenfunhashCode(): Int publicopenfuntoString(): String }
...
val str: Any = "Hello, World!"
kotlin 에선 반환값이 없을때 void 대신 Unit 객체를 반환한다. Unit 은 싱글톤 인스턴스로 void 를 포함한 모든 데이터를 객체로 표기하기 위한 kotlin 의 의지가 보이는 클래스이다.
1 2 3 4 5 6 7 8 9 10 11 12 13
// public object Unit { // override fun toString() = "kotli.unit" // }
funreturnVoid(): Unit { // 명시적 반환 생략 가능 println("hello world") return }
funmain() { val u = returnVoid() // hello world println(u) // kotlin.Unit }
정말 반환하지 않는 함수를 표현하기 위해서는 Nothing 클래스를 사용한다. 무한반복이나 Exception 이 발생할 경우 반환값이 Nothing 이 된다.
1 2 3
fundoNothing(): Nothing { throw Exception("Nothing at all") }
변수에 null 을 할당하면 Nothing? 타입으로 추론된다.
1 2
val n = null// Nothing? type println(n isNothing?)
또한 if...else 문을 사용하여 아래와같이 반환할 때에도 Nothing 을 사용하며 그다음 줄부턴 컴파일러가 해당 변수를 String 으로 추론한다.
1 2 3 4
// true 일경우 문자열, false 일경우 Nothing 반환 val x = if (Random.nextBoolean()) "T"elsethrow Exception("F") // 이후 코드부턴 String? 으로 추론 println(x::class.simpleName)
interface
kotlininterface 는 default 키워드 없이 메서드 정의가 가능하다. 실제로 컴파일된 바이트코드를 보면 java 의 default 키워드를 사용하기 보단 클래스를 추가적으로 생성해 doubleUp 역할을 하는 static 메서드를 운영하는 방식으로 구성된다. 만약 바이트 코드를 생성시 default 키워드를 사용하는 방식으로 하고 싶다면 빌드옵션에 -Xjvm-default=all 을 적용하면 된다.
java 에선 하나의 타입제약만 가능하지만 kotlin 에선 where 키워드를 사용하면 여러 타입에 대해 제약설정할 수 있다.
1 2 3 4 5
// AutoCloseable, Appendable 다중 상속된 객체만 입력 가능 fun<T>useAndAppendAndClose(input: T)where T : AutoCloseable, T : Appendable { input.append() input.close() }
아래는 java 의 와일드카드(?) 역할을 하는 스타 프로젝션(*) 문법이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// java 와일드 카드 문법 interfaceFruit { }
classBananaimplementsFruit { }
publicclassTestMainJava {
publicstaticvoidreceiveFruits(List<?> fruits) { System.out.println("Number of fruits:" + fruits.size()); }
// 델리게이션을 위한 키워드 by classSmartPhone( privateval phone: Dialable = Phone(), privateval camera: Snappable = Camera() ) : Dialable by phone, Snappable by camera {
@NotNull public String takePicture() { returnthis.camera.takePicture(); } }
다음과 같이 by 키워드를 사용해 객체 필드명과 map 의 key 값이 일치할 경우 value 에 존재하는 객체를 위임한다.
1 2 3 4 5 6 7 8 9 10 11
dataclassProject(val map: MutableMap<String, Any?>) { val name: String by map var priority: Intby map var completed: Booleanby map }
funmain() { var map: MutableMap<String, Any?> = mutableMapOf("name" to "Learn Kotlin", "priority" to 5, "completed" to true) var p = Project(map) println(p) // Project(map={name=Learn Kotlin, priority=5, completed=true}) }
변수 델리게이션
변수에 by 키워드로 위임자 설정을 하면 [get, set] 에 대한 접근을 모두 가로챌 수 있다.
[operator fun getValue, operator fun setValue], operator 키워드로 메서드를 객체 내부에 정의한다.
funmain() { var comment: String by PoliteString("Some nice message") println(comment) // Some nice message comment = "This is stupid" println(comment) // This is s***** println("comment is of length: ${comment.length}") // comment is of length: 37 }
java 에는 존재하지 않는 문법이다 보니 당황스럽지만 컴파일된 바이트코드를 뜯어보면 대략 아래와 같다.
overridefunsetValue(thisRef: Example, property: KProperty<*>, value: String) { this.value = value } }
classExample { val readOnly by MyReadDelegate("Initial ReadOnly Value") var readWrite by MyReadWriteDelegate }
funmain() { val e = Example() val ro: String = e.readOnly println(ro) e.readWrite = "NEW" println(e.readWrite) }
LongDelegate
DoubleDelegate
BigIntegerDelegate
PropertyDelegateProvider
델리게이션 생성시 추가로 커스텀한 로직을 추가하여 동적으로 델리게이션 생성이 가능하다. 입력 property 의 조건에 따라 다른 델리게이션을 반환해야 하는 등의 작업 수행이 가능하다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
@SinceKotlin("1.4") publicfuninterface PropertyDelegateProvider<in T, out D> { /** * Returns the delegate of the property for the given object. * * This function can be used to extend the logic of creating the object (e.g. perform validation checks) * to which the property implementation is delegated. * * @param thisRef the object for which property delegate is requested. * @param property the metadata for the property. * @return the property delegate. */ publicoperatorfunprovideDelegate(thisRef: T, property: KProperty<*>): D }
위의 변수 델리게이션 생성때와 마찬가지로 getValue, setValue 메서드를 operator 로 지정해줘도 되지만 확실한 가독성을 위해 PropertyDelegateProvider 사용을 권장.
overridefungetValue(thisRef: Any?, property: KProperty<*>): String { return value // 저장된 값을 반환 }
overridefunsetValue(thisRef: Any?, property: KProperty<*>, value: String) { println("Setting value '${value}' to ${property.name}") this.value = value // 값을 설정 } }
classDelegateProvider : PropertyDelegateProvider<Any?, StringDelegate> { overridefunprovideDelegate(thisRef: Any?, property: KProperty<*>): StringDelegate { println("Providing delegate for ${property.name}") return CustomDelegate() // 커스텀 delegate 반환, 대리자 생성시 별도의 추가 로직 수행 가능 } }
classExample { var exampleProperty by DelegateProvider() // DelegateProvider를 통해 delegate를 초기화 }
funmain() { val example = Example() // Providing delegate for exampleProperty println(example.exampleProperty) // Initial Value example.exampleProperty = "New Value"// Setting value 'New Value' to exampleProperty println(example.exampleProperty) // New Value }
val message: List<String> by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED, initializer = { mutableListOf("hi", "hello", "by") .also { println("initial complete") } })
SYNCHRONIZED(defaulit): 오직 하나의 스레드만 Lazy 인스턴스를 초기화할 수 있게 락을 사용 PUBLICATION: 초기화 함수가 여러 번 호출될 수 있지만 첫 번째 리턴값만 사용됨 NONE: 락이 사용되지 않음
아래와 같이 if 문의 단축 평가같은 상황에서도 lazy 키워드를 사용하면 효율적인 코드작성이 가능하다. 실제 temperature 변수 참조가 이루어지기 전까지는 getTemperature 메서드가 호출되지 않는다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
fungetTemperature(city: String): Double { println("fetch from webservice for $city") return30.0 }
funmain() { // val temperature = getTemperature("seoul") val temperature: Doubleby lazy { getTemperature("seoul") } val showTemperature = false if (showTemperature && temperature > 20) println("Warm") else println("Nothing to report") }
// 출력값 // Nothing to report
observable, vetoable, notNull 델리게이션
변경 감지에는 observable 변경 감지 + 적용 여부를 결정에는 vetoable
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
funmain() { // init by 1 var watched: Intby Delegates.observable(1) { prop, old, new -> println("${prop.name} changed from $old to $new") }
// init by 0 var checked: Intby Delegates.vetoable(2) { prop, old, new -> println("Trying to change ${prop.name} from $old to $new") new >= 0 }
watched = 10// watched changed from 1 to 10 checked = -10// Trying to change checked from 2 to -10 println(watched) // 10 println(checked) // 2 }
checked 변수의 경우 vetoable 적용 여부 조건에 부합하지 않아 값이 변경되지 않았다.
observable, vetoable 도 ObservableProperty 래핑클래스로 관리되며 primitive type 도 체크 가능하다.
객체 컨텍스트 안에서 코드 블록을 실행할 목적으로 만든 영역함수(scope function) 가 있다. 영역함수 를 사용하면 반복적이고 무의미한 객체 접근 코드를 생략해서 코드노이즈를 줄일 수 있다.
1 2 3 4
inlinefun<T> T.apply(block: T.() -> Unit): T inlinefun<T, R> T.run(block: T.() -> R): R inlinefun<T> T.also(block: (T) -> Unit ): T inlinefun<T, R> T.let(block: (T) -> R ): R
내부의 T.() -> R 과 같은 형식이 낯설지만 제너릭 타입 T 의 람다 확장함수 라 할 수 있다.
일부 영역함수 들은 대체 가능할 정도로 기능이 겹치는 경우가 많다. 기능은 비슷하지만 위 url 에서 어떤 상황에서 어떤 영역함수를 사용해야하는지 불문율? 같은 권장사항을 작성해두었다.
사용하는 상황을 요약하면 아래와 같다.
apply 는 객체의 속성을 초기화할 때 사용
also 는 객체를 가공하지 않고 부가적인 코드를 실행할 때 사용
run 원하는 결과값을 생성하고 반환할 때 주로 사용.
let분기에 따라 다른값을 반환, null 체크에 많이 사용.
apply, also
apply 블록 내부에서 객체 컨텍스트 를 가져와 속성을 초기화하거나 메서드를 호출할 때 사용한다.
1 2 3 4 5 6 7 8
funmain() { val person = Person() .apply { name = "test" age = 29 } println(person) // Person(name=test, age=29) }
also 블록 내부에서 함수 컨텍스트 를 가져와 코드 흐름을 방해하지 않고 부가적인 코드를 실행시킬 때 사용한다.
1 2 3 4 5 6 7 8 9
funmain() { val person = Person() .apply { this.name = "test" this.age = 29 } .also { ps -> println(ps) } // Person(name=test, age=29) .also { println("my name is ${it.name} and ${it.age}") } // my name is test and 29 }
run, let
객체를 유지하지 않고 표현식의 결과를 유지하고 싶을 때 run영역함수를 사용한다.
1 2 3 4 5 6 7 8
funmain() { val greetMsg: String = Person().run { name = "hello" age = 29 "my name is $name and $age" } println(greetMsg) // my name is hello and 29 }
let 영역함수는 주로 아래와 같이 조건별 분기처리, :? 연산자와 같이 사용해 null 조건을 처리하는데 많이 사용한다.
funmain() { val p = Person("test", 20) with(p) { println("my name is $name and $age") } }
with 은 표현식을 반환하지만 also 처럼 코드 흐름을 방해하지 않고 부가적인 코드를 실행시 사용한다. 대부분 반환할 표현식을 with 코드안에 적지 않는다.
어노테이션
1 2 3 4 5 6 7 8
classExample( @field:Ann val foo: String, @get:Ann val bar: String, @param:Ann val quux: String )
생성자에서 정의한 필드에 대해 getter, setter 가 자동 생성되다 보니 어노테이션이 어디에 위치해야 할지 명시해줘야 한다.
@field: - 필드에 적용 @get: - getter 메서드에 적용 @param: - 생성자 파라미터에 적용
DSL
DSL(Domain Specific Languages: 도메인 특화언어) 는 컨텍스트 주도적(context driven) 패턴을 가진 언어이다. DSL 내부에서 같은 컨텍스트를 공유하여 유창하고 간결하게 코드를 작성할 수 있다. 컨테스트 공유과정에서 에러발생 확률도 줄일 수 있다.
kotlin 은 개발초기부터 간결한 함수표현식, 리터럴표현식, infix 표현식, 영역함수 등을 제공하여 코드노이즈를 최소화 하고 DSL 패턴을 사용하기 쉽게 구성된 언어이다.