- 클래스와 인터페이스
- 뻔하지 않은 생성자와 프로퍼티
- 데이터 클래스
- 클래스 위임
- object 키워드 사용
💡 일종의 추상된 틀로, 클래스에서 구현해야 하는 메서드들의 집합을 정의한다. 다른 클래스들이 해당 인터페이스를 구현함으로써, 공통된 행동이나 규약을 정의하고 일관성 있는 구조를 갖도록 도와준다. 클래스는 하나의 클래스만을 상속받을 수 있지만, 인터페이스는 여러 개를 구현할 수 있다.
interface Clickable {
fun click()
}
class Button : Clickable {
override fun click() = println("I was clicked")
}
Button().click() // I was clicked
✅ 메서드 시그니처 : 프로그래머가 디자인한 메서드 구조를 의미하며, 메서드 이름과 파라미터 리스트로 구성된다.
// 인터페이스 안에 본문이 있는 메소드 정의하기
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"를 지정하면
// 어떤 상위 타입의 멤버 메소드를 호출할지 지정할 수 있다.
super<Clickable>.showOff()
super<Focusable>.showOff()
}
}
fun main(args: Array<String>) {
val button = Button()
button.showOff()
button.setFocus(true)
button.click()
}
//// 결과
// I'm clickable!
// I'm focusable!
// I got focus.
// I was clicked.
override fun showOff() = super<Clickable>.showOff()
상속을 위한 설계와 문서를 갖추거나, 그럴 수 없다면 상속을 금지하라 (Feat. Effective Java)
특별히 하위 클래스에서 오버라이드하게 의도된 클래스와 메소드가 아니라면 모두 final로 만들라는 뜻
// 열린 메소드를 포함하는 열린 클래스 정의하기
// 이 클래스는 열려있다. 다른 클래스가 이 클래스를 상속할 수 있다.
open class RichButton : Clickable {
// 이 함수는 final이다. 하위 클래스가 이 메소드를 오버라이드할 수 없다.
fun disable() {}
// 이 함수는 열려있다. 하위 클래스에서 이 메소드를 오버라이드해도 된다.
open fun animate() {}
// 이 함수는 (상위 클래스에서 선언된) 열려있는 메소드를 오버라이드한다.
// 오버라이드한 메소드는 기본적으로 열려있다.
override fun click() {}
}
// 오버라이드 금지하기
open class RichButton : Clickable {
// 여기 있는 "final"은 쓸데 없이 붙은 중복이 아니다.
// "final"이 없는 "override" 메소드나 프로퍼티는 기본적으로 열려있다.
final override fun click() {}
}
열린 클래스와 스마트 캐스트
클래스의 기본적인 상속 가능 상태를 final로 함으로써 얻을 수 있는 큰 이익은 다양한 경우에 스마트 캐스트가 가능하다는 점이다. 스마트 캐스트는 타입 검사 뒤에 변경될 수 없는 변수에만 적용 가능하다. 클래스의 프로퍼티의 경우 이는 val이면서 커스텀 접근자가 없는 경우에만 스마트 캐스트를 쓸 수 있다는 의미다. 이 요구사항은 또한 프로퍼티가 final이어야만 한다는 뜻이기도 하다. 프로퍼티가 final이 아니라면 그 프로퍼티를 다른 클래스가 상속하면서 커스텀 접근자를 정의함으로써 스마트 캐스트의 요구 사항을 깰 수 있다. 프로퍼티는 기본적으로 final이기 때문에 따로 고민할 필요 없이 대부분의 프로퍼티를 스마트 캐스트에 활용할 수 있다. 이는 코드를 더 이해하기 쉽게 만든다.
// 이 클래스는 추상클래스다. 이 클래스의 인스턴스를 만들 수 없다.
abstract class Animated {
// 이 함수는 추상함수다. 이 함수에는 구현이 없다. 하위 클래스에서는 이 함수를 반드시 오버라이드해야 한다.
abstract fun animate()
// 추상 클래스에 속했더라도 비추상 함수는 기본적으로 파이널이지만 원한다면 open으로 오버라이드를 허용할 수 있다.
open fun stopAnimating() {}
fun animateTwice() {}
}
변경자 | 이 변경자가 붙은 멤버는... | 설명 |
---|---|---|
final | 오버라이드할 수 없음 | 클래스 멤버의 기본 변경자다. |
open | 오버라이드할 수 있음 | 반드시 open을 명시해야 오버라이드할 수 있다. |
abstract | 반드시 오버라이드해야 함 | 추상 클래스의 멤버에만 이 변경자를 붙일 수 있다. 추상 멤버에는 구현이 있으면 안 된다. |
override | 상위 클래스나 상위 인스턴스의 멤버를 오버라이드하는 중 | 오버라이드하는 멤버는 기본적으로 열려있다. 하위 클래스의 오버라이드를 금지하려면 final을 명시해야 한다. |
class SecretClass {
val secretData: String = "This is a secret."
}
class ExternalCode {
fun useSecretData(secretInstance: SecretClass) {
// 외부 코드에서 SecretClass의 내부 데이터에 직접 접근
println(secretInstance.secretData)
}
}
fun main() {
val secretInstance = SecretClass()
val externalCode = ExternalCode()
// 외부 코드가 SecretClass의 내부 데이터에 직접 의존
externalCode.useSecretData(secretInstance)
}
class SecretClass {
private val secretData: String = "This is a secret."
fun revealSecret() {
println(secretData)
}
}
class ExternalCode {
fun useSecretClass(secretInstance: SecretClass) {
// 외부 코드에서는 SecretClass의 revealSecret 함수만 사용 가능
secretInstance.revealSecret()
// secretInstance.secretData에는 접근 불가능
}
}
fun main() {
val secretInstance = SecretClass()
val externalCode = ExternalCode()
// 외부 코드는 SecretClass의 내부 구현에 직접 의존하지 않음
externalCode.useSecretClass(secretInstance)
}
자바에서는 패키지가 같은 클래스를 선언하기만 하면 어떤 프로젝트의 외부에 있는 코드라도 패키지 내부에 있는 패키지 전용 선언에 쉽게 접근할 수 있다. 그래스 모듈의 캡슐화가 쉽게 깨진다.
변경자 | 클래스 멤버 | 최상위 선언 |
---|---|---|
public(기본 가시성임) | 모든 곳에서 볼 수 있다. | 모든 곳에서 볼 수 있다. |
internal | 같은 모듈 안에서만 볼 수 있다. | 같은 모듈 안에서만 볼 수 있다. |
protected | 하위 클래스 안에서만 볼 수 있다. | (최상위 선언에 적용할 수 없음) |
private | 하위 클래스 안에서만 볼 수 있다. | 같은 파일 안에서만 볼 수 있다. |
internal open class TalkativeButton : Focusable {
private fun yell() = println("Hey!")
protected fun whisper() = println("Let's talk")
}
// 오류: "public" 멤버가 자신의 "internal" 수신 타입인 "TalkativeButton"을 노출함
fun TalkativeButton.giveSpeech() {
// 오류: "yell"에 접근할 수 없음: "yell"은 "TalkativeButton"의 "private" 멤버임
yell()
// 오류: "whisper"에 접근할 수 없음: "whisper"는 "TalkativeButton"의 "protected" 멤버임
whisper()
}
어떤 클래스의 기반 타입 목록에 들어있는 타입이나 제네릭 클래스의 타입 파라미터에 들어있는 타입의 가시성은 그 클래스 자신의 가시성과 같거나 더 높아야 하고, 메소드의 시그니처에 사용된 모든 타입의 가시성은 그 메소드의 가시성과 같거나 더 높아야 한다는 규칙
어떤 함수를 호출하거나 어떤 클래스를 확장할 때 필요한 모든 타입에 접근할 수 있게 보장해준다.
✅ 도우미(helper) 클래스 : 특정 클래스의 작업을 도와주는 역할을 하는 클래스이다. 모든 메소드가 정적 메소드인 Utility 클래스와 달리 모든 메소드는 정적 메소드가 아니며, 여러 개의 helper class의 인스턴스가 있을 수 있다. 특정 클래스를 도와주기 위한 클래스이므로 private으로 외부의 접근을 막는 것이 좋고, 다른 helper class와 의존성이 생기지 않도록 해야 한다.
class Car(val brand: String, val model: String, val year: Int) {
// Engine 클래스를 Car 클래스 내에 캡슐화
class Engine(val type: String, val horsepower: Int) {
fun start() {
println("Engine started")
}
}
// Helper 클래스를 Car 클래스 내에 캡슐화
private class Helper {
fun performMaintenance() {
println("Performing maintenance")
}
}
// Car 클래스의 멤버 함수에서 Engine 및 Helper를 활용
fun startCar() {
val engine = Engine("V6", 300)
engine.start()
val helper = Helper()
helper.performMaintenance()
println("Car $brand $model started.")
}
}
fun main() {
val myCar = Car("Toyota", "Camry", 2022)
myCar.startCar()
}
//// 결과
// Engine started
// Performing maintenance
// Car Toyota Camry started.
// 직렬화할 수 있는 상태가 있는 뷰 선언
interface State: Serializable
interface View {
fun getCurrentState(): State
fun restoreState(state: State) {}
}
// 자바에서 내부 클래스를 사용해 View 구현하기
public class Button implements View {
@Override
public State getCurrentState() {
return new ButtonState();
}
@Override
public void restoreState(State state) { /*...*/ }
public class ButtonState implements State { /*...*/ }
}
// 중첩 클래스를 사용해 코틀린에서 View 구현하기
class Button : View {
override fun getCurrentState(): State = ButtonState()
override fun restoreState(state: State) { /*...*/ }
class ButtonState : State { /*...*/ }
}
코틀린 중첩 클래스에 아무런 변경자가 붙지 않으면드 자바 static 중첩 클래스와 같다.
이를 내부 클래스로 변경해서 바깥쪽 클래스에 대한 참조를 포함하게 만들고 싶다면 inner 변경자를 붙여야 한다.
클래스 B 안에 정의된 클래스 A | 자바에서는 | 코틀린에서는 |
---|---|---|
중첩 클래스(바깥쪽 클래스에 대한 참조를 저장하지 않음) | static class A | class A |
내부 클래스(바깥쪽 클래스에 대한 참조를 저장함) | class A | inner class A |
내부 클래스 Inner 안에서 바깥쪽 클래스 Outer의 참조에 접근하려면 this@Outer라고 써야한다.
class Outer {
val outerProperty: String = "Outer Property"
inner class Inner {
fun getOuterReference(): Outer = this@Outer
}
}
fun main() {
val outer = Outer()
val inner = outer.Inner()
// Inner 클래스를 통해 Outer 클래스의 참조에 접근
val outerReference = inner.getOuterReference()
println(outerReference.outerProperty) // 출력: "Outer Property"
}
💡 특정 클래스의 하위 클래스를 제한하는 클래스로, 상속 계층 구조에서 제한된 하위 클래스 집합을 정의할 때 사용된다. when 식과 같은 경우에 모든 하위 클래스를 처리하는지 여부를 컴파일러가 확인할 수 있다.
// 인터페이스 구현을 통해 식 표현하기
interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr
fun eval(e: Expr): Int =
when (e) {
is Num -> e.value
is Sum -> eval(e.right) + evel(e.left)
else -> // else 분기가 꼭 있어야 한다.
throw IllegalArgumentException("Unknown expression")
}
// 기반 클래스를 sealed로 봉인한다.
sealed class Expr {
// 기반 클래스의 모든 하위 클래스를 중첩 클래스로 나열한다.
class Num(val value: Int) : Expr()
class Sum(val left: Expr, val right: Expr) : Expr()
}
// "When" 식이 모든 하위 클래스를 검사하므로 별도의 "else" 분기가 없어도 된다.
fun eval(e: Expr): Int =
when (e) {
is Num -> e.value
is Sum -> eval(e.right) + evel(e.left)
}
sealed class Expr private constructor() {
class Num(val value: Int) : Expr()
class Sum(val left: Expr, val right: Expr) : Expr()
}
💡 생성자 : 객체 지향 프로그래밍에서 클래스로부터 객체를 생성할 때 호출되는 메소드로, 객체의 초기화 작업을 담당한다. 객체가 생성될 때 한 번만 호출되며, 해당 클래스의 인스턴스 변수들을 초기화하거나 다양한 설정 작업을 수행한다.
💡 주 생성자 : 클래스 이름 옆에 괄호로 둘러싸인 코드로, 생성자 파라미터를 지정하고 그 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의하는 두 가지 목적에 쓰인다.
💡 초기화 블록 : 클래스의 객체가 생성될 때 실행되는 초기화 코드를 작성하기 위한 블록으로, 주 생성자에서 직접적으로 처리하기 어려운 복잡한 초기화 작업이나 추가 로직을 처리하는 데 주로 사용된다.
class User(val nickname: String)
class User constructor(_nickname: String) {
val nickname: String
init {
nickname = _nickname
}
}
class User(_nickname: String) { // 파라미터가 하나뿐인 주 생성자
val nickname = _nickname // 프로퍼티를 주 생성자의 파라미터로 초기화한다.
}
프로퍼티를 초기화하는 식이나 초기화 블록 안에서만 주 생성자의 파라미터를 참조할 수 있다는 점에 유의하라.
class User(val nickname: String) // "val"은 이 파라미터에 상응하는 프로퍼티가 생성된다는 뜻이다.
class User(val nickname: String, val isSubscribed: Boolean = true) // 생성자 파라미터에 대한 디폴트 값을 제공한다.
public class User {
private final String nickname;
private final boolean isSubscribed;
// 생성자 파라미터에 대한 디폴트 값을 제공한다.
public User(String nickname) {
this(nickname, true);
}
public User(String nickname, boolean isSubscribed) {
this.nickname = nickname;
this.isSubscribed = isSubscribed;
}
public String getNickname() {
return nickname;
}
public boolean isSubscribed() {
return isSubscribed;
}
}
val hynn = User("현석") // isSubscribed 파라미터에는 디폴트 값이 쓰인다.
println(hyun.isSubscribed) // true
val gye = User("계영", false) // 모든 인자를 파라미터 선언 순서대로 지정할 수도 잇다.
println(gye.isSubscribed) // false
val hey = User("혜원", isSubscribed = false) // 생성자 인자 중 일부에 대해 이름을 지정할 수도 있다.
println(hey.isSubscribed) // false
open class User(val nickname: String) { ... }
class TwitterUser(nickname: String) : User(nickname) { ... }
open class Button // 인자가 없는 디폴트 생성자가 만들어진다.
class RadioButton: Button()
class Secretive private constructor() {} // 이 클래스의 (유일한) 주 생성자는 비공개다.
유틸리티 함수를 담아두는 역할만을 하는 클래스는 인스턴스화할 필요가 없고, 싱글턴인 클래스는 미리 정한 팩토리 메소드 등의 생성 방법을 통해서만 객체를 생성해야 한다. 코틀린에서는 이런 경우를 언어에서 기본 지원한다. 정적 유틸리티 함수 대신 최상위 함수를 사용할 수 있고, 싱글톤을 사용하고 싶으면 객체를 선언하면 된다.
class Singleton private constructor(private val data: String) {
// 싱글톤 객체
companion object {
private var instance: Singleton? = null
// 미리 정한 팩토리 메소드
fun getInstance(): Singleton {
if (instance == null) {
instance = Singleton("Default Data")
}
return instance!!
}
}
fun getData(): String {
return data
}
}
fun main() {
// 객체 생성은 팩토리 메소드를 통해서만 허용
val singletonInstance = Singleton.getInstance()
// 객체의 메소드 호출
println(singletonInstance.getData()) // Default Data
}
💡 주 생성자를 보완하거나 다른 초기화 동작을 수행하기 위해 클래스에 추가적인 생성자를 정의할 때 사용되는 생성자다.
class Person {
var name: String = ""
var age: Int = 0
constructor(name: String) {
this.name = name
}
constructor(age: Int) {
this.age = age
}
constructor(name: String, age: Int) {
this.name = name
this.age = age
}
}
class Person(val name: String = "", val age: Int = 0)
인자에 대한 디폴트 값을 제공하기 위해 부 생성자를 여럿 만들지 말라. 대신 파라미터의 디폴트 값을 생성자 시그니처에 직접 명시하라.
open class View {
constructor(ctx: Context) { // 부 생성자
// 코드
}
constructor(ctx: Context, attr: AttributeSet) { // 부 생성자
// 코드
}
}
// 상위 클래스의 생성자를 호출한다.
class MyButton : View {
constructor(ctx: Context) : super(ctx) {
// ...
}
constructor(ctx: Context, attr: AttributeSet) : super(ctx, attr) {
// ...
}
}
class MyButton: View {
constructor(ctx: Context): this(ctx, MY_STYLE) { // 이 클래스의 다른 생성자에게 위임한다.
// ...
}
constructor(ctx: Context, attr: AttributeSet): super(ctx, attr) {
// ...
}
}
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: Int) : User {
override val nickname = getFacebookName(accountId) // 프로퍼티 초기화 식
}
println(PrivateUser("test@kotlinlang.org").nickname) // test@kotlinlang.org
println(SubscribingUser("test@kotlinlang.org").nickname) // test
SubscribingUser와 FacebookUser의 nickname 구현 차이에 주의하라.
interface User {
val email: String
// 프로퍼티에 뒷받침하는 필드가 없다. 대신 매번 결과를 계산해 돌려준다.
val nickname: String
get() = email.substringBefore('@')
}
class BasicUser(override val email: String) : User {
// email 프로퍼티를 오버라이드해야 한다.
// nickname은 이미 인터페이스에서 구현되어 있으므로 따로 오버라이드하지 않아도 된다.
}
fun main() {
val user = BasicUser("example@example.com")
// email 프로퍼티 사용
println("Email: ${user.email}")
// nickname 프로퍼티 사용 (자동으로 상속됨)
println("Nickname: ${user.nickname}")
}
// 세터에서 뒷받침하는 필드 접근하기
class User(val name: String) {
var address: String = "unspecified"
set(value: String) {
println("""
Address was changed for $name:
"$field" -> "$value".""".trimIndent()) // 뒷받침하는 필드 값 읽기
field = value
}
}
val user = User("Alice")
user.address = "Elsenheimerstrasse 47, 80687 Muenchen"
user.address = "new value"
처럼 필드 설정 구문을 사용한다.field
라는 특별한 식별자를 통해 뒷받침하는 필드에 접근할 수 있다.뒷받침하는 필드가 있는 프로퍼티와 그런 필드가 없는 프로퍼티에 어떤 차이가 있나?
- 클래스의 프로퍼티를 사용하는 쪽에서 프로퍼티를 읽는 방법이나 쓰는 방법은 뒷받침하는 필드의 유무와는 관계가 없다.
- 컴파일러는 디폴트 접근자 구현을 사용하건 직접 게터나 세터를 정의하건 관계없이 게터나 세터에서 field를 사용하는 프로퍼티에 대해 뒷받침하는 필드를 생성해준다.
- 다만 field를 사용하지 않는 커스텀 접근자 구현을 정의한다면 뒷받침하는 필드는 존재하지 않는다.
public final class CustomAccessorExample {
private int counter; // 이 부분이 뒷받침 필드를 나타냅니다.
public final int getCounter() {
return this.counter;
}
private final void setCounter(int var1) {
this.counter = var1;
}
// doubledCounter의 경우에는 별도의 뒷받침 필드가 생성되지 않고, getter에서 직접 계산
public final int getDoubledCounter() {
return this.getCounter() * 2;
}
public final void incrementCounter() {
this.setCounter(this.getCounter() + 1);
}
}
class CustomAccessorExample {
// 뒷받침 필드가 자동으로 생성됨
var counter = 0
private set
// 뒷받침 필드가 자동으로 생성됨
val doubledCounter: Int
get() {
// 여기서는 field를 사용하지 않고, 직접 계산하여 반환
return counter * 2
}
fun incrementCounter() {
counter++
}
}
fun main() {
val example = CustomAccessorExample()
println(example.counter) // 출력: 0
println(example.doubledCounter) // 출력: 0
example.incrementCounter()
println(example.counter) // 출력: 1
println(example.doubledCounter) // 출력: 2
}
// 비공개 세터가 있는 프로퍼티 선언하기
class LengthCounter {
var counter: Int = 0
private set // 이 클래스 밖에서 이 프로퍼티의 값을 바꿀 수 없다.
fun addWord(word: String) {
counter += word.length
}
}
val lengthCounter = LengthCounter()
lengthCounter.addWord("Hi!")
println(lengthCounter.counter) // 3
class Client(val name: String, val postalCode: Int)
class Client(val name: String, val postalCode: Int) {
override fun toString() = "Client(name=$name, postalCode=$postalCode)"
}
val client1 = Client("오현석", 4122)
println(client1) // Client(name=오현석, postalCode=4122)
val client1 = Client("오현석", 4122)
val client2 = Client("오현석", 4122)
// 코틀린에서 == 연산자는 참조 동일성을 검사하지 않고 객체의 동등성을 검사한다.
// 따라서 == 연산은 equals를 호출하는 식으로 컴파일된다.
println(client1 == client2) // false
// Client에 equals() 구현하기
class Client(val name: String, val postalCode: Int) {
override fun equals(other: Any?): Boolean {
if (other == null || other !is Client)
return false
return name == other.name &&
postalCode == other.postalCode
}
override fun toString() = "Client(name=$name, postalCode=$postalCode)"
}
val processed = hashSetOf(Client("오현석", 4122))
println(processed.contains(Client("오현석", 4122))) // false
// Client에 hashCode 구현하기
class Client(val name: String, val postalCode: Int) {
...
override fun hashCode(): Int = name.hashCode() * 31 + postalCode
}
💡 데이터 관리에 최적화된 클래스로 toString(), equals(), hashCode(), copy(), componentN() 5가지 유용한 함수들을 내부적으로 컴파일러가 자동 생성해준다.
// Client를 데이터 클래스로 선언하기
data class Client(val name: String, val postalCode: Int)
- 인스턴스 간 비교를 위한 equals
- HashMap과 같은 해시 기반 컨테이너에서 키로 사용할 수 있는 hashCode
- 클래스의 각 필드를 선언 순서대로 표시하는 문자열 표현을 만들어주는 toString
데이터 클래스의 모든 프로퍼티를 읽기 전용으로 만들어서 데이터 클래스를 불변 클래스로 만들라고 권장한다.
WHY) HashMap 등의 컨테이너에 데이터 클래스 객체를 담는 경우엔 불변성이 필수적이다.
데이터 클래스 인스턴스를 불변 객체로 더 쉽게 활용할 수 있게 코틀린 컴파일러는 한 가지 편의 메소드를 제공한다.
class Client(val name: String, val postalCode: Int) {
...
fun copy(name: String = this.name,
postalCode: Int = this.postalCode) =
Client(name, postalCode)
}
val lee = Client("이계영", 4122)
println(lee.copy(postalCode = 4000)) // Client(name=이계영, postalCode=4000)
class DelegatingCollection<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)
}
class DelegatingCollection<T>(
innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList { }
class CountingSet<T>(
val innerSet: MutableCollection<T> = HashSet<T>()
) : MutableCollection<T> by innerSet {
var objectsAdded = 0
override fun add(element: T): Boolean {
objectsAdded++
return innerSet.add(element)
}
override fun addAll(c: Collection<T>): Boolean {
objectsAdded += c.size
return innerSet.addAll(c)
}
}
val cset = CountingSet<Int>()
cset.addAll(listOf(1, 1, 2))
println("${cset.objectsAdded} objects were added, ${cset.size} remain")
// 3 objects were added, 2 remain
- 객체 선언은 싱글턴을 정의하는 방법 중 하나다.
- 동반 객체(companion object)는 인스턴스 메소드는 아니지만 어떤 클래스와 관련 있는 메소드와 팩토리 메소드를 담을 때 쓰인다. 동반 객체 메소드에 접근할 때는 동반 객체가 포함된 클래스의 이름을 사용할 수 있다.
- 객체 식은 자바의 무명 내부 클래스 대신 쓰인다.
object Payroll {
val allEmployees = arrayListOf<Person>()
fun calculateSalary() {
for (person in allEmployees) {
...
}
}
}
Payroll.allEmployees.add(Person(...))
Payroll.calculateSalary()
object CaseInsensitiveFileComparator : Comparator<File> {
override fun compare(file1: File?, file2: File?): Int {
return file1.path.compareTo(file2.path, ignoreCase = true)
}
}
println(CaseInsensitiveFileComparator.compare(File("/User"), File("/user"))) // 0
val files = listOf(File("/Z"), File("/a"))
println(files.sortedWith(CaseInsensitiveFileComparator)) // [/a, /Z]
대규모 소프트웨어 시스템에서는 객체 생성을 제어할 방법이 없고 생성자 파라미터를 지정할 수 없어 객체 선언이 항상 적합하지는 않다. 단위 테스트를 하거나 소프트웨어 시스템의 설정이 달라질 때 객체를 대체하거나 객체의 의존관계를 바꿀 수 없다. 따라서 의존관계 주입 프레임워크와 코틀린 클래스를 함께 사용해야 한다.
// 중첩 객체를 사용해 Comparator 구현하기
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)]
class A {
companion object {
fun bar() {
println("Companion object called")
}
}
}
A.bar() // Companion object called
// 부 생성자가 여럿 있는 클래스 정의하기
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))
}
}
val subscribingUser = User.newSubscribingUser("bob@gmail.com")
val facebookUser = User.newFacebookUser(4)
println(subscribingUser.nickname) // bob
클래스를 확장해야만 하는 경우에는 동반 객체 멤버를 하위 클래스에서 오버라이드할 수 없으므로 여러 생성자를 사용하는 편이 더 나은 해법이다.
// 동반 객체에 이름 붙이기
class Person(val name: String) {
companion object Loader { // 동반 객체에 이름을 붙인다.
fun fromJSON(jsonText: String): Person = ...
}
}
person = Person.Loader.fromJSON("{name: 'Dmitry'}")
person.name // Dmitry
person2 = Person.Loader.fromJSON("{name: 'Brent'}")
person2.name // Brent
interface JSONFactory<T> {
fun fromJSON(jsonText: String): T
}
class Person(val name: String) {
companion object : JSONFactory<Person> {
override fun fromJSON(jsonText: String): Person = ... // 동반 객체가 인터페이스를 구현한다.
}
}
fun loadFromJSON<T>(factory: JSONFactory<T>): T {
...
}
loadFromJSON(Person) // 동반 객체의 인스턴스를 함수에 넘긴다.
// 동반 객체에 대한 확장 함수 정의하기
class Person(val firstName: String val lastName: String) {
companion object { } // 비어잇는 동반 객체를 선언한다.
}
fun Person.Companion.fromJSON(json: String) : Person { // 확장 함수를 선언한다.
...
}
val p = Person.fromJSON(json)
동반 객체에 대한 확장 함수를 작성할 수 있으려면 원래 클래스에 동반 객체를 꼭 선언해야 한다는 점에 주의하라. 설령 빈 객체라도 동반 객체가 꼭 있어야 한다.
// 무명 객체로 이벤트 리스너 구현하기
window.addMouseListener(
object : MouseAdapter() { // MouseAdapter를 확장하는 무명 객체를 선언한다.
// MouseAdapter의 메소드를 오버라이드한다.
override fun mouseClicked(e: MouseEvent) {
// ...
}
override fun mouseEntered(e: MouseEvent) {
// ...
}
}
)
val listener = object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { ... }
override fun mouseEntered(e: MouseEvent) { ... }
}
객체 선언과 달리 무명 객체는 싱글턴이 아니다. 객체 식이 쓰일 때마다 새로운 인스턴스가 생성된다.
fun countClicks(window: Window) {
var clickCount = 0
window.addMouseListener(object: MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
clickCount++
}
})
}
객체 식은 무명 객체 안에서 여러 메소드를 오버라이드해야 하는 경우에 훨씬 더 유용하다. 메소드가 하나뿐인 인터페이스를 구현해야 한다면 코틀린의 SAM 변환(함수 리터럴을 변환해 SAM으로 만듦) 지원을 활용하는 편이 낫다. SAM 변환을 사용하려면 무명 객체 대신 함수 리터럴을 사용해야 한다.