Chapter 4. 클래스, 객체, 인터페이스

sua·2021년 7월 19일
0

Kotlin In Action

목록 보기
4/9
post-thumbnail

4.1 클래스 계층 정의

4.1.1 코틀린 인터페이스

[인터페이스 특징]

  • 추상 메소드와 구현이 있는 메소드 모두 정의 가능
  • 상태(필드) 들어갈 수 x
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()
    }
}

fun main(args: Array<String>) {
	val button = Button()
    button.showOff() // I'm clickable! I'm focusable!
    button.setFocus(true) // I got focus.
    button.click() // I was clicked.
}



4.1.2 open, final, abstract 변경자: 기본적으로 final

  1. open 변경자 : 어떤 클래스의 상속을 허용하기 위해 클래스 앞에 붙이는 변경자
    -> 오버라이드 허용하고 싶은 메소드나 프로퍼티 앞에도 붙여야 함
    open class RichButton : Clickable { // 다른 클래스가 이 클래스 상속 가능
    	fun disable() {} // 하위 클래스에서 이 메소드 오버라이드 불가능
        open fun animate() {} // 하위 클래스에서 이 메소드 오버라이드 가능
        override fun click {} // 열려있는 메소드를 오버라이드, 오버라이드한 메소드는 기본적으로 열려있음
        final override fun click2 {} // 하위 클래스에서 오버라이드하지 못하게 금지
    }
  1. abstract 변경자 : 추상 클래스와 추상 멤버를 선언하기 위한 변경자/ 추상 멤버 앞에 open 변경자를 명시할 필요 없음
abstract class Animated { // 이 클래스는 추상클래스, 클래스의 인스턴스 만들 수 없음
	abstract fun animate() // 하위 클래스에서 반드시 오버라이드 해야함
    open fun stopAnimating() {} // 추상 클래스에 속했더라도 비추상 함수는 open으로 오버라이드를 허용해야 함
}

[코틀린의 상속 제어 변경자]

  • final - 오버라이드 할 수 없음, 클래스 멤버의 기본 변경자
  • open - 오버라이드 할 수 있음, 반드시 open을 명시해야 오버라이드 가능
  • abstract - 반드시 오버라이드해야 함, 추상 클래스의 멤버에만 이 변경자를 붙일 수 있고 추상 멤버에는 구현이 있으면 안됨
  • override - 상위 클래스나 상위 인스턴스의 멤버를 오버라이드하는 중, 오버라이드하는 멤버는 기본적으로 열려있으므로 하위 클래스의 오버라이드를 금지하려면 final을 명시해야 함



4.1.3 가시성 변경자: 기본적으로 공개

가시성 변경자 : 코드 기반에 있는 선언에 대한 클래스 외부 접근을 제어

[코틀린의 가시성 변경자]

변경자클래스 멤버최상위 선언
public 모든 곳에서 볼 수 있음 모든 곳에서 볼 수 있음
internal 같은 모듈 안에서만 볼 수 있음 같은 모듈 안에서만 볼 수 있음
protected 하위 클래스 안에서만 볼 수 있음 (최상위 선언에 적용할 수 없음)
private 같은 클래스 안에서만 볼 수 있음 같은 파일 안에서만 볼 수 있음



4.1.4 내부 클래스와 중첩된 클래스: 기본적으로 중첩 클래스

클래스 B 안에 정의된 클래스 A자바에서는코틀린에서는
중첩 클래스(바깥쪽 클래스에 대한 참조를 저장하지 않음 static class A class A
내부 클래스(바깥쪽 클래스에 대한 참조를 저장함) class A inner class A

[내부 클래스 안에서 바깥쪽 클래스 참조에 접근]

class Outer {
	inner class Inner {
    		fun getOuterReference() : Outer = this@Outer
        }
}



4.1.5 봉인된 클래스: 클래스 계층 정의 시 계층 확장 제한

sealed 변경자 : 상위 클래스에 붙이면 상위 클래스를 상속한 하위 클래스 정의를 제한 가능 -> sealed 클래스의 하위 클래스를 정의할 때는 반드시 상위 클래스 안에 중첩시켜야 함




4.2 뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언

4.2.1 클래스 초기화: 주 생성자와 초기화 블록

주 생성자 : 클래스 이름 뒤에 오는 괄호로 둘러싸인 코드
[주 생성자 코드들]

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) // val은 이 파라미터에 상응하는 프로퍼티가 생성된다는 뜻

[기반 클래스의 생성자 호출]

open class User(val nickname: String) {}
class TwitterUser(nickname: String) : User(nickname) {}

[클래스 외부에서 인스턴스화 금지하는 방법]

class Secretive private constructor() {}



4.2.2 부 생성자: 상위 클래스를 다른 방식으로 초기화

open class View {
	constructor (ctx: Context) {
    	// 부 생성자
    	}
        constructor(ctx: Context, attr: Attribute) {
        	// 부 생성자
        }
}
class MyButton : View {
	// 상위 클래스의 생성자 호출
	constructor(ctx: Context) : super(ctx) { }
    	constructor(ctx: Context, attr: Attribute) : super(ctx, attr) {}
}
// 자신의 다른 생성자 호출하는 경우
class MyButton : View {
	constructor(ctx: Context) : this(ctx, MY_STYLE) {} // 이 클래스의 다른 생성자에게 위임
    	constructor(ctx: Context, attr: Attribute) : 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) {
	var address: String = "unspecitifed"
    		set(value: String) {
            		println(""" Address was changed for $name: 
                    		"$field" -> "$value".""".trimIndent()) // 뒷받침하는 필드 읽기
                        field = value // 뒷받침하는 필드 값 변경하기
            	}
}
val user = User("Alice")
user.address = "EE"
Address was changed for Alice: "unspecified" -> "EE"



4.2.5 접근자의 가시성 변경

기본적으로 접근자의 가시성은 프로퍼티의 가시성과 같음 -> get이나 set 앞에 가시성 변경자 추가해서 접근자 가시성 변경 가능
[비공개 세터가 있는 프로퍼티 선언하기]

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




4.3 컴파일러가 생성한 메소드: 데이터 클래스와 클래스 위임

4.3.1 모든 클래스가 정의해야 하는 메소드

// Client 클래스의 초기 정의
class Client(val name: String, val postalCode: Int)
  1. 문자열 표현: toString()
class Client(val name: String, val postalCode: Int) {
	override fun toString() = "Client(name=$name, postalCode=$postalCode)"
}

val Client1 = Client("오현석", 4122)
println(client1) // Cliene(name=오현석, postalCode=4122)
  1. 객체의 동등성: 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.poastlCode
	}
}
  1. 해시 컨테이너: hashCode() - HashMap과 같은 해시 기반 컨테이너에서 키로 사용할 수 있음
class Client(val name: String, val postalCode: Int) {
	...
    	override fun hashCode() : Int = name.hashCode() * 31 + postalCode
}

=> 코틀린에서는 3가지 메소드를 자동으로 생성해줄 수 있음



4.3.2 데이터 클래스: 모든 클래스가 정의해야 하는 메소드 자동 생성

data 변경자 : 클래스 앞에 붙이면 필요한 메소드를 컴파일러가 자동으로 생성해줌 => 이러한 클래스를 '데이터 클래스'

  • equals, hashcode, toString 외에도 생성해줌
  • copy() 메소드 : 데이터 클래스의 불변성을 위함
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)



4.3.3 클래스 위임: by 키워드 사용

class CountingSet<T> ( val innerSet: MutableCollection<T> = HashSet<T> () ) : MutableCollection<T> by innerSet {
	var objectsAdded = 0
    
    	override fun add(element: T) : Boolean {
        	objectsAdded++
            return innserSet.add(element)
        }
        override fun addAll(c: Collection<T>): Boolean {
        	objectAdded += 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




4.4 object 키워드: 클래스 선언과 인스턴스 생성

4.4.1 객체 선언: 싱글턴을 쉽게 만들기

객체 선언 : 클래스 선언 + 그 클래스에 속한 단일 인스턴스 선언
-> object 키워드로 시작

  • 생성자 쓸 수 x
object Payroll {
	val allEmployees = arrayListOf<Person>()
    fun calculateSalary() {
    	for (person in allEmployees) {
        	...
        }
    }
}

Payroll.allEmployees.add(Person(...))
Payroll.calculateSalary()
  • 클래스나 인터페이스 상속 가능
  • 일반 객체(클래스 인스턴스)를 사용할 수 있는 곳에서는 항상 싱글턴 객체 사용 가능
  • 클래스 안에서 객체 선언 가능
// 중첩 객체를 사용해 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)]



4.4.2 동반 객체 : 팩토리 메소드와 정적 멤버가 들어갈 장소

동반 객체 : 클래스 안에 정의된 객체 중 하나에 companion이라는 표시를 붙이면 그 클래스의 동반 객체로 만들 수 있음 -> 자바의 정적 메소드 호출이나 정적 필드 사용 구문과 같아짐

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

A.bar() // Companion object called
  • 자신을 둘러싼 클래스의 모든 private 멤버에 접근 가능
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



4.4.3 동반 객체를 일반 객체처럼 사용

  1. 동반 객체에 이름 붙이기
class Person(val name: String) {
	companion object Loader { 
    		fun fromJSON(jsonText: String) : Person = ...
        }
}

person = Person.Loader.fromJSON("{name: 'Dmitry')")
person.name //Dmitry
  1. 동반 객체에서 인터페이스 구현
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) // 동반 객체의 인스턴스를 함수에 넘김
  1. 동반 객체 확장
[동반 객체에 대한 확장 함수 정의하기]
class Person(val firstName: String, val lastName: String) { 
	companion object {}
}

fun Person.Companion.fromJSON(json:String) : Person { //확장함수 선언 }

val p = Person.fromJSON(json)



4.4.4 객체 식: 무명 내부 클래스를 다른 방식으로 작성

무명 객체 -> object 키워드 사용

[무명 객체로 이벤트 리스너 구현]
fun countClicks(window: Window) {
	var clickCount = 0 // 로컬변수 정의
    	window.addMouseListener {
        	object : MouseAdapter() { // MouseAdapter를 확장하는 무명 객체 선언
                	override fun mouseClicked(e: MouseEvent) {
                    		clickCount++ // 로컬 변수의 값 변경 가능
                    	}
                    	override fun mouseEntered(e: MouseEvent) {}
            }
    }
profile
가보자고

0개의 댓글

관련 채용 정보