private set


 외부에서 set을 할 수 없도록 하기 위해서 private set을 선언해줄 수 있습니다. 기본 setter의 구현을 변경할 필요가 없으면 body를 정의할 필요없이 접근자를 정의할 수 있습니다.

class ExampleUnitTest {

    @Test

    fun kotlin() {

        var student = Person()

        //student.name = "Park" // 외부에서 set 불가

        student.id = 200

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

        student.printName()


    }


    class Person() {

        var id = 0

            get() =  100

        var name = "Suzuki"

            private set


        fun printName() {

            this.name = "Park" // class 내부에서는 set 가능

            println("this.name ${this.name}")

        }

    }

}

 student.id 100 student.name Suzuki

 this.name Park 



Backing Fields


 이전에 언급했던 getter와 setter을 생략하지 않고 선언하는 방법, 즉 custom 접근자를 작성할 때 backing field를 사용할 수 있습니다. 코틀린 클래스에서는 field를 가질 수 없지만 custom 접근자를 사용할 때 backing field가 필요한 경우가 있습니다. 이때 코틀린은 field라는 식별자를 사용하는 backing field를 제공합니다.

 backing field가 없다면 어떤 문제가 있을까요? 다음의 코드를 보시면 이해가 빠를 것 같습니다.

코틀린

     class Person() {

        var id = 0

            get() =  100

        var name = "Suzuki"

            set(value) { name = value }

    }

 

여기까지 보면 얼핏 생각하기에 name에 value를 할당하여 제대로 set하는 것처럼 보입니다.


Java

       public final void setName(@NotNull String value) {

         Intrinsics.checkParameterIsNotNull(value, "value");

         this.setName(value);

      }


 Java 코드를 보니 재귀함수가 보이고 무한루프에 빠져서 StackOverFlow가 발생할 것으로 보입니다.

아니나 다를까 다음과 같이 친절하게 알려줍니다.



 그럼 field를 사용해봅시다.


코틀린

     class Person() {

        var id = 0

            get() =  100

        var name = "Suzuki"

            set(value) { field = value }

    }


Java

       public final void setName(@NotNull String value) {

         Intrinsics.checkParameterIsNotNull(value, "value");

         this.name = value;

      }


 field 식별자는 오직 property 접근자에서만 사용할 수 있습니다. property를 위한 backing field는 적어도 하나 이상의 기본 접근자를 사용하거나 custom 접근자에서 field 식별자를 사용할 때 만들어집니다.


 따라서 다음과 같은 경우에는 backing field가 없습니다.

val isEmpty: Boolean

    get() = this.size == 0


 참고로 코틀린 1.1 이후부터는 property type을 getter로부터 유추할 수 있다면 생략 가능합니다.

 val isEmpty get() = this.size == 0  // type Boolean을 가짐


Getters and Setters


 코틀린에서 property를 선언할 때 생략없는 full syntax는 다음과 같습니다.

 var <propertyName>[: <PropertyType>] [= <property_initializer>]

    [<getter>]

    [<setter>]


 여기서 [ ]로 표시된 property_initializer, getter, setter은 선택사항입니다. 생략할 수 있다는 말입니다.


앞서 property를 공부할 때 다음과 같은 예제를 보았습니다.

  class Person {

  var id: Int = 0

  var name: String = "Suzuki"

 }


 getter, setter은 전혀 보이지 않습니다. 그런데 initializer는 사용했습니다. initializer을 생략을 하려면 어떻게 할 수 있을까요?

일단 initializer를 지워보면 Error:(22, 9) Property must be initialized or be abstract가 발생합니다.


 

 따라서 abstract로 선언하는 방법이 있고 위와 같이 constructor의 파라미터로 옮길 수 있습니다.

    abstract class Person {

        abstract var id: Int

        var name: String = "Suzuki"

    } 

or

    class Person(var id: Int) {

        var name: String = "Suzuki"

    } 


 property type도 생략할 수 있습니다. 단, initializer로 부터 type을 유추할 수 있을 때만 가능합니다.

     class Person() {

        var id = 0 // var id: Int = 0

        var name = "Suzuki" // var name: String = "Suzuki"

    }


 getter, setter는 생략되었는데 문제가 없는지 살펴봅시다. 위의 Person 클래스를 Java 코드로 Decompile 하면 다음과 같습니다.

 Decomplie 하는 방법은 Tools > Kotlin > Show Kotlin Bytecode 선택 후 Kotlin Bytecode 창에서 Decompile을 누르시면 됩니다.


    public static final class Person {

      private int id;

      @NotNull

      private String name = "Suzuki";


      public final int getId() {

         return this.id;

      }


      public final void setId(int var1) {

         this.id = var1;

      }


      @NotNull

      public final String getName() {

         return this.name;

      }


      public final void setName(@NotNull String var1) {

         Intrinsics.checkParameterIsNotNull(var1, "<set-?>");

         this.name = var1;

      }

   }

 

코틀린 코드에서는 선언한 적이 없는 getter와 setter가 존재합니다. 이것으로 미루어 볼 때 코틀린에서는 기본적으로 getter와 setter를 선언할 필요가 없습니다. 

 property 사용 방법을 떠올려보면 getter와 setter를 사용하지 않고 변수명으로 직접 get/set을 하였습니다.

 val student = Person()

 student.id = 10 // 직접 set을 하고 있음

 println("student.name ${student.id}") // 직접 get 하고 있음

이것이 가능한 이유가 Java 코드로 Decompile 했을 때 getter/setter가 존재하기 때문이라는 것을 알 수 있습니다.


 getter와 setter을 생략하지 않고 선언하는 방법도 있습니다.

    class Person() {

        var id = 0

        get() =  100

        var name = "Suzuki"

        set(value) {

            field = "Kim"

        }

    }


 위 Decomplie 결과물을 참고하여 getter/setter를 추가했을 때의 코드를 Java 코드로 직접 변환해봅시다.

    public static final class Person {

      private int id;

      @NotNull

      private String name = "Suzuki";


      public final int getId() {

         return 100;

      }


      public final void setId(int var1) {

         this.id = var1;

      }


      @NotNull

      public final String getName() {

         return this.name;

      }


      public final void setName(@NotNull String value) {

         Intrinsics.checkParameterIsNotNull(value, "value");

         this.name = "Kim";

      }

   }


 이제 id의 getter는 100만 리턴해주고 name의 setter는 Kim으로만 assign을 합니다.

그럼 아무리 id의 값을 reassign 하더라도 student.id는 100을 리턴하게 됩니다. name은 아무리 reassign하더라도 Kim으로만 assign됩니다. 확인해볼까요?

class ExampleUnitTest {

    @Test

    fun kotlin() {

        var student = Person()

        student.name = ""

        student.id = 200

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


    }


    class Person() {

        var id = 0

        get() =  100

        var name = "Suzuki"

        set(value) {

            field = "Kim"

        }

    }

}

 student.id 100 student.name Kim 


상속 (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 


+ Recent posts