클래스
코틀린에서 클래스는 class 키워드를 사용해서 선언한다.
class Persion{/* ... *}
클래스 선언은 클래스 이름, 클래스 해더(유명 매개변수, 기본 생성자 및 기타 항목 지정) 및 중괄호로 묶인 클래스 본문으로 구성된다. 헤더와 본문 모두 선택사항이다. 만약 클래스에 본문이 없으면 중과호를 생략할 수 있다.
class Empty
생성자(Constructors)
코틀린의 클래스는 기본(primary) 생성자와 하나 이상의 보조(secondary) 생성자를 가질 수 있다.
기본생성자는 클래스 헤더의 한 부분으로 클래스 이름 뒤에 위치한다
class Person constructor(firstName: String){}
기본생성자 뒤에 어노테이션이나 가시성 수식어(접근제어자)가 없으면 constructor키워드 생략 할 수 있다.
class Person (firstName: String){}
기본생성자는 코드를 포함할 수 없다. 초기화 코드는 init 키워드 접두사가 있는 초기화 블록에 배치할 수 있다.
인스턴스를 초기화하는 동안 init 블록은 속성 이니셜라이저와 함께 작성된 순서대로 실행된다.
class Person (name: String){
val firstProperty = "First property: $name".also(::println)
init{
println("First 선언 블록 = $name")
}
val secondProperty = "Second property : ${name.length}".also(::println)
init{
println("Second 선언 블록 = ${name.length}")
}
}
First property: hello
First 선언 블록 = hello
Second property : 5
Second 선언 블록 = 5
클래스 본문에 선언된 속성 이니셜라이저에서도 사용할 수 있습니다.
class Customer(name: String){
val customerKey = name.uppercase()
}
기본생성자에서 프로퍼티를 선언하고 초기화하기 위한 간결한 구문이 있다.
class Person(val firstName: String, val lastname: String, var age: Int){}
위 선언에는 클래스 속성의 기본값을 제시할 수 있다.
class Person(val firstName: String, val lastname: String, var age: Int = 10){}
기본생성자의 속성이 2가지 이상인 경우 후행 쉼표를 사용해 깔끔하게 작성할 수 있다.
class Person(
val firstName: String,
val lastname: String,
var age: Int = 10
){
...
}
일반 속성과 마찬가지로 읽기 전용(val)과 변경가능(var) 둘 다 가능하다.
만일 생성자에 어노테이션이나 가시성 수식어가 있는 경우 constructor 키워드가 필요하고 수정자가 그 앞에 온다.
class Customer public @Inject constructor(name: String){ ... }
보조 생성자(Secondary constructors)
보조 생성자는 constructor를 접두사로 붙인다.
class Person(val pets: MutableList<Pet> = mutableListOf())
class Pet {
constructor(owner: Person) {
owner.pets.add(this) // 오너의 펫에 넣는다.
}
}
클래스에 기본생성자가 있다면, 각 보조 생성자는 직접 또는 다른 보조 생성자를 통해 간접적으로 기본 생성자에게 위임해야 한다. 동일한 클래스의 다른 생성자에 대한 위임은 this 키워드를 사용하여 수행된다.
fun main(){
val p = Person("hi im ddddddddddd")
Person("hello", p)
Person("world", 2)
}
class Person(var name: String) {
val children: MutableList<Person> = mutableListOf()
init {
name += "test"
}
//1번
constructor(name: String, parent: Person) : this(name) {
parent.children.add(this)
println("name : $name")
println("parent name : ${parent.name}")
parent.children.forEach{
println("children name : ${it.name}")
}
}
//2번
constructor(name: String, num: Int) : this(name){
println("name: $name | num : $num")
println("name: ${this.name} | num : $num")
}
}
//1번
name : hello
parent name : hi im dddddddddddtest
children name : hellotest
//2번
name: world | num : 2
name: worldtest | num : 2
조금 자세히 설명하자면 생성자 오버로딩과 유사하다고 볼 수 있다. 1번과 2번의 차이는 2번째 인자의 차이뿐이다. 2번째 인자가 Person객체인지, 정수인지에 따라 다르게 호출되는 것이다.
this(name)의 역할은 보조 생성자를 기본 생성자에게 위임(기능을 떠넘김, 기본 생성자의 일부가 됨) 하는 동시에 보조 생성자에서 기본 생성자의 초기화 블록 결과물을 사용할 수 있게 한다. 2번 예제를 보면 알 수 있다.
기본 생성자에 대한 위임은 보조 생성자의 첫 번째 문에 액세스 하는 순간 발생하므로 모든 초기화 블록 및 속성 초기화 프로그램의 코드는 보조 생성자의 본문보다 먼저 실행된다.
클래스에 기본 생성자가 없어도 암묵적으로 위임은 일어나며 초기화 블록은 계속 실행된다.
fun main(){
Constructors(1)
}
class Constructors {
init {
println("Init block")
}
constructor(i: Int) {
println("Constructor $i")
}
}
Init block
Constructor 1
어떠한 생성자도 없다면 public 한 인자 없는 생성자(기본 생성자)를 생성한다.
클래스의 인스턴스 생성(Creating instances of classes)
객체 생성과 같은 말이다. 자바와 다르게 new를 사용하지 않는다.
val invoice = Invoice()
val customer = Customer("Joe Smith")
클래스 멤버(Class member)
클래스 멤버에는 다음 것들이 있다.
- 생성자와 초기화 블록(constructor, init {})
- 함수(fun)
- 속성(property)
- 중첩 및내부 클래스
- 오브젝트 선언
추상 클래스
abstract로 선언할 수 있다. 특징으로 해당 클래스의 구현을 갖지 않는다. 인터페이스와 차이점은 상태를 가질 수 있다는 점이다.
abstract class Polygon {
abstract fun draw()
}
class Rectangle : Polygon() {
override fun draw() {
// draw the rectangle
}
}
비 추상 open 멤버를 추상으로 재 정의 할 수 있다. (open은 상속에서 자세히 나옴)
open class Polygon {
open fun draw() {
// some default polygon drawing method
}
}
abstract class WildShape : Polygon() {
// Classes that inherit WildShape need to provide their own
// draw method instead of using the default on Polygon
abstract override fun draw()
}
컴페니언 오브젝트
코틀린은 자바와 C#과 달리 정적 메서드가 없다. 클래스 변수나, 인스턴스 없이 클래스 내부에 접근해야 하는 함수를 작성해야 한다면 컴페니언 오브젝트로 작성할 수 있다.
클래스 이름만 한정자로 사용하여 해당 멤버에 액세스 할 수 있다.
class Test{
companion object{
val test = 1
var ttest = 2
}
}
상속(Ingeritance)
코틀린의 모든 클래스는 최상위 클래스인 Any를 상속한다. 상위 타입이 없다면 기본 Any를 상속한다.
Any는 java.lang.Object가 아니다. 특히 Any는 equals(), hashCode(), toString() 이외에 다른 멤버를 갖지 않는다.(바꿔 말하면 모든 클래스에는 3가지 메서드가 구현되어 있다는 소리다)
기본적으로 코틀린의 클래스의 기본상속 변경자는 final(상속 불가)이다. 클래스가 상속 가능하도록 만드려면 open 키워드를 클래스 앞에 붙여주면 된다.
기본상속 변경자는 3가지 이다.
- final : 상속이 불가능, 기본 변경자이며 생략 가능하다
- open : 상속이 가능하다, 무분별한 상속을 막기 위해 상속을 위해 open이 필수적
- abstract : 반드시 상속을 해야 한다, 추상 클래스로 공통화된 메서드와 프로퍼티를 정의할 때 사용
open class Base(p: Int)
상속받으려면 클래스 헤더 뒤에 콜론을 붙이고 부모 타입을 적어주면 된다.
class Derived(p:Int) : Base(p)
파생 클래스(Derived)에 기본 생성자가 있는 경우 매개 변수에 따라 기본 생성자에서 기본 클래스를 초기화할 수 있으며 초기화해야 한다.
파생 클래스에 기본 생성자가 없다면, 각 보조 생성자에 super 키워드로 기반 타입을 초기화하거나 초기화돼 있는 다른 생성자를 호출해야 한다.
주요 생성자가 있으면 this로 기반 타입을 초기화해 주면 되고, 없으면 super로 상위 클래스의 것을 가져오면 된다.
class MyView : View{
constructor(ctx: Context) : super(ctx)
constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}
오버라이딩 방법(Overriding methods)
메서드 오버 라이딩은 명시적으로 해야 한다. '나는 이 메서드를 오버라이딩 할 것입니다'라고 미리 말해줘야 한다는 것이다
오버라이딩 가능한 멤버는 open식별자로 명시해 줄 수 있다.
open class Base{
open fun v(){} //오버라이딩 가능
fun nv() {} //불가능
}
class Derived() : Base(){
override fun v(){} // 하지 않을 경우 에러 발생!!!
override fun nv(){} //컴파일 에러 발생!!!!
}
Base 하위 클래스에서 override 없이 open식별자가 없는 함수와 같은 시그니처로 함수를 정의하는 것은 할 수 없다.
만약 Derived의 하위 클래스에서 오버 라이딩을 막고 싶다면 final키워드를 통해 제한할 수 있다.
class Derived() : Base(){
final override fun v(){}
}
속성 재정의(overriding property)
프로퍼티 오버 라이딩의 방법은 메서드랑 똑같다. open키워드를 붙여주면 된다.
주의할 점은 val 속성을 var 속성으로 재정의 할 순 있지만 그 반대는 안된다.
왜냐하면 var 재정의는 get메서드 밖에 없는 val 속성에 set메서드를 선언하기 때문이다.
open class Foo{
open val x: Int get(){ ... }
}
class Bar1 : Foo(){
override var x : Int = ...
}
기본 생성자에 선언한 프로퍼티도 override 할 수 있다.
interface Foo{
val count: Int
}
class Bar1(override val count: Int) : Foo //주요생성자에서 오버라이딩 했다.
class Bar2 : Foo{
override var count: Int = 0
}
파생 클래스 초기화 순서(Derived class initialization order)
상속받은 파생 클래스의 새 인스턴스를 생성하는 동안 부모 클래스 초기화가 첫 번째 단계로 수행된다.(기본 클래스의 인수 평가만 먼저 실행). 즉 파생 클래스가 초기화되기 전에 실행된다.
fun main(){
Derived("test", "hello")
}
open class Base(val name:String){
init{
println("Base Class 초기화 중")
}
open val size: Int = name.length.also { println("Base Class 인자 길이 : $it") }
}
class Derived(
name: String,
private val lastName: String
) : Base(name.replaceFirstChar { it.uppercase() }.also { println("Base Class의 인자 : $it") }){
init {
println("Derived Class 초기화 중")
}
override val size: Int = (super.size + lastName.length).also { println("Derived Class 인자 길이 : $it") }
}
Base Class의 인자 : Test
Base Class 초기화 중
Base Class 인자 길이 : 4
Derived Class 초기화 중
Derived Class 인자 길이 : 9
Base()가 먼저 초기화되고 Derived()가 초기화됨을 볼 수 있다.
위 결과로 알 수 있는 것은 부모 클래스의 생성자가 실행될 때 파생 클래스에서 선언되거나 재정의 된 속성이 아직 초기화되지 않았음을 의미한다. 파생 클래스의 속성을 부모 클래스 초기화 로직에 사용하는 것은 런타임 실패와 같은 잘못된 결과를 초래할 수 있다. 따라서 부모 클래스를 설계할 때 생성자, 속성 초기화 또는 init {} 안에서 open 멤버를 사용하는 것을 피해야 한다.
상위 클래스 구현 호출
자바와 마찬가지로 하위 클래스는 super키워드를 통해 상위 클래스에 접근할 수 있다.
open class Foo {
open val x: Int
get() = 1
open fun f() {
println("Foo.f()")
}
}
class Bar : Foo() {
override fun f() {
super.f()
println("Bar.f()")
}
override val x: Int
get() = super.x
}
내부 클래스는 외부 클래스의 이름으로 한정된 라벨을 사용하여 외부 클래스의 상위 클래스에 접근할 수 있다.
class Bar : Foo(){
override fun f() { ... }
inner class Baz{
fun g () {
super@Bar.f() // Foo의 f()
}
}
}
재정의 규칙(Overriding rules)
오버라이딩 시 상위 클래스에서 시그니처가 같은 멤버를 여러 개 상속받으면, 반드시 멤버를 재정의하고 'super <A>'과 같이 상위 클래스의 이름을 꺽쇠 괄호 안에 넣어 한정시켜줘야 한다.
open class A{
open fun f() {print("A")}
}
interface B {
fun f() {print("B")}
}
class C() : A(), B{
override fun f() { //시그니처가 같은 멤버를 오버라이딩
super<A>.f()
super<B>.f()
}
}
참고
Classes | Kotlin
kotlinlang.org
Inheritance | Kotlin
kotlinlang.org