- 클래스와 인터페이스
- 뻔하지 않은 생성자와 프로퍼티
- 데이터 클래스
- 클래스 위임
- object 키워드 사용
코틀린의 클래스 / 인터페이스는 자바의 클래스 / 인터페이스와 다르다
ex) 인터페이스에 프로퍼티 선언이 들어갈 수 있음
코틀린 선언은 기본적으로 final이며 public이다
중첩 클래스는 기본적으로 내부 클래스가 아니다
-> 즉, 코틀린 중첩 클래스에는 외부 클래스에 대한 참조가 없다
ex)
// 중첩 클래스
class OuterClass {
class NestedClass
}
// 내부 클래스
class OuterClass {
inner class InnerClass
}
내부 클래스와 중첩 클래스의 특징
공통점
-> 클래스 내부에 다른 클래스로 정의됩니다.외형적 차이점
-> inner 키워드를 쓰면 내부클래스, 안쓰면 중첩클래스입니다.기능적 차이점
-> 중첩클래스는 외부클래스(여기서는 OuterClass)의 참조를 가지지 않지만 내부클래스는 외부클래스의 인스턴스를 참조를 가집니다.
코틀린에서 클래스 계층을 정의하는 방식과 자바 방식을 비교
코틀린의 가시성과 접근 변경자에 대해 살펴보기
코틀린 가시성/접근 변경자는 자바와 비슷하지만 아무것도 지정하지 않은 경우 기본 가시성은 다름
sealed는 클래스 상속을 제한
// 간단한 인터페이스 선언하기
interface Clickable {
fun click()
}
이 코드는 click()이라는 추상 메소드가 있는 interface를 정의한다
이 interface를 구현하는 모든 비추상 클래스(또는 구체적 클래스)는
click()에 대한 구현을 제공해야 한다!
// 단순한 인터페이스 구현하기
// 클래스 이름뒤에 콜론(:)을 붙임으로써 클래스 확장과 인터페이스 구현을 모두 처리
class Button: Clickable {
override fun click() = println("I was clicked")
}
>>> Button().click()
I was clicked
-> 클래스는 인터페이스를 원하는 만큼 개수 제한 없이 구현할 수 있지만, 클래스는 오직 하나만 확장 가능
override 변경자는 상위 (클래스 / 인터페이스)에 있는 프로퍼티나 메소드를 오버라이드 한다는 표시
-> 실수로 상위 클래스의 메소드를 오버라이드하는 경우를 방지
상위 클래스에 있는 메소드와 시그니처가 같은 메소드를 우연히 하위 클래스에서 선언하는 경우 컴파일이 안되기 때문에 override를 붙이거나 메소드 이름을 바꿔야 함
인터페이스 메소드도 디폴트 구현을 제공할 수 있다
// 인터페이스 안에 본문이 있는 메소드 정의하기
interface Clickable {
fun click() // 일반 메소드 선언
fun showOff() = println("I'm clickable") // 디폴트 구현이 있는 메소드
}
이 인터페이스를 구현하는 클래스는 click()에 대한 구현을 제공해야 함
showOff() 의 경우 새로운 동작을 정의할 수도, 디폴트 구현을 사용할 수도 있음
// 동일한 메소드를 구현하는 다른 인터페이스 정의
interface Focusable {
fun setFocus(b: Boolean) =
println("I ${if (b) "got" else "lost"} focus.")
fun showOff() = println("I'm focusable")
}
-> 클래스가 구현하는 두 상위 인터페이스에 정의된 showOff 구현을 대체할 오버라이딩 메소드를 직접 제공하지 않으면 컴파일러 오류 발생
// 상속한 인터페이스의 메소드 구현 호출하기
class Button: Clickable, Focusable {
override fun click() = println("I was clicked")
// 이름과 시그니처가 같은 멤버 메소드에 대해 둘 이상의 디폴트 구현이
// 있는 경우 인터페이스를 구현하는 하위 클래스에서 명시적으로
// 새로운 구현을 제공해야 한다
// 상위 타입의 이름을 <> 사이에 넣어서 "super"를 지정하면
// 어떤 상위 타입의 멤버 메소드를 호출할지 지정할 수 있다
override fun showOff() {
super<Clickable>.showOff()
super<Focusable>.showOff()
}
}
// 상속된 구현 중 단 하나만 호출해도 된다면 다음과 같이 사용 가능
override fun showOff() = super<Clickable>.showOff()
// 클래스의 인스턴스를 만들고 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
}
↪ Button 클래스는 Focusable 인터페이스 안에 선언된 setFocus의 구현을 자동으로 상속한다
자바에서 코틀린의 메소드가 있는 인터페이스 구현하기
코틀린은 자바6와 호환되게 설계됐다
-> 인터페이스의 디폴트 메소드를 지원 ❌
코틀린은 디폴트 메소드가 있는 인터페이스를 일반 인터페이스와 디폴트 메소드 구현이 정적 메소드로 들어있는 클래스를 조합해 구현한다인터페이스에는 메소드 선언만 들어가며, 인터페이스와 함께 생성되는 클래스에는 모든 디폴트 메소드 구현이 정적 메소드로 들어간다
디폴트 인터페이스가 포함된 코틀린 인터페이스를 자바 클래스에서 상속해 구현하고 싶다면 코틀린에서 메소드 본문을 제공하는 메소드를 포함하는 모든 메소드에 대한 본문을 작성해야 한다
-> 자바에서는 코틀린의 디폴트 메소드 구현에 의존할 수 없다
취약한 기반 클래스
-> 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 깨져버린 경우에 생김
-> 어떤 클래스가 자신을 상속하는 방법에 대해 정확한 규칙을 제공하지 않는다면 코딩한 사람의 의도와는 다른 방식으로 메소드를 오버라이드할 위험 존재💣
어떤 클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야 한다
오버라이드를 허용하고 싶은 메소드/프로퍼티 앞에도
open 변경자를 붙여야 한다
// 열린 메소드를 포함하는 열린 클래스 정의하기
// 클래스는 열려있고, 다른 클래스가 이 클래스를 상속할 수 있다
open class RichButton: Clickable {
// 이 함수는 final. 하위 클래스가 이 메소드 오버라이드 불가능
fun disable() {}
// 이 함수는 열려있다 하위 클래스에서 이 메소드 오버라이드 가능
oepn fun animate() {}
// 이 함수는 ( 상위 클래스에서 선언된 ) 열려있는 메소드를 오버라이드 한다
// 오버라이드한 메소드는 기본적으로 열려있다
override fun click() {}
}
↪ 기반 클래스나 인터페이스의 멤버를 오버라이드하는 경우 그 메소드는 기본적으로 열려있다
↪ 오버라이드하는 메소드의 구현을 하위 클래스에서 오버라이드하지 못하게 금지하려면 오버라이드하는 메소드 앞에 final을 명시해야 한다
// 오버라이드 금지하기
open class RichButton: Clickable {
final override fun click()
}
열린 캐스트와 스마트 캐스트
코틀린에서도 클래스를 abstract로 선언 가능
-> abstract로 선언한 추상 클래스는 인스턴스화할 수 없다
추상 멤버 앞에 open 변경자를 명시할 필요 ❌
// 추상 클래스 정의하기
// 이 클래스는 추상클래스이므로, 인스턴스 만들 수 없다
abstract class Animated {
abstract fun animate()
// 추상 클래스에 속했더라도 비추상 함수는 기본적으로 final 이지만
// 원한다면 open으로 오버라이드 허용 가능
open fun stopAnimatin() {}
fun animateTwice() {}
}
변경자 | 이 변경자가 붙은 멤버는... | 설명 |
---|---|---|
final | 오버라이드할 수 없음 | 클래스 멤버의 기본 변경자 |
open | 오버라이드할 수 있음 | 반드시 open을 명시해야 오버라이드 가능 |
abstract | 반드시 오버라이드해야 함 | 추상 클래스의 멤버에만 이 변경자를 붙일 수 있음. 추상 멤버에는 구현이 있으면 안 된다 |
override | 상위 (클래스/인스턴스)의 멤버를 오버라이드 하는 중 | 오버라이드하는 멤버는 기본적으로 열려있다 하위 클래스의 오버라이드를 금지하려면 final을 명시해야 함 |
가시성 변경자는 코드 기반에 있는 선언에 대한 클래스 외부 접근을 제어한다
-> 어떤 클래스의 구현에 대한 접근을 제한함으로써 그 클래스에 의존하는 외부 코드를 깨지 않고도 클래스 내부 구현을 변경할 수 있다
코틀린은 패키지를 네임스페이스를 관리하기 위한 용도로만 사용
-> 패키지를 가시성 제어에 사용 ❌
변경자 | 클래스 멤버 | 최상위 선언 |
---|---|---|
public(기본 가시성) | 모든 곳에서 볼 수 있다 | 모든 곳에서 볼 수 있다 |
internal | 같은 모듈 안에서만 볼 수 있다 | 같은 모듈 안에서만 볼 수 있다 |
protected | 하위 클래스 안에서만 볼수 있다 | (최상위 선언에 적용할 수 없음) |
private | 같은 클래스 안에서만 볼 수 있다 | 같은 파일 안에서만 볼 수 있다 |
어떤 클래스의 기반 타입 목록에 들어있는 타입이나 제네릭 클래스의 타입 파라미터에 들어있는 타입의 가시성은 그 클래스 자신의 가시성과 같거나 더 높아야 하고, 메소드의 시그니처에 사용된 모든 타입의 가시성은 그 메소드의 가시성과 같거나 더 높아야 한다
protected 멤버는 오직 어떤 클래스나 그 클래스를 상속한 클래스 안에서만 보인다
외부 클래스가 내부/중첩 클래스의 private 멤버에 접근할 수 없다
클래스 안에 다른 클래스 선언 가능
-> 도우미 클래스를 캡슐화하거나 코드 정의를 그 코드 사용하는 곳 가까이에 두고 싶을 때 유용
중첩 클래스
명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한 없음
// 직렬화할 수 있는 상태가 있는 뷰 선언
interface State: Serializable
interface View {
fun getCurrentState(): State
fun restoreState(state: State) { }
}
// 중첩 클래스를 사용해 코틀린에서 View 구현하기
class Button: View {
override fun getCurrentState(): State = ButtonState()
override fun restoreState(state: State) { }
class ButtonState: State { }
}
클래스B 안에 정의된 클래스 A | 자바에서는 | 코틀린에서는 |
---|---|---|
중첩 클래스(바깥쪽 클래스에 대한 참조를 저장하지 않음) | static class A | class A |
내부 클래스(바깥쪽 클래스에 대한 참조를 저장함) | class A | inner class A |
내부 클래스 Inner 안에서 바깥쪽 클래스 Outer의 참조에 접근하려면
this@Outer 라고 써야 함
class Outer {
inner class Inner {
fun getOuterReference(): Outer = this@Outer
}
}
// 인터페이스 구현을 통해 식 표현하기
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 exression")
}
// 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) {
is Expr.Num -> e.eval
is Expr.Sum -> eval(e.right) + eval(e.left)
}
상위 클래스에 sealed 변경자를 붙이면 그 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있다
when 식이 모든 하위 클래스를 검사하므로 별도의 else 분기가 없어도 된다
sealed 클래스는 자동으로 open 변경자
sealed 인터페이스를 정의할 수 없는 이유
sealed interface를 만들 수 있다면 그 interface를 자바 쪽에서 구현하지 못하게 막을 수 있는 수단이 코틀린 컴파일에게 없기 때문
코틀린은 주 생성자와 부 생성자를 구분한다
-> 주 생성자:
클래스를 초기화할 때 주로 사용하는 간략한 생성자, 클래스 본문 밖에서 정의
-> 부 생성자:
클래스 본문 안에서 정의
초기화 블록을 통해 초기화 로직을 추가할 수 있다
class User(val nickname: String)
클래스 이름뒤에 오는 괄호로 둘러싸인 코드를 주 생성자
주 생성자 목적
1. 생성자 파라미터를 지정
2. 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의
class User constructor(_nickname: String) {
val nickname: String
init { // 초기화 블록
nickname = _nickname
}
// 생성자 프로퍼티와 프로퍼티의 이름을 같게 하고
// this.ninkname = nickname 으로 써도 됨
}
-----------------------------------------
// 주 생성자 앞에 별 다른 애노테이션이나 가시성 변경자가 없다면
// contructor를 생략해도 된다
class User(_nickname: String) {
val nickname = _nickname
}
constructor와 init
constructor:
주 생성자나 부 생성자 정의를 시작할 때 사용init:
초기화 블록을 시작
- 초기화 블록에는 클래스의 객체가 만들어질 때 실행될 초기화 코드가 들어감
- 초기화 블록은 주 생성자와 함께 사용
- 주 생성자는 제한적이기 때문에 초기화 블록이 필요
-> 클래스 안에 여러 초기화 블록 선언 가능
클래스에 기반 클래스가 있다면 주 생성자에서 기반 클래스의 생성자를 호출할 필요가 있다
기반 클래스를 초기화 하려면 기반 클래스 이름 뒤에 괄호를 치고 생성자 인자를 넘긴다
open class User(val nickname: String) { }
class TwitterUser(nickname: String): User(nickname) { }
-----------------------------------------------
open class Button // 인자가 없는 디폴트 생성자가 만들어진다
class RadioButton: Button()
인터페이스에는 생성자가 없기 때문에
이름 뒤에 괄호가 붙었는지 살펴보면 기반 클래스 와 인터페이스 구별 가능
ex)
interface Expr
class Num(val value: Int): Expropen class User(val nickname: String)
class TwitterUser(nickname: String): User(nickname)
open class View {
// 부 생성자들
constructor(ctx: Context) {
}
constructor(ctx: Context, attr: AttributeSet) {
}
}
----------------------------------------------------
이 클래스를 확장하면서 똑같이 부생성자를 정의할 수 있다
open class View {
// 부 생성자들
constructor(ctx: Context)
: super(ctx) {
}
constructor(ctx: Context, attr: AttributeSet)
: super(ctx, attr) {
}
}
open class View {
// 이 클래스의 다른 생성자에게 위임한다
constructor(ctx: Context): this(ctx, MY_STYLE) {
}
constructor(ctx: Context, attr: AttributeSet: super(ctx, attr) {
}
}
interface User {
val nickname: String
}
↪ User 인터페이스를 구현하는 클래스가 nickname의 값을 얻을 수 있는 방법을 제공해야 한다
↪ 인터페이스는 아무 상태도 포함할 수 없으므로 상태를 저장할 필요가 없다면 인터페이스를 구현한 하위 클래스에서 상태 저장을 위한 프로퍼티 등을 만들어야 한다
// 인터페이스의 프로퍼티 구현하기
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('@') <- 커스텀 게터
}
// 페이스북 계정의 ID를 저장
class FacebookUser(val accountId: Int): User {
override val nickname = getFacebookName(accountId)
}
>>> println(PrivateUser("test@kotlinlang.org").nickname)
test@kotlinlang.org
>>> println(SubscribingUser("test@kotlinlang.org").nickname)
test
PrivateUser는 주 생성자 안에 프로퍼티를 직접 선언하는 간결한 구문을 사용
-> 이 프로퍼티는 User의 추상 프로퍼티를 구현하고 있으므로 override를 표시 해야 한다
SubscribingUser는 커스터 게터로 nickname 프로퍼티를 설정
-> 이 프로퍼티는 뒷받침하는 필드에 값을 저장 X
매번 이메일 주소에서 별명을 계산해서 반환한다
FacebookUser에서는 초기화 식으로 nickname 값을 초기화
SubscribingUser와 FacebookUser의 nickname 구현 차이
- SubscribingUser는 매번 호출될 때마다 substringBefore를 호출해 계산하는 커스텀 게터를 활용
- FacebookUser는 객체 초기화 시 계산한 데이터를 뒷받침하는 필드에 저장했다가 불러오는 방식을 활용
interface User {
val email: String
val nickname: String
// 프로퍼티에 뒷받침하는 필드가 없다
// 매번 결과를 계산해 돌려준다
get() = email.substringBefore('@')
}
↪ 하위 클래스는 추상 프로퍼티인 email을 반드시 오버라이드해야 한다
↪ nickname은 오버라이드하지 않고 상속할 수 있다
접근자의 본문에서는 field라는 특별한 식별자를 통해 뒷받침하는 필드에 접근할 수 있다
게터에서는 field 값을 읽을 수만 있고,
세터에서는 field 값을 읽거나 쓸 수 있다
변경 가능 프로퍼티의 게터와 세터 중 한쪽만 직접 정의해도 된다
-> 게터는 필드 값을 그냥 반환해주는 뻔한 게터이기 때문에 굳이 직접 정의할 필요 X
뒷받침하는 필드가 있는 프로퍼티 / 없는 프로퍼티의 차이점
컴파일러는 디폴트 접근자 구현을 사용하건 직접 게터/세터를 정의하건 관계없이 게터/세터에서 field를 사용하는 프로퍼티에 대해 뒷받침하는 필드를 생성해준다
field를 사용하지 않는 커스텀 접근자 구현을 정의한다면 뒷받침하는 필드는 존재 X(프로퍼티가 val안 경우에는 게터에 field가 없으면 되지만,
var인 경우엔 게터/세터에 field가 없어야 한다)
접근자의 가시성은 기본적으로 프로퍼티의 가시성과 같다
원한다면 get이나 set 앞에 가시성 변경자를 추가해서 접근자의 가시성을 변경할 수 있다
class LengthCounter {
var counter: Int = 0
private set <- 이 클래스 밖에서 이 프로퍼티의 값을 바꿀 수 없다
fun addWord(word: String) {
counter += word.length
}
}
// Client에 toString() 구현하기
class Client(val name: String, val postlCode: Int) {
override fun toString() =
"Client(name = $name, postalCode = $postalCode)"
}
>>> val client = Client("김승완",2505)
>>> println(client)
Client(name = 김승완, postalCode = 2505)
코틀린에서는 == 연산자가 두 객체를 비교하는 기본적인 방법
== 는 내부적으로 equals를 호출해서 객체를 비교한다
클래스가 equals를 오버라이드하면 == 를 통해 안전하게 클래스의 인스턴스를 비교할 수 있다참조 비교를 위해서는 === 연산자를 사용할 수 있다
hashCode가 지켜야 하는
"equals() 가 true를 반환하는 두 객체는 반드시 같은 hashCode() 를 반환해야 한다" 라는 제약이 존재
hashCode()의 값이 같을 경우에만 동등성 연산이 실행된다
// Client에 hashCode 구현하기
class Client(val name: String, val postalCode: Int) {
...
override fun hashCode(): Int = name.hashCode() * 31 + postalCode
}
//Client를 데이터 클래스로 선언하기
data class Client(val name: String, val postalCode: Int)
자바에서 요구하는 모든 메소드를 포함한다
1. 인스턴스 간 비교를 위한 equals
2. HashMap과 같은 해시 기반 컨테이너에서 키로 사용할 수 있는 hashCode
3. 클래스의 각 필드를 선언 순서대로 표시하는 문자열 표현을 만들어주는 toString
equals와 hashCode는 주 생성자에 나열된 모든 프로퍼티를 고려해 만들어진다
생성된 equals 메소드는 모든 프로퍼티 값의 동등성을 확인
hashCode 메소드는 모든 프로퍼티의 해시 값을 바탕으로 계산한 해시 값을 반환
-> 주 생성자 밖에 정의된 프로퍼티는 equals/hashCode를 계산할 때 고려의 대상이 X
데이터 클래스의 프로퍼티가 val일 필요 X
데이터 클래스의 모든 프로퍼티를 읽기 전용으로 만들어서 데이터 클래스를 불변 클래스로 만들기를 권장
-> HashMap 등의 컨테이너에 데이터 클래스 객체를 담는 경우엔 불변성이 필수적
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("김승완,2505)
Client(name = 김승완, postalCode = 2505)
코틀린은 기본적으로 클래스를 final로 취급한다
모든 클래스를 기본적으로 final로 취급하면 상속을 염두에 두고 open 변경자로 열어둔 클래스만 확장할 수 있다
종종 상속을 허용하지 않는 클래스에 새로운 동작을 추가해야 할 때가 있다
-> 이럴 때 사용하는 일반적인 방법이 데코레이터(Decorator) 패턴이다
데코레이터 패턴:
상속을 허용하지 않는 기존 클래스 대신 사용할 수 있는 새로운 클래스(데코레이터)를 만들되, 기존 클래스와 같은 인터페이스를 데코레이터가 제공하게 만들고, 기존 클래스를 데코레이터 내부에 필드로 유지하는 것입니다. 이 때 새로 정의해야하는 기능은 데코레이터의 메소드에 새로 정의하고 기존 기능이 그대로 필요한 부분은 데코레이터의 메소드가 기존 클래스의 메소드에게 요청을 전달(forwarding)합니다.
-> 단점: 준비 코드가 상당히 많이 필요
인터페이스를 구현할 때 by 키워드를 통해 그 인터페이스에 대한 구현을 다른 객체에 위임 중이라는 사실을 명시할 수 있다
// 기본 틀
interface A { ... }
class B : A { }
val b = B()
// C를 생성하고, A에서 정의하는 B의 모든 메서드를 C에 위임합니다.
class C : A by b
----------------------------------------------
interface Base {
fun print()
}
class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}
class Derived(b: Base) : Base by b
fun main() {
val b = BasImpl(10)
Derived(b).print()
}
----------------------------------------------
// 다음으로는 위임으로 구현된 인터페이스의 멤버를 오버라이딩
interface Base {
fun printMessage()
fun printMessageLine()
}
class BaseImpl(val x: Int) : Base {
override fun printMessage() { print(x) }
override fun printMessageLine() { print(x) }
}
class Derived(b: Base) : Base by b {
override fun printMessage() { print("abc")}
}
fun main() {
val b = BaseImpl(10)
Derived(b).printMessage()
Derived(b).printMessageLine()
}
>>> abc10
----------------------------------------------
주의!
이 방법으로 오버라이딩 된 멤버는 위임된 객체의 멤버에서 호출되는 것이 아닙니다.
해당 멤버는 자체적으로 구현된 인터페이스 멤버에만 접근할 수 있습니다.
interface Base {
val message: String
fun print()
}
class BaseImpl(val x: Int) : Base {
override val message = "BaseImpl: x = $x"
override fun print() { println(message) }
}
class Derived(b: Base) : Base by b {
// This property is not accessed from b's implementation of 'print'
override val message = "Message of Derived"
}
fun main() {
val b = BaseImpl(10)
val derived = Derived(b)
derived.print()
println(derived.message)
}
>>> BaseImpl: x = 10
Message of Derived
상속과 델리게이션 중 어떤 것을 선택해야 할 지
- 클래스의 객체가 다른 클래스의 객체가 들어갈 자리에 쓰여야 한다면 상속을 사용해라
- 클래스의 객체가 단순히 다른 클래스의 객체를 사용만 해야 한다면 델리게이션을 사용해라
싱글턴 패턴
객체의 인스턴스를 1개만 생성하여 계속 재사용하는 패턴
객체 선언
클래스 선언과 그 클래스에 속한 단일 인스턴스의 선언을 합친 선언
ex)
object Payroll {
val allEmployees = arrayListOf<Person> ()
fun calculateSalary() {
for (person in allEmployees) {
...
}
}
}
객체 선언은 object 키워드로 시작
-> 클래스를 정의하고 클래스의 인스턴스를 만들어서 변수에 저장하는 모든 작업을 단 한 문장으로 처리
생성자(주/부 생성자)는 객체 선언에 쓸 수 없다
일반 클래스 인스턴스와 달리 싱글턴 객체는 객체 선언문이 있는 위치에서 생성자 호출 없이 즉시 만들어진다
-> 객체 선언에는 생성자 정의가 필요 X
변수와 마찬가지로 객체 선언에 사용한 이름 뒤에 .를 붙이면
객체에 속한 메소드나 프로퍼티에 접근할 수 있다
객체 선언도 클래스나 인터페이스 상속 가능
-> 인터페이스 구현에 다른 상태가 필요하지 않을 경우에 유용
클래스 안에서 객체를 선언 가능
-> 그런 객체도 인스턴스는 단 하나
코틀린 클래스 안에는 정적인 멤버가 없다
-> 자바 static 키워드를 지원 X
-> 코틀린에서는 패키지 수준의 최상위 함수와 객체 선언을 활용
클래스 안에 정의된 객체 중 하나에 companion 을 붙이면 그 클래스의 동반 객체로 만들 수 있다
-> 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다
팩토리 메소드는 선언된 클래스의 하위 클래스 객체를 반환할 수 도 있다
생성할 필요가 없는 객체를 생성하지 않을 수도 있다
동반 객체는 클래스 안에 정의된 일반 객체이다
-> 동반 객체에 이름을 붙이거나,인터페이스를 상속하거나, 동반 객체 안에 확장 함수와 프로퍼티를 정의할 수 있다
특별히 이름을 지정하지 않으면 Companion이 된다
클래스에 동반 객체가 있으면 그 객체 안에 함수를 정의함으로써 클래스에 대해 호출할 수 있는 확장 함수를 만들 수 있다
-> c라는 클래스 안에 동반 객체가 있고 그 동반 객체(c.Companion) 안에 func를 정의하면 외부에서는 func()를 c.func()로 호출할 수 있다
동반 객체에 대한 확장 함수를 작성할 수 있으려면 원래 클래스에 동반 객체를 꼭 선언 해야 한다
-> 빈 객체라도 동반 객체가 꼭 있어야 한다
무명 객체
익명 클래스로부터 생성되는 객체를 뜻한다
익명 클래스는 다른 클래스들과 달리 이름을 가지지 않는 클래스이다
-> 이름을 가지지 않는 익명 클래스로부터 무명 객체를 생성할 수 있다
보통 함수를 호출하면서 인자로 무명 객체를 넘기기 때문에 클래스와 인스턴스 모두 이름이 필요하지 않다
-> 객체에 이름을 붙여야 한다면 변수에 무명 객체를 대입하면 된다
코틀린 무명 클래스
여러 인터페이스를 구현하거나 클래스를 확장하면서 인터페이스를 구현할 수 있다
객체 선언과 달리 무명 객체는 싱글턴이 X
객체 식이 쓰일 때마다 새로운 인스턴스가 생성된다
코틀린의 인터페이스는 디폴트 구현을 포함할 수 있고, 프로퍼티도 포함할 수 있다
모든 코틀린 선언은 기본적으로 final이며 public이다
선언이 final이 되지 않게 만들려면 open을 붙여야 한다
internal 선언은 같은 모듈 안에서만 볼 수 있다
중첩 클래스는 기본적으로 내부 클래스가 아니다
바깥쪽 클래스에 대한 참조를 중첩 클래스 안에 포함시키려면 inner 키워드를 중첩 클래스 선언 앞에 붙여서 내부 클래스로 만들어야 한다
sealed 클래스를 상속하는 클래스를 정의하려면 반드시 부모 클래스 정의 안에 중첩 클래스로 정의해야 한다
초기화 블록, 부 생성자를 통해 클래스 인스턴스를 더 유연하게 초기화 가능하다
🔥field 식별자를 통해 프로퍼티 접근자 안에서 프로퍼티의 데이터를 저장하는 데 쓰이는 뒷받침하는 필드를 참조할 수 있다
데이터 클래스를 사용하면 컴파일러가 equals, toString, hashCode 등의 메소드를 자동으로 생성해준다
🔥클래스 위임을 사용하면 위임 패턴을 구현할 때 필요한 수많은 성가신 준비 코드를 줄일 수 있다
🔥객체 선언을 사용하면 코틀린답게 싱글턴 클래스를 정의할 수 있다
🔥동반 객체는 자바의 정적 메소드와 필드 정의를 대신한다
🔥동반 객체도 다른 객체와 마찬가지로 인터페이스를 구현할 수 있다
외부에서 동반 객체에 대한 확장 함수와 프로퍼티를 정의할 수 있다
🔥코틀린 객체 식은 자바의 무명 내부 클래스를 대신한다
코틀린 객체 식은 여러 인스턴스를 구현하거나 객체가 포함된 영역에 있는 변수의 값을 변경할 수 있는 등 자바 무명 내부 클래스보다 더 많은 기능을 제공한다
kotlin class에 정의되어야 하는 equals, hashCode, toString - kotlinworld ( Click )
kotlin의 팩토리 메소드에 대한 이해 ( Click )
kotlin의 팩토리 메소드에 대한 이해2 - kotlinworld ( Click )