티스토리 뷰

반응형
SMALL

추상 클래스와 추상 메소드

추상 함수

추상 메소드(Abstract Method)는 미완성 함수 혹은 실행 영역이 없는 함수를 의미합니다. 메소드는 선언부와 실행부 2가지가 존재하는데 추상 메소드는 실행부가 없는 형태를 띄고 있습니다.

추상 메소드(Abstract Method)를 포함하는 클래스는 반드시 추상 클래스(Abstract Class)가 됩니다.

1
2
3
4
5
6
abstract class AbstractTest1 {
    fun myFun1() {
        //....
    }
    abstract fun myFun2()
}
cs

 

abstract는 클래스 선언부에 존재하고 있습니다. 그리고 abstract라는 예약어(Keyword)는 클래스 내부에서만 사용할 수 있습니다.

또한 클래스 내부에 선언된 메소드에서만 사용할 수 있으며 최상위 레벨에 선언된 메소드에는 사용할 수 없습니다.

1
2
3
4
5
6
7
8
9
10
abstract class AbstractTest1 {
    fun myFun1() {
        //....
    }
    abstract fun myFun2()
}
 
abstract fun myFun3() {   // Error
 
}
cs

(참고) 위 소스코드에서 myFun3은 최상위 레벨에서 선언된 함수이기 때문에 에러가 나게 됩니다. 

 

추상 프로퍼티

일반적인 OOP 언어에서는 Abstract Class와 Abstract method를 지원하고 있습니다. 코틀린 역시 지원하고 있죠.

코틀린은 한 발짝 더 나아가서 Property도 Abstract Property를 제공하고 있습니다.

1
2
3
4
abstract class AbstractTest2 {
    val data1: String = "terry"
    abstract val data2: String
}
cs

코틀린에서 프로퍼티를 선언할 때 값을 초기화하지 않으면 컴파일 시 에러가 발생하게 됩니다. 

하지만 추상 프로퍼티는 값을 초기화하지 않더라도 컴퍼일 시 에러가 발생하지 않습니다.

 

추상 클래스 이용

추상 클래스는 추상 클래스 자체로는 객체 생성이 불가합니다. 즉, Instance화가 불가능하다는 뜻입니다.

추상 클래스를 상속받는 하위 클래스를 작성하고 작성된 하위 클래스를 Instance화하여 사용해야 합니다.

하위 클래스를 정의할 때에는 상속받은 추상 클래스의 모든 추상 프로퍼티와 추상 메소드를 재정의해야 하고 만약에 정의하고 싶지 않다면 하위클래스에서도 abstract 키워드를 붙여야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
abstract class Super {
    val data1: Int = 10
    abstract val data2: Int
    fun myFun1() {
        
    }
    abstract fun myFun2()
}
 
class Sub: Super() {
    override val data2: Int = 10
    override fun myFun2() {
    }
}
 
fun main(args: Array<String>) {
    val obj1 = Super()    // Error Occurred
    val obj2 = Sub()
}
cs

위에서 abstract가 붙은 Super클래스를 바로 인스턴스화 하려고 하는 부분 (16줄)이 에러가 나는 것을 확일 할 수 있습니다.

Sub클래스를 보면 abstract 프로퍼티와 abstract 메소드를 모두 재정의하고 있습니다.

아, 여기서 하나 짚고 넘어가야 할 것은 추상 클래스는 open을 사용하지 않아도 다른 모든 클래스가 상속받을 수 있습니다.


인터페이스

클래스를 사용하는 목적은 객체를 생성해 클래스에 묶인 프로퍼티와 함수를 이용하는 것에 있습니다.

인터페이스를 사용하는 주목적은 추상 함수를 선언하는 것입니다.

인터페이스 선언 및 구현

인터페이스는 객체 생성이 불가능하기 때문에 클래스에서 구현하여 사용해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface MyInterface {
    var data1: String
    fun myFun1() {
        //...
    }
    fun myFun2()
}
 
class MyClass: MyInterface {
    override var data1: String = "Hello World"
 
    override fun myFun2() {
        // ...
    }
}
 
fun main(args: Array<String>) {
    val obj = MyInterface()   // Error
    val obj1 = MyClass()
    
    obj1.myFun1()
    obj1.myFun2()
}
cs

 

인터페이스 자체는 객체를 생성할 수 없기 때문에 18줄 처럼 코딩하는 일은 없어야 하겠습니다.

추상 클래스와 약간의 차이가 있다면 abstract를 안써도 된다는 점입니다.

물론 사용해도 됩니다. 인터페이스는 기본적으로 abstract를 깔고가기 때문에 abstract를 추가해도 문제 없이 동작합니다.

Java에서 인터페이스를 구현할 때 implements라는 키워드를 사용했습니다.

Kotlin에서는 상속과 마찬가지로 : (콜론)을 사용합니다.

 

일반적인 클래스를 상속할 때에는 생성자 관계 표현이 중요하므로 '상위클래스명 ( )' 처럼 작성하는데 인터페이스를 클래스가 아니기 때문에 생성자가 없습니다. 따라서 ( ) 소괄호를 작성하지 않습니다. 구현하고자 하는 인터페이스 이름만 작성하면 됩니다.

 

다른 인터페이스를 상속받는 인터페이스

인터페이스를 선언할 때 다른 인터페이스를 상속 받을 수도 있으며 한꺼번에 많은 인터페이스를 상속받을 수도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface MyInterface1 {
    fun myFun1()
}
 
interface MyInterface2 {
    fun myFun2()
}
 
interface MyInterface3 : MyInterface1, MyInterface2 {
    fun myFun3()
}
 
class MyClass1: MyInterface3 {
    override fun myFun1() { }
    override fun myFun2() { }
    override fun myFun3() { }
}
cs

MyClass1이라는 클래스는 MyInterfacd3을 상속받습니다. 이 인터페이스를 MyInterface 1과 MyInterface 2를 상속 받기 때문에 MyInterface1과 MyInterface2에서 선언한 메소드를 모두 정의하여 사용할 수 있습니다.

 

클래스에서 여러 인터페이스 구현

1
2
3
4
5
6
7
8
9
10
11
12
interface MyInterface1 {
    fun myFun1()
}
 
interface MyInterface2 {
    fun myFun2()
}
 
class MyClass1: MyInterface1, MyInterface2 {
    override fun myFun1() { }
    override fun myFun2() { }
}
cs

 

클래스에서 상속과 인터페이스 혼용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface MyInterface1 {
    fun myFun1()
}
 
interface MyInterface2 {
    fun myFun2()
}
 
open class Super {
 
}
 
class MyClass1: Super(), MyInterface1, MyInterface2 {
    override fun myFun1() { }
    override fun myFun2() { }
}
cs

 

객체 타입으로서의 인터페이스

인터페이스는 클래스가 아니기 때문에 인터페이스를 생성하여 사용할 수 없습니다. 하지만 인터페이스는 클래스 객체의 타입으로 이용할 수는 있습니다. 인터페이스를 구현한 클래스의 객체를 생성(인스턴스화)하고 이 객체를 구현된 인터페이스 타입으로 선언해서 이용하는 것이 가능하다는 것입니다. 소스코드를 통해 살펴봅시다.

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
interface MyInterface1 {
    fun myInterfaceFun()
}
 
open class Super1 {
    fun mySuperFun() {
        println("mySuperFun()...")
    }
}
 
class Sub1: Super1(), MyInterface1 {
    override fun myInterfaceFun() {
        println("myInterfaceFun call...")
    }
}
 
fun main(args: Array<String>) {
    val obj1: Sub1 = Sub1()
    val obj2: Super1 = Sub1()
    val obj3: MyInterface1 = Sub1()
    
    obj1.mySuperFun()
    obj1.myInterfaceFun()
    
    obj2.mySuperFun()
    obj2.myInterfaceFun() // ERROR
    
    obj3.mySuperFun()     // ERROR
    obj3.myInterfaceFun()
}
cs

22번 줄 부터 29번 줄까지는 생성된 객체의 메소드를 호출해서 사용하는 부분입니다.

3개의 객체가 모두 Sub1 타입이라고 하더라도 타입이 모두 다르기 때문에 호출할 수 있는 메소드에 차이가 있습니다.

객체의 타입에 따라 그 타입에 선언된 메소드만 사용할 수 있습니다.

즉, 26줄은 obj2가 Super1를 상속받기 때문에 myInterfaceFun()은 불가합니다.
28줄은 obj3이 MyInterface를 구현하고 있기 때문에 mySuperFun()은 불가합니다.

 

인터페이스와 프로퍼티

인터페이스는 주로 추상 메소드를 선언하고 인터페이스를 구현하는 클래스에서 추상 메소드를 재정의해서 사용하는 것을 주목적으로 하고 있습니다. 그런데 원한다면 인터페이스 내에 프로퍼티를 추가할 수 있습니다.

이 때 규칙이 있는데 규칙은 아래와 같습니다.

1. 추상형으로 선언하거나 get(), set() 함수를 정의해야 한다.
2. 추상 프로퍼티가 아니라면 val은 get() 함수를 꼭 선언해야 한다.
3. 추상 프로퍼티가 아니라면 var은 get(), set() 함수를 꼭 선언해야 한다.
4. 인터페이스의 프로퍼티를 위한 get(), set() 함수에서는 field를 사용할 수 없다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface MyInterface {
    var prop1: Int
    
    val prop2: String = "kim" // ERROR
    
    val prop2: String // ERROR
        get() = field
    
    var prop3: String // ERROR
        get() = "kim"
    
    val prop4: String
        get() = "kim"
    
    var prop5: String
        get() = "kim"
        set(value) {
            
        }
}
cs

프로퍼티를 선언한 예입니다.

위의 규칙에 위배된 부분에 대해서는 오류가 발생하게 됩니다.

 

오버라이드 함수 식별

클래스에서 다른 클래스를 상속받거나 인터페이스를 구현할 때 상속받을 클래스에 정의된 함수명과 인터페이스에 정의된 함수명이 중복되는 경우가 있습니다. 이 때 함수를 식별해서 사용해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface Interface1 {
    fun funA()
}
 
interface Interface2 {
    fun funA()
}
 
open abstract class Super2 {
    abstract fun funA()
}
 
class Sub2: Super2(), Interface1, Interface2 {
    override fun funA() {
        println("Sub2 funA...")
    }
}
 
fun main(args: Array<String>) {
    val obj1 = Sub2()
    obj1.funA()
}
cs

Super2, Interface1, Interface2 모두 같은 funA라는 추상 함수를 가지고 있습니다.

하지만 모두 구현체를 가지지 않으므로 한번만 정의를 해주면 됩니다.

 

같은 이름의 추상 메소드와 구현된 함수가 있을 때

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
interface Interface3 {
    fun funA() {
        println("Interface3 funA()...")
    }
}
 
open abstract class Super3 {
    abstract fun funA()
}
 
class Sub3: Super3(), Interface3 {
    override fun funA() {
        super.funA()
        println("Sub3 funA...")
    }
    
    fun some() {
        super.funA()
    }
}
 
fun main(args: Array<String>) {
    val obj1 = Sub3()
    obj1.funA()
    obj1.some()
}
cs

위의 코드를 보면 Sub3 클래스에서 Super3을 상속받고 Interface3을 구현했습니다. 그런데 Super3에는 funA()라는 추상 메소드가 있고 Interface3에 funA()라는 구현된 메소드가 있습니다. 이름은 같은데 하나는 추상이고 하나는 구현된 것입니다.

Super3의 funA는 Sub3에서 14번줄 처럼 재정의를 해야 합니다. 

만약에 위의 코드에서 Interface3의 funA를 호출하고 싶으면 어떻게 해야할까요??

 

funA는 Sub3클래스의 메소드가 아니기 때문에 super.funA()를 통해 interface3의 funA를 호출할 수 있습니다.

 

같은 이름으로 구현된 함수가 여러 개일때

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Interface4 {
    fun funA() {
        println("Interface4 funA...")
    }
}
 
interface Interface5 {
    fun funA() {
        println("Interface5 funA...")
    }
}
 
class Sub4: Interface4, Interface5 { // ERROR
    
}
cs

같은 이름의 추상 메소드를 갖고있는 여러 개의 인터페이스를 상속하려는 경우 에러가 발생합니다.

즉, 메소드명 충돌 에러가 발생하게 됩니다.

 

이런 상황에서 같은 이름의 함수를 재정의할 후 어느 것을 이용할 지 혹은 둘 다 이용할 지 타입으로 식별해서 명시해야합니다.

위의 소스를 다음 처럼 작성할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface Interface4 {
    fun funA() {
        println("Interface4 funA...")
    }
}
 
interface Interface5 {
    fun funA() {
        println("Interface5 funA...")
    }
}
 
class Sub4: Interface4, Interface5 { // ERROR
    override fun funA() {
        super<Interface4>.funA()
    }
}
 
fun main(args: Array<String>) {
    val obj = Sub4()
    obj.funA()
}
cs

super<Interface4>.funA() 라는 부분이 Interface4에 있는 funA를 호출하겠다는 의미입니다.

 

이렇게 코틀린에서의 추상 메소드, 추상 클래스, 인터페이스에 대해 살펴보았습니다.

 

반응형
LIST

'프로그래밍언어 > Kotlin' 카테고리의 다른 글

Kotlin 함수형 프로그래밍  (0) 2019.08.11
Kotlin 데이터 클래스  (0) 2019.08.09
Kotlin 접근 제한자  (0) 2019.07.28
Kotlin 상속 (Kotlin Override) - 2  (0) 2019.07.28
Kotlin 상속 (Kotlin Override) - 1  (0) 2019.07.28
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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
글 보관함