인터페이스, 상속, 가시성
인터페이스와 구현
package ch08
interface Clickable {
fun click()
}
class Button: Clickable {
override fun click() {
println("Button is clicked.")
}
}
fun main() {
Button().click()
}
Kotlin
복사
•
자바의 implements와 extends는 “:”으로 대체됨 (둘다 공통)
•
인터페이스의 메서드 구현은 override 변경자를 반드시 붙여야 함
다중 인터페이스 구현
•
Clickable과 Focusable이 둘다 showOff()라는 기본구현 메서드가 있는 경우 컴파일 오류
발생
◦
무조건 구현을 변경해야 한다.
open, final, abstract 변경자 : 기본적으로 final
•
무분별한 상속과 오버라이드는 취약한 기반 클래스(fragile base class) 문제를 일으킬 수 있음
◦
Java : 기본적으로 모든 클래스, 메서드는 상속할 수 있고, final class는 상속을 할 수 없음
◦
Kotlin : 기본적으로 모든 클래스, 메서드는 final class(아무 표시가 없어도 final), open class인 경우에만 상속 가능
// 오픈 클래스 : 상속 가능
open class RichButton : Clickable {
// final method : 오버라이드 불가
fun disable() {}
// open method : 오버라이드 가능
open fun animate() {}
// 오버라이드 메서드 : 오버라이드 가능
override fun click() {}
}
Kotlin
복사
public, private, internal, protected : 가시성 변경자
•
자바는 기본적으로 package-private(코틀린에서는 사라짐)
•
코틀린은 기본적으로 public
•
자바와의 차이점
◦
internal : 신규 가시성이며, 모듈(gradle 모듈이나 intellij 프로젝트 세팅에서 보이는 모듈 단위) 안에서만 볼 수 있는 가시성
◦
private : 클래스 내에서 사용하면 자바처럼 해당 클래스에서만 사용 가능, 파일 내에서 사용하면 해당 파일 안에서만 볼 수 있음
봉인된 클래스(sealed class)
•
의도적으로 확장을 제한하여 편의성을 향상시키는 클래스 타입
<sealed class가 없는 경우>
interface Error
class FileError(val fileName: String) : Error
class DatabaseError(val dbmsType: DbmsType) : Error
enum class DbmsType {
MYSQL, MARIA, ORACLE, H2
}
fun getCharacter(error: Error) = when (error) {
is FileError -> "Error is occurred at ${error.fileName}"
is DatabaseError -> "Error on DBMS : ${error.dbmsType}"
else -> throw IllegalArgumentException("Unknown error type")
}
Kotlin
복사
•
when에서 불필요한 else 추가 필요
◦
컴파일러는 Error Interface 구현체에 어떤 것들이 있는지 알 수가 없음
◦
이런 분기가 여러군데에 있는 경우 그리고 새로운 Error Interface 구현체를 생성하는 경우 모든 곳에 신규 클래스를 추가해야 하는데 누락할 경우 심각한 문제가 발생할 수 있음
<sealed class가 있는 경우>
sealed class Error {
class FileError(val fileName: String) : Error()
class DatabaseError(val dbmsType: DbmsType) : Error()
class OsError(val osType: OsType) : Error()
}
enum class OsType {
WINDOWS, MACOS, LINUX
}
enum class DbmsType {
MYSQL, MARIA, ORACLE, H2
}
fun getCharacter(error: Error) = when (error) {
is FileError -> "Error is occurred at ${error.fileName}"
is DatabaseError -> "Error on DBMS : ${error.dbmsType}"
is Error.OsError -> "Error on OS : ${error.osType}"
}
fun main() {
println(getCharacter(Error.OsError(OsType.LINUX)))
}
Kotlin
복사
•
sealed class 내부에 해당 클래스의 구현체를 모두 나열함
•
when 절에서 모든 구현체를 조건에 넣으면 else 구문 불필요
•
신규 구현체 만들 때는 자동으로 컴파일 에러 발생하여 수정이 쉬움
다양한 생성자, 프로퍼티
주 생성자(primary constructor)와 초기화 블록(init block)
// 실제로 가장 많이 활용되는 버전
class User(val userName: String)
Kotlin
복사
•
클래스 이름 뒤에 오는 () 부분이 주 생성자
// 생성자 및 초기화에 가장 명시적인 버전
class User constructor(_userName: String) {
val userName: String
init {
userName = _userName
}
}
Kotlin
복사
•
위 생성자를 가장 명시적인 방법으로 변경한 버전
◦
생성자는 constructor 키워드로 명시적으로 생성
◦
init block에서는 클래스 객체가 생성될 때 생성자에서 받은 값으로 프로퍼티를 초기화 함
// 위 버전을 다소 간소화 한 버전
class User(_userName: String) {
val userName: String = _userName
}
Kotlin
복사
•
constructor 키워드 생략
•
init 블록 대신 프로퍼티에 바로 초기화
•
생성자 파라미터에 val 또는 var가 붙으면
◦
해당 파라미터와 동일한 이름으로 프로퍼티를 생성해준다는 뜻
◦
생성자 파라미터에 val(var)이 없는 것과 있는 것을 혼합하는 것도 가능
// 기본 파라미터 값을 갖는 생성자
data class User(
val userName: String,
val level: Int = 1
)
fun main() {
println(User("Jekyll"))
println(User("Hyde", 10))
}
Kotlin
복사
•
생성자의 파라미터 옆에 기본 값을 적어주면 해당 값이 기본 값이 되어 생략 가능
// java
public class Parent {
private final String familyName;
public Parent(String familyName) {
this.familyName = familyName;
}
}
public class Child extends Parent {
private final String subName;
public Child(String familyName, String subName) {
super(familyName);
this.subName = subName;
}
}
// kotlin
open class Parent(
val familyName: String
)
class Child(
val subName: String,
familyName: String
) : Parent(familyName)
Kotlin
복사
•
확장하는 클래스의 생성자를 호출해줘야 함
•
확장하는 클래스에 프로퍼티 없는 생성자만 있는 경우도 호출이 필요함
부 생성자(secondary constructor)
open class Parent(
val familyName: String
)
class Child : Parent {
private val subName: String
constructor(subName: String) : this(subName, "")
constructor(subName: String, familyName: String) : super(familyName) {
this.subName = subName
}
}
Kotlin
복사
•
클래스 구현부에 constructor 키워드를 활용해서 여러 개의 부 생성자 생성 가능
•
this() : 다른 생성자 호출
•
super() : 베이스 클래스 생성자 호출
Backing field(뒷받침하는 필드)
•
프로퍼티의 getter/setter 를 커스터마이징 하는 방법
class Account {
var balance: Long = 0
set(value) {
if (value < 0) throw IllegalStateException("잔액은 0원 이상만 가능합니다.")
field = value
}
var accountName: String = ""
get() = "계좌이름:$field"
}
fun main() {
val account = Account()
account.balance = 100
account.accountName = "급여계좌"
}
Kotlin
복사
•
프로퍼티 아래에 get(), set(value) 두 개를 활용해서 단순 getter/setter 가 아닌 커스터마이징 가능
•
get(), set() 에서만 사용가능한 키워드
◦
field : backing field에 접근
접근자의 가시성 변경
•
private set을 통해 setter만 감추고 getter만 열어줄 수 있음
package ch11
class Account {
var balance: Long = 0
private set
var accountName: String = ""
get() = "계좌이름:$field"
fun increaseBalance(value: Int) {
if (this.balance + value > 1_000_000) throw IllegalStateException("Balance cannot be over 1 million")
this.balance += value
}
fun decreaseBalance(value: Int) {
if (this.balance - value < 0) throw IllegalStateException("Balance cannot be exception")
this.balance -= value
}
}
fun main() {
val account = Account()
account.increaseBalance(100)
account.decreaseBalance(100)
account.accountName = "급여계좌"
println("${account.accountName} 잔액 : ${account.balance}")
}
Kotlin
복사
•
계좌의 잔액을 수정하는 것과 같이 중요한 수정이 발생하는 필드는 private set 을 통해 함부로 수정되는 것을 방지할 수 있다.
data class
데이터를 저장하는 주 용도로 활용하는 클래스 타입, 다양한 기본 기능을 제공한다.
(lombok의 @Data 와 유사)
data class User(val name: String, val age: Int)
Kotlin
복사
•
클래스 키워드에 data만 붙이면 된다.
•
기본 제공 기능
◦
toString() : Object의 기본 toString() 대신 “User(name=John, age=42)” 와 같은 형식으로 내부의 데이터를 알아보기 쉽게 표현해주는 메서드 제공
◦
equals(), hashCode(): 두 클래스를 기본적인 동일성 검증 대신 동등성 검증으로 확인할 수 있도록 함
object 키워드 : 클래스 선언 + 객체 생성 (싱글턴)
object Family {
val members = mutableListOf<Person>()
}
fun main() {
Family.members.add(Person("snow", true))
}
Kotlin
복사
•
자바에서의 싱글톤과 같은 목적으로 클래스 선언과 객체 생성을 동시에 진행
◦
직접 생성할 필요가 없으며, 별도 인스턴스 생성 불가
◦
싱글톤 패턴과 같이 어플리케이션에 인스턴스가 단 한 개만 필요한 경우에 활용하면 좋다.
•
Comparator의 구현처럼 단 한 번만 사용되는 경우에도 사용 가능
•
자바에서의 Util Class처럼 내부에 static method만 존재하고 별도 인스턴스 생성하지 않는 경우에도 사용 가능
◦
하지만 이런 경우에는 클래스 없이 파일에 바로 함수를 만들어서 사용하는(최상위 함수, top level function)이 더 낫다
•
익명 클래스에도 object 키워드 사용 가능
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { /*...*/ }
override fun mouseEntered(e: MouseEvent) { /*...*/ }
})
Kotlin
복사
companion object : 자바의 static method를 대체하는 용도
class NewChild(
private val subName: String,
familyName: String,
private val age: Int
) : Parent(familyName) {
companion object {
const val MAX_CHILDREN_COUNT = 4
fun ofDefaultAge(
subName: String, familyName: String
): NewChild = NewChild(subName, familyName, 10)
fun ofDefaultAge(
age: Int
): NewChild = NewChild(
"defaultName",
"defaultFamilyName",
10
)
}
}
data class User(val userName: String, val age: Int)
class Knight(val grade: String)
fun main() {
println(User("Jonathan", 24))
}
Kotlin
복사
•
자바에서 static으로 상수 값을 저장하거나 factory 생성자를 만들던 방식을 동일하게 활용 가능