Developer성현 2024. 9. 16. 14:20

Class

class는 속성과 함수로 이루어져 있습니다. 지금까지 사용했던 변수들도 모두 클래스로 이루어진 것이죠

이번에는 클래스를 직접 만들고 사용해 보겠습니다.

 

사용법은 함수와 비슷합니다.

class People(val name: String, val age: Int)

함수를 만들 때는 fun 을 사용한것처럼 클래스를 만들때는 class 키워드를 사용하시면 됩니다.

다음으로 클래스 이름을 작성하시면 되는데 함수와의 차이점은 이름 첫 글자가 함수는 소문자로 작성해야 했지만 클래스는 대문자로 시작해야 합니다.

그리고 소괄호 내부에 속성을 적으시면 됩니다. 여기서도 함수와의 차이점은 함수는 변수키워드를 작성하지 않고 기본으로 val로 선언이 되었지만 클래스는 직접 정의할 수 있습니다. 변수명을 짓는 것과 동일하게 하면 됩니다.

 

이제 클래스를 사용해 보겠습니다.

fun main() {
    val a = People("이름", 22)

    println("이름: ${a.name} 나이: ${a.age}")

}

class People(val name: String, val age: Int)

클래스 인스턴스를 생성하는 방법도 함수를 호출하는 방법과 동일합니다.

인스턴스스의 값을 읽는 방법은 인스턴스 변수명에. 을 붙이고 속성명을 붙여주시면 됩니다.

 

만약 속성값을 변경하고 싶은 경우에는 클래스를 생성할 때 val 대신 var을 사용하면 됩니다.

fun main() {
    val a = People(name = "이름")

    a.name = "코틀린"

    println("이름: ${a.name} 나이: ${a.age}")

}

class People(var name: String, val age: Int = 22)

또한 추가로 인스턴스를 생성할 때 속성값을 적을 때 속성값 명을 적은 뒤 대입 연산자로 넣어줘도 됩니다.

그리고 클래스를 선언 시 초기값을 지정해 줄 수 있으며 초기값이 지정되어 있으면 인스턴스를 생성 시 생략하면 초기값으로 생성이 됩니다.

 

처음에 변수들도 클래스로 만들어진 거라고 말씀드렸죠 그래서 변수마다 사용할 수 있는 함수들이 있어서 정수를 문자열로 바꿔주거나 문자열을 정수 또는 실수로 변환해 주는 함수를 제공해 줍니다.

마찬가지로 저의도 클래스 내부에 함수를 만들 수 있습니다.

 

한번 이름과 나이를 5번씩 출력해 주는 기능을 함수로 만들어 보겠습니다.

fun main() {
    val a = People(name = "이름")

    a.dataPrint()
}

class People(var name: String, val age: Int = 22){
    fun dataPrint(){
        println("이름: $name 나이: $age")
    }
}

클래스 내부에 지금까지 사용한 방식대로 함수를 작성하시면 됩니다.

 

 

생성자

자바에서도 클래스를 만들게 되면 기본으로 생성자를 작성하지 않아도 기본생성자가 만들어지게 됩니다.

코틀린에서도 클래스를 작성할 시 기본으로 생성자가 만들어지게 되는데요

자바와의 차이점은 자바는 인자값을 넘기기 위해서는 무조건 명시적으로 생성자를 만들어 줘야 합니다. 그리고 생성자를 명시적으로 만들면 객체가 생성될 시 기본으로 실행되는 초기화블록 도 만들어지게 됩니다.

하지만 코틀린은 인자를 넘기게 되더라도 초기화블록을 직접 만들어 줘야 합니다.

fun main() {
    val a = People(name = "이름")
}

class People(var name: String, val age: Int = 22){
    init {
        println("이름: $name 나이: $age")
    }
}

코틀린은 init 키워드를 이용해서 초기화 블럭을 만들 수 있습니다.

코드를 보시면 객체를 생성하면 init 초기화 블록이 실행되는 것을 볼 수 있습니다.

 

보조 생성자

보조 생성자는 주 생성자(init)에서 초기화하는 식에서 추가적인 로직을 구성해야 하거나 다른 인자값을 추가하거나 고정시켜야 하는 상황 등에 사용할 수 있습니다.

fun main() {
    val a = People(name = "이름", 22)
    val b = People(name = "이름")
}

class People(var name: String, val age: Int){
    init {
        println("이름: $name 나이: $age")
    }

    constructor(name: String): this(name, 20){
        println("보조 생성자")
    }
}

보조생성자는 constructor를 이용해서 생성할 수 있습니다.

중요한 것은 반드시 주 생성자 init을 호출해야 한다는 것입니다. 주 생성자는 this 키워드를 이용해서 호출할 수 있으며 보조생성자의 파라미터를 주 생성자가 인가값으로 받는 구조로 되어 있습니다. 하지만 위에서 보조생성자는 이름 인자값만 받고 나이 인자는 주 생성자를 호출하는 곳에서 고정시키고 있습니다.

 

즉 main에서 People 객체를 생성시킬 때 name과 age 가 같이 인자값으로 주어지면 주 생성자가 호출이 되고

name 만 인자값이 주어지면 보조 생성자가 호출되며 this키워드를 통해 주 생성자에게 name 인자값이 매개변수로 주어지고 age는 20으로 고정되어 주 생성자가 호출되게 됩니다.

 

추가로 보조생성자는 여러 개 만들 수 있습니다.

fun main() {
    val a = People(name = "이름", 22)
    val b = People(name = "이름")
    val c = People(name = "이름", "추가 보조생성자")
}

class People(var name: String, val age: Int){
    init {
        println("이름: $name 나이: $age")
    }

    constructor(name: String): this(name, 20){
        println("보조 생성자")
    }

    constructor(name: String, str: String): this(name, 20){
        println(str)
    }
}

 

상속

상속이란 기존에 있는 클래스의 기능을 가져다가 새로운 기능을 추가하거나 새로 기존에 있는 기능을 변경할 수 있는 것입니다. 간단한 예시 코드로 알아보겠습니다.

fun main() {
    ClassA().a()
    ClassB().b()

    ClassB().a()

}

open class ClassA(){
    open fun a(){
        println("나는 A Class")
    }
}

class ClassB() : ClassA(){
    fun b(){
        println("나는 B Class")
    }
}

ClassA, ClassB 2개의 클래스를 만들고 ClassA 에는 a함수, ClassB 에는 b함수를 정의했습니다.

하지만 ClassA 앞에 open이라는 키워드가 붙어 있습니다. 이는 ClassA를 상속할 수 있도록 정의를 하는 것입니다.

그리고 ClassB에 " : ClassA()"을 붙여 ClassA를

이제 ClassB 클래스는 ClassA 클래스를 그대로 사용할 수 있게 되었습니다.

 

main함수에서 각각 자신이 가지고 있는 함수 a와 b는 당연히 사용할 수 있고  ClassB는 ClassA에

 

오버라이딩

오버라이딩은 부모 클래스에 존재하는 함수를 다시 상속을 받은 클래스에서 동일한 이름과 매개변수를 가지는 함수를 재구현 하는 것을 말합니다.

fun main() {
    ClassA().a()
    ClassB().b()

    ClassB().a()

}

open class ClassA(){
    open fun a(){
        println("나는 A Class")
    }
}

class ClassB() : ClassA(){
    fun b(){
        println("나는 B Class")
    }
    override fun a(){
        println("나는 재구현 된 A Class 함수")
    }
}

재구현 하고자 하는 함수에 open을 붙이면 재구현을 할 수 있도록 허용이 됩니다.

그리고 상속받는 클래스에서 재구현 허고자 하는 함수 앞에 override를 붙이고 함수를 재구현 하시면 상속을 받는 클래스틑 통해 해당 함수를 실행시키면 재구현 된 함수가 실행이 됩니다.

 

추상클래스

위에서는 클래스에 함수를 정의해 놓고 상속을 받아 사용하거나 재구현을 하여 사용하는 방법을 알아보았는데요

추상 클래스는 함수의 내용을 구현하지 않고 정의만 하는 것입니다. 그리고 상속받은 클래스에서 구현부를 만들어 사용하는 것입니다.

fun main() {
    ClassA().b()
}

abstract class AbstractA(val test: Int){
    abstract fun a()
    fun b(){
        println("구현된 함수 b")
    }
}

class ClassA : AbstractA(1){
    override fun a(){
        println("추상함수 정의")
    }
}

추상 클래스는 클래스 앞에 abstract 키워드를 붙이면 됩니다.

추상 클래스 내부에는 추상함수와 일반함수를 정의할 수 있는데요 추상 함수에는 클래스 앞에 붙인 것처럼 abstract 키워드를 붙여주면 됩니다. 구현부가 없는 추상 함수는 {}를 붙이면 안 됩니다.

그리고 추상클래스를 상속받는 방법은 일반 클래스와 동일합니다. 중요한 것은 추상클래스는 반드시 상속을 받는 클래스에서 구현을 해주어야 합니다. 

 

 

추상클래스는 일반 클래스와 완전히 동일하지만 단지 함수를 추상화시킬 수 있다는 것이 중요합니다.

 

인터페이스

인터페이스란 추상클래스처럼 추상함수를 만들 수 있는 점에서는 비슷하지만 그 외에는 다른 특징들을 가지고 있습니다.

fun main() {
    ClassA().a()
    ClassA().b()
    ClassA().c()
    ClassA().d()
}

interface InterfaceA{
    fun a()
    fun b(){
        println("재구현 가능 함수b")
    }
}

interface InterfaceB{
    fun c()
    fun d(){
        println("재구현 가능 함수 d")
    }
}

class ClassA() : InterfaceA, InterfaceB{
    override fun a() {
        println("추상함수 a 구현")
    }

    override fun c() {
        println("추상함수 c 구현")
    }

    override fun b() {
        println("함수 b 재구현")
    }
}

인터페이스는 자체적으로 생성자를 만들 수 없습니다. 그리고 클래스, 추상클래스 모두 상속을 하나밖에 받지 못하는 반면 인터페이스는 여러 개를 상속받을 수 있습니다.

또한 추상클래스에서 재정의 함수를 만들기 위해서는 open, 추상함수를 만들기 위해서는 abstract 키워드를 사용해야 하지만 인터페이스는 2가지 키워드를 사용하지 않아도 자동으로 추상, 재정의 함수가 만들어지게 됩니다.

모든 구현부가 있는 함수는 재수현이 가능한 함수로 정의되고 구현부가 없는 함수는 추상함수로 정의됩니다.

 

이상으로 클래스에 관한 설명을 마치겠습니다. 감사합니다.