해당 글은 Kotlin in Action
도서를 읽으며 정리한 내용입니다
[4장] 클래스, 객체, 인터페이스
클래스 계층 정의
[ 코틀린 인터페이스 ]
- Kotlin은 클래스와 인터페이스에 대해 기본적으로
final
/ public
을 제공
- Kotlin의 인터페이스는 추상 메소드 뿐 아니라 구현이 있는 메소드도 정의 가능
- Java 8의 디폴트 메소드와 유사
- Java의
default
처럼 특별한 키워드를 붙히지 않는다
- Kotlin에서 상속 / 구현시 콜론(
:
)을 통해서 표기한다
- Kotlin에서 오버라이드 시
override
변경자를 통해 함수나 프로퍼티 앞에 지정
- Java와 다르게 override시 변경자를 꼭 사용해야한다
- 여러 인터페이스를 구현시 메소드가 겹친다면 ?
- 어느 쪽도 선택되지 않는다
- 해당 메소드를 오버라이드(
override
) 하지 않으면 컴파일러 오류가 발생
interface Clickable {
fun click()
fun showOff() = println("I'm clikable!")
}
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()
}
}
[ open, final, abstract 변경자 : 기본 final ]
- open 변경자
- Java에서는 final키워드를 통해 클래스의 상속을 명시적으로 금지 했다
- Kotlin에서는 클래스, 메소드에 대해 기본적으로 final이 설정된다
- 이 때, open 변경자를 통해서 클래스, 메소드를 상속받게 할 수 있다
- 취약한 기반 클래스(fragile base class) 문제
- 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로서 깨지는 문제
- 기반 클래스를 작성한 사람의 의도와 다른 방식으로 메소드를 오버라이드하는 위험
- 이러한 문제 예방을 위해, Kotlin에서는 기본적으로 final을 유지
- 상속을 허용하려면 해당 클래스(class)와 메소드(method) 모두에게 open 변경자를 붙혀야 한다
- 주의
- 추상 메서드에 대해서는 기본적으로 open되어 있어서 바로 구현하면 된다
- 즉, 인터페이스(interface)의 경우 모두 기본 open 변경자가 있음을 기억하자
- 그리고 더 이상 오버라이드(override)를 막기 위한 용도로 final을 사용할 수도 있다
open class RichButton : Clickable {
fun disable() {}
open fun animate() {}
override fun click() {}
}
abstract class Animated {
abstract fun animate()
open fun stopAnimating() {}
fun animateTwice() {}
}
- 상속 제어 변경자 정리 (class / method / property)
변경자 | 붙은 멤버 | 설명 |
---|
final | override 불가능 | 클래스 멤버의 기본 변경자 |
open | override 가능 | 명시해야 오버라이드 가능 |
abstract | override 필수 | 추상 클래스 멤버에만 적용 |
override | override 하는중 | override된 멤버는 기본적으로 open |
[ 가시성 변경자 : 기본적으로 public ]
- 가시성 변경자(접근 변경자)
- Java와 다르게
internal
이라는 접근 변경자가 추가됨
- 최상위 선언을 할 때 private을 사용할 수 있다
- Kotlin에서 protected는 해당 클래스 혹은 상속 클래스 에서만 사용 가능
- (중요) 클래스를 확장한 함수에서는 private / protected 멤버를 사용할 수 없다
변경자 | 클래스 멤버 | 최상위 선언 |
---|
public | 모든 곳에서 가능 | 모든 곳에서 가능 |
internal | 같은 모듈 안에서만 가능 | 같은 모듈 안에서만 가능 |
protected | 하위 클래스 안에서만 가능 | (최상위 선언에 적용할 수 X) |
private | 같은 클래스 안에서만 가능 | 같은 파일 안에서만 가능 |
[ 봉인된 클래스 : 클래스 계층 정의시 계층 확장 제한 ]
- sealed 변경자
- 동일 파일에 정의된 하위 클래스 외에 다른 하위 클래스는 존재하지 않는다는 것을 컴파일러에게 알려주는 기능
- when에서 sealed 클래스를 처리할 때에는 불필요한 분기가 사라진다
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)
}
뻔하지 않은 생성자와 프로퍼티
- Kotlin에서는 주 생성자와 / 부 생성자가 존재
- Kotlin에서는
초기화 블록
을 통해서 초기화 로직을 추가할 수 있다
[ 클래스 초기화 : 주 생성자와 초기화 블록 ]
- 주 생성자
- 클래스 이름 뒤에 괄호
()
로 둘러 싸인 형태의 생성자
- 생성자 파라미터를 지정하고, 파라미터로 초기화되는 프로퍼티를 정의한다
- 많이 생략해서 쓰지만,
constructor
라는 키워드를 사용
class User constructor(_nickname: String) {
val niuckname: String
init {
nickname = _nickname
}
}
class User constructor(val nickname: String = "hue") {
...
}
- 상속, 구현을 한 클래스에서 기반 클래스의 생성자를 호출할 수 있다
- 일반 클래스 상속시 값을 넘기지 않는다면, 빈 괄호 필요 ->
User()
- 인터페이스를 구현할 때에는 생성자가 존재하지 않으므로 괄호가 없다 ->
TwitterUser : User
- 추가적으로, 외부에서 생성을 막기위해 private 변경자를 통해 생성자를 막을 수 있다
open class User(val nickname: String ) { ... }
class TwitterUser(nickname: String) : User(nickname) { ... }
class Secreative private constructor() { ... }
[ 부 생성자 : 상위 클래스를 다른 방식으로 초기화 ]
- 부 생성자
- 주 생성자 외에 추가적인 생성자가 필요할 경우 생성
- constructor 키워드를 명시해서 생성
super
키워드를 통해 상위 클래스 생성자를 호출할 수 있다
this
키워드를 통해 자신의 다른 생성자(주
or 부
)를 호출할 수 있다
- 주 생성자와 부 생성자가 둘 다 있는 경우
=> 반드시, 부 생성자에서 주 생성자를 호출해야 한다 (아니면 컴파일 오류 발생)
=> 왜냐하면, 주 생성자에 필수 값을 명시하기 때문에, 부 생성자를 통한 생성시 이것들을 놓치지 않기 위함
- 보통 주 생성자에 default value를 지정해서 부 생성자를 여러개 만들지 않는 것을 권장
class Button(var text: String){
constructor(id: Int, text: String) : this(text){
println(id)
this.text = text
}
}
val b = Button(1, "hue")
println(b.text)
[ 인터페이스(interface) 내부에 있는 프로퍼티(property) 구현 ]
- 필수적으로 override 키워드를 사용해서 생성
- 추상 프로퍼티인 경우
뒷받침하는 필드
나 게터
등의 정보가 없다
- 상태를 저장할 필요가 있다면 하위 클래스에서
상태 저장을 위한 프로퍼티
를 만들어야 한다
(var이나 val로 선언 후 값을 초기화 하면, 뒷받침하는 필드를 생성해서 자동으로 getter가 만들어짐)
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 = "특정 값"
}
val pu = PrivateUser("hue1")
println(pu.nickname)
val su = SubscribingUser("hue2@kakaocorp.com")
println(su.nickname)
val fu = FacebookUser(8)
println(fu.nickname)
[ 뒷받침하는 필드가 없는 경우 ]
- interface 내부에 뒷받침하는 필드가 없는 경우에는 필수적으로 오버라이드 하지 않아도 된다
interface User {
val email: String
val nickname: String
get() = email.substringBefore('@')
}
[ 게터와 세터에서 뒷받침하는 필드에 접근 => $field ]
- 프로퍼티 중, 뒷받침하는 필드를 가지는 경우
field
이름의 변수로 값에 접근할 수 있다
- getter에서는 field값을 읽기만 가능 / setter에서는 field값을 읽거나 쓸 수 있다
- 추가적으로 getter와 setter에 가시성을 변경할 수도 있다
(public
, internal
, protected
, private
)
class User(val name: String){
var address: String = "unspecified"
internal set(value: String) {
println("${field} -> ${value}")
field = value
}
}
val u = User("hue")
u.address = "kakao"
컴파일러가 생성한 메소드 : 데이터 클래스와 클래스 위임
- Kotlin은 equals, hashCode, toString 처럼 기계적인 생성이 필요한 작업을 보이지 않는 곳에서 처리해준다
- 결과적으로 data 키워드를 통한 데이터 클래스를 통해 이러한 메소드 생성을 컴파일러에게 위임할 수 있다
[ toString() ]
- 인스턴스의 문자열 표현을 얻기 위한 메소드
- 주로 디버깅과 로깅시 이 메소드를 사용
class Client(val name: String, val postalCode, Int){
override fun toString() = "name=$name, postalCode=$postalCode"
}
val client1 = Client("hue",1234)
println(client1)
[ equals() / hashCode() ]
- equals() 와 hashCode() 오버라이딩
- 객체를 올바르게 비교하기 위해서는 euqals()와 hashCode() 모두 오버라이딩 해야 한다
- 왜냐하면, JVM 언어에서는 euqals()가 true를 반환하는 두 객체는 반드시 같은 hashCode() 를 반환하는 규칙이 있기 때문
- 즉, 개발자는 직접 정의한 객체에 대해 올바른 비교를 위해 euqals()뿐만 아니라, hashCode()도 오버라이딩을 꼭 해주어야 한다
- == 연산
- Java
- 원시(primitive) 타입인 경우 값을 비교하지만, 참조(reference) 타입은 주소를 비교했다
- 즉, 객체의 값을 비교하기 위해서는 euqals()를 오버라이딩 해서 값을 비교했었다
(이미 있는 참조 객체들은 이미 구현이 되어있고, 직접 정의하는 객체는 직접 작성)
- Kotlin
- == 연산을 하면, 내부적으로 equals를 호출한다
-> 우리는 직접 정의한 객체의 euqals()만 오버라이딩 하면, ==로 객체 비교 가능!
- === 연산을 통해서 참조 비교 가능
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.postalCode)
}
override fun hashCode() : Int = name.hashCode() * 31 + postalCode
}
[ 데이터 클래스 ]
- 앞서 직접 구현한 toString() / equals() / hashCode() 오버라이딩을 컴파일러에게 위임하는 클래스
data
변경자를 클래스 이름 앞에 붙혀 선언
- euqals()와 hashCode()는 주 생성자에 나열된 모든 프로퍼티(property)를 고려해 자동 생성 됨
- 주 생성자 밖에 선언된 프로퍼티는 대상에서 제외된다는 점을 꼭 기억하자!
- 데이터 클래스는 불변 객체인 것을 권장한다
- 불변 객체인 경우 프로그램을 추론하기 쉬워지며
- 스레드가 사용중인 데이터를 변경할 수 없어서, 스레드 동기화 필요가 줄어든다
copy()
메서드
- 코틀린 컴파일러가 데이터 클래스 인스턴스를 불변 객체로 쉽게 활용하기 위해 제공하는 편의 메소드
- 복사본을 만들어 반환하는 메소드
data class Client(val name: String, val postalCode, postalCode)
[ 클래스 위임 : by 키워드 ]
- 상속을 허용하지 않는 클래스에서 새로운 동작을 추가할 때
- 데코레이터(decorator) 패턴 사용
=> 1. 상속을 허용하지 않는 클래스 대신 사용할 수 있는 새로운 클래스를 정의
=> 2. 새로운 클래스가 기존 클래스와 같은 인터페이스를 제공하도록 만든다
(기존 클래스를 데코레이터 내부에 필드로 유지)
=> 3. 새로 정의해야 하는 기능을 데코레이터의 메소드에 새로 정의
=> 4. 기존 기능이 필요하면 데코레이터의 메소드가 기존 클래스의 메소드에게 요청을 전달
- by 키워드 : 기존 기능을 간편하게 위임 가능
- 일일히 기능을 위임하는 메소드를 작성할 필요가 없어진다
- 만약 위임하는 기능 중 변경이 필요하면 override를 통해 대체할 수 있다
class DelegatingCollection<T> (
innterList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList {}
object 키워드 : 클래스 선언과 인스턴스 생성
- object 키워드는 모든 경우 클래스를 정의하면서 동시에 인스턴스를 생성한다
- 객체 선언을 통한 싱글턴(singleton) 정의
- 동반 객체 인스턴스
- 객체 식 (Java의 무명 내부 클래스)
[ 객체 선언 : 싱글턴을 쉽게 만들기 ]
- 코틀린은
객체 선언
을 통해서 싱글턴을 언어에서 기본 지원
- 객체 선언
클래스 선언
+ 단일 인스턴스의 선언
동시에 처리
object
라는 키워드 사용
- 주 / 부 생성자 모두 존재할 수 없다
=> 생성자 호출 없이 즉시 만들어지기 때문!
object Payroll {
val allEmployees = arrayListOf<Person>()
fun calculateSalary() {
...
}
[ 동반 객체 : 팩토리 메소드와 정적 멤버가 들어갈 장소 ]
- Kotlin은 static 키워드를 지원하지 않는다
최상위 함수
/ 객체 선언
2개를 통해 자바의 정적 메소드 역할을 거의 대신할 수 있다
- 하지만,
최상위 함수
는 클래스의 private 멤버에 접근할 수 없다는 한계가 있다
=> 결국 클래스 내부에 객체를 만들어서 이용하게 된다
=> 이 때, 클래스 내부에 객체를 동반 객체
로 만들면, 클래스의 이름으로 바로 접근할 수 있다!
- 동반 객체
companion
와 object
키워드를 통해, 클래스 내부에 동반 객체
를 생성해서 클래스로 접근하는 것
- 필요하다면
companion object {이름}
을 통해 이름을 부여할 수도 있음
(기본으로 Companion
이라는 이름으로 지정됨)
- 즉, Java에서 정적 메소드 호출이나 필드 사용과 동일하게 사용이 가능해 진다
- 팩토리 메서드 패턴을 구현하기에 유용하다
=> 불필요하게 객체를 생성하지 않고 바로 접근해서 사용할 수 있기 때문!
(객체의 생성을 다른 인스턴스를 통해 하는 것)
class A private constructor(val secret: String) {
fun pp() = println("Call A method")
companion object {
fun bar() : A {
return A("hue")
}
}
}
val a = A.bar()
a.pp()
[ 객체 식 : 무명 내부 클래스를 다른 방식으로 작성 ]
- 무명 객체를 정의할 때에도
object
키워드를 사용할 수 있다
- 무명 객체는 싱글톤이 아니다
(객체 식이 쓰일 때 마다 새로운 인스턴스가 생성)
var listener = object : MouseAdapter() {
override fun mouseClicked(e : MouseEvent){ ... }
}
[ 요약 ]
- Kotlin의
인터페이스
도 Java와 비슷하게 디폴트 구현
을 포함할 수 있고, 프로퍼티
도 포함
할 수 있다
모든 Kotlin 선언
은 기본적으로 final
/ public
이다
선언이 final이 되지 않게
하려면, open
을 사용해야 한다
internal
선언은 같은 모듈 안
에서만 볼 수 있다
sealed 클래스
를 통해 계층 확장 제한
을 컴파일러에게 알릴 수 있다
초기화 블록(init)
과 부 생성자(constructor)
를 활용해 클래스 인스턴스
를 더 유연하게 초기화
할 수 있다
field 식별자
를 통해 프로퍼티 접근자(getter / setter)
에서 데이터를 저장
하는 뒷받침하는 필드
를 참조
할 수 있다
data 키워드
로 데이터 클래스
를 사용하면, 컴파일러
가 equals
, hashCode
, toString
, copy
등의 메소드를 자동으로 생성
해준다
by 키워드
를 통한 클래스 위임
은 위임 패턴을 구현할 때 필요한 성가신 코드를 줄일 수 있다
object 키워드
를 사용해 객체 선언
을 하면, 싱글턴 클래스
를 정의할 수 있다
company 키워드
를 통한 동반 객체
는 Java의 정적 메소드
와 필드
정의를 대신
한다
동반 객체
에서도 확장 함수
와 확장 프로퍼티
를 정의할 수 있다
- Kotlin의
객체 식
은 자바의 무명 내부 클래스
를 대신한다
(object 키워드
를 사용하지만, 무명 클래스
의 경우 싱글턴이 X
)