본문으로 바로가기
336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

코틀린의 클래스와 인터페이스는 자바 클래스, 인터페이스와는 약간 다르다. 예를 들어 인터페이스에 프로퍼티 선언이 들어갈 수 있다. 자바와 달리 코틀린 선언은 기본적으로 final 이며 public 이다. 게다가 중첩 클래스는 기본적으로 내부 클래스가 아니다. 즉, 코틀린 중첩 클래스에는 외부 클래스에 대한 참조가 없다.


코틀린 인터페이스


코틀린 인터페이스는 자바 8 인터페이스와 비슷하다. 코틀린 인터페이스 안에는 추상 메서드뿐 아니라 구현이 있는 메서드도 정의할 수 있다. 다만 인터페이스에는 아무런 상태도 들어갈 수 없다.


아래 코드와 같이 코틀린에서 인터페이스를 정의할 수 있다.


1
2
3
interface Clickable {
    fun click()
}



클래스에 인터페이스를 상속받아 구현하는 방법을 아래에서 볼 수 있다. 자바에서는 extends와 implements 키워드를 사용하지만 코틀린에서는 클래스 이름 뒤에 콜론을 붙이고 인터페이스와 클래스 이름을 적는 것으로 클래스 확장과 인터페이스 구현을 모두 처리한다.

자바와 달리 코틀린에서는 override 변경자를 꼭 사용해야 한다. override 변경자는 실수로 상위 클래스의 메서드를 오버라이드하는 경우를 방지해준다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main(args: Array<String>) {
    var btn = Button()
    btn.click()
}
 
interface Clickable {
    fun click()
}
 
class Button: Clickable {
    override fun click() {
        println("Click!")
    }
}




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
fun main(args: Array<String>) {
    var btn1 = Button()
    btn1.click() //I was Clicked
    btn1.setFocus(true//I got focus.
    btn1.showOff() //I'm clickable!  I'm focusable!
}
 
interface Clickable {
    fun click()
    fun showOff() = println("I'm clickable!")
}
 
interface Focusable {
    fun setFocus(b: Boolean) =
            println("I ${if (b) "got" else "lost"} focus.")
 
    fun showOff() = println("I'm focusable!")
}
 
class Button : Clickable, Focusable {
    override fun click() {
        println("I was Clicked")
    }
 
    override fun showOff() {
        super<Clickable>.showOff()
        super<Focusable>.showOff()
    }
}



class 한정자(open, final, abstrac) 기본적으로 final


java와 다른점은 한정자 없이 사용한 class는 final이 기본 속성이라는것과 상속이 가능한 클래스로 만드려면 open 키워드를 명시적으로 붙여야 한다.


상속은 OOP 개념에서 코드를 재활용하는 편리한 수단지만 fragile base class problem을 일으킨다.


※  fragile base class problem: 부모 클래스가 명확하게 상속하는 방법과 규칙에 대해 정의하지 않는다면 해당 부모를 상속받는 자식들은 부모 클래스 작성당시의 의도와 다르게 상속받아 사용될 수 있다. 이런 경우 부모 클래스가 바뀌면 하위 클래스가 영향을 받아 side-effect 발생하는 경우가 발생한다. (부모 클래스 변경시 이를 상속받는 모든 하위 클래스의 구현을 일일이 확인할수 없으므로 발생함.)


코틀린에서는 기본적으로 상속을 못하게끔 되어 있다. 따라서 상속할수 있도록 만들어 주려면 명시적으로 class 앞에 open을 사용해야 한다. (자바는 기본값이 반대입니다.)


1
2
3
4
5
6
7
8
9
10
11
12
13
interface Clickable {
    fun click()
    fun showOff() = println("I'm clickable!")
}
 
open class RichButton : Clickable { // open -> 이클래스는 다른 클래스가 상속받을 수 있다.
 
    fun disable() {} // 자식 클래스가 override 할수 없다.
 
    open fun animate() {} // 자식 클래스가 override 할수 있다.
 
    override fun click() {} // 자식 클래스가 또다시 override 할수 있다. (override 함수는 기본값이 open이다)
}



만약 override 한 함수를 자식클래스에서 다시 override 하지 못하도록 하려면 앞에 final을 붙여줘야 합니다.

1
2
3
open class RichButton : Clickable { 
    final override fun click() {} 
}



abstract class의 경우는 자바와 성격이 동일합니다. 단 abstract 함수는 open을 붙이지 않아도 기본값이 open 이다.

마지막으로 interface에 정의된 함수 역시 자바와 성격이 같다. 내부 함수는 명시하지 않아도 open이 기본이며, final을 붙일수 없다.


자바처럼 코틀린도 클래스를 abstract로 선언할 수 있다. abstract로 선언한 추상 클래스는 인스턴스화 할 수 없다. 하위 클래스에서 추상 멤버를 오버라이드할 때 추상 멤버 앞에 open 변경자를 명시할 필요가 없다.


1
2
3
4
5
abstract class Animated {
    abstract fun animate() //추상 함수 하위 클래스에서 반드시 구현
    open fun stopAnimating() {} //추상 클래스에 속했더라도 비추상 함수는 기본적으로 final이지만 open으로 오버라이드를 허용할 수 있다.
    fun animateTwice() {}
}



open: 상속이 가능한 클래스로 명명함

final: 상속이 불가능한 클래스로 명명함. (java와 동일) - 기본값 

abstract: 추상 클래스임 (java와 동일)

override : 상위 클래스나 상위 인스턴스의 멤버를 오버라이드 한다.


가시성 변경자: 기본적으로 공개


코틀린은 자바와 가시성 변경자가 비슷하다. 하지만 코틀린의 기본 가시성은 자바와 다르게 아무 변경자도 없는 경우  모두 public이다.

코틀린은 패키지를 네임스페이스를 관리하기 위한 용도로만 사용한다. 그래서 패키지를 가시성 제어에 사용하지 않는다.


기본값은 public 이다. 

자바에서는 기본값은 package-private 이지만, 코틀린에서는 package-private 속성이 없다.


internal이란 키워드가 추가되었으며, 이는 모듈한정이다.

코틀린에서 새로 추가된 키워드로 사용 범위를 모듈로 한정해 준다. android studio를 사용하면 코드를 모듈단위로 분리할 수 있고, 모듈은 각각 complie 될수 있는 하나의 단위이다.


Top-level 에 대해서도 가시성을 제공한다.

최상위 함수, 변수, 클래스에도 가시성을 사용할 수 있다.


private으로 지정하면 해당 파일 안에서만 접근이 가능하다.


public :  모든 곳에서 볼 수 있다.

internal : 같은 모듈 안에서만 볼 수 있다.

protected :  하위 클래스 안에서만 볼 수 있다.

private : 같은 클래스 안에서만 볼 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Focusable {
    fun setFocus(b: Boolean) =
            println("I ${if (b) "got" else "lost"} focus.")
 
    fun showOff() = println("I'm focusable!")
}
 
internal open class TalkativeButton : Focusable {
    private fun yell() = println("Hey!"
    protected fun whisper() = println("Let's talk!")
}
 
fun TalkativeButton.giveSpeech() { //오류 public멤버가 internal의 TalkativeButton 호출  
    yell() //오류 private인 yell 접근할 수 없음
    whisper() //protected인 whisper 접근할 수 없음
}
cs


위의 예제에서 public  함수인 giveSpeech 안에는 그보다 가시성이 더 낮은 타입인 TalkativeButton을 참조할 수 없다. 위에서 컴파일 오류를 없애려면 giveSpeech 확장 함수의 가시성을 interanal로 바꾸거나 TalkativeButton 클래스의 가시성을 public 으로 바꿔야 한다.


자바에서는 같은 패키지 안에서 protected 멤버에 접근할 수 있지만 코틀린에서는 그렇지 않다. 


코틀린의 자바 가시성 규칙의 또 다른 차이는 코틀린에서는 외부 클래스가 내부 클래스나 중첩된 클래스의 private  멤버에 접근할 수 없다. 


내부 클래스와 중첩된 클래스: 기본적으로 중첩 클래스


코틀린도 자바처럼 내부에 클래스를 만들 수 있다 다만 자바와의 차이는 코틀린의 중첩 클래스는 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없다.


코틀린 중첩 클래스에 아무런 변경자가 붙지 않으면 자바 static 중첩 클래스와 같다. 이를 내부 클래스로 변경해서 바깥쪽 클래스에 대한 참조를 포함하게 만들고 싶다면 inner 변경자를 붙여야 한다. 


자바와 코틀린의 중첩 클래스와 내부 클래스의 관계

 클래스  B 안에 정의된 클래스 A 

자바에서 

코틀린에서 

중첩 클래스(바깥쪽 클래스에 대한 참조를 저장하지 않음) 

 static class A

class A 

내부 클래스(바깥쪽 클래스에 대한 참조를 저장함) 

 class A 

inner class A 


코틀린에서 바깥쪽 클래스의 인스턴스를 가리키는 참조를 표기하는 방법도 자바와 다르다. 내부 클래스 Inner 안에서 바깥쪽 클래스 Outer의 참조에 접근하려면 this@Outer 라고 써야 한다.


1
2
3
4
5
class Outer {
    inner class Inner {
        fun getOuterRef(): Outer = this@Outer
    }
}



봉인된 클래스: 클래스 계층 정의 시 계층 확장 제한


코틀린 컴파일은 when을 사용해 expr타입의 값을 검사할 때 꼭 디폴트 분기인 else문을 강제한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface Expr {} //Expr 인터페이스 선언
 
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr
 
fun eval(e: Expr): Int {
 
    if(e is Num) {
 
        return e.value
    }
 
    if(e is Sum) {
 
        return eval(e.left) + eval(e.right)
    }
 
    throw IllegalArgumentException("Unknown expression")
}



이런 문제 해법을 위해 sealed 클래스가 그 답이다.상위 클래스에 sealed 변경자를 붙이면 그 상위 클래스에 상속한 하위 클래스 정의를 제한할 수 있다. sealed 클래스의 하위 클래스를 정의할 때는 반드시 상위 클래스 안에 중첩시켜야 한다.


1
2
3
4
5
6
7
8
9
10
sealed class Expr { //기반 클래스를 sealed로 봉인한다.
    class Num(val value: Int): Expr() //기반 클래스의 모든 하위 클래스를 중첩 클래스로 나열한다.
    class Sum(val left: Expr, val right: Expr): Expr()
}
 
fun eval(e: Expr): Int = 
    when (e) { //when식이 모든 하위클래스를 검사하므로 별도의 else 분기가 없어도 된다.
        is Expr.Num -> e.value
        is Expr.Sum -> eval(e.left) + eval(e.right)
    }



when 식에서 sealed 클래스의 모든 하위 클래스를 처리한다면 디폴트 분기(else) 가 필요 없다. sealed로 표시된 클래스는 자동으로 open이다.


주 생성장와 초기화 블록


자바에서는 클래스의 선언은 {} 사이에 들어간다. 하지만 위애서는 val선언만 존재한다. 이렇게 클래스 이름 뒤에 오는 괄호로 둘러싸인 코드를 주 생성자 라고 한다.


주 생성자는 생성자 파라미터를 지정하고 그 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의하는 두 가지 목적에 쓰인다.


1
class User(val nickName: String)



위의 코드를 명시적은 선언으로 풀어서 어떻게 동작하는지 보자면 아래와 같다


1
2
3
4
5
6
7
class User constructor(val _nickName: String) {
    val nickName: String
    
    init {
        this.nickName = _nickName
    }
}



constructor 키워드는 주 생성자나 부 생성자 정의를 시작할 때 사용한다. init  키워드는 초기화 블록을 시작한다. 초기화 블록은 객체가 인스턴스화 될 때 실행될 초기화 코드가 들어간다. 초기 화 블록은 주 생성자와 함께 사용된다. 주 생성자는 제한적이기 때문에 별도의 코드를 포함할 수 없으므로 초기화 블록이 필요하다.


위의 예제는 nickNamme 프로퍼티를 초기화 하는 코드를 nickname 프로퍼티 선언에 포함시킬 수 있어서 초기화 코드를 초기화 블록에 넣을 필요가 없다. 또 주 생성자 앞에 별다른 어노테이션이나 가시성 변경자가 없다면 constructor을 생략해도 된다. 이런 변경을 적용하면 아래 코드와 같다.


1
2
3
class User(_nickName: String) {
    val nickName = _nickName
}



또한 디폴트 값을 생성자에 정의할 수 있다.


1
class User(val nickName: String, val isSubscribed: Boolean = true)



모든 생성자 파라미터에 디폴트 값을 지정하면 컴파일러가 자동으로 파라미터가 없는 생성자를 만들어준다. 그렇게 자동으로 만ㄷ르어진 파라미터 없는 생성자는 디폴트 값을 사용해 클래스를 초기화 한다.


클래스에서 상속받은 클래스를 초기화 하려면 괄호를 치고 생성자 인자를 넘긴다.


1
2
open class User(val nickName: String)
open class KakaoUser(nickName: String) : User(nickName)



클래스를 정의할 때 별도로 생성자를 정의하지 않으면 컴파일러가 자동으로 아무 일도 하지 않는 디폴트 생성자를 만들어준다.


1
2
open class Button //인자가 없는 디폴트 생성자가 만들어진다
class BackButton : Button()



Button생성자는 아무 인자도 받지 않지만, Button 클래스를 상속한 하위 클래스는 반드시 Button클래스의 생성자를 호출해야 한다. 그래서 빈 괄호가 들어가게 된다.


어떤 클래스를 외부에서 인스턴스화 하지 못하게 막고 싶다면 모든 생성자를 private로 만들면 된다.


1
class Secretive private constructor() {}



부 생성자: 상위 클래스를 다른 방식으로 초기화


부 생성자를 설명하기 위해 아래 예제가 있다. 아래 클래스는 주 생성자를 선언하지 않고, 부 생성자만 2가지 선언한다. 


1
2
3
4
5
6
7
8
9
10
11
12
class Context
class AttributeSet
 
open class View {
    constructor(ctx: Context) {
                
    }
    
    constructor(ctx: Context, attr: AttributeSet) {
        
    }
}



위의 클래스를 확장하여 똑같이 부 생성자를 정의할 수 있다.


1
2
3
4
5
6
7
8
9
class MyButton: View {
    constructor(ctx: Context) : super(ctx) {
        
    }
    
    constructor(ctx: Context, attr: Attribute) : super(ctx, attr) {
        
    }
}



또한 자바와 마찬가지로 생성자에서 this()를 통해서 클래스 자신의 다른 생성자를 호출할 수 있다.


1
2
3
4
5
6
7
8
9
class MyButton: View {
    constructor(ctx: Context) : this(ctx, MY_STYLE) {
 
    }
 
    constructor(ctx: Context, attr: Attribute) : super(ctx, attr) {
 
    }
}



클래스에 주 생성자가 없다면 모든 부 생성자는 반드시 상위 클래스를 초기화하거나 다른 생성자에게 생성을 위임해야 한다.


인터페이스에 선언된 프로퍼티 구현


코틀린에서는 인터페이스에 추상 프로퍼티 선언을 넣을 수 있다. 다음은 추상 프로퍼티 선언이 들어있는 인터페이스 선언의 예다.


1
2
3
interface User {
    val nickName: String
}



인터페이스에 있는 프로퍼티 선언에는 뒷받침하는 필드나 게터 등의 정보가 들어있지 않다. 인터페이스에 상태를 저장할 필요가 있다면 인터페이스를 구현한 하위 클래스에서 상태 저장을 위한 프로퍼티를 만들어야 한다.


아래는 인터페이스를 구현한 몇가지 예제이다. 

PrivateUser은 주 생성자 안에 프로퍼티를 직접 선언하였고, SubscribingUser은 커스텀 게터로 nickname 프로퍼티를 설정한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface User {
    val nickName: String
}
 
class PrivateUser(override val nickName: String) : User
 
class SubscribingUser(val email: String) : User {
    override val nickName: String
        get() = email.substringBefore('@')
}
 
class FaceBookUser(val accountId: String) : User {
    override val nickName = getFacebookName(accountId)
}
 
fun getFacebookName(accountId: String= accountId
 
fun main(args: Array<String>) {
    println(PrivateUser("theo.dev@kakaomobility.com").nickName)
    println(SubscribingUser("theo.dev@kakaomobility.com").nickName)
}
cs


인터페이스에는 추상 프로퍼티뿐 아니라 게터와 세터가 있는 프로퍼티를 선언할 수도 있다. 


인터페이스에 email 추상 프로퍼티와 커스텀 게터가 있는 nickNAme 프로퍼티가 있다. 하위 클래스에서는 추상 프로퍼티인 email을 반드시 오버라이드해야 한다. 반면 nickNAme은 오버라이드하지 않고 상속할 수 있다.


1
2
3
4
5
interface User {
    val email: String
    val nickName : String
        get() = email.substringBefore('@'//프로퍼티에 뒷받침하는 필드가 없다. 대신 매번 결과를 계산해서 돌려준다.
}



게터와 세터에 뒷받침하는 필드에 접근


어떤 값을 저장하되 그 값을 변경하거나 읽을 때마다 정해진 로직을 실행하는 유형의 프로퍼티를 만드는 방법이 있다. 값을 저장하는 동시에 로직을 실행할 수 있게 하기 위해서는 접근자 안에서 프로퍼티를 뒷받침하는 필드에 접근할 수 있어야 한다.


아래 예제에서는 저장된 값의 변경 이력을 출력하려고 한다. 


아래 예제 접근자 본문에서 field라는 특별한 식별자를 통해 뒷받침하는 필드에 접근할 수 있다. 게터에서는 field 값을 읽을 수만 있고, 세터에서는 field 값을 읽거나 쓸 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
class User(val name: String) {
    var address: String = "unspecified"
        set(value: String) {
            println("""
                Address was changed for $name: "$field" -> "$value".
            """.trimIndent())
            field = value
        }
}
 
fun main(args: Array<String>) {
    val user = User("Alice")
    user.address = "서울시 영등포구 대방동"
}



접근자의 가시성 변경


접근자의 가시성은 기본적으로 프로퍼티의 가시성과 같다. 원한다면 get이나 set  앞에 가시성 변경자를 추가해서 접근자의 가시성을 변경할 수 있다.


1
2
3
4
5
6
7
8
class LengthCounter {
    var count: Int = 0
        private set
 
    fun addWord(value: String) {
        count += value.length
    }
}
cs


코틀린에서의 equals, hashCode, toString


코틀린에서 toString 구현


1
2
3
class User(val name: String, val age: Int) {
    override fun toString() = "User(name=$name, age=$age)"
}



코틀린에서 equals 구현


1
2
3
4
5
6
7
class Client(val name: String, val postalCode: Int) {
    override fun equals(other: Any?): Boolean {
        if(other == null || other !is Client) 
            return false
        return this.name == other.name && this.postalCode == other.postalCode
    }
}


 

코틀린에서 hashCode()


1
2
3
4
5
class Client(val name: String, val postalCode: Int) {
    override fun hashCode(): Int {
        return name.hashCode() * 31 + postalCode
    }
}



코틀린에서 data 선언하기


어떤 클래스가 데이터를 저장하는 역할을 수행한다면 toString, equals, hashCode를 반드시 재정의해서 사용해야 한다. 코틀린에서는 클래스 앞에 data라는 변경자를 앞에 붙이면 필요한 메서드를 컴파일러가 자동적으로 만들어준다.


데이터 클래스와 불변성: copy 메서드


데이터 클래스 인스턴스를 불변 객체로 더 쉽게 활용할 수 있게 코틀린 컴파일러는 한 가지 편의 메서드를 제공한다. 그 메서드는 객체를 복사 하면서 일부 프로퍼티를 바꿀 수 있게 해주는 copy 메서드이다.


1
2
3
4
5
6
7
8
class Client(val name: String, val postalCode: Int) {
    fun copy(name: String = this.name, postalCode: Int = this.postalCode) = Client(name, postalCode)
}
 
fun main(args: Array<String>) {
    val client = Client("ktko"111)
    val copy = client.copy()
}



클래스 위임: by 키워드 사용


IDE가 생성해주는 코드를 사용하지 않고도 위임을 쉽게 사용할 수 있게 해주는 코틀린 기능인 클래스 위임이 있다.

인터페이스를 구현할 때 by 키워드를 통해 그 인터페이스 대한 구현을 다른 객체에 위임 중이라는 사실을 명시할 수 있다.


아래는 Collectin 같이 비교적 단순한 인터페이스를 구현하면서 아무 동작도 변겨앟지 않은 데코레이터를 만들 때 아래와 같이 복잡한 코드를 작성해야 한다.


1
2
3
4
5
6
7
8
9
class DelegationCollection<T> : Collection<T>{
    private val innerList = arrayListOf<T>()
 
    override val size: Int get() = innerList.size
    override fun isEmpty(): Boolean = innerList.isEmpty()
    override fun contains(element: T): Boolean = innerList.contains(element)
    override fun iterator(): Iterator<T> = innerList.iterator()
    override fun containsAll(elements: Collection<T>): Boolean = innerList.containsAll(elements)
}



아래는 코틀린에서 제공하는 by 키워드를 이용하여 재작성한 코드이다.


1
2
3
4
class DelegationCollection<T>(
    innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList{}
 



Object 키워드: 클래스 선언과 인스턴스 생성


코틀린에서는 object 키워드를 다양한 상황에서 사용하지만 클래스를 정의하면서 동시에 인스턴스를 생성한다는 공통점이 있다. 

Object 키워드를 사용하는 여러 상황이 있다.


1. 객체 선언은 싱글턴을 정의하는 방법 중 하나다.

2. 동반 객체는 인스턴스 메서드는 아니지만 어떤 클래스와 관련 있는 메서드와 팩토리 메서드를 담을 때 쓰인다.

3. 객체 식은 자바의 무명 내부 클래스 대신 쓰인다.


객체 선언 싱글턴을 쉽게 만들기


코틀린은 객체 선언 기능을 통해 싱글턴을 언어에서 기본 지원한다. 객체 선언은 클래스 선언과 그 클래스에 속한 단일 인스턴스의 선언을 합친 선언이다.


아래는 회사 급여 대장을 만드는 것이다. 한 회사에 여러 급여 대장이 필요하지 않으니 싱글턴을 쓰는게 맞다.


1
2
3
4
5
6
7
8
9
object Payroll {
    val allEmployees = arrayListOf<Any>()
    
    fun calculateSalary() {
        for (Any in allEmployees) {
            //...//
        }
    }
}



객체 선언은 object 키워드로 시작한다. 객체 선언은 클래스를 정의하고 그 클래스의 인스턴스르 만들어서 변수에 저장하는 모든 작업을 단 한 문장으로 처리한다.


클래스와 마찬가지로 객체 선언 안에도 프로퍼티, 메서드, 초기화 블록 등이 들어갈 수 있다. 하지만 생성자는(주 생성자, 부 생성자) 쓸 수 없다. 변수와 마찬가지로 객체 선언에 사용한 이름 뒤에 마침표 .를 붙이면 객체에 속한 메서드나 프로퍼티에 접근할 수 있다.


1
2
Payroll.allEmployees.add(Person(...))
Payroll.calculateSalary()
cs


객체 선언도 클래스나 인스턴스를 상속할 수 있다. 프레임워크를 사용하기 위해 특정 인터페이스를 구현해야 하는데, 그 구현 내부에 다른 상태가 필요하지 않은 경우에 이런 기능이 유용하다. 구체적인 예로 아래에 파일의 경로를 대소문자 관계 없이 비교해주는 Comparator를 구현한다.


1
2
3
4
5
object CaseInsensitiveFileComparator : Comparator<File> {
    override fun compare(file1: File, file2: File): Int {
        return file1.path.compareTo(file2.path, ignoreCase = true)
    }
}



일반 객체를 사용할 수 있는 곳에서는 항상 싱글턴 객체를 사용할 수 있다. 또한 클래스 안에서 객체를 선언할 수 있다. 그런 객체도 인스턴스는 단 하나뿐이다.


1
2
3
4
5
6
7
data class Person(val name: String) {
    object NameComparator : Comparator<Any> {
        override fun compare(p1: Any, p2: Any): Int {
            //....
        }
    }
}



동반 객체: 팩토리 메서드와 정적 멤버가 들어갈 장소


코틀린 언어는 자바 static 키워드를 지원 하지 않는다. 대신 패키지 수준의 최상위 함수와 객체 선언을 활용한다. 하지만 최상위 함수는 private으로 표시된 클래스 비공개 멤버에 접근할 수 없다. 클래스의 인스턴스와 관계없이 호출해야 하지만 클래스 내부 정보에 접근해야 하는 함수가 필요할 때는 클래스에 중첩된 객체 선언의 멤버 함수로 정의해야 한다.


companion이라는 틀별한 표시를 붙이면 그 클래스의 동반 객체로 만들 수 있다. 동반 객체의 프로퍼티나 메소드에 접근하려면 그 동반 객체가 정의된 클래스 이름을 사용한다.


1
2
3
4
5
6
7
class A {
    companion object {
        fun bar() {
            println("companion object called")
        }
    }
}



아래는 부 생성자 2개가 있는 클래스가 있고, 이 클래스를 동반 객체 안에서 팩토리 클래스를 정의하는 방식이다.


1
2
3
4
5
6
7
8
9
10
class User {
    val nickName: String
    constructor(email: String) {
        nickName = email.substringBefore("@")
    }
    
    constructor(facebookAccountId: Int) {
        nickName = facebookAccountId.toString()
    }
}



위의 예제를 동반 객체로 변경하자


1
2
3
4
5
6
class User private constructor(val nickName: String) {
    companion object {
        fun newSubscribingUser(email: String= User(email.substringBefore("@"))
        fun newFacebookUser(accountId: Int= User(accountId.toString())
    }
}



메인 함수에서 호출을 한다.


1
2
val sUser = User.newSubscribingUser("ktko@gmail.com")
println(sUser.nickName)



위와 같이 동반객체를 팩토리 형식으로 사용하면 유용하고, 목적에 따라 팩토리 메서드 이름을 정할 수 있다. 또한 팩토리 메서드 안에서 선언된 클래스의 하위 클래스 객체를 반환할 수 있다.


동반 객체를 일반 객체처럼 사용


1
2
3
4
5
6
7
8
9
class Person(val name: String){
    companion object Loader{
        fun fromJSON(jsonText: String): Person = //...
    }
}
 
fun main(args: Array<String>) {
    Person.Loader.fromJSON("")
}



위에 처럼 특별한 이름을 지정하지 않으면 동반 객체는 자동으로 Companion이 되며 위의 예제에서는 동반 객체의 이름을 Loader로 지정하여 호출하는 것을 알 수 있다.


동반 객체에서 인터페이스 구현은 책에서.. 추후 업데이트..