Today I Learned

클래스 계층과 상속 본문

Kotlin/다재다능 코틀린 프로그래밍

클래스 계층과 상속

하이라이터 2023. 8. 24. 17:38
728x90

인터페이스와 추상 클래스 생성

인터페이스 만들기

  • 코틀린의 인터페이스는 추상 메소드를 작성하는 명세에 의한 설계를 할 수 있고, default 키워드 없이 인터페이스 안에 메소드를 구현할 수 있다.
  • 또한 인터페이스 안에 컴패니언 객체를 작성하여 static 메소드를 가질 수 있다.
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 인터페이스를 구현한다는 것을 명시하기 위해서 주 생성자 뒤에 콜론(:)을 사용하고 Remote를 적었다.
  • TVRemote는 Remote의 추상 메소드를 구현해야 하며, 베이스 클래스나 인터페이스의 메소드를 오버라이드할 땐 override 키워드를 사용한다.
val tv = TV()
val remote: Remote = TVRemote(tv)
println("Volume: ${tv.volume}") //Volume:0
remote.up()
println("After incresing: ${tv.volume}") //Volume:1
remote.doubleUp()
println("After doubleUp: ${tv.volume}") //Volume:3
  • Java에서 인터페이스 역시 static 메소드를 가질 수 있지만, 코틀린에서는 static 메소드를 인터페이스 안에 직접 만들 수 없다.
  • 인터페이스에 static 메소드를 작성하기 위해선 컴패니언 객체를 사용해야 한다.
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()
    }
  }
}
  • 두 개의 리모컨을 묶어서 동시에 명령을 내릴 수 있는 combine() 메소드를 만들었다.
  • 컴패니언 객체는 Remote 인터페이스에 바로 작성하고, 컴패니언 객체의 메소드에 접근하기 위해 Remote 인터페이스를 사용한다.
val anotherTV = TV()
val combinedRemote = Remote.combine(remote, TVRemote(anotherTV))
combinedRemote.up()
prinltn(tv.volume) //4
prinltn(anotherTV.volume) //1

 

추상 클래스 생성하기

  • abstract로 추상 클래스를 선언할 수 있으며, 추상 메소드도 abstract라고 표시한다.
abstract class Musician(val name: String, val activeFrom: Int) {
  abstract fun instrumentType() : Stirng
}
class Cellist(name: String, activeFrom: Int): Musician(name, activeFrom) {
  override fun instrumentType() = "String"
}
val ma = Cellist("YO-YO Ma", 1961)
  • instrumentType() 메소드가 베이스 클래스에서 구현되어 있지 않기 때문에 abstract라고 표시되어있다.
  • 자식 클래스에서 오버라이딩할 때 override 키워드가 필요하다.
  • 추상 클래스와 인터페이스의 주된 차이는 다음과 같다.
    • 인터페이스에 정의된 속성엔 백킹 필드가 없다. 인터페이스는 구현 클래스로부터 속성을 얻는 것을 추상 메소드에 의존한다. 반면에 추상 클래스는 백킹 필드를 가진다.
    • 인터페이스는 한 번에 여러 개를 구현할 수 있지만, 클래스는 추상 클래스든지 일반 클래스든지 하나만 확장 가능하다.

 

인터페이스? 추상클래스?

  • 인터페이스는 필드를 가질 수 없지만 클래스에서 여러 개의 인터페이스를 한 번에 구현할 수 있다.
  • 추상 클래스는 필드를 가질 수 있지만 한 번에 하나의 클래스만 확장할 수 있다.
  • 여러 클래스 사이에서 상태를 다시 사용해야 한다면 추상 클래스가 좋은 선택이다. 추상 클래스에서는 공통 상태를 구현할 수 있다. 그리고 클래스에서 구현할 때 추상 클래스가 제공해주는 상태를 재사용하며 메소드를 오버라이드한다.
  • 하나 이상의 명세와 요구 사항을 만족하는 클래스를 원하지만 각각의 클래스들이 각각의 구현을 하는 것을 원한다면 인터페이스가 좋은 선택이다. 인터페이스를 사용하면 클래스들이 각각의 상태를 구현하게 하면서 공통 메소드를 인터페이스에 옮겨놓을 수 있다.

 

중첩 클래스와 내부 클래스

  • 앞선 예제에서 TV가 Remote 인터페이스를 직접 구현하지 않고 TVRemote라는 분리된 클래스에 구현했다. 이에 대한 장단점을 알아보자.
  • 먼저 TV 인스턴스 하나에 여러 개의 TVRemote를 가질 수 있다는 장점이 있다. 또한 TVRemote 인스턴스들은 TV 인스턴스의 상태와 분리된 채로 내부 상태를 가질 수 있다.
  • 반면 Remote 인터페이스를 구현하고 있는 TVRemote 메소드들은 TV의 public 메소드로만 사용해야한다는 단점이 있다. 그리고 TVRemote 내부에 TV의 참조를 추가로 가지고 있어야만 한다.
  • 내부 클래스를 사용하면 장점은 유지하면서 단점은 피할 수 있다.
  • 코틀린의 클래스는 다른 클래스에 중첩될 수 있다. 중첩 클래스는 외부 클래스의 private 멤버에 접근할 수 없지만, inner 키워드를 사용하면 내부 클래스로 변하면서 제약이 사라진다.
class TV {
  private var volume = 0
  val remote: Remote
  get() = TVRemote()
  override fun toString(): String = "Volume: ${volume}"
  inner class TVRemote: Remote {
    override fun up() { tv.volume++ }
    override fun down() { tv.volume-- }
    override fun toString(): String = "Remote: ${this@TV.toString()}"
  }
}

 

  • TV의 volume 속성이 private이기 때문에 TV 인스턴스 외부에서 접근할 수 없지만, TVRemote는 내부 클래스이기 때문에 private 멤버를 포함한 모든 멤버에 직접 접근이 가능하다.
  • 내부 클래스의 속성이나 메소드가 외부 클래스의 멤버와 이름이 일치한다면 특별한 this 표현식을 이용하여 내부 클래스에서 외부 클래스의 멤버에 접근할 수 있다.
  • 중첩 클래스나 내부 클래스에 특별한 상태가 필요하다면 외부 클래스에서 하듯이 속성을 생성하면 된다. 그리고 클래스 안에 내부 클래스를 생성하는 대신에 메소드 안에서 익명 내부 클래스를 생성할 수 있다.
class TV {
  private var volume = 0
  val remote: Remote get() = object: Remote {
    override fun up() { tv.volume++ }
    override fun down() { tv.volume-- }
    override fun toString(): String = "Remote: ${this@TV.toString()}"
  }
  override fun toString(): String = "Volume: ${volume}"
}

 

상속

  • 코틀린에서 클래스가 베이스 클래스로서 사용되게 하려면 명시적인 권한을 제공해야 한다.
  • 이와 유사하게, 메소드를 작성할 때 개발자는 코틀린에게 자식 클래스가 해당 메소드를 오버라이드하는 것이 가능하다는 사실을 알려줘야 한다.
  • 인터페이스와 다르게 코틀린의 클래스는 디폴트가 final이며, 이는 클래스로부터 상속을 받을 수 없다는 뜻이다.
  • open이라고 명시되어있는 클래스로부터만 상속을 받을 수 있으며, 자식클래스에서는 override라고 명시해줘야 한다.
open class Vehicle(val year: Int, oepn var color: String) {
  open val km = 0
  final override fun toString() = "year: $year, Color: $color, KM: $km"
  fun repaint(newColor: String) {
    color = newColor
  }
}
  • 위 클래스에서 open으로 명시된 color 파라미터와 km 속성만 오버라이드할 수 있다.
  • 베이스 클래스인 Any의 toString() 메소드를 오버라이드했지만, final로 더이상 오버라이드하지 못하도록 방지해놨다.
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
  }
}
  • Vehicle 클래스에서 파생된 car 클래스를 작성했다. Car의 생성자의 파라미터들은 Vehicle 클래스로 전달된다.
  • Java와 다르게 코틀린은 implements와 extends를 구분하지 않는다.
val car = Car(2019, "orange")
println(car.year) //2019
println(car.color) //orange
car.drive(10)
println(car) //year:2019, color:orange, KM:10
try {
  car.drive(-30)
} catch(ex: RuntimeException) {
  prinltn(ex.message) //can't set negative value
}
  • Car 클래스를 부모로 자식 클래스를 생성할 수도 있다.
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 클래스는 color의 값을 저장하지 않는 대신, getter와 setter를 모두 오버라이드해서 베이스 클래스의 속성에 값을 가지고 오고, 저장도 한다.
  • Car 클래스가 color를 오버라이드하지 않았기 때문에, FamilyCar 클래스의 color는 Vehicle 클래스의 속성을 사용한다.

 

씰드 클래스

  • 코틀린의 한쪽 극단에는 자식 클래스가 하나도 없는 클래스인 final 클래스가 존재한다.
  • 반대 극단엔 어떤 클래스가 상속을 받았느지는 전혀 알 수 없는 open과 abstract 클래스가 있다.
  • 그 중간 영역으로 클래스를 만들 때 작성자가 지정한 몇몇 클래스에만 상속할 수 있도록 하는 sealed 클래스가 있다.
  • sealed 클래스는 동일한 파일에 작성된 다른 클래스들에 확장이 허용되지만 그 외의 클래스들은 확장할 수 없는 클래스이다.
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 Pipe(suit: String, val number: Int) : Card(suit) {
  init {
    if(numver < 2 || number > 10) {
      throw RuntimeException("pip has to.be between 2 and 10")
    }
  }
}
  • sealed 클래스의 생성자는 private이 표기되지 않았지만 private으로 취급되기 때문에 이 클래스로부터 객체를 인스턴스화 할 수 없다.
  • 하지만 sealed 클래스로부터 상속받은 클래스의 생성자를 private으로 명시하지 않으면 상속받은 클래스를 통해 객체를 생성할 수 있다.
fun process(card: Card) = when (card) {
  is Ace -> "${card.javaClass.name} of ${card.suit}"
  is King, is Queen, is Jack -> "$card"
  is Pip -> "${car.number} of ${card.suit}"
}
  • sealed 클래스의 자식 클래스의 인스턴스 생성은 간단하지만, when 표현식을 사용할 때 else를 사용하면 안된다. when에 sealed 클래스의 자식 클래스가 어떤 타입이어도 속할 수 있는 조건이 있을때 else를 사용하면 else는 절대 사용되지 않는다는 경고가 나타난다.
  • else를 추가하면 나중에 새로운 sealed 클래스가 추가되었을 때 새로운 케이스가 처리되지 않았음을 알리는 컴파일 오류가 나타나지도 않고, 프로그램이 의도되지 않은 코드를 실행하게 될 수도 있다.

 

Enum의 생성과 사용

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"
}
//...

println(process(Ace(Suit.DIAMONDS)))
println(process(Queun(Suit.CLUBS)))
println(process(Pip(Suit.SPADES, 2)))

 

728x90
Comments