이번 시간에는 코틀린의 클래스에 대해 살펴보자. 가장 많이 사용할 것 같은 문법들만 소개하겠다. 나머지는 실제로 레퍼런스를 보면 되겠다. 레퍼런스에 있는 것을 다 하면 좋겠지만 그럴 여력이 없다. 기본적인 것과 자주 사용하는 것 혹은 조금 특이한 것들만 소개할 예정이다. 이전 중간에도 return, break,continue, 라벨 등은 건너 띄었다.

클래스

class Product {
}

기본적인 형태는 위와 같다. 자바와 비슷한 형태이지만 class 앞에 접근제한자가 없다. 대부분의 클래스는 public 이므로 기본값은 public 접근제한자 이다.

class Product

만약 클래스 본체가 없는 경우에는 컬리브레이스를 생략해도 된다.

생성자

코틀린의 생성자는 자바와 형태가 조금다르다.

class Product constructor(name: String) {

}

constructor 키워드를 사용해 생성자를 만들 수 있다. 자바와 동일하게 기본 생성자는 컴파일러가 만들어 주지만 생성자가 한개 이상 있을 때에는 기본생성자를 만들어 주지 않는다.

class Product(name: String) {

}

생성자 필드에 어노테이션이나 접근제한자가 존재 하지 않는다면 위와 같이 constructor 키워드를 생략할 수도 있다.

class Product constructor(name: String) {
    init {
        println(name)
    }
}

init 키워드를 이용해서 name에 접근 할 수 있다. 하지만 조작할 수는 없어 읽기 전용인 듯하다.

class Product constructor(name: String) {
    val length = name.length
    init {
        println(name)
        println(length)
    }
}

위와 같이 클래스 본체에서도 name에는 접근할 수 있다. 위와 동일하게 조작할 수는 없다. 그렇다면 다른점은 무엇일까? 흠

class Product private constructor(name: String) {
    val length = name.length
    init {
        println(name)
        println(length)
    }
}

생성자에 접근제한자 혹은 어노테이션을 선언하고 싶다면 위와 같이 constructor 키워드 앞에 작성해주면 된다.

class Product constructor(name: String) {
    constructor(name: String, price: Int) : this(name) {
        println(price)
    }
}

위와 같이 보조 생성자를 만들 수 있다. 조금 희한한건 생성자 필드 앞에 valvar 를 넣지 않으면 생성자 혹은 init 블럭, 클래스 바디에서만 접근 가능하다.

class Product constructor(name: String) {
    constructor(name: String, price: Int) : this(name) {
        println(price)
    }

    fun nameLength() : Int {
        return name.length  //컴파일 에러
    }
}

만약 위와 같은 코드가 있다면 nameLength() 함수 안에 name은 컴파일 에러가 발생한다.

class Product constructor(val name: String) {
}

생성자 필드에 valvar를 선언 할 수 있다 이 때에는 클래스 안에 있는 함수도 접근 가능하다. 마찬가지로 valfinal이 내포 되어 있으므로 읽기 전용이고 var는 읽기 쓰기 둘다 가능하다.

인스턴스 생성

val product = Product("iphone 7")

위와 같이 인스턴스를 생성 할 수 있다. 자바와 다른점은 new 키워드가 필요 없다.

상속

코틀린은 자바와 마찬가지로 최상위 클래스를 갖고 있다. Any라는 클래스가 코틀린의 최상위 클래스이다. 슈퍼타입을 지정하지 않으면 자동으로 모든 클래스는 Any를 상속하고 있다.

class NewProduct(name: String, price: Int) : Product(name, price) {

}

생성자는 자바와 동일하다. 상위 클래스의 생성자는 하위 클래스에도 존재 해야 된다. 하지만 위의 코드는 컴파일 에러가 발생한다. 기본적으로 코틀린의 클래스는 final이 내포 되어 있다. 다들 알겠지만 class의 final이 선언 되어 있으면 상속을 하지 못한다.

open class Product constructor(name: String) {
  //..
}

위와 같이 open 키워드를 사용하여 상속을 허용하도록 할 수 있다. 이것은 이펙티브 자바 item 17 계승을 위한 설계와 문서를 갖추거나 그럴 수 없다면 계승을 금지하라 를 채용하였다.

오버라이딩

open class Super {
    fun print(): String {
        return "super.print()"
    }
}

class Sub : Super() {
    override fun print(): String {
        return "sub.print()"
    }
}

자바와 동일하게 override 어노테이션을 이용해서 오버라이딩을 할 수 있다. 자바에서는 override 어노테이션을 생략해도 되지만 코틀린에서는 생략하면 컴파일 에러가 발생한다. 명시적이라 더 좋은듯 하다. 하지만 위의 코드는 컴파일 에러가 발생한 코드이다. 함수 역시 기본이 final이 내포 되어있다. 마찬가지로 상위 메서드에 open 키워드를 선언하면 오버라이딩 할 수 있다.

open class Super {
    open fun print(): String {
        return "super.print()"
    }

    final override fun toString(): String {
        return "super.toString()"
    }
}

만약 오버라이딩 한 메서드에 하위 클래스에게 오버라이딩을 허용하고 싶지 않다면 final 을 선언해주면 하위 클래스는 오버라이딩을 할 수 없다.

추상 클래스

abstract class Super {

    abstract fun foo()
}
class Sub : Super() {
    override fun foo() {

    }
}

자바와 마찬가지로 abstract 키워드를 사용해서 추상 클래스를 만들 수 있다. 또한 abstract 사용해서 추상함수를 만들 수 있다. 여기서는 open이라는 키워드가 별도로 필요 없다.

Companion Objects

조금 낯선 단어일 지 모른다. 하지만 스칼라를 했다면 익숙한 단어이다. 스칼라의 컴페니언 오브젝트와 비슷한 개념이다. 코틀린에서도 마찬가지로 static한 메서드 필드가 개념이 존재 하지 않는다. 하지만 비슷한 개념으로 컴페니언 오브젝트라는 것이 있는데 싱글톤 객체라고 생각하면 되겠다. 언어에서 싱글톤을 지원한다. 이런말이 있지 않는가? 어제의 패턴이 오늘의 언어가 되었다.

class Customer {
    companion object Factory {
        fun create() = Customer()
    }
}

위와 같이 companion object 를 만들 수 있다. 클래스 안에 companion object 키워드를 넣어 만들어 준다.

val create = Customer.create()

그리고 위와 같이 생성할 필요 없이 자바의 static 메서드 처럼 사용하면 된다. 다른 방법으로 companion object 명을 지정해주지 않아도 된다.

class Customer1 {
    companion object {
        fun create() = Customer1()
    }
}

val companion = Customer1.Companion
companion.create()

만약 object명을 지정해 주지 않으면 Companion라는 명을 사용해도 된다.

Sealed Classes

실드 클래스는 열거형 클래스의 확장이다. 실드 클래스의 하위클래스는 상태를 포함할 수 있는 여러 인스턴스를 가질 수 있다.
실드 클래스를 선언하려면 클래스 앞에 sealed를 선언하면 된다. 실드 클래스는 하위 클래스를 가질 수 있지만 모든 하위 클래스는 실드 클래스 선언 자체에 중첩해야 한다. 아래와 같이 말이다.

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
}

실드 클래스의 최대 장점은 when식과 함께 사용할 수 있다. 모든 경우를 확실하게 다루고 있다면 else 절을 추가할 필요가 없다.

print(eval(Expr.Sum(Expr.Const(99.1232), Expr.Const(1821.299))))

위의 형태로 사용할 수 있다.

이렇게 오늘은 클래스에 대해서 알아봤다. 다음 시간에는 프로퍼티와 필드 그리고 인터페이스에 대해서 알아보자.