"황영덕 저자"의 코틀린 프로그래밍을 읽고 정리한 글입니다.
클래스
를 더욱 구조화
하기 위해서는 특수한 목적을 가진 여러 클래스를 정의해야 한다. 오늘은 그런 목적을 가진 클래스를 살펴보려고 한다.
추상 클래스
는 대략적인 설계의 명세와 공통의 기능을 구현한 클래스이다. 즉, 구체적이지 않은 것이다. 추상 클래스
를 상속하는 하위 클래스
는 추상 클래스의 내용을 더 구체화 해야 한다.
오잉? 그럼 추상 클래스랑 인터페이스는 같은 거 아냐? 🤔
엄밀히 말하면 다르다. 인터페이스
역시 대략적인 설계 명세를 구현하고 인터페이스
를 상속하는 하위 클래스
에서 이를 구체화하는 것은 동일하다. 하지만 인터페이스에서는 프로퍼티의 상태 정보를 저장할 수 없다.
다시 말하면 인터페이스
에서는 프로퍼티의 초기화
가 불가능하다는 것이다.
interface Vehicle {
val name : String
val color : String
val weight : Double
}
interface Pet {
val name : String = "puppy" // 불가능!
}
Pet Interface
의 경우 정의와 동시에 초기화까지 해주고 있는데 인터페이스
에선 불가능하다.
추상 클래스
는 위에서 말한 것처럼 구체화되지 않은 클래스이기 때문에 일반적인 객체를 생성하는 방법으로 인스턴스화될 수 없다. 일단 한번 정의해보자. 우선 추상 클래스
를 정의하기 위해선 abstract
라는 키워드를 사용해야 한다. 또한 클래스 내에서의 프로퍼티나 메소드도 abstract로 선언할 수 있다. 이를 상속받는 클래스에서 구체화하겠다는 의미가 된다. 하지만 이를 사용하기 위해선 해당 클래스가 추상 클래스가 되어야한다.
// abstract로 정의한 추상 클래스이다. 주생성자를 사용했다.
abstract class Vehicle(val name : String, val color : String, val weight : Double) {
// abstract로 정의한 추상 프로퍼티이므로 하위 클래스에서 반드시 재정의해야한다.
abstract var maxSpeed : Double
// 초기값을 갖는 일반 프로퍼티 (인터페이스에서는 불가능)
var year = "2021"
// abstract로 정의한 추상 메소드이므로 하위 클래스에서 반드시 재정의해야한다.
abstract fun start()
abstract fun stop()
fun displaySpecs() {
println("Name : $name, Color : $color, Weight : $weight, Year : $year, MaxSpeed : $maxSpeed")
}
}
Vehicle 클래스
는 abstract로 정의한 추상 클래스이므로 기본적인 설계만 정의하고 있다. abstract
를 사용한 maxSpeed, start(), stop()은 반드시 하위 클래스에서 재정의 해줘야한다.
여기서 잠깐!
클래스를 상속하기 위해선 부모 클래스를 open 키워드로 정의해야 하는데 추상 클래스에서도 해줘야 하나요? 🤔
이미 코드를 통해 느꼈겠지만, 추상 클래스
에서는 open 키워드
를 사용하지 않아도 된다.
추상 프로퍼티
나 추상 메소드
도 마찬가지다!
위에서 정의한 Vehicle 클래스를 상속해보자.
class Car(name : String, color : String, weight : Double, override var maxSpeed : Double) : Vehicle(name, color, weight) {
override fun start() {
// 재정의
println("Car start")
}
override fun stop() {
// 재정의
println("Car Stop")
}
}
class Bicycle(name : String, color : String, weight : Double, override var maxSpeed : Double) : Vehicle(name, color, weight) {
override fun start() {
// 재정의
println("Bicycle start")
}
override fun stop() {
// 재정의
println("Bicycle Stop")
}
}
fun main() {
val car = Car("Matiz", "Yellow", 1000, 150)
val bicycle = Bicycle("Bike", "Red", 150, 100)
// 새로운 값 할당
car.year = "2020"
car.displaySpec()
car.start()
bicycle.displaySpec()
bicycle.start()
displaySpec
은 추상 클래스
가 갖고 있던 일반 메소드
이다. start
와 stop
은 추상클래스
를 상속받은 자식 클래스
에서 오버라이딩한 메소드
이다. 추상 클래스에서 abstract로 정의한 프로퍼티나 메소드들은 자식 클래스에서 반드시 재정의되어야한다.
그렇다면 혹시, 추상 클래스를 상속받는 하위 클래스를 정의하지 않고도 추상 클래스를 사용할 수 있을까? 🤔
사용할 수 있다! object
키워드를 사용하면 된다. (object 이 친구 만능이네...?)
abstract class Printer {
abstract fun print()
}
val myPrinter = object : Printer() {
override fun print() {
println("print 메소드가 재정의되었습니다")
}
}
fun main() {
myPrinter.print()
}
코틀린
에서는 다른 언어와 다르게 메소드
에 구현 내용을 포함할 수 있다.
interface Pet {
var category : String // 추상 프로퍼티
fun feeding() // 추상 메소드
fun patting() { // 구현부를 포함할 수 있다. 구현부를 포함하면 일반 메소드
println("Keep patting")
}
}
추상클래스
와 다르게 abstract
키워드를 사용하지 않는다.
또한 앞서 말한 것처럼 상태를 저장할 수가 없으므로 프로퍼티에 기본 값을 가질 수가 없다.
class Cat(override var category : String) : Pet { // 주생성자를 이용
override fun feeding() {
println("Feeding 메소드가 구현되었습니다.")
}
}
fun main() {
val obj = Cat("Small")
obj.feeding() // 구현된 메소드
obj.patting()// 일반 메소드
}
아니 선생님, 인터페이스에서는 프로퍼티에 값을 저장할 수 없다고 하셨잖아요 😥
하지만 예외가 있다. val
로 선언한 프로퍼티
는 게터
를 통해서 필요한 내용을 구현할 수 있다.
interface Pet {
var category : String // 추상 프로퍼티
val message : String // val로 선언하면 게터의 구현이 가능하다.
get() = "I'm cutty"
fun feeding() // 추상 메소드
fun patting() { // 구현부를 포함할 수 있다. 구현부를 포함하면 일반 메소드
println("Keep patting")
}
}
게터
가 사용 가능하지만 그렇다고 보조필드
가 사용가능한 것은 아니다.
인퍼페이스를 사용하면 뭐가 좋을까? 일단 코드의 재사용성이 올라간다. 한마디로 간결해진다는 점이다. 또 인터페이스를 이용해서 클래스 간의 의존성을 제거할 수 있다. 뿐만 아니라 다중 상속도 가능하다. 코틀린에서는 자바와 다르게 클래스에서 부모 클래스를 상속할 때는 1개의 클래스만 가능하다. 하지만 인터페이스를 활용하면 다중 상속이 가능해진다!
아래는 두 개의 인터페이스를 통해 다중 상속을 구현한 코드이다.
interface Bird {
val wings : Int
fun fly()
fun jump() { // 구현된 일반 메소드
println("Bird jump")
}
}
interface Horse {
val maxSpeed : Int
fun run()
fun jump() { // 구현된 일반 메소드
println("jump!, max speed : $maxSpeed")
}
}
class Pegasus : Bird, Horse {
override val wings : Int = 2
override val maxSpeed : Int = 100
override fun fly() {
println("The Pegasus Fly!")
}
override fun run() {
println("The Pegasus Run!")
}
override fun jump() {
super<Horse>.jump() {
println("The Pegasus jump")
}
}
}
구현부가 있는 메소드의 경우 필요에 따라 오버라이딩을 진행하면 된다. 만약 인터페이스에서 구현한 메소드의 이름이 같은 경우 super<인터페이스 이름>.메소드 명
을 통해 구분할 수 있다.