1. Factory method
Factory 메소드는 Interface 내부 또는 private 생성자를 가진 클래스의 companion object
에 위치한다.
자바와의 상호 운영을 위해서는 @JvmStatic
어노테이션을 적용 할 것을 권장한다.
interface ImageReader {
fun read(file: File): Bitmap
companion object {
fun newImageReader(format: String) = when (format) {
"jpg" -> JpegReader()
"gif" -> GifReader()
else -> throw IllegalStateException("Unknown format")
}
}
}
2. Builder
100% Kotlin 만을 사용하다면 아래에서 보여 주듯 Builder 구현이 불필요하다.
class Burger(
val patties: Int,
val hasCheese: Boolean = false,
val hasTomato: Boolean = false
)
val doubleCheeseBurger = Burger(patties = 2, hasCheese = true)
자바에서 여러 오버로드 메서드를 노출하고 싶다면 @JvmOverloads
어노테이션을 적용한다.
class Foo @JvmOverloads constructor(x: Int, y: Double = 0.0) {
@JvmOverloads fun f(a: String, b: Int = 0, c: String = "abc") {
...
}
}
->
// 생성자:
Foo(int x, double y)
Foo(int x)
// 메서드
void f(String a, int b, String c) { } void f(String a, int b) { }
void f(String a) { }
만약 상호 운영이 확실하지 않다면 type-safe builder(aka DSL)를 구현하는 방법이 있다.
data class Burger(val patties: Int, val toppings: List<Toppings>)
enum class Toppings { Cheese, Tomato }
fun burger(patties: Int, init: BurgerBuilder.() -> Unit) =
BurgerBuilder(patties).apply(init).build()
class BurgerBuilder(private val patties: Int) {
private var toppings: List<Toppings> = mutableListOf()
fun toppings(init: List<Toppings>.() -> Unit) {
toppings.apply(init)
}
operator fun Toppings.unaryPlus() {
toppings += this
}
fun build() = Burger(patties, toppings)
}
val doubleCheeseBurger = burger(patties = 2) {
toppings {
+Cheese
}
}
3. Singleton
자바의 패턴을 그대로 복사하여 singleton을 구현할 수도 있지만, kotlin은 object
선언을 지원하여 간단하게 Singleton을 선언 할 수 있게 해준다.
object Singleton {
//...
}
Singleton.myFunction()
object
클래스의 bytecode를 Java 클래스로 변환하면 다음과 같다.
public final class Singleton {
public static final Singleton INSTANCE;
static {
INSTANCE = new Singleton();
}
//...
}
만약 object
로 선언된 클래스에 public static가 존재한다면 항상 lazy initialize를 보장한다고 할 수 없지만 (정적 멤버에 접근하려 한다면 클래스가 생성되기 때문이다), kotlin에서는 object 선언 없이는 정적 멤버를 만들 수 없다.
또한 static 블록에서 초기화되므로 Thread-safe하다.
4. 불필요한 인스턴스의 생성을 막자
당연하지만 kotlin에서도 java와 동일한 방식을 사용할 수 있다.
class DontCreateMe private constructor()
하지만 위와 같은 정통적 방식을 사용하는 것이 옳은지를 스스로에게 물어 볼 필요가 있다.
Kotlin의 extension functions으로 관련 객체에 유틸리티 함수를 직접 적용할 수 있다. 즉 TextUtils.isEmpty()
보다는 fun CharSequence.isEmpty()
이 더 나은 구현이 될 수 있다. 우연히도 이는 이미 Kotlin standard library에 존재한다. 자바와의 상호운용성을 높이기 위해 @file:JvmName
어노테이션을 파일의 최상단에 추가한다.
@file:JvmName("SnackbarUtils")
import android.support.annotation.ColorInt
import android.support.design.widget.Snackbar
fun Snackbar.setBackgroundColor(@ColorInt color: Int) {
view.setBackgroundColor(color)
}
5. Dependency injection
Dagger
6. 불필요한 객체의 생성을 피하자
Kotlin의 Int
가 프라이머티브 타입 int
가 될 지 참조 타입 Integer
가 될 지는 컴파일러가 정한다.
Kotlin에서 특별히 조심해야 하는 상황을 보자
Nullable types
Int?
같은 Nullable type은 언제나 Integer
로 컴파일 된다. 프라이머티브 타입이 null
을 가지게 할 방법이 없기 때문이다.
Kotlin을 개발할 때는 항상 "null이 될 필요가 있나?"를 스스로에게 물어보자.
Objects in collections
콜렉션은 일반적으로 null
을 허용하며 제네릭 참조 타입만을 사용 할 수 있다.
안드로이드에서는 이 Auto-boxing을 피하기 위해 SparseIntArray
등을 제공한다.
제네릭 클래스의 타입 인자
타입 인자는 언제나 참조 타입이여야 한다.
자세한 내용은 다음을 보기 ->
https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-1-fbb9935d9b62
https://discuss.kotlinlang.org/t/kotlin-and-auto-boxing-of-parameters-and-result/1908/2
9. use extension function
Kotlin은 Java 9의 try-with-resources
를 지원하지 않는다. 대신 use
extension function을 가지고 있다. 하지만 try-with-resources
와 달리 use
는 단일 Closeable
리소스에만 적용할 수 있다. 결국 다수의 closeable
에는 중첩이 필요하다(try-finally
보다는 낫지만...).
inputStream.use {
outputStream.use {
// do something with the streams
outputStream.write(inputStream.read())
}
}
다수의 리소스드ㄹ을 다루어야 할 일이 많다면 다음 extension function이 도움이 될 것이다.
private inline fun <T : Closeable?> Array<T>.use(block: ()->Unit) {
var exception: Throwable? = null
try {
return block()
} catch (e: Throwable) {
exception = e
throw e
} finally {
when {
exception == null -> forEach { it?.close() }
else -> forEach {
try {
it?.close()
} catch (closeException: Throwable) {
exception.addSuppressed(closeException)
}
}
}
}
}
arrayOf(inputStream, outputStream).use {
// do something with the streams
outputStream.write(inputStream.read())
}
10. equals를 오버라이드 할 때는 보편 계약을 따르자
data class User(val name: String, val age: Int)
성능을 위해서는 다를 가능성이 가장 높고 (즉, 가장 유니크하고) 비교의 비용이 가장 적은 필드가 equals에서 첫 번째로 비교되어야 한다.
그리고 Kotlin 1.2에서는 선언의 순서에 따라 데이터 클래스의 프로퍼티를 비교한다.
data class User(val age: Int, val name: String)
11. equals를 오버라이드 할 때는 hashCode를 오버라이드 하자
data class User(val id: Int, val name: String, val age: Int)
위 데이터 클래스는 다음과 같은 hasCode 함수가 생성된다.
public int hashCode() {
return (id * 31 + (name != null?name.hashCode():0)) * 31 + age;
}
만약 id
가 유일한 값, 즉 uniqueKey라면 다음과 같이 오버라이드하는 것이 성능에 유리할 것이다.
override fun hashCode(): Int {
return id
}
13. clone은 신중하게 오버라이드하자
kotlin은 data class를 생성하면 Effective Java의 충고에 따라 copy
함수를 생성한다.
14. COMPARABLE 인터페이스의 구현을 고려하자
kotlin에서는 java.util.Comparable
대신 kotlin.Comparable
을 구현해야 한다. kotlin 버전이 더 많은 확장 함수를 제공한다.
4
5
를