[kotlin] class, object, interface -3

dante Yoon·2022년 4월 2일
3

kotlin

목록 보기
4/9
post-thumbnail

kotlin in action을 보고 정리한 글입니다.

The "object" keyword: declaring a class and creating an instance, combined

object keyword가 쓰이는 상황은 다음과 같다.

  • singleton 객체를 만들 때
  • companion objects가 factory methods 를 가질 수 있거나 클래스 인스턴스가 호출되지 않고 클래스 이름으로 멤버에 접근할 때
  • object expression이 쓰일 때

Object declarations: singletons made easy

코틀린에서는 class declaration + single instance declaration을 통해 object declaration을 하고 이 방법을 통해 싱글톤을 만든다.

object Payroll {
  val allEmployees = arrayListOf<Person>()
  
  fun calculateSalary() {
    for (person in allEmployees) {
     ...
    }
  }
}

급여는 유일해야 한다.object keyword를 통해 object declaration을 할 수 있으며
object는 다른 클래스와 동일하게 properties, methods, initializer block을 선언할 수 있다. 클래스와 다른 점은 constructors를 가질 수 없다는 것이다. (primary, secondary 둘 다) 또한 생성자 호출을 하지 않고 정의를 하면 바로 생성된다.

다음과 같이 호출한다.

Payroll.allEmployees.add(Person(...))

Payroll.calculateStory()

object declaration은 클래스와 동일하게 다른 class, interface를 상속할 수 있다.
프레임워크가 interface 구현을 필요로 하는데 어떤 상태도 가지지 않을 때 용이하다.
java.util.Comparator interface를 예로 들면, Comparators는 어떤 데이터도 갖고 있지 않기 때문에, 싱글톤으로 존재하면 된다. 이런 상황에서 유용하게 사용할 수 있다.

유틸성 object를 만들어보자

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

val files = listOf(File("/Z"), File("/a"))
println(files.sortedWith(CaseInsensitiveFileComparator))

클래스 내부에서도 object를 선언할 수 있다.

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

val persons = listOf(Person("Bob"), Person("Alice"))
println(persons.sortedWith(Person.NameComparator))
// [Person(name=Alice), Person(name=Bob)]

이 때 NameComparator는 Person의 인스턴스마다 있는 것이 아니다.

println(persons.sortedWith(persons.NameComparator))
// Unresolved reference: NameComparator

Companion objects: a place for factory methos and static members

코틀린 클래스에는 static member가 존재하지 않는다. static이라는 키워드가 존재하지 않는다. 이를 대체하기 위해 package-level의 function과 object declaration이 존재한다.
대부분의 상황에서는 top-level functions를 사용하는 것을 추천한다. 하지만 top-level function의 경우 클래스의 private member에 접근할 수 없다.

p.96 figure 4.5

게다가 클래스 인스턴스가 없이 클래스 내부에 접근하고 싶은 상황이 있기 때문에 object declaration도 필요하다. factory method가 그 예시이다.

자바의 static 메소드와 동일한 역할을 하는 object를 만들기 위해 companion keyword를 사용한다.

class A {
  companion object {
    fun bar() {
      println("Companion object called")
    }
  }
}

A.bar()
// Companion object called

privatge constructor를 호출하기 좋은 곳도 companion object이다. 클래스의 모든 private 멤버를 호출할 수 있기 때문에, Factory pattern을 구성하기에 이상적인 선택지이다.

companion object 내부에 factory method를 만드는 것을 해보자. 먼저 두 개의 constructor를 만들고 그 후 factory method로 변경할 것이다.

class User {
  val nickname: String
  
  constructor(email: String) {
    nickname = email.substringBefore("@")
  }
  
  constructor(facebookAccountId: Int) {
    nickname = getFacebookName(facebookAccountId)
  }
}
class User private constructor(val nickname: String) {
  companion object {
    fun newSubscribingUser(email: String) = 
      User(email.substringBefore("@"))
    
    fun newFacebookUser(accountId: Int) = 
      User(getFacebookName(accountId))
  }
}

primary constructor를 private visiblity로 선언했고,
newFacebookUser를 factory method로 만들었다.

val subscribingUser = User.newSubscribingUser("bob@gmail.com")
val facebookuser = User.newFacebookUser(4)

companion object member는 subclass에서 상속이 불가능하다.

Companion objects as regular objects

companion object는 클래스 내부에서 선언되며 이름을 명명할 수 있고 인터페이스를 구현할 수 있으며 extension functions / properties를 선언할 수 있다.

class Person(val name: String) {
  companion object Loader {
    fun fromJSON(jsonText: String): Person = ...
  }
}

person = Person.Loader.fromJSON("{name: 'Dmitry'}")
person.name
// Dmitry
person2 = Person.fromJSON("{name: 'Brent'}")
person2.name
// Brent

json을 deserialize 하는 함수를 만들었다. Person.Loader.fromJSON으로 호출하든, Person.fromJSON으로 호출하든 결과는 동일하다.

Implementing interfaces in companion objects

다른 object declarations와 마찬기지로 companion object또한 인터페이스를 구현할 수 있다. json으로 부터 deserialize 하는 공통 방법을 제공하고 싶을 때 다음과 같이 작성할 수 있다.

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

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

그 후에 loadFromJSON을 통해 deserialize 한다.

fun loadFromJSON<T>(factory: JSONFactory<T>): T {
  ...
}

loadFromJSON(Person)

JSONFactory의 instance로 Person이 loadFromJSON의 인자로 사용되었다.

Companion object extensions

자바의 static method처럼 클래스를 이용해 호출하고 싶은 메소드는 어떻게 만들 수 있는가?
class가 companion object를 가지고 있으면, companion object에 extension function을 만들 수 있다. 클래스 C에 companion object Companion이 있고 해당 object에 extension function을 만들면, C.func()으로 호출할 수 있다.

// business logic module
class Person(val firstName: String, val lastName: String) {
  companion object { }
}

// client/server communication module
fun Person.Companion.fromJSON(json: String): Person {
  ...
}

val p = Person.fromJSON(json)

companion extension function을 작성하기 위해서는 클래스 내부에 companion object를 선언해야 한다.

Object expressions: anonymous inner class rephrased

object 키워드는 싱글톤 객체만을 위해서 쓰이지는 않고 anonymous objects를 위해서 사용되기도 한다. 이것은 자바의 anonoymous inner class를 대체한다.

자바의 event listener를 코틀린으로 어떻게 변경하는지 보자.

window.addMouseListener(
  object: MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) {
      // ...
    }

    override fun mouseEntered(e: MouseEvent) {
      // ...
    }
  }
)

object declaration과 동일하나 named가 아니라는 점이 차이점이다.
object expression은 class를 선언하고 class instance를 만드나, 이름을 따로 할당하지는 않는다. 그럴 필요가 없는 것이, 함수 내부에서 사용되기 때문이다. 만약 이름을 object에 붙여야 한다면, 변수에 object를 할당하면 된다.

val listener = object : MouseAdapter() {
  override fun mouseClicked(e: MouseEvent) { ... }
  override fun mouseEntered(e: MouseEvent) { ... }
}

자바의 anonoymous inner class와 다른 점은, 자바의 anonoymous inner class는 하나의 클래스와 하나의 인터페이스만 extend/implementation 할 수 있다면, 코틀린의 anonoymous object는 multiple interfaces / no interface를 implementation을 할 수 있다.

object declaration와 다르게 anonymous object는 싱글톤이 아니다. 매번 object expression은 실행되고, 새로운 object가 생성된다.

자바와 동일하게 object expression은 object expression이 만들어진 함수에 선언된 변수에 접근할 수 있고 변경할 수 있다. 하지만 자바와는 다르게 final variable 뿐만 아니라 final keyword가 붙지 않는 변수에도 접근이 가능하다.

fun countClicks(window: Window) {
  var clickCount = 0
  
  window.addMouseListener(object: MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) {
      clickCount++
    }
  })
}
profile
성장을 향한 작은 몸부림의 흔적들

0개의 댓글