object 키워드는 클래스를 정의하는 동시에 인스턴스를 생성한다.
object 키워드를 사용하는 여러가지 상황은 다음과 같다.
코틀린은 객체 선언을 통해 싱글턴을 언어에서 기본 지원한다.
객체 선언은 그 클래스에 속한 단일 인스턴스의 선언을 합친 선언이다.
예를 들어 객체 선언을 사용해 회사 급여 대장을 만들 수 있다.
한 회사에 여러 급여 대장이 필요하지는 않을 테니 싱글턴을 쓰는 것이 정당해 보인다.
object Payroll {
val allEmployees = arrayListOf<Person>()
fun calculateSalary(){
for(person in allEmployees){
/*...*/
}
}
}
객체 선언은 object 키워드로 시작한다.
객체 선언은 클래스를 정의하고 그 클래스의 인스턴스를 만들어 변수에 저장하는 모든 작업을 한 문장으로 처리한다.
클래스와 마찬가지로 객체 선언 안에도 프로퍼티와 메서드, 초기화 블록 등이 들어갈 수 있다.
하지만 객체 선언에는 생성자(주 생성자, 부 생성자)를 쓸 수 없다.
일반 클래스 인스턴스와 달리 싱글턴 객체는 객체 선언문이 있는 위치에서 생성자 호출 없이 즉시 만들어진다.
따라서 객체 선언에는 생성자 정의가 필요 없다.
객체 선언에 제공하고 싶은 초기 상태가 있다면 그 객체의 본문에서 직접 제공해야 한다.
변수와 마찬가지로 객체 선언에 사용한 이름 뒤에 점( . )을 붙이면 객체에 속한 메서드나 프로퍼티에 접근할 수 있다.
Payroll.allEmployees.add(Person(/*...*/))
Payroll.calculateSalary()
객체 선언도 클래스나 인스턴스를 상속할 수 있다.
어떤 인터페이스를 구현해야 하는데 그 구현 내부에 다른 상태가 필요하지 않은 경우 이런 기능이 유용하다.
구체적인 예제로 두 파일 경로를 대소문자 관계없이 비교해주는 Comparator를 구현해보자.
import java.io.File
object CaseInsensitiveFileComparator: Comparator<File> {
override fun compare(file1: File, file2: File): Int {
return file1.path.compareTo(file2.path, ignoreCase = true)
}
}
fun main() {
println(CaseInsensitiveFileComparator.compare(File("/User"), File("/user"))) //0
}
일반 객체를 사용할 수 있는 곳에서는 항상 싱글턴 객체를 사용할 수 있다.
예를 들어 Comparator를 인자로 받는 함수에게 이 객체를 인자로 넘길 수 있다.
fun main() {
val files = listOf(File("/Z"), File("/a"))
println(files.sortedWith(CaseInsensitiveFileComparator)) //[\a, \Z]
}
클래스 안에서 객체를 선언할 수도 있다. 그런 객체도 인스턴스는 하나 뿐이다.
바깥 클래스의 인스턴스 마다 별도의 인스턴스가 따로 생기는 것이 아니다.
data class Person(val name: String) {
object NameComparator: Comparator<Person>{
override fun compare(p1: Person, p2: Person): Int = p1.name.compareTo(p2.name)
}
}
fun main(){
val persons = listOf<Person>(Person("Bob"), Person("Alice"))
println(persons.sortedWith(Person.NameComparator))
//[Person(name=Alice), Person(name=Bob)]
}
코틀린 객체를 자바에서 사용하기
코틀린 객체 선언은 유일한 인스턴스에 대한 정적인 필드가 있는 자바 클래스로 컴파일 된다.
이때 이 인스턴스의 이름은 항상 INSTANCE다.
코틀린 클래스는 자바의 static 키워드를 지원하지 않는다.
그 대신 패키지 수준의 최상위 함수와 객체 선언을 활용한다.
대부분의 경우 최상위 함수를 활용하는 편을 더 권장한다.
하지만 최상위 함수는 private으로 선언된 클래스 비공개 맴버에 접근할 수 없다.
비공개 맴버에게 접근해야 하는 함수의 대표적인 예로 팩토리 메서드를 들 수 있다.
팩토리 메서드는 객체 생성을 책임지기 때문에 클래스의 private 맴버에 접근할 수 있다.
클래스 인스턴스와 상관없이 호출해야 하지만, 클래스 내부 정보에 접근해야 하는 함수가 필요할 때도 클래스에 내포된 객체 선언의 맴버 함수로 정의할 수 있다.
이렇게 클래스 안에 저의된 객체 중 하나에 companion이라는 특별한 표시를 붙일 수 있다.
그렇게 하면 객체 맴버에 접근할 때 자신을 감싸는 클래스의 이름을 통해 직접 사용할 수 있다.
class MyClass {
companion object {
fun callMe() {
println("Companion object called")
}
}
}
fun main() {
MyClass.callMe() //Companion object called
}
동방 객체가 자신에 대응하는 클래스에 속한다는 점을 염두에 두는 것이 중요하다.
따라서 해당 클래스의 인스턴스는 동반 객체의 맴버에 접근할 수 없다. 이것이 자바의 정적 맴버와 코틀린의 동반 객체의 다른 점이다.
class MyClass {
companion object {
fun callMe() {
println("Companion object called")
}
}
}
fun main() {
val myClass = MyClass()
myClass.callMe()
}
//Unresolved reference 'callMe'.
companion object는 private 생성자를 호출하기 좋은 위치다.
동반 객체는 자신을 둘러싼 클래스의 모든 private 맴버에 접근할 수 있다.
따라서 동반 객체는 바깥쪽 클래스의 private 생성자도 호출할 수 있다.
이로 인핸 동반 객체는 팩토리 패턴을 구현하기 가장 적합한 위치가 될 수 있다.
예제로 부 생성자가 2개 있는 클래스를 살펴보고, 그 클래스를 동반 객체 안에서 선언된 팩토리 메서드를 사용하는 방식으로 변경해보자.
class User{
val nickname: String
constructor(email: String){
nickname = email.substringBefore('@')
}
constructor(socialAccountId: Int){
nickname = getSocialNetworkName(socialAccountId)
}
}
이 같은 로직을 표현하는 더 유용한 방법으로 클래스의 인스턴스를 생성하는 팩토리 메서드가 있다.
여기서는 생성자를 통해 User 인스턴스를 만들 수 없고 팩토리 메서드를 만들어야만 한다.
class User private constructor(val nickname: String) {
companion object {
fun newSubscribingUser(email: String) = User(email.substringBefore('@'))
fun newSocialUser(accountId: Int) = User(getNameFromSocialNetwork(accountId))
}
}
클래스 이름을 통해 그 클래스에 속한 동반 객체의 메서드를 호출할 수 있다.
class User private constructor(val nickname: String) {
companion object {
fun newSubscribingUser(email: String) = User(email.substringBefore('@'))
fun newSocialUser(accountId: Int) = User(getNameFromSocialNetwork(accountId))
}
}
fun main() {
val subscribingUser = User.newSubscribingUser("bob@gmail.com")
val socialUser = User.newSocialUser(4)
println(subscribingUser.nickname) // bob
}
팩토리 메서드는 아주 유용하다. 목적에 따라 팩토리 메서드 이름을 정할 수 있다.
또한 팩토리 메서드는 그 팩토리 메서드가 선언된 클래스의 하위 클래스 객체를 반환할 수도 있다.
또한 생성할 필요가 없는 객체를 생성하지 않을 수 있다.
하지만 클래스를 확장해야 하는 경우에는 동반 객체 맴버를 하위 클래스에서 오버라이드 할 수 없다.
동반 객체는 클래스 안에 정의된 일반 객체다.
따라서 다른 객체 선언처럼 동반 객체에 이름을 붙이건나,
동반 객체가 인터페이스를 상속받거나,
동반 객체 안에 확장 함수와 프로퍼티를 정의할 수 있다.
class Person(val name: String){
companion object Loader {
fun fromJson(jsonText: String): Person = /*...*/
}
}
fun main() {
val person = Person.Loader.fromJson("""{"name": "Dmitry"}""")
println(person.name) // Dmitry
val person2 = Person.fromJson("""{"name": "Brent"}""")
println(person2.name) //Brent
}
대부분의 경우 클래스의 이름을 통해 동반 객체에 속한 맴버에 접근할 수 있으므로 동반 객체에 이름을 지정해 줄 필요는 없다.
하지만 필요하다면 동반 객체에도 이름을 부여할 수 있다.
클래스에는 동반 객체가 하나뿐이므로 이름 지정 여부와 관계없이 항상 클래스 이름을 통해 동반 객체에 접근할 수 있다.
특별히 이름을 지정해 주지 않으면 동반 객체의 이름은 자동으로 Companion이 된다.
이런 싱글턴 동반 객체를 코틀린 표준 라이브러리에서도 볼 수 있다.
코틀린의 Random 클래스에는 companion object Default가 있어 기본 난수 생성기를 제공한다.
val chance = Random.nextInt(from = 0, until = 100)
val coin = Random.Default.nextBoolean()
동반 객체도 인터페이스를 구현할 수 있다.
인터페이스를 구현하는 동반 객체를 참조할 때도 객체를 두럴싼 클래스 이름을 바로 사용할 수 있다.
시스템에 Person을 포함한 다양한 타입의 객체가 있다고 가정하자.
이 시스템에서는 모든 타입의 객체를 생성하는 일반적인 방법을 제공하고 싶다.
JSON 역직렬화를 통해 객체를 만드는 JSONFactory 인터페이스가 존재한다고 하자.
Person 클래스 안에더 다음과 같이 companion object로 JSONFactory 인터페이스를 구현할 수 있다.
interface JSONFactory<T> {
fun fromJSON(jsonText: String): T
}
class Person(val name: String) {
companion object : JSONFactory<Person> {
override fun fromJSON(jsonText: String): Person = /*...*/
}// 인터페이스를 구현하는 동반 객체
}
추상 팩토리를 통해 엔티티를 적재하는 함수가 있다면 Person 객체를 그 팩토리에 넘길 수 있다.
fun <T> loadFromJSON(factory: JSONFactory<T>): T {
/*...*/
}
loadFromJSON(Person)
확장 함수를 사용하면 코드 기반의 다른 곳에서 정의된 클래스의 인스턴스에 대해 새로운 메서드를 정의할 수 있다.
자바의 정적 메서드나 코틀린의 동반 객체 메서드처럼 기존 클래스 자체에 대해 호출할 수 있는 새로운 함수를 정의하려면 어떻게 해야 할까?
클래스에 동반 객체가 있다면 그 객체 안에 함수를 정의함으로써 클래스에 대해 호출할 수 있는 확장 함수를 만들 수 있다.
C 라는 클래스 안에 동반 객체가 있고 그 동반 객체 안에 func를 정의하면 외부에서는 C.func()로 호출할 수 있다.
class Person(val firstName: String, val lastName: String) {
companion object{
}
}
fun Person.Companion.fromJSON(json: String): Person {
/*...*/
}
val p = Person.fromJSON(json)
마치 Person 클래스 안에서 fromJSON 함수를 정의한 것처럼 호출할 수 있다.
하지만 실제로는 Person 밖에서 정의한 확장 함수다. 맴버 함수처럼 보이지만 실제로는 맴버 한수가 아니다.
동반 객체에 대한 확장 함수를 작성하려면 원래 클래스에 동반 객체가 선언되어 있어야 한다.
object 키워드를 싱글턴과 같은 객체를 정의하고 그 객체에 이름을 붙일 때만 사용하는 것은 아니다.
익명 객체를 정의할 때도 object 키워드를 사용한다.
익명 객체는 자바의 익명 내부 클래스를 대신한다.
전현적인 이벤트 리스너를 코틀린에서 구현해보자.
사용자와 버튼의 상호작용을 지정하기 위해 MouseListener 인스턴스를 취하는 Button 클래스를 다룬다고 생각하자.
interface MouseListener {
fun onEnter()
fun onClick()
}
class Button(private val listener: MouseListener) { /*...*/ }
객체 식을 사용하면 임의의 MouseListener 구현을 생성해서 Button 생성자에게 넘길 수 있다.
fun main() {
Button(object : MouseListener { // 객체 식을 사용해 익명 객체를 만든다.
override fun onEnter() { /*...*/ }
override fun onClick() { /*...*/ }
// MouseListener 인터페이스의 메서드를 구현한다.
})
}
사용한 구문은 객체 선언과 같다. 다른 점은 객체 이름이 빠졌다는 점이다.
객체 식은 클래스를 정의하고 그 클래스에 속한 인스턴스를 생성하지만
그 클래스나 인스턴스에 이름을 붙이지 않는다.
이런 경우 보통 함수를 호출하면서 인자로 익명 객체를 검기기 때문에
클래스와 인스턴스 모두 이름이 필요하지않다.
하지만 이름을 붙여야 한다면 변수에 익명 객체를 대입하면 된다.
val listener = Button(object : MouseListener {
override fun onEnter() { /*...*/ }
override fun onClick() { /*...*/ }
})
코틀린 익명 객체는 인터페이스를 하나 구현할 수도 있고,
여럿을 구현할 수도 있으며,
인터페이스를 구현하지 않을 수도 있다.
자바 익명 클래스처럼 객체 식 안의 코드도 그 식이 포함된 함수의 변수에 접근할 수 있다.
하지만 자바와 다르게 final이 아닌 변수도 객체 식 안에서 사용할 수 있다.
따라서 객체 식 안에서 변수의 값을 변경할 수 있다.
fun main() {
var clickCount = 0
Button(object : MouseListener {
override fun onEnter() { /*...*/ }
override fun onClick() {
clickCount++
}
})
}