[Kotlin in Action] 04장 - 클래스, 객체, 인터페이스
4장 - 클래스, 객체, 인터페이스
4.1 클래스 계층 정의
- 아무것도 지정하지 않은 경우 기본 가시성이 자바와 다르다.
- sealed 변경자
- 클래스 상속을 제한
4.1.1 코틀린 인터페이스
interface Clickable {
fun click()
}
class Button : Clickable {
override fun click() = println("I was clicked")
}
-> Button().click()
- 코틀린에서는 override 변경자를 꼭 사용해야 한다.
- 인터페이스 멤버는 항상 열려 있으며 final로 변경할 수 없다.
- 인터페이스 멤버의 경우 final, open, abstract를 사용하지 않는다.
코틀린에서 default 구현
interface Clickable {
fun click() // 일반 메소드 선언
fun showOff() = println("I'm clickable") // 디폴트 구현이 있는 메소드
}
interface Focusable {
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()
}
}
4.1.2 open, final, abstract 변경자: 기본적으로 final
- 자바의 클래스와 메소드는 기본적으로 상속에 대해 열려있지만 코틀린의 클래스와 메소드는 기본적으로 final 이다.
- 어떤 클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야 한다.
- 오버라이드를 허용하고 싶은 메소드나 프로퍼티의 앞에도 open 변경자를 붙여야 한다.
open class RichButton : Clickable { // 이 클래스는 열려있다.
fun disable() {} // 이 함수는 final 이다.
open fun animate() {} // 이 함수는 열려있다.
override fun click() {} // 이 함수는(상위 클래스에서 선언된) 열려있는 메소드를 오버라이드한다. 오버라이드한 메소드는 기본적으로 열려있다.
}
오버라이드 금지하기
open class RichButton : Clickable {
final override fun click() // final이 없는 override 메소드나 프로퍼티는 기본적으로 열려있으므로 final로 금지시킨다.
}
추상 클래스 정의하기
abstract class Animated {
abstract fun animate() // 추상 함수는 항상 열려 있다.
open fun stopAnimating() {} // 추상 클래스에 속하더라도 비추상 함수는 기본적으로 파이널이지만 원한다면 open 으로 오버라이드를 허용 가능
fun animateTwice() {}
}
- abstract로 선언한 추상 클래스는 인스턴스화할 수 없다.
- 추상 멤버 앞에 open 변경자를 명시할 필요가 없다.
4.1.3 가시성 변경자: 기본적으로 공개
- 코틀린의 기본 가시성은 자바와 다르게 모두 공개(public) 이다
- 자바의 기본 가시성인 패키지 전용(package-private)은 코틀린에 없다.
- 코틀린은 페키지를 네임스페이스를 관리하기 위한 용도로만 사용한다.
- 패키지 전용 가시성에 대한 대안으로 코틀린에는 internal 이라는 새로운 가시성 변경자를 도입했다.
변경자 | 클래스 멤버 | 최상위 선언 |
---|---|---|
public(기본 가시성임) | 모든 곳에서 볼 수 있다. | 모든 곳에서 볼 수 있다. |
internal | 같은 모듈 안에서만 볼수 있다. | 같은 모듈 안에서만 볼수 있다. |
protected | 하위 클래스 안에서만 볼 수 있다. | (최상위 선언에 적용할 수 없음) |
private | 같은 클래스 안에서만 볼 수 있다. | 같은 클래스 안에서만 볼 수 있다. |
- 코틀린 가시성 규칙은 단순한다.
- protected 멤버는 오직 어떤 클래스나 그 클래스를 상속한 클래스 안에서만 보인다.
- 클래스를 확장한 함수는 그 클래스의 privaate이나 protected멤버에 접근할 수 없다.
- 외부 클래스가 내부 클래스나 중첩된 클래스의 private 멤버에 접근할 수 없다.
4.1.4 내부 클래스와 중첩된 클래스: 기본적으로 중첩 클래스
- 코틀린의 중첩 클래스는 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없다.
중첩 클래스를 사용해 코틀린에서 View 구현하기
class Button : View {
override fun getCurrentState(): State = ButtonState()
class ButtonState : State {} // 자바의 정적 중첩 클래스와 대응된다.
}
- 내부 클래스 Inner 안에서 바깥쪽 클래스 Outer의 참조에 접근하려면 this@Outer라고 써야 한다.
class Outer {
inner class Inner {
fun getOuterReference(): Outer = this@Outer
}
}
4.1.5 봉인된 클래스: 클래스 계층 정의 시 계층 확장 제한
- sealed 변경자
- 상위 클래스를 상속한 하위 클래스 정의를 제한
sealed 클래스로 식 표현하기
sealed class Expr { // 기반 클래스를 sealed로 봉인한다.
class Num(val value: Int) : Expr() // 기반 클래스의 모든 하위 클래스를 중첩 클래스로 나열한다.
class Sum(val left: Expr, val right: Expr) : Expr()
}
fun eval(e: Expr): Int =
when (e) { // "when" 식이 모든 하위 클래스를 검사하므로 별도의 "else" 분기가 없어도 된다.
is Expr.Num -> e.value
is Expr.Sum -> eval(e.right) + eval(e.left)
}
4.2 뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언
4.2.1 클래스 초기화: 주 생성자와 초기화 블록
다양한 초기화 생성 방법
class User(val nickname: String) //파라미터가 하나뿐인 주 생성자
class User(val nickname: String, val isSubscribed: Boolean = true) // 디폴트 값을 제공하는 생성자
class TwitterUser(nickname: String) : User(nickname) {} // 기반 클래스를 초기화하려면 기반 클래스 이름 뒤에 괄호를 치고 생성자를 넘긴다.
open class Button // 인자가 없는 디폴트 생성자 생성
class RadioButton: Button() // Button 클래스를 상속한 하위 클래스는 반드시 Button 클래스의 생성자를 호출해야 한다.
class Secretive private constructor() {} // 비공개 생성자 - 클래스 외부에서 인스터늣화하지 못하게 막을려면 private 선언
4.2.2 부 생성자: 상위 클래스를 다른 방식으로 초기화
부 생성자를 생성하는 방법
open class View {
constructor(ctx: Context) {}
}
open class MyButton : View {
constructor(ctx: Context) : super(ctx) {} // 상위 클래스의 생성자 호출
}
open class MyButton : View {
constructor(ctx: Context) : this(ctx, MY_STYLE) {} // 이 클래스의 다른 생성자에게 위임
constructor(ctx: Context, attr: AttributeSet): super(ctx, attr) {}
}
4.2.3 인터페이스에 선언된 프로퍼티 구현
추상 프로퍼티 선언이 들어있는 인터페이스 선언의 예
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) // 프로퍼티 초기화 식 - 필드에 저장했다가 불러오는 방식
}
4.2.4 게터와 세터에서 뒷받침하는 필드에 접근
세터에서 뒷받침하는 필드 접근하기
class User(val name: String) {
val address: String = "unspecified"
set(value: String) {
println("""
Adress was changed for $name:
"$field" -> "$value".""".trimIndent()) // 뒷받침하는 필드 값 읽기
field = value // field라는 특별한 식별자를 통해 뒷받침하는 필드 값 변경
}
}
4.2.5 접근자의 가시성 변경
비공개 세터가 있는 프로퍼티 선언하기
class LengthCounter {
var counter: Int = 0
private set // 이 클래스 밖에서 이 프로퍼티의 값을 바꿀 수 없다.
fun addWord(word: String) {
counter += word.length
}
}
4.3 컴파일러가 생성한 메소드: 데이터 클래스와 클래스 위임
- 코틀린 컴파일러가 데이터 클래스의 유용한 메소드를 자동으로 만들어 준다.
- equals, hashCode, toString
4.3.1 모든 클래스가 정의해야 하는 메소드
- 문자열 표현 : toString()
Client에 toString() 구현하기
class Client(val name: String, val postalCode: Int) {
override fun toString() = "Client(name=$name, postalCode=$postalCode)"
}
- 객체의 동등성: equals()
Client에 equals() 구현하기
class Client(val name: String, val postalCode: Int) {
override fun equals(other: Any?) = Boolean {
if(other == null || other !is Client) // 코틀린의 is 는 자바의 instanceof 와 같다.
return false
return name == other.name && postalCode == other.postalCode
}
}
- 해쉬 컨테이너: hashCode()
Client에 hashCode() 구현하기
class Client(val name: String, val postalCode: Int) {
override fun hashCode(): Int = name.hashCode() * 31 + postalCode
}
4.3.2 데이터 클래스: 모든 클래스가 정의해야 하는 메소드 자동 생성
- data 변경자를 클래스 앞에 붙이면 필요한 메소드를 컴파일러가 자동으로 만들어 준다.
data class Client(val name: String, val postalCode: Int)
4.3.3 클래스 위임: by 키워드 사용
- 데코레이터 패턴 - 상속을 허용하지 않는 클래스 대신 사용할 수 있는 새로운 클래스를 만들되 기존 클래스와 같은 인터페이스를 데코레이더가 제공하게 만들고, 기존 클래스를 데이레이터 내부에 필드로 유지하는 것
- by 키워드로 간단하게 구현 가능
class DelegatingCollection<T>(
innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList()
4.4 object 키워드: 클래스 선언과 인스턴스 생성
4.4.1 객체 선언: 싱글턴을 쉽게 만들기
- 싱글턴을 언어에서 긴본 지원
- 객체 선언 - 클래스 선언과 클래스에 속한 단일 인스턴스의 선언을 합친 선언
object Payroll {
val allEmployees = arrayListOf<Person>()
fun calculateSalary() {
for (person in allEmployees) {
}
}
}
중첩 객체를 사용해 Comparator 구현하기
data class Person(val name: String) {
object NameComparator : Comparator<Person> {
override fun compare(p1: Person, p2: Person): Int = p1.name.compareTo(p2.name)
}
}
- 클래스 안에서 싱글턴 객체를 생성해도 인스턴스는 단 하나뿐이다.
4.4.2 동반 객체: 팩토리 메소드와 정적 멤버가 들어갈 장소
- 코틀린 언어는 자바 static 키워드를 지원하지 않는다.
- companion 표시를 붙이면 그 클래스의 동반 객체로 만들 수 있다.
- 동반 객체의 멤버를 사용하는 구문은 자바의 정적 메소드 호출이나 정적 필드 사용 구문과 같아진다.
class A {
companion object {
fun bar() {
println("Companion object called")
}
}
}
-> A.bar()
- 동반 객체는 팩토리 패턴을 구현하기 가장 적합한 위치
- 바깥쪽 클래스의 private 생성자도 호출 가능
부 생성자를 팩토리 메소드로 대신하기
class User private constructor(val nickname: String) {
companion object {
fun newSubscribingUser(email: String) = User(email.substringBefore('@'))
}
}
-> User.newSubscribingUser("bob@gmail.com")
4.4.3 동반 객체를 일반 객체처럼 사용
동반 객체에 이름 붙이기
class Person(val name: String) {
companion object Loader {
fun fromJSON(jsonText: String): Person = ...
}
}
// 아래 두가지 방법 모두 가능
-> Person.Loader.fromJSON("{name: 'Dmitry')"})
-> Person.fromJSON("{name: 'Dmitry')"})
동반 객체에서 인터페이스 구현
interface JONSFactory<T> {
fun fromJSON(jsonText: String) : T
}
class Person(val name: String) {
companion object : JSONFactory<Person> {
override fun fromJSON(jsonText: String) : Person = ... // 동반 개체가 인터페이스를 구현한다.
}
}
동반 객체 확장
// 비즈니스 로직 모듈
class Person(val firstName: String, val lastName: String) {
companion object { // 비어있는 동반 객체 선언
}
}
// 클라이언트/서버 통신 모듈
fun Person.Companion.fromJSON(json: String) : Person { // 확장 함수 선언, 동반 객체 기본 이름인 Companion 사용
}
-> Person.fromJSON(json)
- 동반 객체 안에서 fromJSON 함수를 정의한 것처럼 fromJSON을 호출 가능
- 실제로는 클래스 밖에서 정의한 확장 함수
4.4.4 객체 식: 무명 내부 클래스를 다른 방식으로 작성
- 무명 객체 정의할 때도 object 키워드 사용
- 자바와 달리 여러 인터페이스를 구현하거나 클래스를 확장하면서 인터페이스 구현 가능
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++ // 로컬 변수의 값을 변경
}
})
}