5편에 이어서 클래스의 상속과 오버라이드, 오버로드에 대해 살펴보겠습니다.

클래스 상속


상속

우선 상속이라는 것은 부모 클래스를 기준으로 자식 클래스가 부모클래스의 특징을 이어 받는 것입니다.

유전학의 개념으로 보면 부모와 자식이 닮아있다는 경우가 되겠지요.

그래서 이전 포스팅에서 A라는 부모 클래스를 B라는 자식 클래스가 상속받고자 할 때 클래스 A에 open이라는 키워드가 붙었습니다.

open class Base (var firstName:String="길동", var lastName:String="홍", var age:Int=24) {
//class Base {

    /*
    var firstName:String
    var lastName:String
    var age:Int

    constructor(firstName:String, lastName:String, age:Int) {
        this.firstName = firstName
        this.lastName = lastName
        this.age = age
    }
    */

    open val fullName:String
        get() = "$firstName $lastName"

}

class Child(firstName:String, lastName:String, age:Int, var address:String) : Base(firstName, lastName, age) {

    // 또는 var address:String

    override val fullName:String        // 오버라이딩하려면 오버리이드 받을 변수, 클래스, 메서드 모두 open이 들어가야 함.
        get() = "$firstName $age $address"
}

우선 생성자는 주석처리된 부분처럼 멤버변수를 따로 선언해서 constructor를 통해 만들어 줄 수도 있지만 클래스명 옆에 바로 선언해주는 것만으로도 생성자를 만들어준다는 것을 5번째 포스팅에서 살펴봤습니다.

여기에서는 Child라는 클래스가 Base라는 클래스를 상속받고 있으며, Child 클래스만의 멤버변수인 address라는 문자열 변수를 갖습니다.

때문에 Child 클래스에서 생성자로 address를 추가해주었고, 부모 클래스인 Base에서 받아오는 변수 또한 선언해주었습니다.

그리고 부모 클래스의 fullName이라는 변수를 오버라이드 했는데 이 때 부모 클래스는 상속을 해주는 입장이기 때문에 open을 붙였고, 자식 클래스는 상속을 받아 재정의하는 입장이기 때문에 상속받은 변수에 override를 붙여주었습니다.

fullName은 별도의 값을 저장해둔 것이 아니라 get()이라는 것을 보면 알 수 있듯이 getter로 지정되어 있는 것입니다.

또 다른 예시를 살펴봅시다.

open class Bird(var name:String, var wing:Int, var color:String) {

    fun fly() = println("fly wing: $wing")

    override fun toString(): String {
        return "Bird(name='$name', wing=$wing, color='$color')"
    }

    open fun allData() = print("$name $wing $color ")


}

Bird라는 클래스는 open을 보면 알 수 있듯이 다른 클래스에 상속해 줄 부모 클래스입니다.

생성자에는 세개의 변수가 존재하고 클래스 내부에는 두개의 멤버 메서드와 오버라이드된 toString이 있습니다.

이 때 allData라는 open이 붙어 있기 때문에 이 멤버 메서드는 다른 클래스가 상속을 받아 오버라이드 할 수 있는 것이지요.

그래서 아래와 같이 두개의 클래스가 상속을 받습니다.

class Lark(name:String, wing:Int, color:String) : Bird(name, wing, color) {
    fun singHitOne() = println("짹짹")
}

// 이렇게도 상속을 받을 수 있다.
class Parrot : Bird {

    var volume:Int

    constructor(name:String, wing:Int, color:String, volume:Int):super(name, wing, color) {
        this.volume = volume
    }

    override fun toString(): String {
        return super.toString() + "Parrot(volume=$volume)"
    }

    override fun allData() {
        super.allData()
        println("$volume")
    }

}

Lark는 코틀린 문법 대로 생성자를 클래스명 옆에 선언해주었고, Parrot은 자바 문법대로 생성자를 클래스 내부에 만들어주었습니다.

여기에서 Parrot 클래스를 보면 생성자에 super를 사용해주었는데 자바에서 마찬가지로 super는 부모 클래스의 생성자를 호출해줍니다.

다시 말해서 constructor(name:String, wing:Int, color:String, volume:Int):super(name, wing, color)는 부모클래스의 생성자까지 상속받아서 초기화 해주겠다는 의미가 됩니다.

부모 클래스의 allData를 오버라이드해서 부모클래스의 allDatasuper로 호출하였으며 추가적으로 volume 값을 출력하게 해주었습니다.

오버로드

메서드 오버로드는 이름은 갖지만 서로 다른 매개변수를 갖는 여러개의 메서드를 정의하는 것입니다.

어떤 클래스에 다음과 같이 메서드를 선언해주었다고 가정해봅시다.

class Calc {

    fun add(x:Int, y:Int):Int = x + y
    fun add(x:Double, y:Double):Double = x + y
    fun add(x:Int, y:Int, z:Int):Int = x + y + z
    fun add(x:Double, y:Double, z:Double):Double = x + y + z

}

add라는 함수의 이름은 갖지만 매개변수로 받아오는 값이 서로 다릅니다. 이것을 오버로드라고 합니다.

이 클래스를 메인함수에서 객체로 생성하여 아래와 같이 사용할 수 있습니다.

fun main(args: Array<String>) {

	val calc = Calc()
    
	println(cal.add(3, 4))
	println(cal.add(3.12, 4.34))
	println(cal.add(3, 4, 5))
	println(cal.add(3.12, 4.34, 7.54))

}

추상 클래스와 인터페이스


추상 클래스

추상 클래스는 abstract class로 보통의 클래스와 마찬가지로 멤버변수를 가질 수 있고 멤버 메서드도 가질 수 있습니다.

그러나 유일한 차이점은 메서드에서 일반 메서드가 아닌 추상 메서드가 선언되어야 한다는 것이고, 추상 메서드는 return값이 없는 다시 말해서 프로토타입만 선언된 형태여야 합니다.

간단하게 예를 들어보면

abstract class Printer {
   abstract fun print()
   fun method() = println("Printer Method()")
}

class MyPrinter : Printer() {
    override fun print() {
        println("출력합니다!!")
    }
}

이렇게 선언되어 있다고 할 때, MyPrinter 클래스는 추상 클래스인 Printer를 상속받아 추상 클래스의 print메서드를 오버라이딩합니다.

또한 추상 클래스는 자바와 마찬가지로 abstract라는 추상키워드임을 명시하는 키워드를 기록해주어야 한다는 것입니다.

추상클래스를 상속받은 MyPrinter 클래스를 객체로 생성하여 사용해보면

fun main(args: Array<String>) {

	val prt = MyPrinter()
	prt.print()

}

처럼 사용할 수 있습니다.

또한 추상 클래스는 추상 메서드 뿐만 아니라 일반 메서드도 선언하여 사용할 수 있습니다.

abstract class Vehicle(val name:String, val color:String, val weight:Double) {

    abstract var maxSpeed:Double    // 추상 property

    var year:Int = 2019

    abstract fun start()            // 추상 method
    abstract fun stop()

    fun displaySpecs() {
        println("Name: $name, Color: $color, Weight: $weight, Year: $year, MaxSpeed: $maxSpeed")
    }

}

여기에서 displaySpecs 메서드는 위에서 선언한 start, stop 메서드와는 다르게 함수 내부가 이미 지정되어 있으므로 추상 메서드라고 볼 수 없으며 이는 일반 메서드로 보아야 합니다.

이처럼 추상 클래스는 일반 메서드, 추상 메서드, 멤버 변수를 가질 수 있으며 멤버 변수에 값을 할당해 줄 수도 있습니다.

최상위 객체 object 사용하여 상속받기

자바에서 Object라는 최상위 객체가 있었습니다. 이를 사용하면 어떤 객체든 접근할 수 있었습니다.

코틀린에서도 이러한 방법이 허용되는데, 이를 변수에 넣어서 메인함수에서 바로 사용하는 방법도 있습니다.

추상 클래스인 Printer와 이를 상속받는 클래스를 만들어서 변수 myPrinter에 저장해 보겠습니다.

abstract class Printer {
    abstract fun print()
    fun method() = println("Printer Method()")
}

val myPrinter = object : Printer() {
    override fun print() {
        println("myPrinter print()")
    }

}

이렇게 object로 상속받아서 변수에 저장한 것을 메인 함수에서 바로 사용할 수 있습니다.

fun main(args: Array<String>) {

	myPrinter.print()

}

인터페이스

인터페이스는 추상 메서드와는 또 다른 개념입니다.

자바에서의 인터페이스는 추상 메서드만을 포함할 수 있는 형태였으나 코틀린의 인터페이스는 멤버변수를 가질 수 있습니다.

예를 들어서 아래와 같은 인터페이스가 있다고 가정하면

interface Foo {
    var bar:Int
    fun method(str:String)
}

이를 클래스로 구현 했을 때

class CreateFoo(val _bar:Int) : Foo {
    override var bar: Int = _bar

    override fun method(str: String) {
        println("$bar $str")
    }
}

이와 같은 형태를 취하고 내부에 들어있는 변수 bar와 메서드 method는 인터페이스로부터 상속받아 구현된 것이므로 override를 붙여주었습니다.

인터페이스로 다중 상속하기

클래스끼리는 다중 상속을 할 수 없지만 클래스가 인터페이스를 다중 상속 받는 것은 가능합니다.

interface Bird {
    var wings:Int
    fun fly()

    fun jump() {    // 메서드를 정의할 수 있다?
        println("Bird jump")
    }
}

interface Horse {
    var maxSpeed:Int
    fun run()

    fun jump() {    // 메서드를 정의할 수 있다?
        println("Horse jump & maxSpeed ")
    }
}

두개의 인터페이스가 준비되어 있습니다. 각각의 인터페이스에는 하나의 변수와 두개의 메서드가 존재합니다.

두개의 인터페이스를 상속받는 Pegasus 클래스를 만들어보겠습니다.

class Pegasus : Bird, Horse {
    override var wings: Int = 2
    override var maxSpeed: Int = 100

    override fun fly() {
        println("Fly Sky~~")
    }

    override fun run() {
        println("Run~~")
    }

    override fun jump() {
        super<Bird>.jump()      // 그냥 jump를 호출하면 상속받는 인터페이스에 모두 jump가 있어서 에러가 발생하므로 제네릭 타입으로 상위 타입을 지정해주자!
        println("Pegasus Jump~~!!")
    }

}

모든 변수와 메서드는 인터페이스로부터 구현되었으므로 모두 override를 붙여주었습니다.

중요한 사실은 jump라는 메서드에 주목해야 하는데 이 메서드는 두개의 인터페이스에 각각 존재하므로 중복되어 충돌을 발생시킬 수 있습니다.

따라서 상위타입을 지정해야 하는데 super를 붙여 제네릭으로 <Bird>에 있는 메서드를 가져다 쓰는 것으로 지정해주었습니다.

이를 메인 함수에서 사용해보면

fun main(args: Array<String>) {

    val pega = Pegasus()
    pega.fly()
    pega.run()
    pega.jump()

}

이와 같이 사용할 수 있겠습니다.


오버라이드


오버라이드는 앞서 살펴본 것처럼 상속을 받는 클래스(Child)에서 상속을 해주는 클래스(Parent)의 메서드나 변수를 재정의 하기 위해 사용하는 것으로 정리했습니다.

오버라이드의 정의를 알아보기 위한 실습

오버라이드가 과연 어떻게 해서 되는 것인지 실습하기 위해 다음과 같은 클래스를 구상하겠습니다.

우선 Animal이라는 클래스와 Pet이라는 인터페이스의 상속을 받는 CatDog 클래스를 만들어 줍니다.

그 다음으로 Cat 객체나 Dog 객체를 매개변수로 받아서 어떤 문장을 출력해주는 메서드를 가진 Master 클래스를 만들 것입니다.

상속해 줄 클래스와 인터페이스 작성하기

우선 상속해 줄 클래스를 작성해줍니다.

class Animal(val name:String) {}

이 클래스는 멤버변수 하나와 그에 따른 생성자 하나만 존재하기 때문에 위와 같이 지정해줍니다.

이 클래스는 상속해 줄 클래스 이기 때문에 앞에 open을 붙여줍니다.

open class Animal(val name:String) {}

그 다음으로 인터페이스를 작성합니다.

인터페이스 내부에는 category 변수와 메시지를 꺼내 쓸 수 있는getter를 만들어 줄 것이고 species라는 문자열 변수와 메서드 feeding(), patting()을 만들어 줄 것입니다. 이 때 patting은 "Keep patting"을 출력하게 해 줄 것입니다.

interface Pet {
    var category:String
    val msgTags:String
        get() = "I love my pet!"

    var species:String
    fun feeding()
    fun patting() {
        println("Keep patting")
    }
}

상속 받는 클래스 작성하기

Cat, Dog 클래스는 Pet 인터페이스와 Animal 클래스를 상속받습니다.

이 때 Animal 클래스에서 name을 받아와야 합니다. 그리고 Pet 인터페이스에서 변수 species와 메서드 feeding을 구현할 것이기 때문에 오버라이드 해줍니다. 또한 생성자로 인터페이스에 있는 category 또한 구현되어야 하기 때문에 생성자에서 오버라이드 해줍니다.

class Cat(name:String, override var category: String) : Pet, Animal(name) {

    override var species: String = "Cat"
    override fun feeding() {
        println("Feeding Cat")
        println("Cat Name: $name")
    }
}

class Dog(name:String, override var category: String) : Pet, Animal(name) {

    override var species: String = "Dog"
    override fun feeding() {
        println("Feeding Dog")
        println("Dog Name: $name")
    }
}

Master 클래스 작성하기

Master 클래스는 객체로 생성되었을 때 Cat이나 Dog 클래스를 매개변수로 받아서 각 클래스 내부에 선언된 species 변수의 값을 출력해주고, 인터페이스의 feeding이 각 클래스에서 구현된 형태를 보여줄 것입니다.

class Master {

    fun playWithPet(pet:Pet) {
        println(pet.species)
        pet.feeding()
    }
}

이 클래스는 내부에 메서드만 존재하고 이 메서드는 Pet이라는 인터페이스 형태를 매개변수로 받아서 species에 할당된 값과 feeding 메서드의 호출 결과를 보여줍니다.

그래서 main 함수에서 각각의 객체를 생성하고 이를 Master 객체 내부의 메서드에 CatDog객체를 Arguments로 하여 실행해보면

fun main(args: Array<String>) {

    val master = Master()
    val dog = Dog("바둑이", "Small")
    master.playWithPet(dog)

    val cat = Cat("야옹이", "Middle")
    master.playWithPet(cat)

}

이와 같은 결과를 얻을 수 있습니다.

profile
tried ? drinkCoffee : keepGoing;

0개의 댓글