Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
Tags
- Kotlin In Action
- 싱글톤 컨테이너
- kotlin in action 정리
- 20055 컨베이어 벨트 위의 로봇
- 20055
- 컨베이어 벨트 위의 로봇 Python
- Python
- Kotlin
- Kotlin in action 10장
- Kotlin in action 5장
- spring
- KotlinInAction
- 자바 ORM 표준 JPA 프로그래밍 7장
- Kotlin in action 6장
- Kotlin in action 3장
- 코틸린인액션
- 백준
- 스프링 핵심 원리
- 스프링 핵심 원리 - 기본편
- 객체 지향 설계와 스프링
- 기능개발 python
- 13460 구슬탈출 2
- 코틀린인액션
- 백준 13460 Python
- 스프링 핵심 원리 이해
- 고급매핑
- 백준 20055 컨베이어 벨트 위의 로봇
- 코틀린
- 스프링 컨테이너와 스프링 빈
- 7장 고급매핑
Archives
- Today
- Total
기록하는 습관
[Kotlin In Action]4장 - 클래스, 객체, 인터페이스 본문
** 이 글은 Kotlin In Action을 읽고 정리한 글입니다. **
1. 클래스 계층 정의
1-1. 코틀린 인터페이스
interface Clickable {
fun click()
}
class Button : Clickable {
override fun click() = println("I was clicked")
}
>>> Button().click() // I was clicked
- 코틀린에서는 클래스 이름 뒤에 콜론(:)을 붙이고 인터페이스와 클래스 이름을 적는 것으로 클래스 확장과 인터페이스 구현을 모두 처리한다.
- override 변경자는 상위 클래스나 상위 인터페이스에 있는 프로퍼티나 메서드를 오버라이드 한다는 표시다.
- 코틀린에서는 override 변경자를 꼭 사용해야 한다.
- 인터페이스 메서드도 디폴트 구현을 제공할 수 있다. 자바와 달리 default 키워드로 꾸밀 필요가 없다.
class Button : Clickable, Focusable {
override fun click() = println("I was clicked")
override fun showOff() {
super<Clickable>.showOff()
super<Focusable>.showOff()
}
}
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!")
}
fun main(args: Array<String>) {
val button = Button()
button.showOff()
button.setFocus(true)
button.click()
}
- 코틀린 컴파일러는 이름과 시그니처가 같은 멤버 메소드에 대해 둘 이상의 디폴트 구현이 있는 경우 하위 클래스에서 직접 구현하게 강제한다.
1-2. open, final, abstract 변경자: 기본적으로 final
- 취약한 기반 클래스: 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 깨져버린 경우에 생긴다.
- 코틀린 클래스와 메서드는 기본적으로 final이다.
- 코틀린은 "상속을 위한 설계와 문서를 갖추거나, 그럴 수 없다면 상속을 금지하라"는 조슈아 블로크의 철학을 따르기 때문.
- 클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야 한다.
- 오버라이드를 허용하고 싶은 메서드나 프로퍼티 앞에도 open 변경자를 붙여야 한다.
- 오버라이드하는 메서드의 구현을 하위 클래스에서 오버라이드하지 못하게 금지하려면 오버라이드하는 메서드 앞에 final을 명시해야 한다.
- abstract로 선언한 추상 클래스는 인스턴스화할 수 없다.
open class RichButton : Clickable {
fun disable() {}
open fun animate() {}
override fun click() {}
}
클래스 내의 변경자 정리
- 인터페이스 멤버의 경우 final, open, abstract를 사용하지 않는다.
- 인터페이스 멤버는 항상 열려 있으며 final로 변경할 수 없다.
1-3. 가시성 변경자: 기본적으로 공개
- 가시성 변경자는 코드 기반에 있는 선언에 대한 클래스 외부 접근을 제어한다.
- 코틀린의 기본 가시성은 자바와 다르게 public이다.
- internal이라는 가시성 변경자는 모듈 내부에서만 볼 수 있다.
- 모듈은 한 번에 한꺼번에 컴파일되는 코틀린 파일을 의미한다. 인텔리J나 이클립스, 메이븐, 그레들 등의 프로젝트가 모듈이 될 수 있다.
- 자바와 차이
- 코틀린에서는 최상위 선언에 대해 private 가시성을 허용한다는 점이다.
- 코틀린에서는 외부 클래스가 내부 클래스나 중첩된 클래스의 private 멤버에 접근할 수 없다는 점이다.
1-4. 내부 클래스와 중첩된 클래스: 기본적으로 중첩된 클래스
- 중첩 클래스(inner class)는 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없다.
- 클래스 계층을 만들되 그 계층에 속한 클래스의 수를 제한하고 싶은 경우 중첩 클래스를 쓰면 편리하다.
class Outer {
inner class Inner {
fun getOuterReference(): Outer = this@Outer
}
}
- 내부 클래스 Inner 안에서 바깥쪽 클래스의 Outer의 참조에 접근하려면 this@Outer라고 써야 한다.
1-5. 봉인된 클래스: 클래스 계층 정의 시 계층 확장 제한
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)
}
- when을 사용해 Expr 타입의 값을 검사할 때 꼭 디폴트 분기인 else 붙기를 덧붙이게 강제한다.
- 상위 클래스에 sealed 변경자를 붙이면 그 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있다. sealed 클래스의 하위 클래스를 정의할 때는 반드시 상위 클래스 안에 중첩시켜야 한다.
- sealed로 표시된 클래스는 자동으로 open된다.
2. 뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언
클래스 초기화: 주 생성자와 초기화 블록
2-1.- 코틀린 클래스의 생성자는 크게 주 생성자와, 부 생성자로 구분할 수 있다.
- 주 생성자는 클래스 본문이 아닌 괄호 안에서 정의
- 부 생성자는 클래스 본문 안에서 정의
- 생성자 파라미터에 디폴트 값을 정의할 수 있다.
class User(val nickname: String) // 주 생성자
class User(val nickname: String, val isSubscribed: Boolean = true)
- constructor는 주 생성자나 부 생성자 정의를 시작할 때 사용한다.
- init은 초기화 블록을 시작한다. 초기화 블록은 주 생성자와 함께 사용된다.
class User constructor(_nickname: String) {
val nickname = _nickname;
}
class User constructor(_nickname: String) {
val nickname: String
init {
nickname = _nickname
}
}
2-2. 부 생성자: 상위 클래스를 다른 방식으로 초기화
- 부 생성자는 constructor 키워드로 시작한다.
- 클래스에 주 생성자가 없다면 모든 부 생성자는 반드시 상위 클래스를 초기화하거나 다른 생성자에게 생성을 위임해야 한다.
- 부 생성자가 필요한 주된 이유는 자바 상호운용성이다. ?
class MyButton : View {
constructor(ctx: Context) : super(ctx) {
// ...
}
constructor(ctx: Context, attr: AttributeSet) : super(ctx, attr) {
// ...
}
}
2-3. 인터페이스에 선언된 프로퍼티 구현
interface User {
val nickname: String
}
- 인터페이스에 있는 프로퍼티 선언에는 뒷받침하는 필드나 게터 등의 정보가 들어있지 않다.
class SubscribingUser(val email: String) : User {
override val nickname: String
get() = email.subStringBefore('@') // 커스텀 게터
}
class FacebookUser(val accountId: Int) : User {
override val nickname = getFacebookName(accountId) // 프로퍼티 초기화 식
}
- SubscribingUser의 nickname은 매 번 호출될 때마다 substringBefore를 호출해 계산하는 커스텀 게터를 활용
- FacebookUser의 nickname은 객체 초기화 시 계산한 데이터를 뒷받침하는 필드에 저장했다가 불러오는 방식을 활용
2-4. 게터와 세터에서 뒷받침하는 필드에 접근
- get, set을 재정의할 때 실제 필드에 접근하고 싶다면 키워드 field를 사용할 수 있다. 이를 백킹 필드라고 한다.
- 값을 저장하는 동시에 로직을 실행할 수 있게 하기 위해서는 접근자 안에서 프로퍼티를 뒷받침하는 필드에 접근할 수 있어야 한다.
- field 키워드를 사용하지 않고 setter를 정의하면 무한 재귀에 빠질 수 있다.
class User(val name: String){
var address: String = "unspecified"
set(value: String){
field = value // 뒷받침하는 필드 값 변경하기
}
}
2-5. 접근자의 가시성 변경
- 접근자의 가시성은 기본적으로 프로퍼티의 가시성과 같다.
- 원한다면 가시성 변경자를 추가해서 접근자의 가시성을 변경할 수 있다.
3. 컴파일러가 생성한 메서드: 데이터 클래스와 클래스 위임
3-2. 데이터 클래스: 모든 클래스가 정의해야 하는 메서드 자동 생성
toString, equals, hashCode를 오버라이딩해야 할 때 IDE는 이런 메서드를 쉽게 생성할 수 있도록 도와준다. 하지만 코틀린은 IDE를 통해 생성할 필요도 없이 data라는 변경자를 클래스 앞에 붙이면 필요한 메서드를 컴파일러가 자동으로 만들어 준다. 이런 클래스를 데이터 클래스라고 부른다.
data class Client(val name: String, val postalCode: Int)
- data 클래스로 정의하면 equals, hashCode, toString, copy 메서드를 컴파일러가 자동으로 만들어준다.
- equals: 모든 프로퍼티 값의 동등성 확인.
- hashCode: 모든 프로퍼티의 해시 값을 바탕으로 계산한 해시값 반환.
- copy: 일부 프로퍼티를 변경하며 객체를 복사하는 메서드.
equals와 hashCode는 주 생성자에 나열된 모든 프로퍼티를 고려해 만들어진다.
- equals 메서드는 모든 프로퍼티 값의 동등성을 확인한다.
- hashCode 메서드는 모든 프로퍼티의 해시 값을 바탕으로 계산한 해시 값을 반환한다.
데이터 클래스의 프로퍼티가 꼭 val일 필요는 없지만, 데이터 클래스의 모든 프로퍼티를 읽기 전용으로 만들어서 데이터 클래스를 불변 클래스로 만들라고 권장한다.
- HashMap 등 컨테이너에 데이터 클래스를 담는 경우 불변성 필수적
- 다중스레드에서 스레드를 동기화해야 할 필요성이 줄어듬
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 innerSet.add(element)
}
override fun addAll(c: Collection<T>): Boolean {
objectsAdded += c.size
return innerSet.addAll(c)
}
}
- by 키워드를 통해 인터페이스에 대한 구현을 다른 객체에 위임할 수 있다.
- 위임하지 않을 메서드는 직접 오버라이드하여 구현할 수 있다.
4. object 키워드 : 클래스 선언과 인스턴스 생성
- 객체 선언(object declaration): 싱글턴을 정의하는 방법 중 하나
- 동반 객체(companion object): 인스턴스 메소드는 아니지만 어떤 클래스와 관련 있는 메소드와 팩토리 메소드를 담을 때 쓰인다.
4-1. 객체 선언: 싱글턴을 쉽게 만들기
코틀린은 객체 선언 기능을 통해 싱글턴을 언어에서 기본 지원한다.
object Payroll {
val allEmployees = arrayListOf<Person>()
fun calculateSalary() {
fun (person in allEmployees) {
...
}
}
}
Payroll.allEmployees.add(Person(...))
Payroll.calculateSalary()
- 싱글턴 객체는 객체 선언문이 있는 위치에서 생성자 호출 없이 즉시 만들어진다.
- 변수와 마찬가지로 객체 선언에 사용한 이름 뒤에 .로 메서드나 프로퍼티에 접근할 수 있다.
일반 객체를 사용할 수 있는 곳에서는 항상 싱글턴 객체를 쓸 수 있다. 아래 예시와 같이 Comparator를 구현하여 compare 메서드가 있는 CaseInsensitiveFileComparator 객체가 있을 때, sortedWith 메서드에서 해당 객체를 전달받아 사용하고 있다.
object CaseInsensitiveFileComparator : Comparator<File> {
override fun compare(file1: File, file2: File): Int {
return file1.path.compareTo(file2.path, ignoreCase = true)
}
}
fun main(args: Array<String>) {
println(CaseInsensitiveFileComparator.compare(
File("/User"), File("/user")))
val files = listOf(File("/Z"), File("/a"))
println(files.sortedWith(CaseInsensitiveFileComparator))
}
중첩객체를 사용해서 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-2. 동반 객체: 팩토리 메소드와 정적 멤버가 들어갈 장소
- 코틀린 언어는 자바 static 키워드를 지원하지 않는다. 대신 코틀린 패키지 수준의 최상위 함수와 객체 선언을 활용한다.
- 동반객체를 사용하면 자바의 정적 메서드 호출이나 정적 필드 사용 구문과 같아진다.
class A {
companion object {
fun bar() {
println("Companion object called")
}
}
}
fun main(args: Array<String>) {
A.bar()
}
동반 객체는 바깥쪽 클래스의 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(args: Array<String>) {
val subscribingUser = User.newSubscribingUser("bob@gmail.com")
val facebookUser = User.newFacebookUser(4)
println(subscribingUser.nickname)
}
- 팩토리 메소드 이름을 정할 수 있고, 팩토리 메소드는 팩토리 메서드가 선언된 클래스의 하위 클래스 객체를 반환할 수 있다.
4-3. 동반 객체를 일반 객체처럼 사용
동반 객체에서 인터페이스 구현
- 동반객체도 인터페이스를 구현할 수 있다.
- 동반 객체에 이름을 붙일 수 있다.
interface JsonFactory<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 { // 확장 함수 선언
...
}
- Person 동반 객체에 fromJson 확장 함수를 정의할 수 있다.
- 동반객체에 대한 확장함수를 작성할 수 있으려면 원래 클래스에 동반 객체를 꼭 선언해야 한다. 설령 빈 객체일지라도 동반 객체가 꼭 있어야 한다.
4-4. 객체 식: 무명 내부 클래스를 다른 방식으로 작성
- 무명 객체를 정의할 때도 obejct 키워드를 쓴다.
- 무명 객체는 자바에서 무명 내부 클래스이다.
- 객체 선언과 달리 무명 객체는 싱글턴이 아니다(새로운 인스턴스 생성).
- 코틀린의 무명 클래스는 여러 인터페이스를 구현할 수 있다.
window.addMouseListener(
object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
// ...
}
override fun mouseEntered(e: MouseEvent) {
}
}
)
'스터디 > Kotlin In Action' 카테고리의 다른 글
[Kotlin In Action]6장 - 코틀린 타입 시스템 (0) | 2023.03.13 |
---|---|
[Kotlin In Action]5장 - 람다로 프로그래밍 (0) | 2023.02.27 |
[Kotlin In Action]3장 - 함수 정의와 호출 (0) | 2023.02.06 |
[Kotlin In Action]2장 - 코틀린 기초 (0) | 2023.01.24 |
[Kotlin In Action]1장 - 코틀린이란 무엇이며, 왜 필요한가? (0) | 2023.01.16 |
Comments