상속 (Inheritance)


 코틀린에서는 super class (이하 부모클래스) 선언하지 않은 클래스는 default super로 Any라는 부모클래스를 가집니다.

 class Computer // 암시적으로 Any를 상속받음


표준 라이브러리에도 잘 설명되어 있습니다.

 Any (출처)

open class Any

The root of the Kotlin class hierarchy. Every Kotlin class has Any as a superclass.

 이 설명만 보면 마치 Java의 java.lang.Object와 같아 보입니다. 하지만 Any는 Object와는 다르게 equals(), hashCode(), toString() 외 다른 멤버는 가지지 않습니다. Object는 clone(), notify() 등 다른 멤버가 더 있습니다. (Object 참고)


상속받는 방법 (부모클래스를 명시적으로 선언하기)


 상속을 받기 위해서는 아래와 같이 부모클래스를 명시적으로 선언해주면 됩니다.

 open class Person(var name: String)

 class Student(name: String) : Person(name) 


 그렇다면 다음과 같이 student 객체를 만들어서 name을 출력하면 어떤식으로 동작을 할까요?

 val student = Student("Kim")

 println("student name = ${student.name}") 

 앞서 공부한 내용을 떠올려보면 Student의 주생성자를 호출할 것입니다. 그 후 부모클래스에 있는 property인 name을 초기화하기 위해서 Person의 주생성자 호출될 것입니다. 마치 부생성자가 this로 주생성자를 호출하여 property를 초기화 하듯이 동작합니다.


 그런데 한가지 신경쓰이는 open이라는 키워드가 있습니다. Java 클래스는 final을 명시했을 때 상속이 불가능하며 특별히 명시하지 않을 때는 final 클래스가 아닙니다. 코틀린 클래스는 open을 명시해야 상속이 가능하며 명시하지 않을 경우 Java의 final 클래스와 같습니다.

 즉, 다음과 같이 사용합니다.

Java에서는 상속 불가능 -> final 키워드

코틀린에서는 상속 가능 -> open 키워드


 Java 코드로 생각해보면 다음과 같습니다. 이렇게 보니 좀 더 이해하기 쉽습니다.

 public class Person { // final 클래스가 아님

 String name;

  public Person(String name) {

   this.name = name;

  }

 }


 public class Student extends Person {

  public Student(String name) {

   super(name);

  }

 }


 다음의 코틀린 코드들을 Java 코드로 변환해보면 코틀린 클래스에 대해서 좀 더 익숙해지는데 도움이 될 것 같습니다.

class ExampleUnitTest {

    @Test

    fun kotlin() {

        val student = Student()

        println("student name = ${student.name} student age = ${student.age}")

    }


    open class Person(var name: String = "Jane")

    class Student(name: String = "kim", var age: Int = 26) : Person(name)

 student name = kim student age = 26 


 class ExampleUnitTest {

    @Test

    fun kotlin() {

        val student = Student()

        println("student name = ${student.name} / student last_name = ${student.last_name} / student age = ${student.age}")

    }


    open class Person(var name: String = "Jane")

    class Student(var last_name: String = "Smith", var age: Int = 26) : Person(last_name)

}

 student name = Smith / student last_name = Smith / student age = 26 


코틀린 클래스의 initializer blocks

 코틀린의 primary constructor(이하 주생성자)는 별도의 코드를 포함할 수 없습니다. 대신 초기화 코드는 init 이라는 키워드를 이용한 initializer blocks에서 선언할 수 있습니다.

    fun kotlin() {

        Student(name = "John")

    }


    class Student(var name: String, var id: Int = 0) {

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

            println("constructor block :: name is $name and id is $id")

        }

        init {

            name = "kim"

            println("init block :: name is $name and id is $id")

        }

    }


 위 코드의 결과는 아래와 같습니다.

init block :: name is kim and id is 100

constructor block :: name is John and id is 100 

 이것으로 init의 호출 순서를 유추해 볼 수 있습니다.

먼저 객체를 생성하면 호출되면 부생성자를 통해 주생성자가 호출됩니다. [name=John, id=100] 이때 initializer block이 존재하기 때문에 해당 block에서 name을 초기화 합니다. [name=kim, id=100]

이 과정이 끝난 후에야 부생성자의 출력문이 동작합니다.


 익숙한(?) Java 코드로 보면 이해가 쉽습니다.

    public static final class Student {

      private String name;

      private int id;


      public Student(String name) {

         this(name, 100);

         System.out.println(constructor block :: name is " + name + " and id is " + this.id);

      }

      public Student(String name, int id) {

         this.name = name;

         this.id = id;

         this.name = "kim"; // init block

         System.out.println("init block :: name is " + this.name + " and id is " + this.id);

      }

   }


 Java 코드를 참고하니 조금 더 생각하기가 쉬워졌습니다. 그러면 다음과 같이 객체를 생성하면 어떻게 동작할까요?

Student(name = "John", id = 500)

 Java 코드로 유추해보면 바로 다음 코드를 실행할 것입니다.

      public Student(String name, int id) {

         this.name = name;

         this.id = id;

         System.out.println("init block :: name is " + this.name + " and id is " + this.id);

      } 


 이걸 그대로 코틀린으로 생각해보면 다음과 같겠죠.

class Student(var name: String, var id: Int = 0) {

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

            println("constructor block :: name is $name and id is $id")

        }

        init {

            println("init block :: name is $name and id is $id")

        }

    }

 곧바로 주생성자를 거쳐 init block을 실행할 것입니다. 따라서 결과는 아래처럼 나옵니다.

init block :: name is John and id is 500 


 이제 여러가지 방법으로 테스트를 진행하면서 익숙해질 수 있습니다. 실제로 위에서 설명한 내용을 유추하고 이해하기 위해서 테스트한 코드는 다음과 같습니다.

class ExampleUnitTest {
@Test
fun kotlin() {
Student(name = "John", id = 500)
Student2(name = "John")
Student3(name = "John")
}

class Student(var name: String, var id: Int = 0) {
constructor(name: String) : this(name, 100) {
println("constructor block :: name is $name and id is $id")
}
init {
name = "kim"
println("init block :: name is $name and id is $id")
}
}

class Student2(var name: String, var id: Int = 0) {
constructor(name: String) : this(name, 100) {
println("constructor block2 :: name is $name and id is $id")
}
init {
name = "kim"
println("init block2-1 :: name is $name and id is $id")
}
init {
id = 200
println("init block2-2 :: name is $name and id is $id")
}
}

class Student3(var name: String, var id: Int = 0) {
constructor(name: String) : this(name, 100) {
println("constructor block3 :: name is $name and id is $id")
}
init {
id = 600
println("init block3-1 :: name is $name and id is $id")
}
init {
name = "kim"
id = 200
println("init block3-2 :: name is $name and id is $id")
}
}

} 

init block :: name is kim and id is 500

init block2-1 :: name is kim and id is 100

init block2-2 :: name is kim and id is 200

constructor block2 :: name is John and id is 200

init block3-1 :: name is John and id is 600

init block3-2 :: name is kim and id is 200

constructor block3 :: name is John and id is 200 


+ Recent posts