4. 클래스, 객체, 인터페이스
_ 클래스와 인터페이스
_ 뻔하지 않은 생성자와 프로퍼티
_ 데이터 클래스와 클래스 위임
_ object 키워드 사용
=== 4.1 클래스와 인터페이스 ===
4-1. 클래스의 상속 개수 제한
- 클래스는 여러개의 interface를 상속받을 수 있지만, class는 하나만 상속 받을 수 있다,
4-2. override 변경자는 실수로 상위 클래스의 메서드를 오버라이드 하는 경우를 방지해줌
- 상위 클래스에 있는 메서드와 시그니처가 같은 메서드를 우연히 하위 클래스에서 선언하는 경우,
컴파일이 안되기 때문에 override를 붙이거나 메서드 이름을 바꿔야만 한다.
4-3. 한 클래스가 2개의 인터페이스를 구현할 때(상속 받을 때) 생길 수 있는 문제점
만약 아래에서 Button에 showOff() 메서드를 구현하지 않는다면 아래와 같은 오류를 마주칠 수 있다.
Class 'Button' must override public open fun showOff(): Unit defined in com.example.~~~~~~~~~~.
Clickable because it inherits multiple interface methods of it
그 이유는 Button이 2개의 인터페이스를 상속 받는데, 둘 다 동일한 시그니처의 showOff()를 가지고 있기 때문에
컴파일러가 어떤 인터페이스의 showOff() 메서드를 호출해야 할지 모호하기 때문이다.
때문에 명확성을 주기위해 동일한 시그니처인 showOff()를 오버라이드 해야한다.
이때 인터페이스의 메서드를 호출하는 방법은 super<구현한 인터페이스 이름>.메서드 이름()이다.
interface Clickable {
fun onClick()
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 onClick() = println("I was clicked")
override fun showOff() { // showOff 메서드를 구현 해야함
if (condition) {
super<Clickable>.showOff()
return
}
super<Focusable>.showOff()
}
}
fun main() {
Button().showOff()
}
4-4. open, final, abstract 변경자 / default is final
- 기존에 자바는 final로 명시적 상속 금지를 하지 않는 모든 클래스를 다른 클래스가 상속할 수 있었다. -> base Calss의 취약성
이후 아래의 조언을 반영하여 kotlin에서는 default를 final로 둬서 상속이 필요한 부분에만 open을 명시하도록 수정됨
- 상속에 대한 조언 : "상속을 위한 설계와 문서를 갖추거나, 그럴 수 없다면 상속을 금지하라" (ref. 이펙티브 자바)
open class Button : Clickable {
fun disable() { } // <- final, 오버라이드 불가
open fun animate() { } // <- open, 오버라이드 가능
// override, open인 메서드를 오버라이드함, 이 메서드는 기본적으로 열려있음
// 따라서, 더 이상 하위에서 오버라이드가 되지 않게 하려면 override앞에 final 키워드를 명시해야 함
override fun onClick() { }
}
4-5. 가시성 규칙 - visibility(가시성) 변경자 / default is public(가시성이 높음)
- A가 B에 접근하기 위해서는 A의 가시성이 B보다 낮거나 같아야 한다.
- 가시성 높음 public > internal > protected > private 가시성 낮음
- internal 가시성으로 명시된 A라는 class가 있을 때, A의 확장함수 getData가 있다고 해보자.
- 우선 아래의 A.getData() 함수에서 A라는 class 자체를 사용할 수 없다.
그 이유는 함수의 가시성은 별도의 지정이 없어 public 이지만, A의 가시성 제한자가 internal이기 때문이다.
접근이 가능하도록 변경하려면 접근하려는 클래스보다 가시성이 낮거나 같아야 한다.
ㄴ> 1. A class의 가시성을 public으로 하거나 / 2. A.getData()의 가시성을 internal 이하로 낮춰야 한다.
- 다음으로, A.getData() 까지 해결을 했지만 함수 본문에서 getColorType과 getContent 역시 가시성 규칙을 위반하므로 호출할 수가 없다
-> 바로 이 부분이 "확장함수는 클래스의 캡슐화를 깨지 않는다" 라는 말을 다시 한 번 이해할 수 있는 예시인 것 같다.
internal open class A {
private fun getColorType(): String = "~"
protected fun getContent(): String = "~"
}
fun A.getData() {
val color = getColorType()
val text = getContent()
}
4-6. 중첩 클래스(nested class)
- 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없다!!!!
- 이외 다른 내용은 다시 봐도 이해불가,,,
4-7. 봉인 클래스(sealed class)
- Expr 마커 인터페이스를 구현한 Num과 Sum class가 있다. when 식을 보면 Int 값을 무조건 반환해야 하기에
Num type과 Sum type의 객체가 아닐 때도 Int를 반환하기 위해 else 분기를 필수로 구현 해야하는 문제가 있다.
ㄴ의미 없는 값을 던지기 보다는 예외를 던지는 코드를 채우는 편인 것 같다.
- 그리고 만약 Expr을 구현하는 새로운 Mul함수를 구현했다고 치자. 이때 when 분기에 추가하지 않으면,
else 분기에 해당하여 별다른 경고나 안내 없이 런타임에 예외(오류)를 발생시킬 것이다.
코틀린은 이런 2가지 문제점에 대한 해법을 제공한다. -> 그것은 바로 sealed class이다.
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) + eval(e.left)
else -> throw IllegalArgumentException("Unknown expression")
}
fun main(args: Array<String>) {
println(eval(Sum(Sum(Num(1), Num(2)), Num(4))))
}
위의 코드를 sealed class로 변경해보자
sealed class Expr {
class Num(val value: Int) : Expr()
class Sum(val left: Expr, val right: Expr) : Expr()
// class Mul(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)
}
fun main(args: Array<String>) {
println(eval(Expr.Sum(Expr.Sum(Expr.Num(1), Expr.Num(2)), Expr.Num(4))))
}
- sealed class로 변경하며 누릴 수 있는 장점 정리 :
1. when 식에서 sealed 클래스의 모든 하위 클래스를 처리한다면 else 분기를 명시하지 않아도 된다.
2. Expr의 하위 클래스 Mul을 추가한 뒤, when 분기에 추가하지 않으면 컴파일 시점에 아래와 같은 오류가 발생하여 런타임 오류를 방지할 수 있다.
'when' expression must be exhaustive, add necessary 'is Mul' branch or 'else' branch instead
- 주의사항 : sealed class는 클래스 외부에 자신을 상속하는 클래스를 둘 수 없다.
=== 4.2 뻔하지 않은 생성자와 프로퍼티 ===
4-8. 클래스 초기화: 주 생성자와 초기화 블록
- 주 생성자의 목적 : 생성자 파라미터(_name)를 지정하고, 그 생성자 파라미터에 의해 초기화 되는 프로퍼티(name)를 정의하기 위함
- init의 필요한 이유 : 주 생성자는 별도의 코드를 포함할 수 없이 제한적이기 때문에, 부가적으로 초기화할 수 있는 블럭이 필요하기 때문
- 새롭게 알게된 점 : init의 초기화 블럭은 필요하다면 여러 개의 초기화 블록을 사용할 수 있다.
// base code
class User constructor(_name: String) {
val name: String
init {
name = _name
}
}
- "_name"에서 맨 앞 밑줄이 있는 이유는 프로퍼티와 생성자 파라미터를 구분하기 위함이다.
ㄴ 위 예제에서는 name을 _name으로 바로 초기화 시킬 수 있어서 init 구문이 필요가 없다.
- 또, 주 생성자 앞에 별다른 애노테이션이나 가시성 변경자가 없다면, constructor을 생략해도 된다.!
// base code -> (init 제거 -> constructor 생략)
class User (_name: String) {
val name = _name
}
- 이때, _name은 프로퍼티를 초기화 하는 식이나 init 블럭 안에서만 참조할 수 있다!
- 한 번 더 간소화를 하면, 위의 코드는 아래의 코드처럼 작성할 수 있고, 모두 같은 기능을 한다.
// base code -> (init 제거 -> constructor 생략) -> val 키워드 사용
class User (val name: String) { // 여기서 val 키워드는 이 파라미터에 상응하는 프로퍼티가 생성된다는 뜻이다.
}
(사용하면서 인지한 개념이지만, 어디서 참조가 가능한 지 명확하게 짚고 넘어갈 수 있다는 점이 좋은 것 같다.)
4-9. 외부에서 클래스의 인스턴스화를 막는 방법
- private 키워드를 쓰면 되는데 생성자의 가시성을 제한하는 것이므로 생략했던 constructor를 다시 명시해줘야 한다.
class secretive private constructor() { }
-> 그럼 인스턴스화도 못하는데 클래스를 왜 쓸까? -> companion obejct에서 객체 생성을 담당하는 팩토리 함수를 만들어서 객체 생성 가능함
4-10. 클래스의 생성자
- 일반적으로 코틀린에서는 생성자가 여럿 있는 경우가 자바에 비해 훨씬 작다.
-> 그 이유는, kotlin은 default parameter 값과 named parameter를 지원하므로 추가적인 생성자를 해결할 수 있기 때문이다.
4-11. 커스텀 게터, Custom getter
- Custom getter는 백킹 필드에 값을 저장하지 않는다. -> 사용될 때 마다 매번 값을 계산해서(email 주소에서 @ 앞의 문자를 따서) 반환한다.
class SubscribingUser(val email: String) : User {
override val nickname: String
get() = email.substringBefore('@')
}
4-12. 커스텀 세터, Custom setter
- 내부에서 filed라는 키워드(식별자)를 통해 접근할 수 있다. (커스텀 세터는 처음 보는 것 같아 추가함)
- var 프로퍼티의 게터와 세터 중 하나만 정의해도 된다! / 나머지는 자동으로 뻔한 getter/setter (get() = field)가 컴파일러에 의해 자동으로 제공된다.
class User(val name: String) {
var address: String = "unspecified"
// 뻔한 custom getter 자동 생성
// get() = field
set(value: String) {
field = value
println("field: $field")
}
}
- 컴파일러는 디폴트 접근자 구현(var address: String = "unspecified")을 사용하든 커스텀 게터/세터를 정의하던 관계없이 게터나 세터에서 field를 사용하는 프로퍼티에 대해 백킹 필드를 생성해준다.
-> 다만, field를 사용하지 않는 커스텀 접근자 구현을 정의한다면 뒷받침하는 필드는 존재하지 않는다.
=> field 값을 사용하지 않으니까 굳이 뒷받침하는 필드가 존재할 이유가 없다! (사용을 안하니까!)
4-13. 게터와 세터의 가시성
- 접근자(게터와 세터)의 가시성은 기본적으로 프로퍼티의 가시성과 같다. (당연함)
- 하지만, 원한다면 get이나 set 앞에 가시성 제한자를 추가해서 접근자의 가시성을 변경할 수 있다.
-> 일반적인 방법인지는 모르겠지만, getter에 private를 붙여 class 내에서만 수정할 수 있게 하여 데이터 무결성?을 좀 더 확보할 수 있을 것 같다.
=== 4.3 데이터 클래스와 클래스 위임 ===
=== 4.4 object 싱글턴 ===
4-a. 다시 한 번 학습할 키워드
* 직렬화(serialization): (데이터 구조나 객체 상태를) 저장하거나 전송할 수 있는 형태로 변환하는 과정
'Kotlin' 카테고리의 다른 글
Kotlin Scope Functions 정리 (1) | 2024.03.11 |
---|---|
[Kotlin in Action 3장] 정주행 (1) | 2024.03.01 |
[Kotlin in Action 1, 2장] 정주행 시작 (0) | 2024.02.27 |
shuffle()와 shuffled()의 차이점 및 배열(Array), 리스트(List) 정리 (0) | 2024.02.19 |