몇가지 추상 메소드들로 구성된 인터페이스를 만들어보자!
코틀린 인터페이스는 interface
키워드를 사용해 정의해준다.
interface Remote {
fun up()
fun down()
fun doubleUp() {
up()
up()
}
}
위에서 작성한 인터페이스를 활용해보자! 기본 생성자 옆에 콜론(:)을 붙여 사용할 인터페이스를 명시해준다.
class TV {
var volume = 0
}
class TVRemote(val tv: TV) : Remote {
override fun up() { tv.volume++ }
override fun down() { tv.volume-- }
}
TVRemote 클래스는 Remote 인터페이스에서 정의한 추상메소드를 의무적으로 오버라이딩 해야한다. 하지만 여기서 위에 doubleUp()
메소드는 예외이다 왜냐하면 코틀린의 인터페이스는 추상 메소드뿐만 아니라 default 메소드도 구현 가능하기 때문이다. 여기서 up(), down()
메소드는 추상 메소드로 간주되어 오버라이딩의 의무가 있지만 ``doubleUp()``` 메소드는 default 메소드로 간주되어 오버라이딩의 의무가 없다.
위 Remote 인터페이스를 Java 8에서 나온 default
키워드를 사용해 구현해보자.
public interface Remote {
void up(); // abstract
void down(); // abstract
default void doubleUp() {
up();
up();
}
}
다시 정리해서 사용해보자
class TV {
var volume = 0
}
class TVRemote(val tv: TV) : Remote {
override fun up() { tv.volume++ }
override fun down() { tv.volume-- }
}
val tv = TV()
val remote: Remote = TVRemote(tv) //Remote 인터페이스를 구현한 TVRemote 인스턴스 생성
println("Volume: ${tv.volume}") //Volume: 0
remote.up()
println("After increasing: ${tv.volume}") //After increasing: 1
remote.doubleUp()
println("After doubleUp: ${tv.volume}") //After doubleUp: 3
Java에서 interface는 static 메소드를 가질 수 있지만 코틀린은 아니다. 코틀린에서 인터페이스에 static 메소드를 구현하려면 companion object를 사용해준다. Remote 인터페이스를 상속받아 combine()
메소드를 통해 2개의 Remote
클래스의 연산을 수행해주는 역할을 한다.
companion object {
fun combine(first: Remote, second: Remote): Remote = object: Remote {
override fun up() {
first.up()
second.up()
}
override fun down() {
first.down()
second.down()
}
}
}
// 사용
val tv = TV()
val remote: Remote = TVRemote(tv)
val anotherTV = TV()
val anotherRemote: Remote = TVRemote(anotherTV)
val combinedRemote = Remote.combine(remote, anotherRemote)
combinedRemote.up()
println(tv.volume) //1
println(anotherTV.volume) //1
abstract
키워드를 클래스 앞에 붙여 추상 클래스를 구현할 수 있다.
abstract class Musician(val name: String, val activeFrom: Int) {
abstract fun instrumentType(): String
}
class Cellist(name: String, activeFrom: Int) : Musician(name, activeFrom) {
override fun instrumentType() = "String"
}
val ma = Cellist("Yo-Yo Ma", 1961)
println(ma.name) // Yo-Yo Ma
println(ma.instrumentType()) // String
인터페이스와 추상 클래스의 차이점?
그렇다면 어느 상황에서 적절하게 사용할까? -> 다중 상속 여부에서 갈림
좀 더 효율적인 구조를 위해 inner class를 사용해보자
class TV {
private var volume = 0
val remote: Remote
get() = TVRemote()
override fun toString(): String = "Volume: ${volume}"
// TVRemote를 TV의 inner class로 옮김
inner class TVRemote : Remote {
override fun up() { volume++ } //바로 outer class property 접근 가능
override fun down() { volume-- }
override fun toString() = "Remote: ${this@TV.toString()}"
}
}
val tv = TV()
val remote = tv.remote
println("$tv") //Volume: 0
remote.up()
println("After increasing: $tv") //After increasing: Volume: 1
remote.doubleUp()
println("After doubleUp: $tv") //After doubleUp: Volume: 3
TVRemote inner class를 사용하면 up() down() 메소드에서 자유롭게 바로 TV의 private 프로퍼티에 접근 가능하다. this@
표현식을 사용해 outer class의 멤버에 접근 가능하다.
anonymous inner 클래스를 통해 똑같이 구현 가능하다. inner
키워드가 없다는 점, 클래스명이 없다는 점빼곤 nested class를 사용하는것과 동일하다.
class TV {
private var volume = 0
val remote: Remote get() = object: Remote {
override fun up() { volume++ }
override fun down() { volume-- }
override fun toString() = "Remote: ${this@TV.toString()}"
}
override fun toString(): String = "Volume: ${volume}"
}
코틀린의 클래스와 메소드는 기본적으로 final
이라서 상속이 불가능하다. 따라서 상속이 가능하려면 open
키워드를 사용해야 한다. 또한 부모 클래스의 val 프로퍼티는 자식 클래스에서 val이나 var로 오버라이딩이 가능하지만 부모 클래스의 var 프로퍼티는 자식 클래스에서 val로 오버라이딩이 불가능하다. 일반적으로 val은 getter, var는 getter와 setter가 있다. 따라서 부모클래스에서 var이 자식클래스에서 val로 오버라이드 된다면 부모클래스에서 정의된 setter를 빼주는것이기 때문에 불가능하다.
open class Vehicle(val year: Int, open var color: String) {
open val km = 0
// 상속 가능
final override fun toString() = "year: $year, Color: $color, KM: $km"
// 상속 불가
fun repaint(newColor: String) {
color = newColor
}
}
toString()
메소드를 오버라이딩하고 있다. repaint()
메소드가 있다.
```kotlin
open class Car(year: Int, color: String) : Vehicle(year, color) {
override var km: Int = 0
set(value) {
if (value < 1) {
throw RuntimeException("can't set negative value")
}
field = value
}
fun drive(distance: Int) {
km += distance
}
}
자식 클래스인 Car 생성자의 매개변수는 부모 클래스인 Vehicle로 넘어간다. 여기서 자식 클래스는 부모 클래스의 km
프로퍼티를 오버라이딩 한다. val -> var
이고 커스텀 setter를 구현했다.
val car = Car(2019, "Orange")
println(car.year) // 2019
println(car.color) // Orange
car.drive(10)
println(car) // year: 2019, Color: Orange, KM: 10 (부모 클래스 toString())
try {
car.drive(-30)
} catch(ex: RuntimeException) {
println(ex.message) // can't set negative value (커스텀 setter)
}
이번엔 Car를 상속받는 FamilyCar 클래스를 만들어 보자
class FamilyCar(year: Int, color: String) : Car(year, color) {
override var color: String
get() = super.color
set(value) {
if (value.isEmpty()) {
throw RuntimeException("Color required")
}
super.color = value
}
}
FamilyCar 클래스는 상위 클래스인 Vehicle 클래스의 color 프로퍼티를 getter와 setter로 사용하고 있다.
val familyCar = FamilyCar(2019, "Green")
println(familyCar.color) //Green
try {
familyCar.repaint("") //Custom Setter 발동
} catch(ex: RuntimeException) {
println(ex.message) // Color required
}
오버라이딩을 할때 접근 제한자 주의해야할 점
Sealed class는 같은 파일 내에서 상위 클래스를 상속받는 자식 클래스의 종류를 제한하고 있는 특성을 가진 클래스이다.
sealed class Card(val suit: String)
class Ace(suit: String) : Card(suit)
class King(suit: String) : Card(suit) {
override fun toString() = "King of $suit"
}
class Queen(suit: String) : Card(suit) {
override fun toString() = "Queen of $suit"
}
class Jack(suit: String) : Card(suit) {
override fun toString() = "Jack of $suit"
}
class Pip(suit: String, val number: Int) : Card(suit) {
init {
if (number < 2 || number > 10) {
throw RuntimeException("Pip has to be between 2 and 10")
}
}
}
fun process(card: Card) = when (card) {
is Ace -> "${card.javaClass.name} of ${card.suit}"
is King, is Queen, is Jack -> "$card"
is Pip -> "${card.number} of ${card.suit}"
}
// when() 구문에서 else는 넣지 않는다 -> 새로운 sealed 클래스가 추가되었을때 오류가 생길 수 있다
fun main() {
println(process(Ace("Diamond"))) // Ace of Diamond
println(process(Queen("Clubs"))) // Queen of Clubs
println(process(Pip("Spades", 2))) // 2 of Spades
println(process(Pip("Hearts", 6))) // 6 of Hearts
}
enum class를 사용하면 코드가 단순해지며 가독성이 더 좋아진다.
enum class Suit { CLUBS, DIAMONDS, HEARTS, SPADES }
sealed class Card(val suit: Suit)
class Ace(suit: Suit) : Card(suit)
class King(suit: Suit) : Card(suit) {
override fun toString() = "King of $suit"
}
// UseCardWithEnum.kt
println(process(Ace(Suit.DIAMONDS))) // Ace of DIAMONDS
println(process(Queen(Suit.CLUBS))) // Queen of CLUBS
println(process(Pip(Suit.SPADES, 2))) // 2 of SPADES
println(process(Pip(Suit.HEARTS, 6))) // 6 of HEARTS
Suit.DIAMONDS
는 Suit 클래스 인스턴스의 static
프로퍼티이다.
enum 클래스는 커스터마이징과 iteration이 가능하다.
// iteration
for (suit in Suit.values()) {
println("${suit.name} -- ${suit.ordinal}")
}
"""
CLUBS -- 0
DIAMONDS -- 1
HEARTS -- 2
SPADES -- 3
"""
// Customizing
enum class Suit(val symbol: Char) {
CLUBS('\u2663'),
DIAMONDS('\u2666'),
HEARTS('\u2665') {
override fun display() = "${super.display()} $symbol"
},
SPADES('\u2660');
open fun display() = "$symbol $name"
}
for (suit in Suit.values()) {
println(suit.display())
}
♣ CLUBS
♦ DIAMONDS
♥ HEARTS ♥
♠ SPADES