Kotlin - 클래스!
클래스
java
의 경우 바이트코드로 변환되고 디컴파일 했을 때 다시 생성되는 java
코드가 상당부분 유사하다.
kotline
의 경우 바이트코드로 변환하고 디컴파일 했을 때 생성되는 java
코드를 보면 마음대로 풀어서 쓴 느낌이다.
특히 kotlin
클래스 문법에서 암시적으로 생략가능한 부분, java
에서 지원하지 않는 특이한 문법들이 많다.
아래 처음보는 형식의 클래스 정의문을 하나씩 살펴보도록 한다.
// 기본생성자 (primary constructor) constructor 키워드생략 가능
class Person (name: String) {
// 멤버변수
private val firstProperty = "First property: $name"
// 보조 생성자 (Secondary constructor)
constructor(i: Int) : this(i.toString()) {
println("Secondary constructor block that prints $i")
}
// 생성자 초기화 init 블럭
init {
println("First initializer block that pr-ints $name")
}
fun printName(): String = this.firstProperty
}
주 생성자: Primary constructor
// Primary constructor
class Person constructor(name: String) { // construct 키워드 명시
val name = name
fun hello() {
println("my name is ${name}")
}
}
// Primary constructor
class Person(name: String) { // construct 키워드 생략 가능
val name = name
fun hello() {
println("my name is ${name}")
}
}
...
// Primary constructor
class Person (val name: String) { // val 키워드 사용시 암시적으로 멤버필드로 사용
fun hello() {
println("my name is ${name}")
}
}
기본생성자에 initial
데이터를 추가 가능.
자동으로 기본생성자 사용시 initial
데이터로 초기화하도록 생성자를 여러개 생성함.
class Person (val name: String = "kouzie") {
fun hello() {
println("my name is ${name}")
}
}
fun main() {
var human1 = Person("test")
var human2 = Person()
human1.hello() // my name is test
human2.hello() // my name is kouzie
}
아래와 같이 일부는 주 생성자, 일부는 내부 멤버변수로 정의할 수 있다.
class Person(val name: String) {
var age = 3
fun print() {
println("${this.name}, ${this.age}")
}
}
fun main() {
// var person = Person("kouzie")
// person.age = 4
// 직관적, 함축적인 이유로 `apply` 블록을 쓰는것을 권장한다.
var person = Person("kouzie").apply { age = 4 }
person.print() // kouzie, 4
}
보조 생성자: Secondary constructor
보조 생성자는 주 생성자를 호출하거나, 다른 보조 생성자를 호출해야만 한다.
주로 주 생성자 밖에 정의된 멤버변수를 초기화 할 때 같이 사용한다.
class Person(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
}
fun hello() {
println("my name is ${name}")
}
}
fun main() {
var human = Person("test", 26) // my name is test and 26 years old
}
init 블럭
생성하면서 특정 코드를 클래스 블럭 내부에서 실행하고 싶다면 init
블럭 사용
직관적인 코드 작성이 불가함으로
init
블럭은 사용하지 않는것을 권장한다.
class Person(val name: String = "kouzie") {
private var age: Int? = null
constructor(name: String, age: Int) : this(name) {
this.age = age;
println("my name is $name and $age years old")
}
init {
println("my name is $name")
}
init {
println("my age is ${age ?: "unknown"}")
}
}
fun main() {
var human = Person("test", 26)
// my name is test
// my age is unknown
// my name is test and 26 years old
}
init
블록은 여러개 정의될 수 있고 보조생성자
보다 먼저 실행된다. 그리고 위와 같이 기본생성자에 정의된 멤버변수는 가져와서 사용할 수 있다.
디컴파일 해보면 init
블럭 내부의 코드가 각 생성자 코드 사이사이에 삽입되어있다.
public Person(@NotNull String name) {
Intrinsics.checkNotNullParameter(name, "name");
super();
this.name = name;
String var2 = "my name is " + this.name;
System.out.println(var2);
StringBuilder var10000 = (new StringBuilder()).append("my age is ");
Integer var3 = this.age;
var2 = var10000.append(var3 != null ? var3 : "unknown").toString();
System.out.println(var2);
}
// $FF: synthetic method
public Person(String var1, int var2, DefaultConstructorMarker var3) {
if ((var2 & 1) != 0) {
var1 = "kouzie";
}
this(var1);
}
private, protected, internal, public
kotlin
은 모든 property
에 public
하게 접근 가능하다, 언뜻보면 객체지향의 캡슐화, 은닉 원칙을 무시하는 것처럼 보이지만
사실 바이트 코드로 변경되면서 모든 필드는 private
에 지원필드인 backing field(get, set)
를 사용하는 코드로 변경된다.
private
키워드로 정의한 경우만getter
가 생성되지 않는다.internal
키워드로 정의한 경우kotlin
만의 가시성 키워드로 같은 모듈에서만 접근가능하다.
어차피 java
도 관례적으로 private
필드 생성후 [getter, setter]
코드를 생성함으로, 이런 보일러 코드를 없에기 위한 kotlin
의 원칙으로 보인다.
kotlin
에선 정말 감추고 싶은 필드만 접근제어자를 통해 감추도록 한다.
kotlin
에선 정말 은닉해야할 데이터만 별도로 private
키워드를 사용하고 나머지는 public
하게 사용할 수 있도록 한다.
클래스 내부에서만 사용되는 property
는 private
으로 정의하는것을 권장한다.
open class TestClass {
private val var1 = 100
protected val var2 = 100
internal val var3 = 100
public val var4 = 100
val var5 = 100
}
java
코드로 디컴파일하면 아래와 같다.
public class TestClass {
private final int var1 = 100;
private final int var2 = 100;
private final int var3 = 100;
private final int var4 = 100;
private final int var5 = 100;
protected final int getVar2() { return this.var2; }
public final int getVar3$kotlin_gradle() { return this.var3; }
public final int getVar4() { return this.var4; }
public final int getVar5() { return this.var5;}
}
backing field(지원 필드)
Backing field
는 kotlin
에서 프로퍼티의 값을 저장하는 데 사용되는 필드를 의미한다.
만약 특정 조건을 통해 내부 필드 정의를 해야한다면 지원필드인 backing field
를 재정의 하면 된다.
예로 특정 필드의 값이 반드시 1~5
사이의 값이어야 할 경우 아래와 같이 지정한다.
backing field
블록 내부에선field
키워드를 통해property
에 접근할 수 있다.
private field + backing field
를 포함하여property
라 부른다.
class Task(val name: String) {
var priority = 3
set(value) {
field = value.coerceIn(1, 5)
}
fun print() {
println("${this.name}, ${this.priority}")
}
}
fun main() {
var myTask = Task("kouzie")
myTask.priority = 7
myTask.print() // kouzie, 5
myTask.apply { priority = -1 }
myTask.print() // kouzie, 1
}
또한 실제 값이 설정되어 있지 않는 property
라 하더라도 backing field
정의가 가능하다.
val
로 property를
지정하고 get
을 정의하면된다.
원래
val
은 최상위property
로 사용하려면 초기화를 해야하지만backing field
로 사용할거면 상관없다.
실제 디컴파일된 코드도getter
함수만 정의되어 있다.
class Rectangle(val width: Int, val height: Int) {
val area: Int
get() = width * height // backing field가 없음
}
class Task(val name: String) {
var priority = 3
set(value) {
field = value.coerceIn(1, 5)
}
private val isLowPriority: Boolean
get() {
return priority < 3
}
fun print() {
println("${this.name}, ${this.priority}, ${this.isLowPriority}")
}
}
fun main() {
var myTask = Task("kouzie").apply { priority = 1 }
myTask.print() // kouzie, 1, true
}
다음과 같은 지연로딩 비슷한 기능을 구현할 때 아래와 같이 사용할 수 있다.
backing field
용 val message: List<String>
정의,
초기화 되지 않는 val
변수에 backing field
만 정의되어 있으면 getter
만 생성되는 특성을 사용해 지연로딩 기능을 구현한다
지연로딩은
null
처리를 하기위한 방법론
class Customer(val name: String) {
private var _message: List<String>? = null
val message: List<String>
get() {
if (_message == null) {
_message = loadMessage()
}
return _message!!
}
private fun loadMessage(): List<String>? {
return listOf(
"Initial contact",
"Convinced them to use Kotlin",
"Sold training class. Sweet."
).also { println("Loaded messages") }
}
}
fun main() {
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
키워드를 사용한다.
class Task(val name: String) {
var priority = 3
private set
}
fun main() {
var myTask = Task("kouzie")
println(myTask.priority)
}
companion object(동반 객체)
static
형식의 컴파일타임에 정의되는 상수와 메서드를 정의할 때 사용
const
키워드를 사용해 상수를 생성한다.
java
의static final
과 비슷 kotlin 에선root 레벨
블록이나companion object
에서만 사용 가능
class Book private constructor(val id: Int, val name: String) {
companion object {
const val myBook = "DEFAULT_BOOK"
fun create(): Book = Book(100, "animal farm")
}
override fun toString(): String {
return "Book(id=$id, name='$name')"
}
}
fun main() {
val book = Book.create();
println(Book.Companion.myBook) // DEFAULT_BOOK
println(Book.myBook) // DEFAULT_BOOK, companion object 접근 키워드 생략 가능
println(book) // Book(id=444, name='animal farm')
}
companion object
도 클래스이기 때문에 이름을 지정하거나 interface
의 상속 및 구현 가능하다.
interface IdProvider {
fun getId(): Int
}
class Book private constructor(val id: Int, val name: String) {
companion object BookFactory : IdProvider{
const val myBook = "DEFAULT_BOOK"
override fun getId(): Int {
return 444
}
fun create(): Book = Book(getId(), "animal farm")
}
override fun toString(): String {
return "Book(id=$id, name='$name')"
}
}
컴파일된 바이트코드를 보면 companion object
를 위한 static final
클래스가 선언되고
해당 클래스를 위한 싱글톤 객체로 생성하여 상수타입의 필드나 메서드에 접근한다.
data class - dto 클래스
데이터를 담는 클래스로 사용, toString
, hashCode
, equals
, copy(얕은복사)
, componentN
등 데이터 처리용 함수들이 자동생성됨
data class Product constructor(
val name: String,
val price: Double = 0.0,
val desc: String? = null
)
필드에 실수타입 있다면
closeTo
함수를 사용해 비교한다.
디스트럭쳐링 문법도 제공한다.
fun main() {
val p1 = Product("product1", 2000.0, true)
val (name, price) = p1;
println("$name $price") // product1 2000.0
}
value class
kotlin 1.5
부터 사용 가능
하나의 멤버변수를 래핑해서 사용할 수 있도록 하는 클래스, 내부적으로 불변 클래스이며 런타임 시 객체로 박싱되지 않고 인라인 코드로 풀어져서 실행됨으로 성능적으로 효과적이다.
@JvmInline
value class Age(val age: Int) { // 하나의 멤버변수만 할당 가능
fun printAge() {
println(this.age)
}
}
fun main() {
val age1 = Age(123)
val age2 = Age(123)
println(age1 == age2) // true, 값이 동일하면 동일한 객체로 간주
println(age1.age) // 123, 직접 프로퍼티에 접근 가능
}
@JvmInline
@Metadata(...)
public final class Age {
private final int age;
public final int getAge() {
return this.age;
}
public static final void printAge_impl/* $FF was: printAge-impl*/(int arg0) {
System.out.println(arg0);
}
public static String toString_impl/* $FF was: toString-impl*/(int arg0) {
return "Age(age=" + arg0 + ')';
}
public String toString() {
return toString-impl(this.age);
}
public static int hashCode_impl/* $FF was: hashCode-impl*/(int arg0) {
return Integer.hashCode(arg0);
}
public int hashCode() {
return hashCode-impl(this.age);
}
public static boolean equals_impl/* $FF was: equals-impl*/(int arg0, Object other) {
if (!(other instanceof Age)) {
return false;
} else {
int var2 = ((Age)other).unbox-impl();
return arg0 == var2;
}
}
public boolean equals(Object other) {
return equals-impl(this.age, other);
}
// $FF: synthetic method
private Age(int age) {
this.age = age;
}
public static int constructor_impl/* $FF was: constructor-impl*/(int age) {
return age;
}
// $FF: synthetic method
public static final Age box_impl/* $FF was: box-impl*/(int v) {
return new Age(v);
}
// $FF: synthetic method
public final int unbox_impl/* $FF was: unbox-impl*/() {
return this.age;
}
public static final boolean equals_impl0/* $FF was: equals-impl0*/(int p1, int p2) {
return p1 == p2;
}
}
// AgeKt.java
import kotlin.Metadata;
@Metadata(...)
public final class AgeKt {
public static final void main() {
int age1 = Age.constructor-impl(123);
int age2 = Age.constructor-impl(123);
boolean var2 = Age.equals-impl0(age1, age2);
System.out.println(var2);
System.out.println(age1);
}
// $FF: synthetic method
public static void main(String[] args) {
main();
}
}
object class - 싱글톤
일반적으로 싱글톤 패턴은 생성자를 private
으로 설정하고 static
으로 정의된 정적 팩토리 메서드를 사용해 구현한다.
object
키워드로 클래스를 정의하면 별도 작업없이 싱글톤 클래스 정의가 가능하다.
object ProductFactory {
var products = mutableListOf<Product>()
fun makeProduct(name: String, price: Double, onSale: Boolean): Product {
val p = Product(name, price, onSale)
products.add(p)
return p
}
}
fun main() {
val pf = ProductFactory;
var p1 = pf.makeProduct("pencil", 1000.0, false);
println(p1) // Product(name=pencil, price=1000.0, onSale=false)
println(pf.products) // [Product(name=pencil, price=1000.0, onSale=false)]
}
생성된 바이트코드를 디컴파일하면 아래와 같다.
static
영역에서 생성한 ProductFactory.INSTANCE
를 사용한다.
public final class ProductFactory {
@NotNull
private static List products;
@NotNull
public static final ProductFactory INSTANCE;
static {
ProductFactory var0 = new ProductFactory();
INSTANCE = var0;
products = (List)(new ArrayList());
}
@NotNull
public final List getProducts() {
return products;
}
public final void setProducts(@NotNull List var1) {
Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
products = var1;
}
@NotNull
public final Product makeProduct(@NotNull String name, double price, boolean onSale) {
Intrinsics.checkParameterIsNotNull(name, "name");
Product p = new Product(name, price, onSale);
products.add(p);
return p;
}
private ProductFactory() {
}
}
object
키워드가 편하긴 하지만 최초생성 시점을 지정할 수 없는점
그로인해 생성자에 인자를 넘길 수 없는점 이 단점이다.
inline class
단순 데이터 래핑용으로 클래스 정의를 하는 경우가 있다. 만약 단 하나의 필드를 위한 래핑 클래스를 정의해야할 경우 inline class
사용을 권장한다.
inline class
는 생성자로 단 하나의 값만 받을 수 있다.
물론 클래스 내에 프로퍼티와 함수를 정의할 수도 있다.
래핑용 클래스가 많아지면 new
연산자가 필요 이상으로 호출되고 힙 영역에 악영향을 끼치게 되는데
아래와 같이 inline class
를 사용하면 컴파일된 바이트 코드에서 new
연산자를 호출하지 않는다.
inline class SSN (val id: String)
fun receiveSSN(ssn: SSN) {
println("Received $ssn")
}
fun main() {
var ssn = SSN("1234567-1111111")
receiveSSN(ssn) // Received SSN(id=1234567-1111111)
println(ssn.id) // 1234567-1111111
}
컴파일된 바이트코드를 보면 final static class
로 SSN
클래스가 정의되고
ssn
객체가 사용되는 영역은 래핑한 원시타입 필드 혹은 static
함수 호출로 변경된다.
아래와 같이 inline class
내부에 init
블록을 통한 검증코드를 추가하거나 별도의 property
추가가 가능하다.
inline class SSN(val id: String) {
init {
val isContainBar: Boolean = (id.contains("-"))
val firstBlockIsSix: Boolean = id.split("-")[0].length == 6
val secondBlockIsSeven: Boolean = id.split("-")[1].length == 7
if (!(isContainBar && firstBlockIsSix && secondBlockIsSeven)) {
throw IllegalArgumentException("invalid id value")
}
}
val isMan: Boolean
get() {
val start: Char = id.split("-")[1][0]
return if (start == '1' || start == '3') true else false
}
}
내포클래스, 이너클래스, 지역클래스
class Outer {
private val bar: Int = 1
// 내포클래스
class Nested {
private val nestVar = 999
fun foo(): Int {
// return nestVar + bar // 내포클래스에선 외부클래스의 접근 불가
return nestVar
}
}
// 이너클래스
inner class Inner {
private val innerVar = 999
fun foo(): Int {
return innerVar + bar // private 키워드임에도 접근 가능
}
}
}
fun main() {
println(Outer.Nested().foo()) // 999
// 이너클래스는 내부맴버취급이기에 외부클래스 생성후 사용 가능하다.
println(Outer().Inner().foo()) // 1000
}
fun localClasses() {
// 지역클래스, 함수내부에서 클래스 선언
open class Amphibian {
open fun foo() = "foo"
}
class Frog : Amphibian() {
override fun foo() = "bar"
}
val amphibian = Frog()
println(amphibian.foo())
}
fun main() {
localClasses()
}
익명클래스, 함수형 인터페이스
인터페이스를 구현하는 익명클래스 문법은 아래와 같다.
interface SomeInterface {
fun someMethod()
}
interface HelloInterface {
fun helloMethod()
}
fun main() {
val anonymousObject = object : SomeInterface, HelloInterface {
override fun someMethod() {
println("some")
}
override fun helloMethod() {
println("hello world")
}
}
anonymousObject.someMethod() // some
anonymousObject.helloMethod() // hello world
}
java
에선 구현메서드가 하나라면 람다형식으로 익명클래스 정의가 가능한데 kotlin 에서도 가능하다.
단 아래와 같이 함수형 인터페이스를 명시하도록 fun interface
키워드를 붙여야 한다.
fun interface SomeInterface {
fun someMethod()
}
fun main() {
val anonymousObject = SomeInterface {
println("Hello, World!")
}
anonymousObject.someMethod()
}
fun main() {
val anonymousObject = Runnable {
println("Hello, World!")
}
anonymousObject.run() // Hello, World!
}
enum
enum class Suit(val symbol: Char) {
CLUBS('\u2663'),
DIAMONDS('\u2666') {
override fun display(): String = super.display() + " ${this.symbol}"
},
HEARTS('\u2665'),
SPADES('\u2660');
open fun display() = "$symbol $name"
}
fun main() {
println(Suit.CLUBS.symbol) // ♣
println(Suit.CLUBS) // CLUBS
println(Suit.valueOf("DIAMONDS").symbol) // ♦
for (value in Suit.values()) {
println(value.display())
// ♣ CLUBS
// ♦ DIAMONDS ♦
// ♥ HEARTS
// ♠ SPADES
}
}
확장함수, 확장속성
확장함수를 정의해서 기존 클래스 재정의 없이 메서드를 확장할 수 있는 방법을 소개함.
// 람다형식 확장함수, 매개변수로 Int, 반환값으로 String 반환
val addInteger1: String.(Int) -> (String) = {
"$this extends String $it"
}
// 확장함수 정의
fun String.addInteger2(num: Int): String {
return "$this extends String $num"
}
fun main() {
println("helloWorld!".addInteger1(1)) // helloWorld! extends String 1
println("helloWorld!".addInteger2(2)) // helloWorld! extends String 2
}
String
에 대한 확장함수를 정의한다.
람다형식 확장함수 는 함수 정의가 아닌 함수 참조 property 를 추가하는 문법.
fun 키워드 확장함수 는 일반적인 함수를 추가로 정의하는 문법.
만약 매개변수가 하나가 아닌 여러개라면 fun 키워드 확장함수를 사용하길 권장
람다형식 확장함수 매개변수가 여러개라면 it
하나로 모든 매개변수를 표현할 수 없다.
기존에 Double
타입에서만 존재하던 pow
함수를 Int
타입에서도 사용할 수 있도록 설정
// 확장함수
fun Int.pow(x: Int) = toDouble().pow(x).toInt()
fun main() {
var a = 3;
var b = a.pow(2);
println(b) // 9
}
확장함수를 통해 클래스에 메서드를 추가한것처럼, 확장속성을 통해 클래스에 마치 속성이 추가된것 처럼 구성할 수 있다.
아래의 경우 area
를 설정하면 radius
를 해당 area
에 맞춰서 변경하도록 설정했다.
data class Circle(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
}
fun main() {
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
}
보다싶이 물리적으로 필드를 추가했다기 보단 추가한것 처럼 보이는 기법이라 할 수 있다.
lateinit
클래스 내부 var 필드에 대해 지연 초기화(late initialization)를 할 수 있도록 하는 키워드,
Int, Double
같은 기본형에는 사용할 수 없다.
주로 아래 환경에서 사용한다.
- Non-null이 보장되어야 하지만 초기화는 나중에 이루어지는 경우.
- 객체를 나중에 명시적으로 초기화하는 것이 코드 흐름상 적절한 경우.
- 테스트 코드, Android 개발, 또는 DI 환경에서 자주 활용.
this::propertyName.isInitialized
를 통해 lateinit
변수가 초기화 되었는지 확인가능하다.
초기화 되지 않는 lateinit
변수에 접근하면 UninitializedPropertyAccessException
이 발생한다.
아래와 같이 static 객체에 의존성 주입할 때에도 사용 가능하다.
@Component
class Tx(_txAdvice: TxAdvice) {
init {
txAdvice = _txAdvice
}
companion object {
private lateinit var txAdvice: TxAdvice
...
}
}
리시버
kotlin
에서 리시버란 함수에서 해당 코드를 실행시키는 context
라 할 수 있다.
대부분의 경우 this
키워드가 가리키는 객체를 리시버
로 생각하면 된다.
암시적 리시버
가리키는 리시버
를 명확하게 알고있을 경우 this
키워드를 생략하는 경우가 많은데, 이런 경우를 암시적 리시버 라 한다.
val printIt: String.(Int) -> Unit = { n: Int ->
println("n is $n, length is $length") // this 없이 String 의 length 속성 참조
}
fun main() {
"hello world".printIt(10) // n is 10, length is 11
}
디스패치 리시버, 익스텐션 리시버
일반적인 경우 리시버
는 하나이지만, 클래스 내부에서 확장함수를 정의하면 디스패치 리시버, 익스텐션 리시버 2개의 리시버를 사용할 수 있다.
class Point(var x: Int,
var y: Int) {
// Pair 의 확장함수
fun Pair<Int, Int>.checkSame(): Boolean {
// this : 익스텐션 리시버
// this@Point: 디스패치 리시버
return this.first == this@Point.x && this.second == this@Point.y
}
fun printIsSame(p: Pair<Int, Int>) {
println(p.checkSame())
}
}
fun main() {
val p = Point(1, 2)
p.printIsSame(Pair(1, 2)) // true
p.printIsSame(Pair(3, 4)) // false
}
멀티플 스코프 리시버
2개 메서드를 정의한다.
String
객체에 대한 확장함수를 매개변수로 전달하고 실행Double
객체에 대한 확장함수를 매개변수로 전달하고 실행
fun top(func: String.() -> Unit) = "hello".func()
fun nested(func: Int.() -> Unit) = (-2).func()
fun main() {
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
키워드로 상속 가능한 클래스임을 알려야 한다.
open class Vehicle(val year: Int, open var color: String) {
open val km = 0
// 이후에 더이상 toString 메서드를 override 하지 못하도록 final 설정
final override fun toString() = "year: $year, Color: $color, KM: $km"
// repain 역시 기본 final 임으로 override 하지 못함
fun repaint(newColor: String) {
color = newColor
}
}
open class Car(year: Int, color: String) : Vehicle(year, color) {
override var km: Int = 0
set(value) {
if (value < 1) throw RuntimeException("can't set negative value")
field = value
}
fun drive(distance: Int) {
km += distance
}
}
fun main() {
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
클래스는 너무 많은 유연성을 제공하다 보니 사용을 자제하는 것이 좋다.
public open class Any {
public open operator fun equals(other: Any?): Boolean
public open fun hashCode(): Int
public open fun toString(): String
}
...
val str: Any = "Hello, World!"
kotlin
에선 반환값이 없을때 void
대신 Unit
객체를 반환한다.
Unit
은 싱글톤 인스턴스로 void
를 포함한 모든 데이터를 객체로 표기하기 위한 kotlin
의 의지가 보이는 클래스이다.
// public object Unit {
// override fun toString() = "kotli.unit"
// }
fun returnVoid(): Unit { // 명시적 반환 생략 가능
println("hello world")
return
}
fun main() {
val u = returnVoid() // hello world
println(u) // kotlin.Unit
}
정말 반환하지 않는 함수를 표현하기 위해서는 Nothing
클래스를 사용한다.
무한반복이나 Exception
이 발생할 경우 반환값이 Nothing
이 된다.
fun doNothing(): Nothing {
throw Exception("Nothing at all")
}
변수에 null
을 할당하면 Nothing?
타입으로 추론된다.
val n = null // Nothing? type
println(n is Nothing?)
또한 if...else
문을 사용하여 아래와같이 반환할 때에도 Nothing
을 사용하며
그다음 줄부턴 컴파일러가 해당 변수를 String
으로 추론한다.
// true 일경우 문자열, false 일경우 Nothing 반환
val x = if (Random.nextBoolean()) "T" else throw Exception("F")
// 이후 코드부턴 String? 으로 추론
println(x::class.simpleName)
interface
kotlin
interface
는 default
키워드 없이 메서드 정의가 가능하다.
실제로 컴파일된 바이트코드를 보면 java
의 default
키워드를 사용하기 보단 클래스를 추가적으로 생성해 doubleUp
역할을 하는 static
메서드를 운영하는 방식으로 구성된다.
만약 바이트 코드를 생성시 default
키워드를 사용하는 방식으로 하고 싶다면 빌드옵션에 -Xjvm-default=all
을 적용하면 된다.
interface Remote {
fun up()
fun down()
fun doubleUp() {
up()
up()
}
}
/*
Decomplied Java Byte Code
public interface Remote {
void up();
void down();
void doubleUp();
public static final class DefaultImpls {
public static void doubleUp(@NotNull Remote $this) {
$this.up();
$this.up();
}
}
}
*/
class TVRemote(private val tv: TV) : Remote {
override fun up() {
tv.volume++
}
override fun down() {
tv.volume--
}
}
static 메서드, 필드를 정의할 땐 companion object
를 사용하면 된다.
interface Remote {
...
companion object {
fun combine(first: Remote, second: Remote): Remote = object : Remote {
override fun up() {
first.up()
second.up()
}
override fun down() {
first.down()
second.down()
}
}
}
}
fun main() {
val tv = TV()
val remote: Remote = TVRemote(tv)
println("Volume: ${tv.volume}") // Volume: 0
remote.up()
println("Volume: ${tv.volume}") // Volume: 1
remote.doubleUp()
println("Volume: ${tv.volume}") // Volume: 3
val anotherTV = TV()
val combinedRemote = Remote.combine(remote, TVRemote(anotherTV))
combinedRemote.up()
println(tv.volume) //4
println(anotherTV.volume) //1
}
abstract class
java
와 유사하게 abstract
키워드를 사용하여 추상클래스 선언이 가능하다.
abstract class Musician(val name: String, val activeFrom: Int) {
abstract fun instrumentType(): String
}
class Cellist(name: String, activeFrom: Int) : Musician(name, activeFrom) {
override fun instrumentType() = "String"
}
제너릭, 와일드카드(스타 프로젝션)
컬렉션에서 필수적으로 사용되는 문법으로 제너릭을 사용하면 입력받는 파라미터, 선언된 클래스 필드에 변성(variance)
기능을 추가할 수 있다.
kotlin
에서도 제너릭의 다양한 디자인을 제공하기 위한 여러 문법이 존재한다.
java
에서 extends
를 사용해 제너릭 타입에 제약조건을 걸듯이, kotlin
에서도 아래와 같이 제약조건 지정이 가능하다.
interface AutoCloseable {
void close();
}
public class TestMain {
public static <T extends AutoCloseable> void useAndClose(T input) {
input.close();
}
}
interface AutoCloseable {
fun close()
}
fun <T : AutoCloseable> useAndClose(input: T) {
input.close()
}
java
에선 하나의 타입제약만 가능하지만 kotlin
에선 where
키워드를 사용하면 여러 타입에 대해 제약설정할 수 있다.
// AutoCloseable, Appendable 다중 상속된 객체만 입력 가능
fun <T> useAndAppendAndClose(input: T) where T : AutoCloseable, T : Appendable {
input.append()
input.close()
}
아래는 java
의 와일드카드(?)
역할을 하는 스타 프로젝션(*)
문법이다.
// java 와일드 카드 문법
interface Fruit { }
class Banana implements Fruit { }
public class TestMainJava {
public static void receiveFruits(List<?> fruits) {
System.out.println("Number of fruits:" + fruits.size());
}
public static void main(String[] args) {
List<Banana> bananas = new ArrayList<>();
receiveFruits(bananas);
}
}
// kotlin 스타 프로젝션 문법
interface Fruit
class Banana : Fruit
fun receiveFruits(fruits: Array<*>) =
println("Number of fruits: ${fruits.size}")
fun main() {
val bananas: Array<Banana> = arrayOf()
receiveFruits(bananas) //OK
}
공병성, 반공병성
기존 java
코드의 경우 와일드카드
를 사용해 클래스간 공변성(covariance), 반공변성(contravariance) 기능을 제공했었다.
// java
// 공변성
void receiveCovarianceFruits(List<? extends Fruit> fruits) { ... }
// 반공변성
void receiveContravarianceFruits(List<? super Fruit> fruits) { ... }
kotlin
에서도 out
, in
키워드를 사용해 공변성 기능을 제공한다.
// 공변성
fun receiveCovarianceFruits(fruits: Array<out Fruit>) =
println("Number of fruits: ${fruits.size}")
// 반공변성
fun receiveCovarianceFruits(fruits: Array<in Fruit>) =
println("Number of fruits: ${fruits.size}")
물론 반공변성
매개변수는 어디까지의 상위클래스가 참조로 들어올지 모름으로, 해당 요소를 참조할 때 java
의 경우 Object
, kotlin
의 경우 Any?
타입을 사용해 로컬변수로 사용한다.
별도의 타입캐스팅 과정을 거쳐야 한다보니
반공변성
을 자주 사용하지 않는다.
kotlin
에선 선언처 가변성(Declaration-site variance)
이라는 java
에는 없는 기능을 제공한다.
List
의 경우 선언문에 이미 out
키워드를 작성해두어서, 함수 매개변수에 out
키워드를 작성하지 않아도 공변성 기능을 제공한다.
일반적인 제너릭 문법을 통한 가변성 기능은
사용처 가변성
이라 부름
// List 선언문
// public interface List<out E> : Collection<E> { ... }
// out 키워드 없이 함수 매개변수 선언
fun receiveFruits(fruits: List<Fruit>) =
println("Number of fruits: ${fruits.size}")
fun main() {
val bananas: MutableList<Banana> = mutableListOf()
receiveFruits(bananas) //OK
}
요소 E
의 하위클래스라면 언제든 매개변수로 입력받을 수 있도록 공변성
을 제공한다.
델리게이션(class delegate)
kotlin
에선 Delgate
(대리자) 클래스를 이용해 여러 편의기능을 지원한다.
요새는 상속보다 대리자를 통해 클래스 위임(Delegation) 하는것이 대세, 대부분 디자인 패턴에서도 상속보단 위임을 통해 클래스를 확장하는 것을 권장한다.
아래와 같이 by
키워드로 위임한 인스턴스의 메서드들을 그대로 사용할 수 있다.
만약 합성할 클래스의 메서드명이 중복될 경우 재정의를 강제하도록 컴파일 에러를 일으킨다.
interface Dialable {
fun dial(number: String): String
fun hello(): String
}
class Phone : Dialable {
override fun dial(number: String) = "Dialing $number..."
override fun hello(): String {
return "world"
}
}
interface Snappable {
fun takePicture(): String
fun hello(): String
}
class Camera : Snappable {
override fun takePicture() = "Taking picture..."
override fun hello(): String {
return "hello"
}
}
// 델리게이션을 위한 키워드 by
class SmartPhone(
private val phone: Dialable = Phone(),
private val camera: Snappable = Camera()
) : Dialable by phone, Snappable by camera {
// 중복 메서드 재정의 필수
override fun hello(): String {
return camera.hello() + " " + phone.hello()
}
}
fun main() {
val smartPhone: SmartPhone = SmartPhone()
println(smartPhone.dial("555-1234")) // Dialing 555-1234...
println(smartPhone.takePicture()) // Taking picture...
println(smartPhone.hello()) // hello world
}
변환된 바이트코드를 보면 아래와 같다.
알아서 위임한 객체들의 메서드를 상위 레벨로 재정의하고 매핑시킨다.
어댑터 패턴과 데코레이트 패턴을 합친것처럼 동작한다.
// java byte code
public final class SmartPhone implements Dialable, Snappable {
private final Dialable phone;
private final Snappable camera;
public SmartPhone(@NotNull Dialable phone, @NotNull Snappable camera) {
super();
this.phone = phone;
this.camera = camera;
}
...
public SmartPhone() {
this((Dialable)(new Phone()), (Snappable)(new Camera()));
}
@NotNull
public String hello() {
return this.camera.hello() + " " + this.phone.hello();
}
@NotNull
public String dial(@NotNull String number) {
Intrinsics.checkParameterIsNotNull(number, "number");
return this.phone.dial(number);
}
@NotNull
public String takePicture() {
return this.camera.takePicture();
}
}
다음과 같이 by
키워드를 사용해 객체 필드명과 map
의 key
값이 일치할 경우 value
에 존재하는 객체를 위임한다.
data class Project(val map: MutableMap<String, Any?>) {
val name: String by map
var priority: Int by map
var completed: Boolean by map
}
fun main() {
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
키워드로 메서드를 객체 내부에 정의한다.
class PoliteString(private var content: String) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return content.replace("stupid", "s*****")
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
content = value
}
}
fun main() {
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 에는 존재하지 않는 문법이다 보니 당황스럽지만 컴파일된 바이트코드를 뜯어보면 대략 아래와 같다.
// java byte code
public final class PoliteString {
private String content;
public PoliteString(@NotNull String content) {
this.content = content;
}
@NotNull
public final String getValue(@Nullable Object thisRef, @NotNull KProperty property) {
Intrinsics.checkNotNullParameter(property, "property");
return StringsKt.replace$default(this.content, "stupid", "s*****", false, 4, (Object)null);
}
public final void setValue(@Nullable Object thisRef, @NotNull KProperty property, @NotNull String value) {
Intrinsics.checkNotNullParameter(property, "property");
Intrinsics.checkNotNullParameter(value, "value");
this.content = value;
}
public static void main(String[] var0) {
PoliteString var10000 = new PoliteString("Some nice message");
KProperty var1 = $$delegatedProperties[0];
PoliteString comment = var10000;
String var2 = comment.getValue((Object)null, var1);
System.out.println(var2);
comment.setValue((Object)null, var1, "This is stupid");
var2 = comment.getValue((Object)null, var1);
System.out.println(var2);
var2 = "comment is of length: " + comment.getValue((Object)null, var1).length();
System.out.println(var2);
}
}
결국 String
대신 PoliteString
객체가 사용되고 String
내용을 참조할때 마다 [getValue, setValue]
를 통해 참조한다.
좀더 명확한 대리자 클래스 선언을 위해 [ReadWriteProperty, ReadOnlyProperty]
를 구현하는 방식으로 사용 가능하다.
class MyReadDelegate(private val value: String) : ReadOnlyProperty<Any?, String> {
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
println("thisRef:${thisRef}, property:${property.name}")
// thisRef:com.beyless.presence.sensor.Example@c8e4bb0, property:readOnly
return value
}
}
object MyReadWriteDelegate : ReadWriteProperty<Example, String> {
private var value: String = "Initial ReadWrite Value"
override fun getValue(thisRef: Example, property: KProperty<*>): String {
return value
}
override fun setValue(thisRef: Example, property: KProperty<*>, value: String) {
this.value = value
}
}
class Example {
val readOnly by MyReadDelegate("Initial ReadOnly Value")
var readWrite by MyReadWriteDelegate
}
fun main() {
val e = Example()
val ro: String = e.readOnly
println(ro)
e.readWrite = "NEW"
println(e.readWrite)
}
- LongDelegate
- DoubleDelegate
- BigIntegerDelegate
PropertyDelegateProvider
델리게이션 생성시 추가로 커스텀한 로직을 추가하여 동적으로 델리게이션 생성이 가능하다.
입력 property 의 조건에 따라 다른 델리게이션을 반환해야 하는 등의 작업 수행이 가능하다.
@SinceKotlin("1.4")
public fun interface 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.
*/
public operator fun provideDelegate(thisRef: T, property: KProperty<*>): D
}
위의 변수 델리게이션 생성때와 마찬가지로 getValue, setValue
메서드를 operator
로 지정해줘도 되지만
확실한 가독성을 위해 PropertyDelegateProvider
사용을 권장.
class CustomDelegate : StringDelegate {
private var value: String = "Initial Value" // 초기값 설정
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
return value // 저장된 값을 반환
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("Setting value '${value}' to ${property.name}")
this.value = value // 값을 설정
}
}
class DelegateProvider : PropertyDelegateProvider<Any?, StringDelegate> {
override fun provideDelegate(thisRef: Any?, property: KProperty<*>): StringDelegate {
println("Providing delegate for ${property.name}")
return CustomDelegate() // 커스텀 delegate 반환, 대리자 생성시 별도의 추가 로직 수행 가능
}
}
class Example {
var exampleProperty by DelegateProvider() // DelegateProvider를 통해 delegate를 초기화
}
fun main() {
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
}
``kotlin class DatabaseDelegateProvider<in R, T>(readQuery: String, writeQuery: String, id: Any) : PropertyDelegateProvider<R, ReadWriteDelegate<R, T» { override operator fun provideDelegate(thisRef: T, prop: KProperty<*>): ReadWriteDelegate<R, T> { if (prop.returnType.isMarkedNullable) { return NullableDatabaseDelegate(readQuery, writeQuery, id) } else { return NonNullDatabaseDelegate(readQuery, writeQuery, id) } } }
### lazy 빌트인 델리게이션
`kotlin` 에선 `null` 처리를 좀더 효율적으로 하기 위한 `lazy` 빌트인 델리게이션 키워드를 제공한다.
```kotlin
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
접근이 일어날 때 값이 null
이라면 () -> T
형식의 초기화 람다를 호출한다.
class Customer(val name: String) {
val message: List<String> by lazy {
mutableListOf("hi", "hello", "by")
.also { println("initial complete") }
}
}
fun main() {
val c1 = Customer("kouzie")
println(c1.message)
// initial complete
// [hi, hello, by]
println(c1.message) // [hi, hello, by]
}
mode
를 추가하여 lazy
코드의 락 처리가 가능하다.
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
메서드가 호출되지 않는다.
fun getTemperature(city: String): Double {
println("fetch from webservice for $city")
return 30.0
}
fun main() {
// val temperature = getTemperature("seoul")
val temperature: Double by 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
fun main() {
// init by 1
var watched: Int by Delegates.observable(1) { prop, old, new ->
println("${prop.name} changed from $old to $new")
}
// init by 0
var checked: Int by 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
도 체크 가능하다.
public abstract class ObservableProperty<T>(initialValue: T) : ReadWriteProperty<Any?, T> {
protected open fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T): Boolean = true
protected open fun afterChange(property: KProperty<*>, oldValue: T, newValue: T): Unit {}
public override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value
}
public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
val oldValue = this.value
if (!beforeChange(property, oldValue, value)) {
return
}
this.value = value
afterChange(property, oldValue, value)
}
}
개발자가 정의한 람다식에 의해 beforeChange
, afterChange
가 재정의 되는 구조이다.
Delegates.notNull()
대리자를 사용하면 초기화 전에 접근시 예외를 발생시킨다.
비슷한 사용예로 lateinit
가 있다.
fun main() {
var str1: String by Delegates.notNull()
println(str1) // IllegalStateException at kotlin.properties.NotNullVar.getValue
}
fun main() {
lateinit var str2: String
println(str2) // UninitializedPropertyAccessException
}
변환된 바이트코드는 아래와 같다.
// java byte code
public static final void main() {
ReadWriteProperty var10000 = Delegates.INSTANCE.notNull();
KProperty var1 = $$delegatedProperties[0];
ReadWriteProperty str1 = var10000;
Object var2 = str1.getValue((Object)null, var1);
System.out.println(var2);
}
public static final void main() {
String str2 = null;
Intrinsics.throwUninitializedPropertyAccessException("str2");
System.out.println(str2);
}
notNull
대리자의 경우 ReadWriteProperty
클래스로 한번 감싸기 때문에 primitive type
도 체크 가능하지만
lateinit
의 경우 변수에 null
을 대입하기에 primitive type
은 사용할 수 없다.
var test:Int = null; // 컴파일 오류
lateinit var str2: Int // 컴파일 오류
public interface ReadWriteProperty<in R, T> {
public operator fun getValue(thisRef: R, property: KProperty<*>): T
public operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}
영역함수(scope function)
객체 컨텍스트 안에서 코드 블록을 실행할 목적으로 만든 영역함수(scope function) 가 있다.
영역함수
를 사용하면 반복적이고 무의미한 객체 접근 코드를 생략해서 코드노이즈를 줄일 수 있다.
inline fun <T> T.apply(block: T.() -> Unit): T
inline fun <T, R> T.run(block: T.() -> R): R
inline fun <T> T.also(block: (T) -> Unit ): T
inline fun <T, R> T.let(block: (T) -> R ): R
내부의 T.() -> R
과 같은 형식이 낯설지만 제너릭 타입 T 의 람다 확장함수 라 할 수 있다.
대략 아래와 같은 형식으로 선언하고 사용할 수 있다.
data class Person(
var name: String = "kouzie",
var age: Int = 0
)
fun main() {
// 객체 컨텍스트
val extFunc1: Person.() -> Unit = {
this.name = "func1"
this.age = 20
}
// 함수 컨텍스트
val extFunc2: (Person) -> Unit = {
it.name = "func2"
it.age = 21
}
val p1 = Person()
val p2 = Person()
p1.extFunc1()
extFunc2(p2)
println(p1) // Person(name=func1, age=20)
println(p2) // Person(name=func2, age=21)
}
T.() -> Unit
, (T) -> Unit
의 형태가 거의 비슷하다.
also
와 apply
내부 구현을 살펴보면
block
함수를 사용해 함수 컨텍스트
를 사용할지,
this
를 사용해 객체 컨텍스트
를 사용할지 차이이다.
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
public inline fun <T> T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
일부 영역함수
들은 대체 가능할 정도로 기능이 겹치는 경우가 많다.
기능은 비슷하지만 위 url 에서 어떤 상황에서 어떤 영역함수를 사용해야하는지 불문율? 같은 권장사항을 작성해두었다.
사용하는 상황을 요약하면 아래와 같다.
apply
는 객체의 속성을 초기화할 때 사용also
는 객체를 가공하지 않고 부가적인 코드를 실행할 때 사용run
원하는 결과값을 생성하고 반환할 때 주로 사용.let
분기에 따라 다른값을 반환,null
체크에 많이 사용.
apply, also
apply
블록 내부에서 객체 컨텍스트
를 가져와 속성을 초기화하거나 메서드를 호출할 때 사용한다.
fun main() {
val person = Person()
.apply {
name = "test"
age = 29
}
println(person) // Person(name=test, age=29)
}
also
블록 내부에서 함수 컨텍스트
를 가져와 코드 흐름을 방해하지 않고 부가적인 코드를 실행시킬 때 사용한다.
fun main() {
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
영역함수
를 사용한다.
fun main() {
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
조건을 처리하는데 많이 사용한다.
fun processString(str: String?): String = str?.let {
when {
it.isEmpty() -> "Empty"
it.isBlank() -> "Blank"
else -> it.uppercase()
}
} ?: "Null"
fun main() {
println(processString("")) // Empty
println(processString(null)) // Null
println(processString("test")) // TEST
}
val p = person?.let {
PersonData(
faceId = it.faceId,
age = it.age,
alreadyExist = it.alreadyExist,
)
}
[run, let]
또한 서로 유사하며 대체가능하지만 개발자가 정해둔 두 영역함수
사용의 일반적 룰이 있다.
with
with
영역함수
는 [let, run]
과 같이 표현식의 결과를 반환하고, 매개변수로 람다형식의 확장함수를 실행시킬 객체를 전달받는다.
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}
fun main() {
val p = Person("test", 20)
with(p) {
println("my name is $name and $age")
}
}
with
은 표현식을 반환하지만 also
처럼 코드 흐름을 방해하지 않고 부가적인 코드를 실행시 사용한다.
대부분 반환할 표현식을 with
코드안에 적지 않는다.
어노테이션
class Example(
@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
패턴을 사용하기 쉽게 구성된 언어이다.
아래는 리터럴표현식을 사용해 DSL
패턴을 사용한 예이다.
class Node(private val name: String) {
// xml 속성
private var attributes: Map<String, String> = mutableMapOf()
// 자식 목록
private var children: List<Node> = mutableListOf()
// text value
private var textValue: String = ""
fun text(value: String) {
textValue = value
}
// 리터럴 표현식 사용
fun element(childName: String,
vararg attributesValues: Pair<String, String>,
block: Node.() -> Unit): Node {
val child = Node(childName)
attributesValues.forEach { child.attributes += it }
children += child
return child.apply(block)
}
private fun toString(indentation: Int): String {
val attributesValues =
if (attributes.isEmpty()) ""
else attributes
.map { "${it.key}=${it.value}" }
.joinToString(" ", " ")
val DEPTH = 2
val indent = " ".repeat(indentation)
return if (textValue.isNotEmpty())
"$indent<$name$attributesValues>$textValue</$name>"
else
"""|$indent<$name$attributesValues>
|${children.joinToString("\n") { it.toString((indentation + DEPTH)) }}
|$indent<$name>""".trimMargin()
}
override fun toString(): String {
return toString(0)
}
}
class XMLBuilder {
fun root(rootElementName: String, block: Node.() -> Unit): Node =
Node(rootElementName).apply(block)
}
fun xml(block: XMLBuilder.() -> Node): Node = XMLBuilder().run(block)
fun main() {
val langsAndAuthors = mapOf(
"JavaScript" to "Eich",
"Java" to "Gosling",
"Ruby" to "Matz")
val xmlString = xml {
root("languages") {
langsAndAuthors.forEach { (name, author) ->
element("language", "name" to name) {
element("author") {
text(author)
}
}
}
}
}
println(xmlString)
}
/*
<languages>
<language name=JavaScript>
<author>Eich</author>
<language>
<language name=Java>
<author>Gosling</author>
<language>
<language name=Ruby>
<author>Matz</author>
<language>
<languages>
*/
@DslMarker
위 코드의 경우 리터럴 표현식에 맞기만 하면 어떠한 람다함수도 모두 허용한다.
그래서 아래와 같은 level 맞지 않는 DSL 표현식도 허용한다.
val xmlString = xml {
root("languages") {
langsAndAuthors.forEach { (name, author) ->
element("language", "name" to name) {
root("author") { // error 가 발생하지 않음
text(author)
}
}
}
}
}
이런 코드가 가능한 이유는 부모 스코프인 XMLBuilder
객체의 리시버에 접근가능하기 때문인데
@DslMarker
어노테이션을 사용하면 현재 스코프의 리시버에만 접근하도록 강제한다.
@DslMarker
annotation class XMLMarker
@XMLMarker
class Node(private val name: String) { ... }
@XMLMarker
class XMLBuilder { ... }