[HeadFirst] Kotlin 인터페이스

timothy jeong·2021년 10월 25일
0

코틀린

목록 보기
8/20

인터페이스 (Interface)

Car 와 Animal 은 개념적으로 상속관계일 수 없다. 하지만 움직인다 는 공통된 행동(메서드)를 가지고 있다. 따라서 중복되는 코드를 줄이기 위해 Animal 과 Car 의 행동을 묶고 싶은데, 상속 없이 어떻게 할 수 있을까?

인터페이스는 상속체계에서 벗어나서 공통된 행위(Common behavior)를 묶어서 관리할 수 있도록 도와준다.

인터페이스와 추상클래스

인터페이스는 인스턴스화 시킬 수 없다는 점에서 추상 클래스와 비슷하다. 하지만 하나의 클래스가 여러개의 인터페이스를 구현할 수 있지만, 추상 클래스는 상속체계에 묶여있으므로, 오로지 하나의 superClass 로 상속 되어야 한다는 차이점이 존재한다.

인터페이스 만들기

interface Movable {

    fun move()
    fun concreteFun() {
        println("this is concrete function")
    }
}

인터페이스의 메서드는 추상화 되거나, 구현될 수 있다. body 가 구현되지 않은 메서드는 컴파일러가 추상 메서드로 인식하기 때문에 굳이 abstract 키워드를 붙일 필요는 없다. 반대의 구현된 메서드의 경우도 별다른 키워드가 필요하지 않다.

interface Movable {

    val velocity: Int
        get() = 20
        set(value) {
            println("Unable to update velocity")
        }
}

추상 클래스와는 다르게 인터페이스는 생성자(Constructor)를 가질 수 없다. 그러므로 인터페이스 body 안에 변수를 선언하는 것이 유일한 방법인데, 인터페이스 프로퍼티는 어떠한 상태를 가질 수 없으므로 변수를 특정 값으로 초기화 할 수 없다. 다만, custom getter 를 만들어서 값을 넣어줄 수는 있다.

또한 field 키워드를 사용할 수 없으므로 진정한 의미의 값을 지정하는 custom setter 를 만들 수 없다. 다만, field 를 호출하지 않는 custom setter 라면 만드는게 가능하다.

인터페이스의 구현

인터페이스를 구현하는 것은 추상 클래스를 상속하는 것과 비슷하다. 다만 인터페이스는 생성자가 없으므로 () 를 표현해줄 필요가 없다.


abstract class Car : Movable {
    override fun move() {
        println("The Car is moving")
    }
}

abstract class Animal() : Movable {
    abstract val image: String
    abstract val food: String
    abstract val habitat: String
    var hunger = 10

    abstract fun eat()

    override fun move() {
        println("The Animal is moving")
    }
}

만약 2개 이상의 인터페이스를 구현한다면 class A : a,b,c {...} 이런식으로 쓰면 된다.

인터페이스와 다형성

Car 의 subClass 도 구성해보자.

interface Movable {

    fun move()
}

abstract class Animal : Movable {
    abstract val image: String
    abstract val food: String
    abstract val habitat: String
    var hunger = 10

    abstract fun eat()

    override fun move() {
        println("The Animals.Animal is moving")
    }
}

abstract class Car: Movable {
    abstract val image: String
    abstract val capacity: Int

    override fun move() {
        println("The Cars.Car is moving")
    }
}

class Hippo : Animal() {
    override val image = "hippo.jpg"
    override val food = "grace"
    override val habitat = "water"

    init {
        hunger = 11
    }

    override fun eat() {
        println("Animals.Hippo eats $food")
    }
}

class Wolf : Animal() {
    override val image = "wolf.jpg"
    override val food = "meat"
    override val habitat = "land"

    init {
        hunger = 8
    }

    override fun eat() {
        println("wolf eats $food")
    }
}

class Hyundai : Car() {
    override val image = "hyundai_car.jpg"
    override val capacity = 4
}

class Kia : Car() {
    override val image = "kia_car.jpg"
    override val capacity = 4
}

그 다음에 mian 문을 다음과 같이 작성해보자.

fun main() {

    val movables = arrayOf(Hippo(), Wolf(), Hyundai(), Kia())
    for (item in movables) {
        item.move()
        // item.eat()
    }

    for (item in movables) {
        item.move()
        if (item is Animal) {
            item.eat()
        }
    }
}

array는 원소로 하나의 type 만 가질 수 있다. 그러므로 movables array 는 하나의 통일된 type 으로 원소들을 인식해야하는데, 주어진 클래스를 공통으로 관통하는 하나의 tpye 은 movable 인터페이스이다. 따라서 movable 을 type 으로 갖는 array 가 된다.

해당 인터페이스에 정의된 메서드는 move 가 유일하기 때문에 item.move() 에는 접근할 수 있지만, item.eat() 에는 접근할 수 없다.

is 연산자

형변환 파트에 이어서 is 연산자가 다시 등장했다. is 연산자를 사용하면 컴파일 단계에서 주어진 객체와 연관된(상속, 구현관계)객체 들을 모두 컴파일러가 알게된다. 그리고 is 가 유효한 범위 내에서 주어진 변수는 smart cast 된 것으로 취급된다.

is 연산자는 연관된 type 을 검사하기 때문에 하위 type, 상위 type 모두 컴파일러가 검사한다.

하지만 항상 smart cast 가 성공적으로 되는 것은 아니다. is 연산자는 클래스 내부에 있는 var 변수를 smart cast 할 수 없다.

calss myMovable {
    var r: moveable = Wolf()
    
    fun myFun() {
        if (r is Wolf) {
            r.eat() // error
       }

이는 var 변수가 갖는 특성 때문인데, var 변수는 값이 변할 수 있고 이는 어쩌면 type 의 변화를 촉발하기 때문이다. 컴파일러는 코드의 다른 부분에서 해당 변수의 값이 변하지 않을 것이라고 장담할 수 없기때문에 클래스 내부의v var 변수에 대해서는 smart cast 를 하지 않는다.

as 연산자

클래스 내부에 있는 var 변수에 대해서는 is 연산이 유효하지 않을 수 있다. 이때는 어떻게 해야할까? 그러한 상황에서 개발자 본인이 적절한 type 으로 명시적으로 형변환 시키는 것을 고려해야한다.

calss myMovable {
    var r: moveable = Wolf()
    val wolf = r as Wolf // 명시적 형변환
    fun myFun() {
        wolf.eat()
       }
 }

이러한 형변환이 의미하는 것은 무엇일까? r 변수와 wolf 변수 모두 동일한 Wolf 의 레퍼런스 값을 가지고 있다. 그러나 r 변수는 참조하고 있는 Wolf 가 movable 만 구현했다고 알고 있지만 wolf 변수는 참조하고 있는 Wolf 가 완전한 Wolf 라는 것을 알고 있다. 같은 참조값을 가지고 있다고 하더라도 참조하고 있는 메모리에 있는 클래스가 구체적으로 어떤건지 아는것은 참조하고 있는 변수의 type에 따라 다를 수 있다.

하지만 이 역시 안전한 방법은 아니다. as 를 하고 나서도 var 이라면 다른 값을 할당받을 수 있는 것 아닌가? 이때는 ClassCastException 이 발생할 수 있으므로 as? 연산자를 이용하자. as? 를 이용하면 형변환이 적절한 경우에만 형변환이 일어난다.

profile
개발자

0개의 댓글