Study Record

[Kotlin] 클래스와 상속, 추상 클래스 본문

안드로이드/Kotlin

[Kotlin] 클래스와 상속, 추상 클래스

초코초코초코 2023. 7. 9. 20:56
728x90

 

😶 클래스 선언

클래스는 class 키워드와 클래스 이름, 클래스 헤더(파라미터, 생성자 등을 포함), 클래스 바디({} 안에 들어가는 내용) 로 이루어져 있다. 헤더와 바디는 선택적 옵션으로 없어도 무방하다.

class Person {
  /* class body */
}

 

 

 

😶 생성자(Constructors)

Kotlin 에서 생성자는 Primary 생성자와 하나 이상의 Secondary 생성자들을 가질 수 있다. Primary 생성자는 클래스 헤더에 속하며 클래스 이름 앞, 타입 파라미터 옵션 앞에 붙는다. Primary 생성자의 constructor 키워드는 생략 가능하다.

// primary 생성자
class Person constructor(firstName: String) {}

// 키워드 생략 가능
class Person(val firstName: String) {}

 

Primary 생성자에 있는 파라미터들은 속성을 초기화할때 사용할 수 있다.

class Person(name: String) {
    val psersonName: String = name.uppercase()
}

예시에서 파라미터로 받은 name 은 클래스 속성으로 취급되지 않는다. 따라서 Person 인스턴스가 name 변수를 참조하려 하면 에러가 난다.

class Person(name: String) {
    // 에러
    fun getName(): String = name
}

fun main() {
    val person = Person("HAHAHA")
    // 에러
    person.name
}

 

대신, Primary 생성자의 파라미터 앞에 val, var 와 같은 키워드를 포함하면 클래스 속성으로 바로 선언할 수 있다.

class Person(val name: String) {}

 

 

속성 default 값 설정

속성 선언 뒤 "=" 기호 뒤에 원하는 기본 값을 입력할 수 있다.

class Person(val name: String, val isEmployed: Boolean = false)

 

+ init

Primary 생성자에는 코드를 사용할 수 없다. 따라서 객체가 생성될 때 초기화할 내용이 있다면 init 블록에 적으면 된다. init 블록은 여러 개 존재할 수 있는데 위에서부터 순차적으로 실행된다.

class Person() {
    
    init{
        println("First initializer block")
    }
    
    init{
        println("Second initializer block")
    }
}

 

 

 

😶 Secondary 생성자(Constructor)

앞에서 Primary 생성자외에 다른 생성자들을 포함할 수 있다. 클래스 바디안에 Primary 생성자를 제외한 constructor 블록으로 정의된 생성자를 모두 Secondary Constructor 라고 부른다.

 

주의해야 할 점이 있는데 Primary Constructor 가 없는 경우(클래스 헤더가 없을 수도 있다) 는 상관없지만 존재한다면 반드시 Secondary Constructor 는 "this" 키워드를 이용하여 Primary Constructor 에게 생성의 일부를 위임해야 한다.

class Person(val name: String) {
    
    constructor(name: string, age: Int) : this(name) {
        println("name : ${name}, age: ${age}")
    }
}

 

Primary Constructor 는 파라미터 앞에 val, var 키워드를 붙여 간편하게 속성으로 정의할 수 있었다. 하지만 Secondary Constructor 는 불가능하다.

 

객체가 생성되면서 Primary Constructor 과정이 가장 먼저 실행된다. 초기화 블록을 포함한 코드들은 모두 Primary Constructor 과정에 포함되기 때문에 Secondary Constructor 는 모든 초기화 블록 실행 후 실행된다.

class Person constructor(firstName: String) {
    val firstProperty = "First property: $firstName".also(::println)
    
    init {
        println("init")
    }
    
    constructor(name: String, age: Int) : this(name) {
        println("second constructor : ${name} , ${age}")
    }
    
    val secondProperty = "Second property".also(::println)
}	

fun main() {
	val person: Person = Person("hahaha", 4)
}
First property: hahaha
init
Second property
second constructor : hahaha , 4

secondProperty 가 Secondary Constructor 보다 먼저 실행된다. 나머지 초기화 과정은 위에서부터 순차적으로 실행된다.

 

 

😶 객체 선언하기

클래스의 인스턴스를 생성하는 과정에서 new 키워드는 사용하지 않는다.

class Invoice() {}

fun main(){
    val inVoice = Invoice()
}

 

 

😶 클래스 계층 구조

채소와 같이 광범위한 카테고리가 있고 그 카테고리 안에 콩류라는 좀 더 구체적인 유형의 카테고리가 있고 그 안에 완두콩, 강낭콩, 병아리콩 등으로 나눌 수 있다. 이렇게 속성과 동작이 비슷한 항목을 카테고리로 분류하고 그룹 내에서 일정 유형의 계층 구조를 만들 수 있다. 

 

콩류 클래스는 채소 클래스를 상속받아 만들면 채소 클래스는 Super 클래스(부모 클래스)가 되고 콩류 클래스는 Child 클래스(자식 클래스)가 된다. 똑같이 강낭콩, 완두콩 클래스가 콩류 클래스를 상속받아 만들어지면 콩류 클래스는 강낭콩, 완두콩 클래스의 Super 클래스이자 Child 클래스가 된다.

 

 

 

😶 Any 클래스

Kotlin 의 모든 클래스는 공통 super 클래스 Any 를 가지고 있다. Any 클래스는 슈퍼 클래스가 선언되지 않은 클래스의 기본 super 클래스이다. Any 클래스에는 기본적으로 equals(), hashCode(), toString() 과 같은 함수들이 포함되어 있어 어떤 클래스에서든 사용할 수 있다.

/*
 * Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
 * that can be found in the LICENSE file.
 */
public open class Any {
    /**
     * Indicates whether some other object is "equal to" this one. Implementations must fulfil the following
     * requirements:
     *
     * * Reflexive: for any non-null value `x`, `x.equals(x)` should return true.
     * * Symmetric: for any non-null values `x` and `y`, `x.equals(y)` should return true if and only if `y.equals(x)` returns true.
     * * Transitive:  for any non-null values `x`, `y`, and `z`, if `x.equals(y)` returns true and `y.equals(z)` returns true, then `x.equals(z)` should return true.
     * * Consistent:  for any non-null values `x` and `y`, multiple invocations of `x.equals(y)` consistently return true or consistently return false, provided no information used in `equals` comparisons on the objects is modified.
     * * Never equal to null: for any non-null value `x`, `x.equals(null)` should return false.
     *
     * Read more about [equality](https://kotlinlang.org/docs/reference/equality.html) in Kotlin.
     */
    @GCUnsafeCall("Kotlin_Any_equals")
    external public open operator fun equals(other: Any?): Boolean

    /**
     * Returns a hash code value for the object.  The general contract of `hashCode` is:
     *
     * * Whenever it is invoked on the same object more than once, the `hashCode` method must consistently return the same integer, provided no information used in `equals` comparisons on the object is modified.
     * * If two objects are equal according to the `equals()` method, then calling the `hashCode` method on each of the two objects must produce the same integer result.
     */
    @OptIn(ExperimentalNativeApi::class)
    public open fun hashCode(): Int = this.identityHashCode()

    /**
     * Returns a string representation of the object.
     */
    public open fun toString(): String {
        val className = this::class.fullName ?: "<object>"
        // TODO: consider using [identityHashCode].
        val unsignedHashCode = this.hashCode().toLong() and 0xffffffffL
        val hashCodeStr = unsignedHashCode.toString(16)
        return "$className@$hashCodeStr"
    }
}

 

 

 

😶 상속

기본적으로 코틀린 클래스는 상속할 수 없다. 따라서, "open" 키워드를 앞에 붙여 상속 가능한 클래스라고 표시해줘야 한다. 상속하는 클래스가 생성자를 가지고 있을 경우 생성자를 초기화해줘야 한다.

open class Base(p: Int)

class Derived(p: Int) : Base(p)

 

 

Primary 생성자 외에 Sceondary 생성자가 존재할 경우 super 키워드를 이용하여 상속한 클래스를 초기화한다.

open class Person {
    constructor(name: String) {
        println("second constructor : ${name}")
    }
    constructor(name: String, age: Int) {
        println("second constructor : ${name} , ${age}")
    }
}	

class Student(firstName: String) : Person(firstName) {
    constructor(name: String, age: Int) : super(name, age)
}

fun main() {
    val student = Student("name", 3)
}
Primary constructor call expected

하지만 다음 예시처럼 Secondary Constructor 로 super 키워드를 이용하면 에러가 나온다. Priamry 생성자를 불러오길 기대한다는 에러인데 Seondary Constructor 를 설명할 때 Primary Constructor 가 클래스에 존재한다면 this 키워드로 생성의 일부를 위임해야 한다고 설명했다.

 

따라서, 이런 경우 Studnet 클래스의  Primary 생성자를 없애고(Primary 생성자는 없어도 무방) Secondary 생성자로 두개의 생성 경우를 모두 정의하면 된다.

open class Person {
    constructor(name: String) {
        println("second constructor : ${name}")
    }
    constructor(name: String, age: Int) {
        println("second constructor : ${name} , ${age}")
    }
}	

class Student : Person {
    constructor(name: String) : super(name)
    constructor(name: String, age: Int) : super(name, age)
}

fun main() {
    val student = Student("name", 3)
}

 

 

 

😶 () 가 있고 없고의 차이

class Person() {}
class Person {}

 

클래스 이름앞에 () 가 붙고 안 붙고에 차이가 있다. () 가 붙으면 Primary Constructor 를 선언한다는 의미이고 () 가 없으면 Person 클래스의 Primary Constructor 는 존재하지 않는다는 의미다.

 

* 원래는 class Person constructor() {} 라고 선언해야 하지만 constructor 키워드는 생략이 가능하므로 class Person() {} 이런 형태가 허락된다. 

 

 

이것이 의미하는 바는 크다. 위의 상속에서 다룬 예시에서 Student 클래스 이름 뒤에 () 가 붙었다면 에러가 나온다.

open class Person {
    constructor(name: String) {
        println("second constructor : ${name}")
    }
    constructor(name: String, age: Int) {
        println("second constructor : ${name} , ${age}")
    }
}	

class Student() : Person {
    constructor(name: String) : super(name)
    constructor(name: String, age: Int) : super(name, age)
}

fun main() {
    val student = Student("name", 3)
}

 

Primary constructor call expected

Student 클래스에 () 가 붙어있는 것은 Primary 생성자가 존재한다는 의미이고 이는 Secondary 생성자가 Primary 생성자에게 위임해야 하는데 하지 않았기 때문에 오류를 뱉는다.

 

 

반대로 상속받는 Person 이름 뒤에 () 가 붙는다면,

open class Person {
    constructor(name: String) {
        println("second constructor : ${name}")
    }
    constructor(name: String, age: Int) {
        println("second constructor : ${name} , ${age}")
    }
}	

class Student : Person() {
    constructor(name: String) : super(name)
    constructor(name: String, age: Int) : super(name, age)
}

fun main() {
    val student = Student("name", 3)
}
Supertype initialization is impossible without primary constructor

Person 클래스에는 Primary 생성자가 존재하지 않는데 Pserson() 으로 Primary 생성자를 호출하니 에러를 뱉는다.

 

 

 

😶 재정의(override)

클래스를 상속했을 때 부모 클래스의 맴버 변수나 함수를 변경하고 싶을 때가 있다. 이럴 때 override 키워드를 사용하여 상속받은 클래스에서 변경할 수 있다. 단, open 키워드를 포함한 멤버만 변경할 수 있다.

open class Shape {
    open fun draw() { /*...*/ }
    fun fill() { /*...*/ }
}

class Circle() : Shape() {
    override fun draw() { /*...*/ }
}

 

override 키워드로 변경한 함수는 저절로 open 키워드를 포함한 것처럼 변경할 수 있다. 더이상 하위 클래스에서 변경하지 않길 원한다면 final 키워드를 앞에 붙이면 된다.

open class Shape {
    open fun draw() { /*...*/ }
    fun fill() { /*...*/ }
}

class Circle() : Shape() {
    final override fun draw() { /*...*/ }
}

이제 Circle 클래스를 상속받은 클래스는 draw 함수를 재정의할 수 없다.

 

 

멤버 변수 override

override 키워드를 사용하여 부모 클래스의 멤버변수를 재정의할 수 있는데 val 을 var 타입으로 변경하거나 var 을 val 타입으로 변경할 수도 있다. 마찬가지로 재정의(override)가능한 멤버 변수 앞에 open 키워드를 적어야 한다.

open class Person {
    open val age: Int = 19
    open val name: String = "홍길동"
}

class Student : Person() {
    override val age: Int = 8
    override var name: String = "아무개"
}

 

 

만약 상속을 받은 맴버끼리 이름이 겹칠 때 super<클래스 이름> 으로 어떤 클래스의 멤버 변수 혹은 함수를 사용할 건지 특정할 수 있다

open class Rectangle {
    open fun draw() { /* ... */ }
}

interface Polygon {
    fun draw() { /* ... */ } // interface members are 'open' by default
}

class Square() : Rectangle(), Polygon {
    // The compiler requires draw() to be overridden:
    override fun draw() {
        super<Rectangle>.draw() // call to Rectangle.draw()
        super<Polygon>.draw() // call to Polygon.draw()
    }
}

 

 

 

toString() 메서드 재정의

객체 인스턴스를 출력하면 객체의 toString() 메드가 호출된다. Kotlin 에서 모든 클래스가 자동으로 toString() 메서드를 상속하며 기본 구현에서는 인스턴스의 메모리 주소가 있는 객체 유형을 반환한다. 따라서, 클래스를 만들 때 toString() 을 좀 더 의미있는 값으로 바꾸고 싶다면 toString() 을 재정의해야 한다.

class Noodles : Item("Noodles", 10) {
   override fun toString(): String {
       return name
   }
}

 

 

😶 추상 클래스

추상 클래스는 완전히 구현되지 않아 인스턴스화할 수 없는 클래스이다. 일반적으로 슈퍼 클래스를 추상 클래스로 만들어 모든 서브클래스에 공통적인 속성과 함수를 포함하게 할 수 있다.

 

추상 클래스 선언은 abstract 키워드로 시작한다.

abstract class Dwelling(private var residents: Int){
    abstract val buildingMaterial: String
}

 

 

추상 클래스 상속

보통의 상속과 같지만 open 키워드를 추상 클래스 앞에 붙이지 않아도 된다는 점이 다르다.

abstract class Polygon {
    abstract fun draw()
}

class Rectangle : Polygon() {
    override fun draw() {
        // draw the rectangle
    }
}

 

 

 

 

Inheritance | Kotlin

 

kotlinlang.org

 

Classes | Kotlin

 

kotlinlang.org

 

728x90

'안드로이드 > Kotlin' 카테고리의 다른 글

[Kotlin] vararg 키워드  (0) 2023.07.17
[Kotlin] null safety  (0) 2023.07.11
[Kotlin] 조건부  (0) 2023.07.03
[kotlin] 기본 용어 (변수, 함수, 주석, repeat())  (0) 2023.06.30
[Kotlin] Collection  (0) 2022.04.07