코틀린 언어 정리 2-3

class

상속

class 상속

단일 상속

코틀린에서 class는 단일 상속만 지원합니다.


interface 상속

다중 상속

interface를 사용 또는 구현하려고 하는 class에서 상속과 같은 방법(문법)으로 적용할 수 있으며 여러 개의 interface를 동시에 적용(다중상속 문법)할 수 있습니다.


override

  • 상위 클래스에서 open으로 선언된 프로퍼티나 멤버함수를 하위 클래스에서 재정의하여 사용 합니다.

  • override된 멤버는 최종적으로 override한 하위 클래스의 멤버로 통합되는 것이 아니라 상위, 하위 클래스 또는 인터페이스에 각각의 멤버가 별도로 존재하며 "super<...>.멤버" 와 같은 방식으로 각 타입의 멤버에 접근할 수 있습니다.

  • override로 선언된 property, member function은 open으로 선언된 것으로 간주됩니다.


property override 규칙

property를 override할 때 val/var, nullable type 등을 변경할 수 있는 경우가 있습니다.


예제

open class TestSuper {

   open val valToVar: Int = 0

   open val nullableToNonNullable: Int? = null

   open val valNullableToVarNonNullable: Int? = null

}


class TestSub : TestSuper() {

   override var valToVar: Int = 0

   override val nullableToNonNullable: Int = 0

   override val valNullableToVarNonNullable: Int = 10

}


fun test() {

   var obj = TestSub()

}


변경 가능한 경우

1. 상위에서 val로 선언한 프로퍼티를 하위에서 var로 변경 가능 ( val->var: ok )

2. 상위에서 val로 선언한 nullable type을 하위에서 non-nullable type으로 변경 가능 ( val nullable -> non-nullable: ok )

3. 1, 2 동시에 변경 가능


하위로 가면서 기능 확장은 가능하지만 예외 허용 범위는 좁아져야 합니다. 상위에서 원래 처리하던 예외의 범위를 넘어가서는 안됩니다.


규칙 이해를 위한 여러 상황들 고려

1.  상위에서 프로퍼티를 사용하는 로직이 읽기/쓰기 가능한데 하위에서 읽기 전용으로 변경하면 하위 프로퍼티 사용 로직에 오류(읽기 전용 변수에 쓰기 시도)가 생길 수 있습니다. ( var -> val: not ok )

2. 상위에서 프로퍼티에 null값이 없는 것을 가정하고 로직을 만들었는데 하위에서 nullable로 변경되어 null값을 추가하면 상위 로직에서 오류(상위에 null값에 대한 예외 처리가 없음)가 생길 수 있습니다. 즉 non-nullable 프로퍼티를 사용하는 로직에 null값이 입력 되는 경우 오류가 발생합니다.( non-nullable -> nullable: not ok )

3. nullable 프로퍼티를 사용하던 기존 코드에서 null값을 입력하는 경우 하위에서 이를 non-nullable 프로퍼티로 참조하면 문제가 발생합니다.( var nullable -> non-nullable: not ok )


overriding conflict

상위에서 final로 선언한 멤버와 충돌하는 경우

C++에서는 가상함수(가상 함수로 선언 시 변수를 참조하는 타입이 아닌 실제 생성된 객체 인스턴스의 타입에 선언된 함수를 실행 합니다. 즉 override한 함수와 비슷하게 동작합니다.)가 아니더라도 상위와 하위에 동일한 이름의 멤버(virtual이 아닌 멤버 함수 및 멤버 변수)가 허용되며 이를 각각 접근할 수 있는 방법을 제공합니다. 하지만 코틀린에서는 상위에서 final로 선언된( 코틀린 class의 멤버는 기본으로 final입니다. ) 멤버가 있는 경우 하위에서 이 멤버와 동일한 멤버( 이름이 동일한 프로퍼티, 이름과 형식이 동일한 멤버 함수 등 )를 선언할 수 없습니다. 즉 상위의 final 멤버를 open/abstract 등으로 수정 불가한 경우 하위에서는 상위에서 final로 선언된 멤버와 동일한 이름의 멤버 프로퍼티, 동일한 이름 및 형식의 멤버 함수 정의를 피해야 합니다.


상위에 정의된 동일한 이름의 함수(override한 함수) 접근

하위 클래스 내부 구현에서 다중 상속 받은 각 상위 클래스의 동일한 이름의 멤버에 접근하려면 "super<interface name>.멤버" 와 같은 방법으로 접근하면 됩니다.



abstract

특징

  • 추상 클래스, 추상 멤버 프로퍼티, 추상 멤버 함수 등을 선언할 때 사용합니다.

  • abstract는 open의 개념을 포함합니다. 즉 상속 가능하다는 것을 표현하기 위해 open을 별도로 명시하지 않아도 됩니다.

  • abstract로 선언된 클래스로 객체를 직접 생성하지 못합니다.

  • 클래스의 멤버 중 하나라도 abstract로 선언했다면 클래스도 abstract로 선언해야 합니다.

  • abstract 클래스의 멤버를 abstract로 선언하려면 각각의 멤버 선언에 abstract를 추가해야 합니다.

  • abstract가 아닌 클래스가 abstract 클래스를 상속받은 경우 abstract 멤버(프로퍼티, 함수)를 반드시 구현해야 합니다.


예제

abstract class AbstractSuper(name: String) {

   val name: String

   abstract var age: Int

   abstract var company: String


   init {

       this.name = name

   }


   abstract fun funAbstract()

   fun funNonAbstract() {

       println("AbstractSuper::funNonAbstract")

   }

}


class SubAA(name: String, age: Int, company: String) : AbstractSuper(name) {

   override var age: Int = age

   override var company: String = company

   override fun funAbstract() {

       println("SubA::funAbstract()")

   }

}


//fun main ( args: Array<String> ) {

fun test() {

   var obj = SubAA("Ree", 30, "unknown")

   obj.funAbstract()

   obj.funNonAbstract()

}


open

class나 property, member function 앞에 상속 가능하다는 것을 명시할 때 사용합니다.


final

class나 property, member function 앞에 상속 불가능하다는 것을 명시할 때 사용합니다. 코틀린 클래스 멤버는 open을 명시하지 않는 경우 기본적으로 final로 처리됩니다.


예제

abstract class SuperA {

   open fun funOpen() {

       println("SuperA::funOpen executed!")

   }


   abstract fun funAbstract()

   final fun funFinal() {

       println("SuperA::funFinal executed!")

   }

}


open class SubA() : SuperA() {

   override fun funOpen() {

       println("SubA::funOpen executed!")

   }


   override fun funAbstract() {

       println("SubA::funAbstract executed!")

   }

}


class SubSubA() : SubA() {

   override fun funAbstract() {

       println("SubSubA::funAbstract executed!")

   }

}


//fun main ( arg: Array<String>) {

fun test() {

   var obj = SubA()

   obj.funOpen()

   obj.funAbstract()

   obj.funFinal()

   var obj1 = SubSubA()

   obj1.funAbstract()

}


기타

코틀린의 모든 class는 Any class를 상속 받는다.

class의 상속은 "is a" 관계이고 interface의 상속은 "has a" 의 관계입니다.


상위 클래스 접근

상위 클래스를 상속 받은 하위 클래스에서는 하위 클래스에서 상위 클래스에 접근하는 키워드로 super를 사용합니다.


super

기본 상위 클래스/인터페이스 접근 방법입니다.


super<interface/class 이름>

class, interface 들을 다중 상속받은 경우 각각의 상위 접근을 구분하는 방법


상위 생성자 접근

하위 클래스에 주생성자가 있는 경우 하위클래스의 주생성자에서만 상위클래스의 생성자에 접근할 수 있습니다.(클래스 이름으로 접근) 주생성자가 없는 경우 보조생성자 선언에서 super 키워드로 접근할 수 있습니다. 여기서 접근한다는 의미는 선언을 통해 호출한다는 의미입니다.


예제

open class Super {} // Super의 주생성자가 기본으로 생성됨


class Sub : Super() {} // Sub의 주생성자가 기본으로 생성됨 -> Sub에서 Super의 주생성자 호출


class Sub1 : Super { // Sub1의 주생성자가 정의되지 않음.

   constructor () : super() {} // Sub1의 보조생성자에서 super키워드로 Super의 생성자(주생성자) 호출

}



상위 클래스 생성자 호출

하위 클래스의 생성자 선언에 상위 클래스의 생성자 호출을 추가하는 방법을 사용합니다.

하위 클래스에서 상위 클래스의 생성자를 호출하는 방법

[하위 클래스에 주생성자가 있는 경우]

주생성자 선언 부분에서 상위 클래스 이름(이후 나올 예제에서 "SuperA")으로 생성자를 호출합니다. 하위 클래스의 보조생성자에서는 상위클래스의 생성자를 바로 호출할 수 없습니다.


[하위 클래스에 주생성자가 없는 경우]

보조생성자 선언 부분에서 super 키워드로 상위 클래스의 생성자를 호출합니다. 상위 클래스의 생성자가 여러 개인 경우 원하는 생성자를 호출하면 됩니다.


예제

open class SuperA(name: String, age: Int) {

   val name: String = name

   var age: Int


   init {

       this.age = age

   }


   constructor (name: String) : this(name, 40) {

       age = 40

   }

}


// 주생성자가 존재하는 경우 상위 클래스의 생성자는 주생성자에서만 접근 가능함

class SubA(name: String, age: Int, company: String) : SuperA(name, age) {

   var company: String = company


   // 주생성자가 존재할 경우 보조생성자에서는 반드시 주생성자를 호출해 주어야 함

   constructor (name: String, age: Int) : this(name, age, "unknown") {}

}


class SubAA : SuperA {

   var company: String


   init {

       company = "unknown"

   }


   // 주생성자가 없는 경우 보조생성자에서 원하는 상위클래스 생성자를 호출할 수 있음

   constructor (name: String, age: Int, company: String) : super(name, age) {

       this.company = company

       println("age: ${this.age}")

   }


   constructor (name: String) : super(name) {

       this.company = "home"

       println("age: ${this.age}")

   }

}


fun test() {

   var obj1 = SubAA("Trumph")

   var obj2 = SubAA("Trumph", 70, "White house")

}


상속 관계 캐스팅

as를 이용하여 상속 관계를 고려한 캐스팅을 할 수 있습니다.

as?를 이용하면 null 허용 객체로 예외가 발생하는 것을 방어하면서 as의 기능을 수행할 수 있습니다.


상속 전반적인 내용에 대한 전체 예제

예제

open class SuperA(name: String, age: Int) {

   val name: String = name

   var age: Int


   init {

       this.age = age

   }


   constructor (name: String) : this(name, 40) {

       age = 40

   }

}


// 주생성자가 존재하는 경우 상위 클래스의 생성자는 주생성자에서만 접근 가능함

class SubA(name: String, age: Int, company: String) : SuperA(name, age) {

   var company: String = company


   // 주생성자가 존재할 경우 보조생성자에서는 반드시 주생성자를 호출해 주어야 함

   constructor (name: String, age: Int) : this(name, age, "unknown") {}

}


class SubAA : SuperA {

   var company: String


   init {

       company = "unknown"

   }


   // 주생성자가 없는 경우 보조생성자에서 원하는 상위클래스 생성자를 호출할 수 있음

   constructor (name: String, age: Int, company: String) : super(name, age) {

       this.company = company

       println("age: ${this.age}")

   }


   constructor (name: String) : super(name) {

       this.company = "home"

       println("age: ${this.age}")

   }

}


fun test() {

   var obj1 = SubAA("Trumph")

   var obj2 = SubAA("Trumph", 70, "White house")


   var objSuper: SuperA? = SubA("Ree", 40, "unknown")

   var objSub: SubA = objSuper as SubA // 문맥상 objSuper이 null이 아니므로 캐스팅 성공

   var objSuper1: SuperA? = null

   var objSub1: SubA = objSuper1 as SubA // 실행 중 예외 발생

   var objSub2: SubA? = objSuper1 as SubA? // objSuper1을 SubA? 로 캐스팅

   var objSub3: SubA? = objSuper1 as? SubA // objSuper1이 null이 아니면 SubA로 캐스팅, null이면 null 리턴

}



+ Recent posts