Kotlin의 기본 클래스를 정리합니다.

Kotlin은 별도의 클래스 정의하지 않고, Util을 생성하기도 합니다.

이러한 방법 및 상속시에 사용 가능한 추상 클래스 등을 살펴보도록 하겠습니다.


기본 클래스

기본 클래스의 형태는 Java와 동일합니다.

class ClassName {

}

별도의 함수 정의가 없다면 다음과 같이 {}을 제외할 수 있지만, 이런 식으로 사용하는 부분은 많지 않겠죠.

class ClassName


생성자

생성자는 항상 overloading을 통해서 여러 개의 형태가 만들어지게 됩니다. Kotlin도 Java와 함께 쓰다 보니 예외는 없습니다.

다만 코틀린으로 클래스를 생성하고, 코틀린에서만 사용하게 되면 default 변수 정의가 가능하기 때문에 별도의 overloading을 정의하지 않아도 됩니다.

fun setItems(val name: String, val age: Int = 0) {}

예를 들면 위와 같이 default를 정의해주면 overloading을 하지 않아도 무관합니다.

다만 Java에서도 같은 함수를 사용하게 만들어야 하고, default를 허용하지 않아서 결국 overloading을 만들어야 합니다.


우선 Java에서의 생성자는 다음과 같습니다.

class ClassName {

  public ClassName(String name) {
    // ...
  }
}

위와 같이 별도의 함수로 생성자를 선언합니다.

코틀린에서는 다음과 같이 클래스 이름 끝에 ()을 포함하여 생성자를 선언해주게 됩니다.

class ClassName(name: String) {
  // Class 정의
}

클래스 생성자의 원형은 다음과 같습니다.

class ClassName constructor(name: String) {
  // Class 정의
}

원래는 constructor가 생성자를 나타냅니다. constructor을 매번 써주는 것도 코드량만 늘어나고 불필요한 사항이라서 생략이 가능한 형태로 제공을 하기 때문에 constructor은 생략할 수 있는 형태로 제공되며, 간단하게 class ClassName(name: String)으 형태로 선언이 가능합니다.


생성자 초기화

Java에서는 생성자에서 많은 일을 할 수 있습니다.

다음과 같이 초기화를 하기도 합니다.

class ClassName {

  private int[] age;

  public ClassName() {
    age = new int[10];
  }
}

코틀린에서는 constructor에서는 이러한 행동을 할 수 없습니다. 하지만, init 블락이 별도로 제공됩니다. init에서 다음과 같이 초기화를 해줄 수 있습니다.

class ClassName(name: String) {
  init {
    println("Initialized with value ${name}")
  }
}

init 블락을 사용하지 않고, 다음과 같이 upperName이라는 String 변수에 생성자에서 넘겨받은 nametoUpperCase하여 바로 대문자로 초기화할 수 있습니다.

class ClassName(name: String) {
  val upperName = name.toUpperCase()
}

별도의 생성자 정의 없이 위와 같이 바로 초기화가 가능한 형태로 사용할 수 있습니다.

추가로 val은 읽기 전용이고, var은 읽기 쓰기가 가능한 형태입니다.

생성자에 val로 정의하였다면 읽기만 가능하고, var로 정의하였다면 읽기 쓰기가 가능한 형태입니다. Java에서는 final입니다. 다음과 같은 형태로 생성자 정의가 가능합니다.

class Person(val name, var age: Int) {
  // ...
}


1개 이상의 생성자

클래스 이름의 선언과 동시에 생성자를 선언하는 Kotlin에서도 1개 이상의 생성자 정의가 가능합니다.

우선 자바에서는 다음과 같이 합니다.

class Person {
  public Person(String name) {
    // name 정의
  }

  public Person(String name, int age) {
    this(name);
    // age 정의
  }
}

Kotlin에서는 constructor을 여러 개로 정의할 수 있는 대 다음과 같은 형태로 생성자 정의가 가능합니다.

class Person(val name: String) {
  constructor(name: String, age: Int) : this(name) {
    // ...
  }
}

첫 번째 생성자는 val name: String만을 초기화할 수 있고, 2번째는 name: String, age: Int를 가지는 생성자입니다.

name은 중복적으로 사용되는 키워드이므로 기존 생성자로 넘겨주기 위해서 this() 키워드를 사용하여 정의 가능합니다.


생성자 private으로 사용하기

Java에서는 싱글톤을 사용하는 경우 private을 정의하여 생성자를 가립니다.

Kotlin에서도 private의 생성자를 만들 수 있는데 다음과 같습니다.

class PrivateConstructor private constructor() {
  // Class 정의
}


클래스 사용하기

클래스를 생성하기 위해서 Java에서는 new 키워드를 사용하게 됩니다.

new를 사용하여 다음과 같이 ClassName 클래스에 접근하여 사용할 수 있습니다.

class ClassName {
  // Class 정의
}

ClassName className = new ClassName();

코틀린에서는 new라고 쓰지 않고도 Class 사용이 가능합니다.

아래와 같이 해당 클래스의 생성자만 정의하면 바로 사용이 가능합니다.

class ClassName {
  // Class 정의
}

val className = ClassName()


Kotlin의 상속

Java에서는 상속을 extendsimplements으로 사용합니다.

public abstract class Base {
  public Base(int age) {

  }
}

// 추상 클래스의 상속을 다음과 같이 사용
public class UseBase extends Base {
  public UseBase(int age) {
    super(age);
  }
}

kotlin에서는 abstractinterface에 대한 별도 구분 없이 :으로 구분합니다.

open class Base(age: Int)

// open으로 생성한 추상 클래스를 다음과 같이 사용
class UseBase(age: Int) : Base(age)

간단하게 위와 같이 :으로 구분하여 상속을 구현하게 됩니다.

Android에서 많이 사용할 View 상속은 다음과 같이 처리할 수 있습니다.

class MyView : View {
    constructor(ctx: Context) : super(ctx) {
      // 정의
    }

    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) {
      // 정의
    }
}


함수 Overriding

함수를 Overriding을 하기 위해서 java에서는 abstract 또는 interface를 사용합니다.

abstractextends을 이용하여 상속을 구현하고, interfaceimplements을 이용하여 상속을 구현합니다.

abstract class AbstractBase {
  abstract void onCreate();
  final void onResume() {} // 상속에서 재 구현 금지
}

interface Base {
  void onStart();
}

public Use extends AbstractBase implements Base {

  // Abstract 상속에 대한 구현
  @Override
  void onCreate() {}

  // interface에 대한 구현
  @Override
  void onStart() {}
}

위와 같이 상속을 구현합니다.

final에 대해서는 아래쪽에서 설명하고, 우선 kotlin에서 InterfaceAbstract:으로 처리한다고 말씀드렸습니다.

다음과 같이 open이라는 키워드를 함께 사용하면 다음과 같습니다.

interface BaseItem {
    fun onStart()
}

open class Base {
    open fun v() {}
    fun nv() {}
}

class AbstractBase() : Base(), BaseItem {
    // Base의 open 키워드를 통해 상속을 구현
    override fun v() {}
    // Interface에서 구현한 상속
    override fun onStart() {}
}


kotlin open

kotlin에서 사용하게 되는 open 키워드는 다음과 같습니다.

  • java에서는 상속의 재 정의를 방지하기 위해 final을 사용합니다.
  • kotlin에서는 반대로 상속의 재 정의를 허용하기 위해서 open을 사용합니다.

java에서는 final을 통해서 상속의 재 정의를 막지만 kotlin에서는 open을 이용하여 함수의 재 정의를 할 수 있도록 허용합니다.

open 클래스의 open 함수가 있다면, 이는 상속을 받아 재 정의가 가능한 형태가 제공됩니다.

그래서 다음과 같이 open 클래스를 구현하게 되면 v()는 재 정의가 가능하고, nv()는 재 정의가 불가능한 형태가 만들어집니다.

open class Base {
    open fun v() {
        print("ABC")
    }
    fun nv() {}
}

open은 변수에서도 사용이 가능한데 다음과 같습니다.

open class Foo {
  open val x: Int get { ... }
}

class Bar(override val x: Int) : Foo() {

}

위의 코드를 예제로 작성하면 다음과 같고 실행하면 12라는 결과를 얻을 수 있습니다.

fun main(args: Array<String>) {
    print(C(12).x)
}

open class A {
    open val x: Int = 0
}

class C(override val x: Int) : A() {
}


Overriding Rules

다음과 같은 코드에서 다중 상속을 허용하게 됩니다.

  • Kotlin의 사이트에 나오는 코드를 그대로 가져왔습니다.
open class A {
  open fun f() { print("A") }
  fun a() { print("a") }
}

interface B {
  fun f() { print("B") } // interface members are 'open' by default
  fun b() { print("b") }
}

class C() : A(), B {
  // The compiler requires f() to be overridden:
  override fun f() {
    super<A>.f() // call to A.f()
    super<B>.f() // call to B.f()
  }
}

해당 C()f() 함수를 실행한 결과는 다음과 같습니다.

  • print(“A”), print(“B”)

open의 클래스 A의 f() 함수와 B interface의 f() 함수가 다중으로 상속되는 상황입니다. 그래서 super<Base>의 형태로 함수를 각각 불러올 수 있습니다. class C()에서 보듯 f() 함수를 정의한 부분을 살펴볼 수 있습니다.

  • super<A>.f()는 A.f()를 하는 것과 동일합니다.
  • super<B>.f()는 B.f()를 하는 것과 동일합니다.

함수 f()open class Af()도 상속받았고, interface Bf()도 함께 상속이 된 상태라서 가능한 rule입니다.

추가로 Java 8 이전 버전을 공부하신 분은 interface에서 함수 정의가 가능함을 의아해하실 수 있을 것 같습니다. 다음의 java 8 virtual extension methods 자료를 참고하시면 되겠습니다.


Abstract class

Java에서 사용하는 추상 클래스 정의는 다음과 같습니다.

public abstract class Base {

  public Base(String name) {
    updateName(name);
  }

  protected abstract void updateName(String name);
}

// Base를 상속 받는 클래스
public class UseName extends Base {
  public UseName(String name) {
    super(name);
  }

  @Override
  protected void updateName(String name) {
    // 정의
  }
}

위와 같이 정의를 하여 사용합니다. kotlin에서도 기본 Abstract은 동일하게 구현하게 됩니다.

abstract class BaseUse(name: String) {

    init {
        updateName(name)
    }

    protected abstract fun updateName(name: String)
}


// Base를 상속 받는 클래스
class UseName(name: String) : BaseUse(name) {

    override fun updateName(name: String) {
        // 정의
    }
}

추가로 kotlin의 open class를 추가하여 다음과 같이 확장도 가능합니다.

open class Base {
  open fun f() {}
}

abstract class AbstractBase : Base() {
  override abstract fun f()
}

open 클래스의 f() 함수는 override가 가능한 형태입니다. 이를 AbstractBase에서 상속받고, abstract으로 확장할 수 있습니다.


Java의 static 메소드 사용하기

java에서는 함수 내에 static을 선언하여 외부에서 접근을 하게 됩니다.

public class ClassName {

  // getInstance() 구현을 위하여 private
  private ClassName() {

  }

  public static ClassName getInstance() {
    return new ClassName();
  }
}

// Use...
ClassName className = ClassName.getInstance();

위와 같은 getInstance의 형태로 사용하게 됩니다.

kotlin에서는 companion 키워드를 사용하여 구현하게 됩니다.

// 생성자 private 처리
class ClassName private constructor() {

  // 외부에서 static 형태로 접근 가능
  companion object {
    fun getInstance() = ClassName()
  }
}

// Use kotlin
val className = ClassName().getInstance()
// Use java
ClassName className = ClassName.Companion.getInstance();


Sealed Classes

kotlin의 마지막 내용인 Sealed Classes 내용입니다.

열거 형태로 자기 자신을 return이 가능하고, 다음과 같이 classobject에 자기 자신을 return하는 클래스 형태를 제공합니다.

대략 다음을 실행하면 eval이라는 함수에 Expr을 셋팅합니다. when으로 동작하는데 ExprSum 클래스를 초기화합니다. 초기화시에는 2개의 Expr을 사용하고, 이를 +하는 함수입니다. 실제로는 expr.number를 가져와서 처리하는 예제입니다.

fun main(args: Array<String>) {
	print(eval(Expr.Sum(Expr.Const(12.44), Expr.Const(12.33))))
}

sealed class Expr {
    class Const(val number: Double) : Expr()
    class Sum(val e1: Expr, val e2: Expr) : Expr()
    object NotANumber : Expr()
}

fun eval(expr: Expr): Double = when(expr) {
    is Expr.Const -> expr.number
    is Expr.Sum -> eval(expr.e1) + eval(expr.e2)
    Expr.NotANumber -> Double.NaN
    // the `else` clause is not required because we've covered all the cases
}

위의 코드를 보아서는 이해가 되기는 하는데… 응용을 어떤 식으로 하면 좋을지는 저도 잘 모르겠습니다.

우선은 코틀린 설명에 나온 코드를 그대로 가져와 보았습니다.


마무리

코틀린 클래스를 정리해보았습니다. 기본 클래스 선언하는 방법부터 클래스 상속/다중 상속 등에 대해서 정리해보았습니다.

코틀린 클래스 문서에 잘 나와 있는 부분을 제 임의로 몇 개 더 추가하여 설명을 해보았습니다.

마지막의 Sealed Classes의 경우 C#에도 존재하는대 사용 방법은 조금 다르게 나와 있습니다.

궁금하신 분은 다음의 C# 설명 자료를 추가로 참고하시면 되겠습니다.


코틀린 관련 포스트 목록

추가 내용


Tae-hwan

Android, Kotlin .. Create a content development.