-
Notifications
You must be signed in to change notification settings - Fork 0
CH04. 클래스, 객체, 인터페이스 (이주영)
leeheefull edited this page Mar 20, 2023
·
1 revision
- 자바와 같이 추상 메소드, 구현 메소드 정의 가능
- 인터페이스 메소드 디폴트 구현 제공
- 콜론(:)으로 클래스 확장과 인터페이스 구현을 모두 처리
- override 변경자를 반드시 사용해야 함
- 상속한 구현 중 중복되는 메소드는 명시적으로 새로운 구현 제공
- super.method() 로 타입 지정
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() } // 상속한 구현 중 하나만 사용할 때는 아래처럼 override fun showOff() = super<Clickable>.showOff() }
- 클래스의 상속을 허용하려면 앞에 open 변경자를 붙여야 함
- 오버라이드 허용하고 싶은 메소드나 프로퍼티 앞에도 open 사용
- 인터페이스 멤버는 final, open, abstract 변경자를 사용하지 않음
- 인터페이스 멤버는 항상 열려있으며 final로 변경 불가
- 추상 멤버는 항상 열려있기 때문에 open 변경자를 명시할 필요 없음
- 비추상 함수는 기본적으로 final이지만 open으로 오버라이드 허용 가능
open class RichButton: Clickable { fun disable() {} // final 함수로 오버라이드 불가 open fun animate() {} // 오버라이드 가능 final override fun click() {} // final을 사용하여 오버라이드 금지 } abstract class Animated { abstract fun animate() open fun stopAnimating() { println("stopAnimating") } fun animateTwice() { println("animateTwice") } }
변경자 | 클래스 멤버 | 최상위 선언 |
---|---|---|
public(기본 가시성) | 모든 곳에서 볼 수 있음 | 모든 곳에서 볼 수 있음 |
internal | 같은 모듈 안에서만 볼 수 있음 | 같은 모듈 안에서만 볼 수 있음 |
protected | 하위 클래스 안에서만 볼 수 있음 | 최상위 선언에 적용 불가 |
private | 같은 클래스 안에서만 볼 수 있음 | 같은 파일 안에서만 볼 수 있음 |
- internal 가시성 변경자는 모듈 내부에서만 볼 수 있음
- internal 변경자는 바이트코드상에서는 public이 됨
- 최상위 선언에 private 가시성을 허용
- 코틀린의 protected 멤버는 오직 어떤 클래스나 그 클래스를 상속한 클래스 안에서만 보임
internal open class TalkativeButton: Focusable { private fun yell() = println("Hey!") protected fun whisper() = println("Let's talk!") } // 오류를 없애려면 giveSpeech를 internal로 바꾸거나 TalkativeButton의 가시성을 public으로 바꿔야 함 //fun TalkativeButton.giveSpeech() { // public 멤버가 internal TalkativeButton 노출 // yell() // yell은 private멤버로 접근 불가 // whisper() // protected멤버로 접근 불가 //}
- 코틀린의 중첩 클래스는 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없
- 코틀린 중첩 클래스에 아무런 변경자가 붙지 않으면 자바 static 중첩 클래스와 같음
- 내부 클래스로 변경해서 바깥쪽 클래스에 대한 참조를 포함하려면 inner 변경자 사용
- 내부 클래스에서 외부 클래스에 접근하려면 this@외부클래스
class ButtonView: View { override fun getCurrentState(): State = ButtonState() override fun restoreState(state: State) { } // class ButtonState : State { } inner class ButtonState: State { fun getCurrentState(): ButtonView = this@ButtonView } }
- sealed 변경자는 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있음
- sealed 클래스의 하위 클래스를 정의할 때는 반드시 상위 클래스 안에 중첩시켜야 함
- sealed 클래스는 자동으로 open이므로 open 변경자를 붙일 필요 없음
sealed class Expr { class Num(val value:Int) : Expr() class Sum(val left: Expr, val right: Expr) : Expr() } fun eval(e: Expr): Int = when (e) { is Expr.Num -> e.value is Expr.Sum -> eval(e.right) + eval(e.left) }
- 이름 뒤에 오는 괄호로 둘러싸인 코드가 주 생성자
- 주 생성자 목적: 파라미터 지정, 생성자 파라미터에 의해 초기화되는 퍼로퍼티를 정의
- constructor 키워드는 주 생성자나 부 생성자 정의를 시작할 때 사용
- init 키워드는 초기화 블록을 시작(클래스 객체가 만들어질 때 실행될 초기화 코드가 들어감)
- init 키워드는 주 생성자와 함께 사용
- 생성자 파라미터 앞에 밑줄(_)은 퍼로퍼티와 구분, this를 사용하여 모호성을 없애도 됨
- 주 생성자 앞에 별다른 애노테이션이나 가시성 변경자가 없으면 constructor 생략
- 클래스 인스턴스 생성시, new 키워드 없이 생성자 직접 호출
- 모든 생성자 파라미터에 디폴트 값을 지정하면 컴파일러가 자동으으로 인자 없는 생성자 만들어줌
- 클래스를 정의할 때 별도로 생성자를 정의하지 않으면 컴파일러가 자동으로 인자 없는 디폴트 생성자 만들어줌
- 클래스 외부에서 인스턴스화하지 못하게 막고 싶다면 모든 생성자를 private으로 만들면 됨
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 isSubscribed: Boolean = true) class Secretive private constructor() {}
- 인자에 대한 디폴트 값을 제공하기 위해 부 생성자를 여럿 만들지 말고, 파라미터의 디폴트 값을 생성자 시그니처에 직접 명시
- super()로 상위 클래스 생성자 호출
- this()로 자신의 다른 생성자 호출
- 클래스에 주 생성자가 없다면 모든 부 생성자는 반드시 상위 클래스를 초기화하거나 다른 생성자에게 생성을 위임해야 함
// 주 생성자 없이 부 생성자 2개 선언 class View { constructor(ctx: Context) {} constructor(ctx: Context, attr: AttributeSet) {} } class MyButton: View { // 상위 클래스의 생성자 호출 constructor(ctx: Context): super(ctx) {} constructor(ctx: Context): this(ctx, MY_STYLE) {} // 이 클래스의 다른 생성자에게 위임 constructor(ctx: Context, attr: AttributeSet): super(ctx, attr) {} }
- 코틀린은 인터페이스에 추상 프로퍼티 선언 가능
- 인터페이스에 있는 프로퍼티 선언에는 뒷받침하는 필드나 게터 등의 정보가 들어있지 않음
- 인터페이스에는 추상 프로퍼티뿐 아니라 게터와 세터가 있는 프로퍼티 선언 가능
// 하위 클래스는 추상 프로퍼티인 email을 반드시 오버라이드 해야하지만 nickname은 오버라이드 하지 않고 상속 가능 interface User { val email: String val nickname: String get() = email.substringBefore('@') } 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) // 프로퍼티 초기화 식 }
- 값을 저장하는 동시에 로직을 실행할 수 있게 하기 위해서는 접근자 안에서 프로퍼티를 뒷받침하는 필드에 접근할 수 있어야 함
- 접근자의 본문에서는 field라는 특별한 식별자를 통해 뒷받침하는 필드에 접근할 수 있음
- 게터에서는 field값을 읽을 수만 있고, 세터에서는 읽거나 쓸 수 있음
- field를 사용하지 않는 커스텀 접근자 구현을 정의한다면 뒷받침하는 필드는 존재하지 않음
class User(val name: String) { var address: String = "unspecified" set(value: String) { println(""" Address was changed for $name: "$field" -> "$value".""".trimIndent()) field = value } }
- 접근자의 가시성은 기본적으로 프로퍼티의 가시성과 같음
- get이나 set 앞에 가시성 변경자를 추가해서 접근자의 가시성 변경 가능
class LengthCounter { var counter: Int = 0 private set // 이 클래스 밖에서 이 프로퍼티의 값 변경 불가 fun addWord(word: String) { counter += word.length } }
- 문자열 표현: toString()
- 객체의 동등성: equals()
- 해시 컨테이너: hashCode()
- equals와 hashCode는 주 생성자에 나열된 모든 프로퍼티를 고려해 만들어짐
- 주 생성자 밖에 정의된 프로퍼티는 equals나 hashCode를 계산할 때 고려의 대상이 아님
data class Client(val name: String, val postalCode: Int)
- 데이터 클래스의 프로퍼티는 val, var 둘 다 써도 되지만, 모든 프로퍼티를 읽기 전용으로 만들어서 불변 클래스로 만드는 것을 권장
- HashMap 등의 컨테이너에 데이터 클래스 객체를 담는 경우엔 불변성이 필수(다중스레드 프로그램의 경우 더 중요)
- copy() 메소드: 객체를 복사하면서 일부 프로퍼티 값을 바꾸거나 복사본을 제거해도 원본을 참조하는 다른 부분에 전혀 영향을 끼치지 않음
data class Client(val name: String, val postalCode: Int) { // 직접 구현한다면.. fun copy(name: String = this.name, postalCode: Int = this.postalCode) = Client(name, postalCode) }
- by 키워드를 통해 그 인터페이스에 대한 구현을 다른 객체에 위임 중이라는 사실을 명시할 수 있음
- 메소드 중 일부 동작을 변경하고 싶은 경우 메소드를 오버라이드하면 컴파일러가 생성한 메소드 대신 오버라이드한 메소드가 쓰임
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) } }
- 객체 선언은 싱글턴을 정의하는 방법 중 하나
- 동반 객체는 인스턴스 메소드는 아니지만 어떤 클래스와 관련 있는 메소드와 팩토리 메소드를 담을 때 쓰임. 동반 객체 메소드에 접근할 때는 동반 객체가 포함된 클래스의 이름을 사용할 수 있음
- 객체 식은 자바의 무명 내부 클래스 대신 쓰임
- 코틀린은 객체 선언 기능을 통해 싱글턴을 기본 지원
- 객체 선언은 object 키워드로 시작
- 클래스를 정의하고 그 클래스의 인스턴스를 만들어서 변수에 저장하는 모든 작업을 단 한 문장으로 처리
- 프로퍼티, 메소드, 초기화 블록 등이 들어갈 수 있지만 생성자는 객체 선언에 쓸 수 없음
- 클래스나 인터페이스 상속할 수 있음
object Payroll { val allEmployees = arrayListOf<Person>() fun calculateSalary() { for (person in allEmployees) { } } } object CaseInsensitiveFileComparator: Comparator<File> { override fun compare(file1: File, file2: File): Int{ return file1.path.compareTo(file2.path, ignoreCase = true) } }
- 최상위 함수는 private으로 표시된 클래스 비공개 멤버에 접근 불가
- 중첩된 객체 선언의 멤버 함수로 정의하여 클래스 내부 정보에 접근
- 클래스 안에 정의된 객체 중 하나에 companion을 붙이면 그 클래스의 동반 객체로 만들 수 있음
- 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근 가능
class User private constructor(val nickname: String) { // 비공개 주 생성자 companion object { // 동반 객체 선언 fun newSubscribingUser(email: String) = User(email.substringBefore('@')) fun newFacebookUser(accountId: Int) = User(getFacebookName(accountId)) } } fun main() { val subscribingUser = User.newSubscribingUser("[email protected]") val facebookUser = User.newFacebookUser(4) println(subscribingUser.nickname) }
동반 객체에 이름을 붙이거나, 동반 객체가 인터페이스를 상속하거나, 동반 객체 안에 확장 함수와 프로퍼티를 정의할 수 있음
-
동반 객체에 이름을 붙이기
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) val person2 = Person2.fromJson("{name: 'Brent'}") println(person.name) }
-
동반 객체에서 인터페이스 구현
interface JSONFactory<T> { fun fromJSON(jsonText: String): T } class Person(val name: String) { companion object: JSONFactory<Person> { override fun fromJSON(jsonText: String): Person { TODO("Not yet implemented") } } } fun loadFromJSON<T>(factory: JSONFactory<T>): T { //... } fun main() { loadFromJSON(Person) }
-
동반 객체 확장
- 클래스 밖에서 정의한 확장 함수를 동반 객체 안에서 함수를 정의한 것처럼 호출 가능 - 동반 객체에 대한 확장 함수를 작성하려면 빈 객체라도 동반 객체를 꼭 선언해야 함
// 비즈니스 로직 모듈 class Person(val firstName: String, val lastName: String) { companion object { // 비어있는 동반 객체 선언 } } // client/server 통신 모듈, 확장 함수 선언 fun Person.Companion.fromJSON(json: String): Person { } fun main() { val p = Person.fromJSON(json) }
- 무명 객체를 정의할 때도 object 키워드 사용
window.addMouseListener( object: MouseAdapter() { // MouseAdapter를 확장하는 무명 객체 선언 // 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++ } }) }