반응형
Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
Tags
- 해시
- 완주하지 못한 선수
- H-index
- @Data
- 롬복 어노테이션
- 스프링 스케쥴러
- K번째수
- 고차원 함수
- @Getter
- 가장 큰 수
- 전화번호 목록
- kubenetes
- 영속 자료구조
- 모던 자바 인 액션
- 정렬
- 알고리즘
- 프로그래머스
- 다리를 지나는 트럭
- 크론 표현식
- Java
- @EnableScheduling
- @Setter
- 기능개발
- 코딩 테스트
- 쿠버네티스
- @configuration
- 커링
- 루씬 인 액션
- 검색 기능 확장
- 스택/큐
Archives
- Today
- Total
Today I Learned
클래스 계층과 상속 본문
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
'Kotlin > 다재다능 코틀린 프로그래밍' 카테고리의 다른 글
델리게이션을 통한 확장 (2) (0) | 2023.09.08 |
---|---|
델리게이션을 통한 확장 (1) (0) | 2023.09.01 |
객체와 클래스 (0) | 2023.08.01 |
오류를 예방하는 타입 안정성 (2) (0) | 2023.08.01 |
오류를 예방하는 타입 안정성(1) (0) | 2023.07.27 |
Comments