educative - kotlin - 8

Class Hierarchies and Inheritance

Creating interfaces

몇가지 추상 메소드들로 구성된 인터페이스를 만들어보자!

코틀린 인터페이스는 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

Creating abstract classes

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

인터페이스와 추상 클래스의 차이점?

  • 인터페이스에서 정의된 프로퍼티들은 field가 없다. 따라서 추상 메소드를 통해 프로퍼티에 접근을 해야한다. 하지만 추상 클래스는 가능하다
  • 인터페이스는 다중 상속이 가능하지만 추상 클래스는 불가능하다.

그렇다면 어느 상황에서 적절하게 사용할까? -> 다중 상속 여부에서 갈림

  • 여러개의 클래스에서 사용해야 한다면 추상 클래스 -> 1개만 만들어서 여러 클래스에 뿌리면 되니까
  • 여러 클래스를 상속받아서 구현해야한다면 -> 당연히 다중상속이 가능한 인터페이스

Nested and Inner Classes

좀 더 효율적인 구조를 위해 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 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}" 
}

Inheritance

코틀린의 클래스와 메소드는 기본적으로 final이라서 상속이 불가능하다. 따라서 상속이 가능하려면 open 키워드를 사용해야 한다. 또한 부모 클래스의 val 프로퍼티는 자식 클래스에서 val이나 var로 오버라이딩이 가능하지만 부모 클래스의 var 프로퍼티는 자식 클래스에서 val로 오버라이딩이 불가능하다. 일반적으로 val은 getter, var는 getter와 setter가 있다. 따라서 부모클래스에서 var이 자식클래스에서 val로 오버라이드 된다면 부모클래스에서 정의된 setter를 빼주는것이기 때문에 불가능하다.

Creating a base class

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
  }
}
  • base class의 첫번째 프로퍼티는 오버라이딩이 불가능한 val이고 두번째 프로퍼티는 오버라이딩이 가능한 var 프로퍼티이다.
  • 상위 클래스인 any에서 정의된 toString() 메소드를 오버라이딩하고 있다.
  • 상속을 금지한 repaint() 메소드가 있다.

Creating a derived class


```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)
}

Extending the class

이번엔 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
}

오버라이딩을 할때 접근 제한자 주의해야할 점

  • public -> private or protected (0)
  • protected -> public (x)

Sealed class

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
}
  • sealed class는 자동으로 open 변경자가 붙는다 -> 자동으로 상속 가능
  • private 생성자를 가지고 있기 때문에 객체를 직접 생성할 수 없다
  • 같은 파일내에서 상속받아 하위 클래스를 구현할 수 있다. -> 다른 파일에서 sealed class 상속 시도 -> compile 에러

enum classes

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 프로퍼티이다.

Customizing enums

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

좋은 웹페이지 즐겨찾기