오늘도 더 나은 코드를 작성하였습니까?

클래스 객체 인터페이스(Object 키워드 : 클래스 선언과 인스턴스 생성) 본문

Kotlin in Action/코틀린 기초

클래스 객체 인터페이스(Object 키워드 : 클래스 선언과 인스턴스 생성)

hik14 2020. 8. 14. 18:33

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

 

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

 

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

 

2. 동반 객체(companion object)는 인스턴스 메서드는 아니지만 어떤 클래스와 관련 있는 메서드와 팩토리 메서드를 담을 때 쓰인다. 동반 객체 메서드에 접근할 때는 동반 객체가 포함된 클래스의 이름을 사용할 수 있다. 

 

3. 무명 내부 클래스(익명 객체) 대신 쓰인다

 

객체 선언

- 클래스 선언과  그 클래스의 단일 인스턴스의 선언을 합친 것이다.

- 클래스를 정의하고 인턴스를 만들어 변수에 저장하는 모든 작업을 단 한 문장 obeject {... }로 줄인 것이다.

- 프로퍼티, 메서드, 초기화 블록 등이 등이 들어갈 수 있다.

- 주 생성자, 부 생성자는 사용할 수 없다. 

- 객체 선언문이 있는 위치에서 생성자 호출 없이 즉시 만들어지기 때문에 생성자 정의가 필요 없다.

data class Person(val name: String, val salary: Int)

object Payroll{
    val allEmployees = arrayListOf<Person>()

    fun calculateSalary(){
        for (person in allEmployees){
            println(person.toString())
        }
    }
}

fun main() {
    Payroll.allEmployees.add(Person("hik",100))
    Payroll.allEmployees.add(Person("huk",300))

    Payroll.calculateSalary()
}

- 클래스나 인터페이스를 상속할 수 있다.

예시)  Comparator는 두 객체를 받아 그중 어느 것이 큰지 알려주는 정수를 반환한다.

보통 클래스마다 1개씩 필요하기 때문에 객체 선언으로 만들면 좋다.

 

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

* o1 > o2  return 1

  o1 = o2  return 0

  o1 < o2  return -1

 

- 클래스 안에서 객체를 선언 가능하다. 단, 인스턴스는 단 하나뿐이다.

data class Person(val name: String){
    object NameComparator: Comparator<Person>{
        override fun compare(o1: Person, o2: Person): Int = o1.name.compareTo(o2.name, ignoreCase = true)
    }
}

fun main() {
    val persons = listOf(Person("alice"), Person("Bob"), Person("star"), Person("hik"))

    println(persons.sortedWith(Person.NameComparator))
}

* 코틀린의 객체 선언은 유일한 인스턴스에 대한 정적인 필드가 있는 자바 클래스로 컴파일된다. 이때 인스턴스의 필드 이름은 INSTANCE이다. 

ClassName.INSTANCE.methodName(arg...)로 호출한다. 자바에선.

 

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

코틀린 클래스에는 정적 멤버가 없다 static 키워드 지원하지 않는다.

대부분은 최상위 함수와 객체 선언으로 대체하고 최상위 함수 선언을 더 권장한다.

하지만 최상위 함수는 비공개 멤버에 접근할 수 없기 때문에 클래스 내부 정보에 접근할 필요가 있을 때는 객체 선언을 사용한다.

 

- 클래스 안에 정의된 객체 선언에 companion 특별한 표시를 붙이면  그 클래스의 동반 객체가 된다.

- 동반 객체의 프로퍼티나 메서드에 접근하려면 그 동반 객체가 정의된 클래스의 이름을 사용한다.

- 동반 객체의 이름은 따로 정의할 수 있지만 안 해도 둘러싼 클래스의 이름으로 호출 가능하다.

- 클래스 내에 2개 이상 companion object를 쓰는 것은 안 된다

 

private 생성자를 호출하기 좋은 위치 

동반 객체는 자신을 포함하는 클래스의 private생성자 호출 가능하다. 따라서 동반 객체는 팩토리 패턴을 구현하기 가장 적합한 위치이다.

 

예시) 

class User {
    val nickName: String
    
    constructor(email:String){
        nickName = email.substringBefore("@")
    }
    
    constructor(facebookAccountId: Int){
        nickName = getFacebookName(facebookAccountId)
    }

    private fun getFacebookName(facebookAccountId: Int): String {
        return "test"
    }
}

 

class User private constructor(val nickname: String){

    companion object{

        fun newSubscribingUser(email: String)
                = User(nickname = email.substringBefore("@"))

        fun newFacebookUser(accountId: Int)
                = User(getFacebookName(accountId))

        private fun getFacebookName(accountId: Int): String = "facebook user"
    }
}

fun main() {

    val subscribingUser = User.newSubscribingUser("hoho@gmail.com")
    val facebookUser = User.newFacebookUser(14)

    println(subscribingUser.nickname)
    println(facebookUser.nickname)
}

* 클래스를 확장해야 되는 경우 동반 객체는 하위 클래스에서 오버라이드 할 수 없기 때문에 여러 개의 생성자를 두는 것이 나은 해법이다.

 

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

 

- 동반 객체 역시 클래 안에 정의된 일반 객체다.

- 이름 붙이기, 상속, 확장 함수, 프로퍼티를 정의할 수 있다.

- 이름을 따로 안 붙이면 Companion이 된다.

 

class Person(val name: String){
    companion object Loader{
        fun fromJSON(jsonText: String) = Person(jsonText.substringAfter("'").substringBefore("'"))
    }
}
fun main() {

    val person1 = Person.fromJSON("{name: 'Dmitry'}")
    val person2 = Person.Loader.fromJSON("{name: 'hik'}")

    println(person1.name)
    println(person2.name)
}

 

동반 객체 인터페이스 구현하기

interface JSONFactory<T>{
    fun fromJSON(jsonText: String): T
}

class Person(val name: String){
    
    companion object: JSONFactory<Person>{
        override fun fromJSON(jsonText: String): Person 
                = Person(jsonText.substringAfter("'").substringBefore("'"))
    }
}

fun <T> loadFromJSON(factory: JSONFactory<T>): T{
	// json 으로부터 각원소를 다시만들어내는 추상팩토리가 가 있다면
}


fun main() {
    // Person class 이름을 사용했다. 
    loadFromJSON(Person)
}

 

*자바에서 사용할 때 코틀린 멤버를 정적 멤버로 만들 필요가 있다면 @JvmStatic 

정적 필드가 필요하다면 @jvmField를 최상위 프로퍼티나 클래스에서 선언된 프로퍼티 앞에 붙인다.

 

동반 객체의 확장

 

- 동반객체에 확장 함수를 정의하려면 원래 클래스에 반드시 동반 객체를 꼭 선언해야 된다.

빈 동반 객체라도 선언해야 된다.

class Person(val name: String, val lastname: String){

    companion object{
    
    }
}

fun Person.Companion.fromJSON(jsonText: String): Person{
    ...
    return Person( ... )
}


객체 식: 무명 내부 클래스를 다른 방식으로 작성

class MainActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        
        button.setOnClickListener(object: View.OnClickListener{
            override fun onClick(p0: View?) {
                TODO("Not yet implemented")
            }
        })
    }
}

*코틀린 무명 클래스는 여러 인 터페이스를 구현하거나 클래스를 확장하면서 인터페이스를 구현할 수 있다.

* 무명객체는 싱글턴이 아니라 객체 식이 사용될 때마다 생성된다.

 

*객체식 안의 코드는 그식이 포함된 함수의 변수에 접근할 수 있다. 자바와 달리 final 아닌 변수도 객체 식 안에서 이용할 수이다.

    fun countClicks(){
    
        var clickCount = 0
        
        button.setOnClickListener(object: View.OnClickListener{
            override fun onClick(p0: View?) {
                clickCount++
            }
        })
    }

* 객체식은 무명 객체 내에서 여러 메서드를 오버라이드 하는 경우 훨씬 더 효과적이다.

메서드가 하나뿐인 인터페이스를 구현해야 된다면 SAM(Single Abstract Method) 변환을 활용하는 편이 낫다.