- 코틀린의 클래스와 인터페이스는 자바 생성자와는 다르다.
(ex. 인터페이스에 프로퍼티 선언이 들어갈 수 있다.)
- 자바와 달리 코틀린 선언은 기본적으로 public(가시성 변경자)이며 final(접근 변경자)이다.
- 가시성 변경자는 외부 접근에 관한 기준, 접근 변경자는 상속에 관한 기준
- 내포 클래스는 기본적으로 내부 클래스가 아니다 => 코틀린 내포 클래스에는 외부 클래스에 대한 암시적 참조가 없다.
- 생성자의 경우 짧은 주 생성자 구문으로도 거의 모든 경우를 잘 처리할 수 있지만 복잡한 초기화 로직을 수행하는 경우를 대비한 완전한 문법도 있다.
- 프로퍼티도 마찬가지로 간결한 프로퍼티 구문으로도 충분히 제 몫을 하지만 필요하면 접근자로 직접 구현할 수 있다.
- 클래스를 data 클래스로 선언하면 컴파일러가 일부 표준 메서드를 생성해준다.
- 코틀린 언어가 제공하는 위임을 사용하면 언어가 위임 기능을 기본 제공하기 때문에 위임을 처리하기 위한 준비 메서드를 직접 작성할 필요가 없다.
- 클래스를 선언하는 동시에 유일한 인스턴스를 만들 때 쓰는 object 키워드
- object를 사용해 싱글턴 클래스, 동반 객체, 객체 식을 표현한다.
4.1 클래스 계층 정의
- 코틀린에서 계층을 정의하는 방식을 살펴보자
- 코틀린의 가시성 변경자와 접근 변경자를 알아보자.
- 코틀린 가시성/접근 변경자는 자바와 비슷하지만 아무것도 지정하지 않은 경우의 기본 가시성은 다르다.
- 코틀린에 새로 도입한 sealed 변경자를 알아보자. sealed는 클래스 상속이나 인터페이스 구현을 제한한다.
- 4.1.1 코틀린 인터페이스
- 코틀린에서는 인터페이스를 구현할 때, class 대신 interface를 사용한다.
interface Clickable{
fun click()
}
- 인터페이스 구현(override 변경자는 필수이다.)
- 코틀린에서는 상속(서브클래싱)이나 구성(인터페이스 구현)에서 모두 클래스 이름 뒤에 콜론(:)을 붙이고 인터페이스나 클래스 이름을 적는 방식을 사용한다.
- 실수로 상위 클래스에 구현된 메서드와 같은 메서드를 구현한 경우 해당 메서드를 override로 표시하거나 메서드 이름을 변경하지 않으면 컴파일되지 않는다.
class Button: Clickable{
override fun click() = println("I was clicked")
}
fun main(){
Button().click()
}
- 인터페이스 안에 본문이 있는 메서드 구현하기
- 인터페이스 메서드는 디폴트 구현을 제공할 수 있다.
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()
}
}
- 자바에서 코틀린의 인터페이스가 있는 메서드 구현하기
- 코틀린은 디폴트 메서드가 있는 인터페이스를 일반 인터페이스와 디폴트 메서드 구현이 정적 메서드로 들어있는 구현 클래스를 조합해 구현한다.
- 따라서 디폴트 메서드가 포함된 코틀린 인터페이스를 자바 클래스에서 상속해 구현하고 싶다면, 코틀린에서 메서드 본문을 제공하는 메서드를 포함해, 모든 메서드에 대한 본문을 작성해야 한다.
- 코틀린 인터페이스에서 디폴트 메서드의 본문을 이용할 수 없고 구현 클래스에서 새로 구현하여 사용해야 한다는 것이다.
/* 자바 */
class JavaButton implements Clickable {
@Override
public void click(){
System.out.println("I was clicked");
}
@Override
public void showOff(){
System.out.println("I'm showing off");
// 자바 코드는 코틀린 인터페이스의 디폴트 구현을 사용할 수 없다.
}
}
4.1.2 open, final, abstract 접근 변경자: 기본적으로 final
- 기본적으로 코틀린 클래스에 대해 하위 클래스를 만들 수 없고, 기반 클래스의 메서드를 하위 클래스가 오버라이드할 수도 없다.
- 코틀린에서 모든 클래스와 메서드는 기본적으로 final이다.
- 자바에서는 final로 명시적으로 지정하지 않는 한 모든 클래스를 다른 클래스가 상속할 수 있고 모든 클래스는 하위 클래스에서 오버라이드할 수 있다.
- 자바 방식은 취약한 기반 클래스 문제를 야기할 수 있는 위험이 있다.
- 취약한 기반 클래스라는 문제는 기반 클래스 구현을 변경함으로써 하위 클래스가 잘못된 동작을 하게 되는 경우를 뜻한다.
- 자바 클래스와 메서드는 기본적으로 상속에 대해 열려있지만 코틀린의 클래스와 메서드는 기본적으로 final이다.
- 어떤 클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야 한다.
- 그와 더불어 override를 허용하고 싶은 메서드나 프로퍼티의 앞에도 open 변경자를 붙여야 한다.
// 열린 메서드를 포함하는 열린 클래스 구현하기
open class RichButton: Clickable {
fun disable(){ /* ... */} // final 함수, 하위 클래스에서 이 메서드 오버라이드 불가
open fun animate(){ /*...*/} // 하위 클래스에서 이 메서드 오버라이드 가능
override fun click(){ /*...*/} // 상위 클래스의 열려있는 메서드를 오버라이드,
// 이 메서드 자체도 열려있다.
}
- 기반 클래스나 인터페이스의 멤버를 오버라이드한 경우에는 기본적으로 open으로 간주된다.
// 열린 메서드를 오버라이드하는 열린 클래스 하위 클래스 선언하기
class ThemedButton : RichButton(){ // disable 메서드는 final -> override 불가
override fun animate(){ /* ... */ } // animate는 명시적으로 열려있는 메서드
override fun click(){ /* ... */} // RichButton에서 click은 명시적 final 선언 X
override fun showOff(){ /* ... */ } // Clickable 인퍼테이스의 showOff override
}
- 하위 클래스에서 오버라이드하는 것을 금지하려면 오버라이드 된 메서드를 명시적으로 final로표시
open class RichButton : Clickable {
final override fun click() { /* ... */ } // final이 없는 override 메서드나
// 프로퍼티는 기본적으로 열려있다.
}
- 열린 클래스와 스마트 캐스트
- 클래스의 기본적인 상속 가능 상태를 final로 함으로써 얻을 수 있는 큰 이점은 다양한 경우에 스마트 캐스트가 가능하다는 점이다.
- 클래스 프로퍼티의 경우
val로 선언돼있으면서 커스텀 접근자가 없는 경우에만 스마트 캐스트가 가능하다.
- 이는 곧 클래스 프로퍼티가
final이어야 한다는 뜻이기도 하다.
- 프로퍼티가
final이 아니라면 해당 프로퍼티를 상속한 다른 클래스에서 해당 프로퍼티에 대한 커스텀 접근자를 정의함으로써 스마트 캐스트의 요구사항을 깰 수 있다.
- 프로퍼티는 기본적으로 final이기 때문에 따로 고민할 필요없이 대부분의 프로퍼티를 스마트 캐스트에 활용할 수 있다. => 코드를 더 이해하기 쉽게 만든다.
- 클래스를 abstract로 선언할 수도 있다.
- abstract로 선언한 추상 클래스는 인스턴스화할 수 없다.
- 추상 클래스에는 구현이 없어 하위 클래스에서 오버라이드해야만 하는 추상 멤버가 있는 것이 보통이다.
- 추상 멤버는 반드시 하위 클래스에서 오버라이드해야만 하기 때문에 항상 열려있으며, open 변경자를 명시할 필요가 없다.
abstract class Animated{
abstract val animationSpeed: Double // 추상 프로퍼티: 하위클래스에서 값 or 접근자
val keyframes: Int = 20 // 열려있지 않은 프로퍼티
open val frames: Int = 60 // 열린 프로퍼티
abstract fun animate() // 추상 함수: 하위 클래스에서 반드시 오버라이드
open fun stopAnimating(){ /* ... */ } // 열린 함수: 하위 클래스 오버라이드 가능
fun animateTwice() { /* ... */ } // 열려있지 않은 함수
}
| 변경자 |
이 변경자가 붙은 멤버는... |
설명 |
| final |
오버라이드 할 수 없음 |
클래스 멤버의 기본 변경자 |
| open |
오버라이드 할 수 있음 |
반드시 open을 명시해야 오버라이드 할 수 있음 |
| abstract |
반드시 오버라이드해야 함 |
추상 클래스의 멤버에만 붙일 수 있다. 추상 멤버에는 구현이 있으면 안 된다. |
| override |
상위 클래스나 인스턴스의 멤버를 오버라이드하는 중 |
오버라이드하는 멤버는 기본적으로 열려있다. 하위 클래스의 오버라이드를 금지하려면 final을 명시해야한다. |
4.1.3 가시성 변경자 : 기본적으로 공개(public)
- 가시성 변경자는 코드 기반에 있는 선언에 대한 클래스 외부 접근을 제어한다.
- 코틀린은 public, protected, private 변경자를 제공한다.
- public 선언은 누구나 볼 수 있고, protected 선언은 하위 클래스에서만 볼 수 있으며,
private 선언은 그 선언이 포함된 클래스 안에서만 볼 수 있다.
- 파일의 최상위 선언이 private이면 같은 파일 안에서만 해당 선언을 볼 수 있다.
- 코틀린에서 아무 변경자도 없는 선언은 모두 공개, 즉 public이다.
- 모듈 안으로만 한정된 가시성을 위해 코틀린은 internal이라는 가시성을 제공한다.
- 코틀린은 자바와 달리 패키지 전용이라는 가시성 개념이 없다.
- 코틀린에서는 최상위 선언에 대해 private 가시성을 허용한다.
- 이 가시성이 허용되는 최상위 선언에는 클래스, 함수, 프로퍼티 등이 포함된다.
- 최상위 선언에 대해 protected 가시성은 허용하지 않는다.
- 코틀린 가시성 변경자
| 변경자 |
최상위 선언 |
클래스 멤버 |
|
| public |
모든 곳에서 볼 수 있다. |
모든 곳에서 볼 수 있다. |
|
| internal |
같은 모듈 안에서만 볼 수 있다. |
같은 모듈 안에서만 볼 수 있다. |
|
| protected |
- |
오직 해당 클래스와 해당 클래스의 하위 클래스 안에서만 볼 수 있다. 최상위 선언은 절대 불가하다. |
|
| private |
같은 파일 안에서만 볼 수 있다. |
같은 클래스 안에서만 볼 수 있다. |
|
| |
|
|
|
internal open class TalkActiveButton {
private fun yell() = println("Hey!")
protected fun whisper() = println("Let's talk!")
}
// 기본적으로 public
fun TalkActiveButton.giveSpeech(){ // ERROR: public 멤버가 internal 클래스를 노출!
yell() // ERROR: private 멤버는 자신의 클래스 안에서만 접근 가능
whisper() // ERROR: protected 멤버는 자신의 클래스나 하위 클래스에서만 접근 가능
}
- 클래스를 확장한 함수(확장 함수)는 그 클래스의
private이나 protected 멤버에 접근할 수 없다.
- 일반적으로 기반 타입의 목록에 들어있는 타입의 가시성은 확장하는 클래스의 가시성과 비교해, 같거나 더 높아야하고, 기반 타입 안의 메서드의 시그니처에 사용된 모든 타입의 가시성은 확장하는 메서드의 가시성과 비교했을 때, 같거나 더 높아야한다.
코틀린은 패키지 전용 가시성이 없다.
- 코틀린에는 자바의 기본 가시성인 패키지 전용(
default)이라는 개념이 없다.
- 코틀린에서는 네임스페이스를 관리하기 위한 용도로만 패키지를 사용하며, 가시성 제어에 사용하지 않는다.
internal 가시성의 장점은 모듈 구현에 대해 진정한 캡슐화를 제공한다는 것이다.
- 자바에서는 패키지가 같은 클래스를 선언하기만 하면 어떤 프로젝트의 외부에 있는 코드라도 패키지 내부에 있는 패키지 전용 선언에 쉽게 접근할 수 있어 캡슐화가 쉽게 깨질 수 있다.
코틀린 가시성 변경자와 자바
- 코틀린의
public, protected, private 변경자는 컴파일된 자바 바이트코드 안에서도 그대로 유진된다.
- 그렇게 컴파일된 코틀린 선언의 가시성은 마치 자바에서 똑같은 가시성을 사용해 선언한 경우와 같다.
- 다만, 자바에서는 클래스를
private으로 만들 수 없으므로 내부적으로 코틀린은 private 클래스를 자바에서의 패키지 전용 클래스(default) 클래스로 컴파일한다.
internal 변경자
- 자바의 패키지 전용 가시성은
internal과는 전혀 다르다.
- 모듈은 보통 여러 패키지로 이뤄지며 서로 다른 모듈에 같은 패키지에 속한 선언이 들어있을 수 있다. =>
internal 변경자는 바이트 코드 상에서는 public으로 컴파일된다.
- 코틀린 선언과 그에 해당하는 자바 선언에 이런 차이가 있기 때문에 코틀린에서는 접근할 수 없는 대상을 자바에서 접근할 수 있는 경우가 생긴다.
- 코틀린에서 다른 모듈에 정의된
internal 클래스나 internal 최상위 선언을 모듈 외부의 자바 코드에서 접근할 수 있다.
- 코틀린에서
protected로 정의한 멤버를 해당 코틀린 클래스와 같은 패키지에 속한 자바 코드에서는 접근할 수 있다.
4.1.4 내부 클래스와 내포된 클래스: 기본적으로 내포 클래스
- 코틀린의 내포 클래스에서 선언된 private 멤버들은 바깥쪽 클래스에서 사용 불가하다.
- 바깥쪽 클래스의 private 멤버들은 내포 클래스에서 사용 가능하다.
- 이는 자바의 클래스 내부에 static으로 선언된 클래스, 즉 내포 클래스와 동일하며, 코틀린의 내포클래스와 자바의 내포 클래스 모두 자신의 바깥쪽 클래스의 인스턴스의 참조를 저장하지 않는다.
- 내포 클래스는 바깥쪽 클래스의 인스턴스에 대한 접근 권한이 없다.
- 코틀린 클래스 내부에 inner 변경자로 선언된 코틀린 내부 클래스는 자바 클래스 내부에
non-static 선언된 자바 내부 클래스와 동일하게, 바깥쪽 클래스의 인스턴스의 참조를 저장한다.
- 바깥쪽 클래스에서는 코틀린 내부 클래스에 선언된 private 멤버에 접근할 수 없다.
- 코틀린 내부 클래스에서는 자신이 속한 바깥쪽 클래스의 현재 인스턴스에 대해 접근할 수 있다.
- 코틀린 내부 클래스 안에서 this와 @ 기호를 이용해
this@Outer 의 형식으로 현재 자신이 속한 바깥쪽 클래스의 인스턴스의 참조를 구할 수 있다.
| 클래스 B 안에 구현된 클래스 A |
자바에서는 |
코틀린에서는 |
| 내포 클래스(바깥쪽 클래스의 인스턴스의 참조를 저장하지 않음) => 바깥쪽 클래스의 인스턴스에 대한 접근 불가 |
static class A |
class A |
| 내부 클래스(바깥쪽 클래스의 인스턴스의 참조를 저장) => 바깥쪽 클래스의 인스턴스에 대한 접근 가능 |
class A |
inner class A |
class Outer{
inner class Inner {
fun getOuterReference(): Outer = this@Outer
}
}
자바와 달리 코틀린의 외부 클래스가 내부 클래스나 내포 클래스의 private 멤버에 접근할 수 없는 이유
- Kotlin의 가시성은 언어 차원에서 엄격하다.
- JVM에서는 중첩 관계가 있는 클래스 간의
private 접근을 허용하지만 Kotlin은 플랫폼(JVM/JS/Native) 간 일관성을 위해 바깥에서 중첩된 공간으로의 private 접근을 금지하고 있다.
- 결국엔 플랫폼간의 일관을 위한 차원에서의 규약이다.
4.1.5 봉인된 클래스: 확장이 제한된 클래스 계층 정의
- 상위 클래스에 sealed 변경자를 붙이면, 그 상위 클래스를 상속한 하위 클래스의 가능성을 제한할 수 있다.
- sealed 클래스의 직접적인 하위 클래스들은 반드시 컴파일 시점에 알려져야 한다.
- sealed 변경자로 인해 봉인된 클래스가 구현된 파일과 같은 파일 안에 sealed 클래스의 직접적인 하위 클래스들이 속해 있어야 한다.
- sealed 클래스와 관련된 모든 하위 클래스들은 같은 파일 안에 위치해야 한다.
- sealed 변경자는 해당 클래스가 추상 클래스임을 명시하므로 sealed 변경자로 인해 봉인된 클래스에는 abstract를 붙일 필요가 없으며, 추상 멤버를 선언할 수 있다.
- 봉인된 클래스, 즉 sealed 클래스를 상속한 직접적인 하위 클래스들은 컴파일 타임 때 컴파일러에게 모두 취합되기 때문에, 컴파일러는 컴파일 타임 때 봉인된 클래스에 대한 when 식의 분기문들에서 봉인된 클래스를 직접적으로 상속한 모든 하위 클래스 중 빠진 것이 없는지를 검사해준다.
- 봉인된 클래스가 아닌 일반 클래스나 인터페이스의 경우엔, 컴파일 타임 때 그 클래스나 인터페이스를 상속한 모든 하위 클래스들을 컴파일러가 취합해주지 않기 때문에 그 클래스나 인터페이스에 대한 when 식의 분기문들에서 빠진 하위 클래스가 없는지를 검사해줄 수 없다.
- 일반 인터페이스(클래스)에 대한 when 식 => 컴파일러가 모든 하위 클래스를 알지 못하기 때문에 else 문으로 대비를 해야한다.
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")
}
- 봉인된 클래스(sealed class)에 대한 when 식 => 컴파일러가 이미 봉인된 클래스에 대한 모든 하위 클래스들을 알고 있기 때문에 컴파일 타임 때 봉인된 클래스에 대한 when 식의 분기문들에서 빠진 하위 클래스가 없는지를 미리 검사해준다.
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 Num -> e.value
is Sum -> eval(e.right) + eval(e.left)
}
- sealed interface도 sealed class와 동일한 규칙을 따른다.
4.2 뻔하지 않은 생성자나 프로퍼티를 갖는 클래스 선언
- 코틀린은 주생성자와 부생성자를 구분한다.
- 코틀린에서는 초기화 블록을 통해 초기화 로직을 추가할 수 있다.
- 4.2.1 클래스 초기화: 주 생성자와 초기화 블록
- 클래스 이름 뒤에 오는 괄호로 둘러싸인 코드를 주 생성자라고 부른다.
class User(val nickname: String)
- 생성자는 생성자 파라미터를 지정하고, 그 생성자 파라미터에 의해 초기화되는 프로퍼티를 구현하는 2가지 목적에 쓰인다.
class User(val nickname: String) 선언의 같은 목적을 달성할 수 있는 가장 명시적인 선언
class User constructor(_nickname: String){
val nickname: String
init {
nickname = _nickname
}
}
- constructor 키워드는 주 생성자나 부 생성자를 시작할 때 사용한다.
- init 키워드는 초기화 블록을 시작하고, 초기화 블록에는 클래스의 객체가 만들어질 때 실행될 초기화 코드가 들어간다.
- 초기화 블록은 주 생성자와 함께 사용된다.
- 필요하다면 클래스 안에 여러 초기화 블록을 선언할 수 있다.
- 생성자 파라미터 _ nickname에서 맨 앞의 밑줄은 프로퍼티와 생성자 파라미터를 구분해준다.
- 프로퍼티를 초기화하는 코드를 프로퍼티 선언에 포함시킬 수 있다.
- 주 생성자 앞에 별다른 어노테이션이나 가시성 변경자가 없다면 constructor를 생략해도 된다.
class User(_nickname: String){
val nickname=_nickname
}
- 프로퍼티를 초기화하는 식이나 초기화 블록 안에서만 주 생성자의 파라미터를 참조할 수 있다는 것에 유의하라
- 주 생성자의 파라미터를 갖고 프로퍼티를 초기화한다면 그 주 생성자 파라미터 앞에 val을 추가하는 방식으로 프로퍼티 정의와 초기화를 간략히 쓸 수 있다.
class User(val nickname: String)
- 함수 파라미터와 마찬가지로 생성자 파라미터에도 기본값을 정의할 수 있다.
class User(val nickname: String, val isSubscribed: Boolean=true)
- 클래스의 인스턴스를 만들려면 new와 같은 추가 키워드 없이 생성자를 직접 호출하면 된다.
fun main(){
val alice = User("Alice")
val bob = User("Bob", false)
val carol = User("Carol", isSubscribed=false)
val dave = User(nickname="Dave", isSubscribed=true)
}
- 기반 클래스의 생성자가 인자를 받아야 하는 경우엔, 하위 클래스의 주 생성자에서 기반 클래스의 생성자를 호출해야 할 필요가 있다.(부 생성자에서 super를 이용해 호출도 가능)
open class User(val nickname:String) { /* ... */ }
class SocialUser(nickname: String) : User(nickname) { /* ... */ }
- 클래스를 정의할 때 별도로 생성자를 정의하지 않으면 컴파일러가 자동으로 아무일도 하지 않는 인자가 없는 디폴트 생성자를 만들어준다.
open class Button
- 어떤 클래스를 정의할 때, 별도로 생성자를 하나도 정의하지 않았어도, 디폴트 생성자가 만들어지기 때문에 해당 클래스를 기반 클래스로 하는 하위 클래스를 정의할 때 기반 클래스의 디폴트 생성자를 호출해야한다.
class RadioButton: Button() { /* ... */}
- 인터페이스는 생성자가 없기 때문에 인터페이스를 구현하는 클래스에서는 인터페이스 이름 뒤 아무 괄호도 없어야 한다.
- 어떤 클래스를 클래스 외부에서 인스턴스화 하지 못하게 막고 싶다면 생성자를 private으로 만들어야 한다.
class Secretive private constructor(private val agentName: String){}
4.2.2 부 생성자: 상위 클래스를 다른 방식으로 초기화
- 부 생성자는 constructor 키워드로 시작한다. 필요에 따라 얼마든지 부 생성자를 선언해도 된다.
- 하위 클래스의 주 생성자에서 기반 클래스의 주 생성자 혹은 부 생성자를 호출할 수도 있지만,
- 하위 클래스의 부 생성자(constructor 키워드로 시작하는 생성자)에서 super를 이용해 기반 클래스의 주 생성자 혹은 부 생성자를 호출할 수도 있다.
- this를 이용해 자신의 클래스에 있는 생성자에게 위임할 수도 있다.
open class Downloader(val name: String) {
constructor(name: String, url: String?): this(name){
}
constructor(name: String, uri: URI?): this(name){
}
...
}
class MyDownloader: Downloader {
constructor(name: String): super(name){ // 기반 클래스 주 생성자에게 위임
}
constructor(name: String, url: String?): super(name, url){ // 기반 클래스
// 부 생성자에게 위임
}
constructor(name: String, uri: URI?): super(name, uri){ // 기반 클래스
// 부 생성자에게 위임
}
}
class MyDownloader: Downloader {
constructor(name: String): super(name){ // 기반 클래스 주 생성자에게 위임
}
constructor(name: String, url: String?): this(name, URI(url)){
// 자신의 클래스
// 부 생성자에게 위임
}
constructor(name: String, uri: URI?): super(name, uri){ // 기반 클래스
// 부 생성자에게 위임
}
}
4.2.3 인터페이스에 선언된 프로퍼티 구현
- 코틀린에서는 인터페이스에 추상 프로퍼티를 선언할 수 있고, 인터페이스의 구현 클래스에서 추상 프로퍼티를 구현할 수 있는 다양한 방식이 있다.
interface User {
val nickname: String
}
class PrivateUser(override val nickname: String) : User
// 주 생성자에 의해 초기화
fun getNameFromSocialNetwork(accountId: Int) : String = "kodee$accountId"
class SocialUser(val accountId: Int) : User{
override val nickname = getNameFromSocialNetwork(accountId)
// 프로퍼티 선언과 함께 초기화
}
class SubscribingUser(val email: String) : User {
override val nickname: String
get() = email.substringBefor('@')
// 커스텀 접근자, 커스텀 게터에 의해 결정
}
- 인터페이스에 추상 프로퍼티뿐 아니라 커스텀 접근자, 커스텀 getter, settter가 있는 프로퍼티를 선언할 수도 있다.
- 인터페이스의 추상 프로퍼티는 해당 인터페이스의 구현 클래스에서 반드시 오버라이드해야하지만, 커스텀 접근자와 커스텀 getter, setter가 있는 프로퍼티는 구현 클래스에서 구현없이 그대로 상속 가능하다.
interface Email {
val email: String
val nickname: String
get() = email.substringBefore('@') // 뒷받침하는 필드(이전 상태를 저장한 필드)
// 없지만 매번 결과를 계산해주기 때문에
// 선언해도 괜찮다.
}
4.2.4 게터와 세터에서 뒷받침하는 필드에 접근
- 프로퍼티의 2가지 유형이 있음을 알 수 있다.(값을 저장하는 프로퍼티와 커스텀 접근자에서 매번 값을 계산하는 프로퍼티)
- 이 두 형을 조합해서 어떤 값을 저장하되 그 값을 변경하거나 읽을 때마다 정해진 로직을 실행하는 유형의 프로퍼티를 만드는 방법을 알아보자
- 값을 저장하는 동시에 로직을 실행할 수 있게 하려면 접근자 안에서 프로퍼티를 뒷받침하는 필드에 접근할 수 있어야 한다.
// 세터에서 뒷받침하는 필드 접근하기
class User(val name: String) {
var address: String = "unspecified"
set(value: String) {
println(
"""
Address was changed for $name:
"$field" -> "$value". // 뒷받침하는 필드(field) 값 읽기
""".trimIndent())
field = value // 뒷받침하는 필드(field) 값 쓰기
}
}
fun main(){
val user = User("Alice")
user.address = "Christoph-Rapparini-Bogen 23"
// Address was changed for Alice:
// "unspecified" -> Christoph-Rapparini-Bogen 23.
}
- getter에서는 field 값을 읽을 수 만 있고, 세터에서는 field 값을 읽거나 쓸 수 있다.
- 뒷받침하는 필드(filed)가 있는 프로퍼티와 그런 필드가 없는 프로퍼티의 차이
- 컴파일러는 디폴트 접근자 구현을 사용하건 직접 게터나 세터를 정의하건 관계없이 게터나 세터에서 field를 사용하는 프로퍼티에 대해 뒷받침하는 필드를 생성해준다.
- 다만 field를 사용하지 않는 커스텀 접근자 구현을 정의한다면, 즉 프로퍼티가 val인 경우엔 게터에 field가 없고 var인 경우엔 게터와 세터 모두에 field가 없다면 컴파일러는 프로퍼티가 아무 정보도 저장하지 않는 것으로 보고 뒷받침하는 필드를 생성하지 않는다.
// 그 자신은 아무 정보도 저장하지 않는 프로퍼티
class Person(var birthYear: Int) {
var ageIn2050 : Int
get() = 2050 - birthYear
set(value) {
birthYear = 2050 - value
}
}
4.2.5 접근자의 가시성 변경
- 접근자의 기본 구현을 바꿀 필요는 없지만 가시성을 바꿀 필요가 있는 때가 있다.
- 접근자의 가시성은 기본적으로는 프로퍼티의 가시성과 같다. 하지만 원한다면 get이나 set 앞에
가시성 변경자를 추가해서 접근자의 가시성을 변경할 수 있다.
class LengthCounter {
var counter: Int = 0
private set
fun addWord(word: String) {
counter += word.length
}
}
fun main() {
val lengthCounter = LengthCounter()
lengthCounter.addWord("Hi!")
println(lengthCounter.counter)
// 3
lengthCounter.counter = 0
//Error : Cannot assign to 'counter' : the setter is private in
}
4.3 컴파일러가 생성한 메서드: 데이터 클래스와 클래스 위임
- 코틀린 컴파일러는 equals, hashCode, toString 등의 메서드를 기계적으로 생성하는 작업을 보이지 않는 곳에서 해준다. => 소스 코드를 깔끔하게 유지할 수 있다.
- 이런 코틀린의 원칙이 잘 드러나는 경우로, 클래스 생성자나 프로퍼티 접근자를 컴파일러가 자동으로 만들어주는 것을 예로 들 수 있다.
4.3.1 모든 클래스가 정의해야 하는 메서드
- 자바와 마찬가지로 모든 코틀린 클래스는 toString, equals, hashCode를 오버라이드 해야 한다.
- 각각이 어떤 메서드인지 살펴보고 코틀린이 어떻게 이런 메서드를 자동으로 생성해줄 수 있는지 알아보자
class Customer(val name: String, val postalCode: Int)
문자열 표현: toString()
- 자바처럼 코틀린의 모든 클래스도 인스턴스의 문자열 표현을 얻을 방법을 제공한다.
- 기본 제공되는 객체의 문자열 표현은 Customer@5e9f23b4(클래스 이름과 객체의 주소를 표현)인데, 이는 그다지 유용하지 않아 이 기본 구현을 바꾸려면 toString 메서드를 오버라이드 해야한다.
class Customer(val name: String, val postalcode: Int) {
override fun toString() = "Customer(name=$name, postalCode=$postalCode)"
}
- 이제 어떤 고객에 대한 문자열 표현은 다음과 같다
fun main(){
val customer1 = Customer("Alice", 342562)
println(customer1)
//Customer(name=Alice, postalCode=342562)
}
객체의 동등성: equals()
- 자바에서 두 객체 간의 == 연산은 두 객체의 참조값이 서로 같은 지를 확인하는 연산, 즉 두 객체 간의 동일성을 확인하는 연산이고, equals() 메서드는 두 객체의 내용이 같은 지를 확인하는 연산, 즉 두 객체 간의 동등성을 확인하는 연산이다.
- 코틀린에서는 자바와 달리 두 객체 간의 == 연산은 equals 메서드를 호출하게 되어 있고, equals 메서드를 오버라이드 하지 않으면 Any 클래스에 정의된 equals 메서드를 호출하는데 이는 === 연산과 동일하게 동작하며 === 연산은 두 객체의 동일성을 확인하므로 equals 메서드를 오버라이드 하지 않은 상태인 클래스의 객체들 간의 == 연산은 두 객체 간의 동일성을 검사하게 된다.
- 따라서 == 연산을 통해 두 객체 간의 동등성을 확인하려면 객체 타입에 해당하는 클래스에 equals 메서드를 동등성을 확인하는 메서드로 오버라이드 해야한다.
val customer1 = Customer("Alice", 342562)
val customer2 = Customer("Alice", 342562)
println(customer1 == customer2)
// false : Customer 클래스에 equals 메서드를 오버라이드 하지 않았기 때문에 동일성을 확인
class Customer(val name: String, val postalCode: Int) {
override fun equals(other: Any?): Boolean {
if(other == null || other !is Customer)
return false
return name == other.name && postalCode == other.postalCode
}
override fun toString() = "Customer(name=$name, postalCode=$postalCode)"
}
fun main(){
val customer1 = Customer("Alice", 342562)
val customer2 = Customer("Alice", 342562)
println(customer1 == customer2)
// true : Customer에 오버라이드한 equals() 메서드로 두 객체 간의 동등성 확인
}
해시 컨테이너: hashCode()
- 자바에서는 equals 메서드를 오버라이드할 때 반드시 hashCode 메서드도 함께 오버라이드해야 한다.
- JVM 언어에서는 equals()가 true를 반환하는 두 객체는 반드시 같은 hashCode()를 반환해야 한다라는 규정이 있다.
fun main() {
val processed = hashSetOf(Customer("오현석", 4122))
println(processed.contain(Customer("오현석", 4122)))
// false
// hashSetOf 메서드 인자로 주어진 객체와 contain 메서드 인자로 주어진 객체 간의
// equals 메서드 연산 결과는 true지만 두 객체의 hashCode가 달라서,
// 즉 Customer 클래스의 hashCode 메서드를 오버라이드 하지 않아서 false 출력
// HashSet은 원소를 비교할 때 비용을 줄이기 위해 먼저 해시 코드를 비교하고
// 해시 코드가 같은 경우에만 실제 값을 비교한다.
}
class Customer(val name: String, val postalCode: Int) {
/* ... */
override fun hashCode(): Int = name.hashCode() * 31 + postalCode
}
4.3.2 데이터 클래스: 모든 클래스가 정의해야 하는 메서드를 자동으로 생성
- 어떤 클래스가 데이터를 저장하는 역할만을 수행한다면 toString, equals, hashCode를 반드시 오버라이드해야 한다.
- data라는 변경자를 클래스 앞에 붙이면 필요한 메서드를 컴파일러가 자동으로 만들어준다.
- data 변경자가 붙은 클래스를 데이터 클래스라고 부른다.
data class Customer(val name: String, val postalCode: Int)
fun main(){
val c1 = Customer("Sam", 11521)
val c2 = Customer("Mart", 15500)
val c3 = Customer("Sam", 11521)
println(c1)
// Customer(name=Sam, postalCode=11521)
println(c1==c2)
// false
println(c1==c3)
// true
println(c1.hashCode())
// 2580770
println(c3.hashCode())
// 2580770
}
데이터 클래스와 copy() 메서드(깊은 복사)
- 데이터 클래스의 모든 프로퍼티를 읽기 전용으로 만들어 데이터 클래스를 불변 클래스(immutable class)로 만들라고 권장
- HashMap 등의 컨테이너에 데이터 클래스 객체를 담는 경우엔 불변성이 필수적(데이터 클래스를 키로 하는 값을 컨테이너에 담은 다음에 키로 쓰인 데이터 객체의 프로퍼티를 변경하면 컨테이너 상태가 잘못될 수 있다. )
- 다중 스레드 프로그램에서 불변 객체를 사용하면 다른 스레드가 현재 사용 중인 데이터를 변경할지 신경 쓸 필요가 없어진다.
- 코틀린 컴파일러는 데이터 클래스 인스턴스를 불변 객체로 더 쉽게 활용할 수 있도록 한 가지 편의 메서드를 제공한다.
- 객체를 복사하면서 일부 프로퍼티를 변경할 수 있게해주는 copy 메서드
fun main(){
val lee = Customer("이계영", 4122)
println(lee.copy(postalCode = 4000))
// Customer(name=이계영, postalCode=4000)
}
- 코틀린 데이터 클래스와 자바 레코드
- 자바 14에 record가 처음 도입
- 개념적으로 레코드는 여러 불변 값으로 이뤄진 그룹을 다룬다는 점에서 코틀린 데이터 클래스와 아주 비슷
- 레코드도 toString, hashCode, equals 등의 메서드를 자동으로 생성해준다.
- 다만 copy와 같은 다른 편의 메서드는 없다
- record의 구조적 제약
- 모든 프로퍼티가 private이며 final
- 레코드는 상위 클래스를 확장할 수 없다
- 클래스 본문 안에서 다른 프로퍼티를 정의할 수 없다.
- 상호 운용성을 위해 코틀린 data class에 @JvmRecord 어노테이션을 추가하면 데이터 클래스도 자바 레코드와 동일한 제약 사항을 지켜야 한다.
- 클래스 위임(class delegation): by 키워드 사용 -> 데코레이터 패턴
- 상속을 허용하지 않는 클래스에서 새로운 동작을 추가해야 할 때가 있다.
- 이 때 사용하는 일반적인 방법이 데코레이터 패턴
- 상속을 허용하지 않는 기존 클래스 대신 사용할 수 있는 새로운 데코레이터 클래스를 만들되,
- 기존 클래스와 같은 인터페이스를 데코레이터가 제공하고
- 기존 클래스를 데코레이터 내부 필드로 유지
- 새로 정의해야 하는 기능은 데코레이터의 메서드로 새로 정의(물론 이 때 기존 클래스의 메서드나 필드를 사용할 수 있다.)
- 기존 기능이 그대로 필요한 부분은 데코레이터의 메서드가 기존 클래스의 메서드에게 요청을 전달
- 데코레이터 패턴을 이용한 상속을 허용하지 않는 클래스에서 새로운 동작 추가 => 준비 코드가 상당히 많이 필요하다
class DecoratingCollection<T> : Collection<T> {
private val innerList = arrayListOf<T>() // ArrayList(final: 상속 불가)
override val size:Int
get() = inner.size
override fun isEmpty(): Boolean = innerList.isEmpty()
override fun contains(element: T): Boolean = innerList.contains(element)
override fun iterator(): Iterator<T> = innerList.iterator()
override fun containsAll(elements: Collection<T>): Boolean =
innerList.containsAll(elements)
}
- 인터페이스를 구현할 때 by 키워드를 통해 그 인터페이스에 대한 구현을 다른 객체에 위임 중이라는 사실을 명시할 수 있다.
class DelegatingCollection<T>(
innerList: Collection<T> = mutableListOf()
): Collection<T> by innerList
- 클래스 안에 있던 모든 메서드 정의가 없어졌다
- 컴파일러가 전달 메서드를 자동으로 생성 -> 자동 생성한 코드의 구현은
DecoratingCollection< T > 클래스에 있던 구현과 비슷하다.
- 메서드 중 일부의 동작을 변경하고 싶은 경우, 메서드를 오버라이드하면 컴파일러가 생성한 메서드 대신 오버라이드한 메서드가 사용된다.
- 기존 클래스의 메서드에 위임하는 기본 구현으로 충분한 메서드는 따로 오버라이드 할 필요가 없다.
class CountingSet<T>(
private val innerSet: MutableCollection<T> = hashSetOf<T>()
) : MutableCollection<T> by innerSet {
var objectsAdded = 0
override fun add(element: T): Boolean {
objectsAdded++
return innerSet.add(element)
}
override fun addAll(elements: Collection<T>): Boolean {
objectsAdded += elements.size
return innerSet.addAll(elements)
}
}
fun main() {
val cset = CountingSet<Int>()
cset.addAll(listOf(1, 1, 2))
}
- CountingSet에 MutableCollection의 구현 방식에 대한 의존 관계가 생기지 않는다.
- 내부 컨테이너인 innerSet이 addAll을 처리할 때 루프를 돌면서 add를 호출할 수도 있지만 최적화를 위해 다른 방식을 선택할 수도 있는 것이다.
4.4 object 키워드: 클래스 선언과 인스턴스 생성을 한꺼번에 하기
- 코틀린에서는 object 키워드가 몇 가지 상황에서 쓰이는데, 모든 경우 클래스를 정의하는 동시에 인스턴스를 생성한다는 공통점이 있다.
- object 키워드를 사용하는 여러 상황
-
- 객체 선언: 싱글턴을 정의하는 한 가지 방법
-
- 동반 객체 : 팩토리 메서드를 담을 때 쓰인다.
-
- 객체 식: 자바의 익명 내부 클래스 대신 쓰인다.
4.4.1 객체 선언: 싱글턴을 쉽게 만들기
- 객체지향 시스템을 설계하다 보면 인스턴스가 하나만 필요한 클래스가 유용한 경우가 많다.
- 자바에서는 보통 클래스의 생성자를 private으로 제한하고 정적인 필드에 그 클래스의 유일한 객체를 저장하는 싱글턴 패턴을 통해 이를 구현
- 객체 선언은 클래스 선언과 그 클래스에 속한 단일 인스턴스의 선언을 합친 선언
object Payroll {
val allEmployees = arrayListOf<Person>()
fun calculateSalary(){
for(person in allEmployees){
/ * ... * /
}
}
}
- 객체 선언은 object 키워드로 시작한다. 객체 선언은 클래스를 정의하고 그 클래스의 인스턴스를 만들어 변수에 저장하는 모든 작업을 한 문장으로 처리
- 클래스와 마찬가지로 객체 선언 안에도 프로퍼티, 메서드, 초기화 블록 등이 들어갈 수 있지만 객체 선언에서는 생성자(주 생성자와 부 생성자 모두)를 쓸 수 없다.
- 싱글턴 객체는 객체 선언문이 있는 위치에서 생성자 호출없이 즉시 만들어진다.
- 객체 선언에 제공하고 싶은 초기 상태가 있다면 그 객체의 본문에서 직접 제공할 필요가 있다.
- Comparator 안에는 데이터를 저장할 필요가 없다.
- 따라서 어떤 클래스에 속한 객체를 비교할 때 사용하는 Comparator는 보통 클래스마다 단 하나씩만 있으면 되기 때문에 Comparator 인스턴스를 만드는 방법으로는 객체 선언이 가장 좋은 방법이다.
object CaseInsensitiveFileComparator : Comparator<File> {
override fun compare(file1: File, file2: File): Int {
return file1.path.compareTo(file2.path, ignoreCase = true)
}
}
fun main(){
println(
CaseInsensitiveFileComparator.compare(File("/User"), File("/user"))
)
}
- 일반 객체를 사용할 수 있는 곳이라면 항상 싱글턴 객체를 사용할 수 있다.
fun main(){
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.compare(p2.name)
}
}
fun main(){
val persons = listOf(Person("Bob"), Person("Alice"))
println(persons.sortedWith(Person.NameComparator))
}
- 코틀린 객체를 자바에서 사용하기
- 코틀린 객체 선언은 유일한 인스턴스에 대한 정적인 필드가 있는 자바 클래스로 컴파일
- 이 때 이 인스턴스 필드의 이름은 항상 INSTANCE다.
CaseInsensitiveFileComparator.INSTANCE.compare(file1, file2)
Person.NameComparator.INSTANCE.compare(person1, person2)
4.4.2 동반 객체: 팩토리 메서드와 정적 멤버가 들어갈 장소
- 코틀린 클래스 안에는 정적인 멤버가 없다. 코틀린은 자바 static 키워드를 지원하지 않는다.
- 대신 최상위 함수와 객체 선언을 활용한다.
- 최상위 함수는 클래스의 비공개(private) 멤버에 접근할 수 없다.
- 비공개 멤버에게 접근해야 하는 팩토리 메서드를 클래스에 선언된 내포 객체 의 멤버 함수로 구현할 수 있다.(내포 객체는 클래스의 비공개 멤버를 호출할 수 있다.)
- 바깥 클래스의 비공개 멤버에 접근하는 멤버를 갖는 내포 객체를 선언 시 companion이라는 특별한 키워드를 붙여줘야 한다.
- companion 키워드가 붙은 내포 객체를 선언 시, 해당 내포 객체를 감싸는 바깥 클래스의 이름을 통해 직접 내포 객체의 멤버에 접근할 수 있다.
- companion 키워드가 붙은 내포 객체를 동반 객체라 한다.
class MyClass {
companion object { // 동반 객체
fun callMe(){
println("Companion object called")
}
}
}
fun main(){
MyClass.callMe();
//Companion object called
}
- 동반 객체는 자신에 대응하는 클래스에 소속된다. 이는 곧 해당 클래스의 인스턴스는 동반 객체의 멤버에 접근이 불가능함을 뜻한다.
- 이것이 자바의 정적 멤버와 코틀린 동반 객체의 멤버간의 차이점이다.
- 자바에선 정적 멤버를 갖는 클래스의 인스턴스에서 해당 클래스의 정적 멤버에 접근이 가능했다.
fun main(){
val myObject = MyClass()
myObject.callMe()
//Error: Unresolved reference: callMe
}
- 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있기 때문에 동반 객체는 바깥쪽 클래스의 private 생성자도 호출할 수 있다. 이로 인해 동반 객체는 팩토리 패턴을 구현하기 가장 적합한 위치가 될 수 있다.
- 부 생성자가 2개 있는 클래스를 동반 객체 안에서 선언되 팩토리 메서드를 사용하는 방식으로 변경해보자
class User {
val nickname: String
constructor(email: String) { // 부 생성자
nickname = email.substringBefore('@')
}
constructor(socialAccountId: Int) { // 부 생성자
nickname = getSocialNetworkName(socialAccountId)
}
}
class User private constructor(val nickname: String) { // 밖에서 인스턴스 생성 불가
companion object{ // 동반 객체 선언
fun newSubscribingUser(email: String) =
User(email.substringBefore('@'))
fun newSocialUser(accountId: Int) =
User(getNameFromSocialNetwork(accountId))
}
}
fun main(){
val subscribingUser = User.newSubscribingUser("bob@email.com")
val socialUser = User.newSocialUser(4)
println(subscribingUser.nickname)
// bob
}
4.4.3 동반 객체를 일반 객체처럼 사용
- 동반 객체는 클래스 안에 정의된 일반 객체다. 따라서 다른 객체 선언처럼 동반 객체에 이름을 붙이거나 , 동반 객체가 인터페이스를 상속하거나, 동반 객체 안에 확장 함수와 프로퍼티를 정의할 수 있다.
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)
// Dmitry
val person2 =Person.fromJSON("""{"name": "Brent"}""")
// Brent
}
- 클래스에는 동반 객체가 하나뿐이므로 이름 지정 여부와 관계없이 항상 클래스 이름을 통해 동반 객체의 멤버에 접근할 수 있다.
- 특별히 이름을 지정하지 않으면 동반 객체의 이름은 자동으로Companion이 된다.
동반 객체에서 인터페이스 구현
- 다른 객체 선언과 마찬가지로 동반 객체도 인터페이스를 구현할 수 있다.
interface JSONFactory<T>{
fun fromJSON(jsonText: String): T
}
class Person(val name: String) {
companion object : JSONFactory<Person> {
override fun fromJSON(jsonText: String): Person = /*...*/
// JSONFactory 인터페이스를 구현하는 동반 객체
}
}
- 추상 팩토리를 통해 엔티티를 적재하는 함수가 있다면 Person 객체를 그 팩토리에 넘길 수 있다.
- 동반 객체가 구현한 JSONFactory의 인스턴스를 넘길 때 동반 객체를 선언한 Perseon 클래스의 이름을 사용
fun <T> loadFromJSON(factory: JSONFactory<T>): T {
/*...*/
}
loadFromJSON(Person) // 동반 객체의 인스턴스를 함수에 전달
- 코틀린 동반 객체와 정적 멤버
- 코틀린 클래스의 동반 객체는 일반 객체와 비슷한 방식으로, 클래스에 정의된 인스턴스를 가리키는 정적 필드로 컴파일된다.
- 동반 객체에 이름을 붙이지 않았다면 자바 측에서 Companion이라는 이름으로 참조에 접근한다.
Person.Companion.fromJSON("...");
- 동반 객체에 이름을 붙였다면 그 이름이 쓰인다.
- 자바에서 사용하기 위해 코틀린 클래스의 멤버를 정적인 멤버로 만들어야 할 필요가 있다.
- @JvmStatic 어노테이션을 코틀린 멤버에 붙이면 된다.
- 정적 필드가 필요하다면 @JvmFiled 어노테이션을 최상위 프로퍼티나 object에 선언된 프로퍼티 앞에 붇인다.
동반 객체 확장
- 동반 객체가 선언된 클래스에서 그 동반 객체에 대한 확장 함수를 정의하려면 동반 객체의 이름을 이용하면 된다.
class Person(val firstName: String, val lastName: String){
companion object {
// 비어있는 동반 객체 선언
}
}
fun Person.Companion.fromJSON(json: String) : Person { // 확장 함수 선언
/*...*/
}
- 다른 보통 확장 함수처럼 fromJSON도 클래스 멤버 함수가 클래스 밖에서 정의한 확장 함수다.
- 동반 객체에 대한 확장 함수를 작성할 수 있으려면 원래 클래스에 동반 객체를 꼭 선언해야 한다.
4.4.4 객체 식: 익명 내부 클래스를 다른 방식으로 작성
- object 키워드는 익명 객체를 정의할 때도 쓰인다.
- 익명 객체는 자바의 익명 내부 클래스를 대신한다.
interface MouseListener {
fun onEnter()
fun onClick()
}
class Button(private val listener: MouseListener) { /*...*/ }
- 객체 식을 사용하면 임의의 MouseListener 구현을 생성해서 Button 생성자에게 넘길 수 있다.
fun main(){
Button(object: MouseListener{ // 객체 식을 사용해 익명 객체를 만든다.
override fun onEnter() { /*...*/ }
override fun onClick() { /*...*/ }
})
}
- 객체 식으로 익명 객체를 만드는 구문은 객체 선언에서와 같은데, 유일한 차이는 객체 식은 객체 이름이 빠진다는 것이다.
- 객체에 이름을 붇여야 한다면 변수에 익명 객체를 대입하면 된다.
val listener = object : MouseListener {
override fun onEnter() { /*...*/ }
override fun onClick() { /*...*/ }
}
- 코틀린 익명 객체는 상당히 유연하다. 익명 객체는 인터페이스를 하나 구현할 수도 있고 여럿을 구현할 수도 있고, 인터페이스를 구현하지 않을 수도 있다.
- 자바의 익명 클래스처럼 객체 식 안의 코드도 그 식이 포함된 함수의 변수에 접근할 수 있다. 하지만 자바와 달리 final이 아닌 변수도 객체 식 안에서 사용할 수 있고 변경할 수 있다.
fun main(){
var clickCount: Int = 0 // 함수의 로컬 변수
Button(object: MouseListener {
override fun onEnter() { /*...*/ }
override fun onClick() {
clickCount++ // 로컬 변수의 값을 변경, 자바에서는 불가
}
})
}
4.5 부가 비용 없이 타입 안전성 추가: 인라인 클래스
- value 키워드를 사용하고 @JvmInline 어노테이션을 붙이면 코틀린 인라인 클래스를 만들 수 있다.
- 인라인 클래스는 타입 안전성을 포기하지 않으면서 불필요하게 객체를 생성하는 비용을 줄일 수 있다. 즉, 불필요한 힙 할당을 방지할 수 있다.
- 실행 시점에 인라인 클래스의 인스턴스는 감싸진 프로퍼티로 대체된다. 따라서 이런 클래스를 인라인 클래스라고 부른다. 즉, 클래스의 데이터가 사용되는 지점에 인라인된다.
- 인라인으로 표시하려면 인라인 클래스가 프로퍼티를 하나만 가져야 하며, 그 프로퍼티는 주 생성자에서 초기화돼야 한다. 인라인 클래스는 클래스 계츠에 참여하지 않는다.
- 즉, 인라인 클래스는 다른 클래스를 상속할 수도 없고, 다른 클래스가 인라인 클래스를 상속할 수도 없다.
- 다만 인터페이스를 상속하거나 메서드를 정의하거나, 계산된 프로퍼티를 제공할 수는 있다.
interface PrettyPrintable {
fun prettyPrint()
}
@JvmInline
value class UsdCent(val amount: Int): PrettyPrintable{ // 인터페이스 상속
val salesTax // 계산된 프로퍼티
get() = amount * 0.06
override fun prettyPrint() = println("${amount}cent")
}
fun main(){
val expense = UsdCent(1_99)
println(expense.salesTax)
// 11.94
expense.prettyPrint()
// 11.94cent
}