코틀린에서 클래스를 선언하기 위해서는 class 키워드가 필요합니다. class 키워드 뒤에 클래스의 이름을 적어주면 클래스를 선언할 수 있습니다.
class Bird
class Bird { }
이렇게 만들어진 클래스는 빈 클래스입니다. 클래스 내부에 여러가지 프로퍼티와 메서드를 정의하여 다양한 기능을 클래스로 구현이 가능합니다.
class Bird {
var name = "bird" // 프로퍼티
fun fly() { println("i can fly") } // 메서드
}
생성자란 클래스를 통해 객체가 만들어질 때 기본적으로 호출되는 함수를 말합니다.
주 생성자는 클래스 이름 뒤에 선언할 수 있고, 부 생성자는 클래스의 내부에서 선언할 수 있습니다.
생성자를 만들지 않으면 default 생성자를 컴파일러가 생성해줍니다.
부 생성자는 클래스의 본문에 함수처럼 선언합니다.
class Bird {
val name: String
constructor(name: String) { this.name = name }
}
생성자를 사용하면 프로퍼티를 생성자에서 초기화 할 수 있습니다.
부 생성자는 한 클래스 내부에 여러개 존재 할 수 있습니다. 하지만 각 부 생성자들의 인자가 똑같아서는 안됩니다.
class Bird {
val name: String
val age: Int
constructor(name: String) { this.name = name; this.age = 0 }
constructor(name: String, age: Int) { this.name = name; this.age = age }
}
이렇게 생성자를 선언하고 호출 시에 인자의 개수에 따라 다른 생성자가 호출됩니다.
Bird("bird") // 첫번째 부 생성자 실행
Bird("bird", 1) // 두번쨰 부 생성자 실행
주 생성자는 클래스 선언부에 생성자를 함께 선언하는 문법을 사용합니다. 이 때, 생성자에 가시성 변경자가 없다면 constructor를 생략할 수 있습니다.
class Bird constructor(name: String) {
...
}
class Bird(name: String) {
...
}
class Bird private (name: String) { // << compile error!
...
}
class Bird private constructor(name: Strinng) { // << Ok!
...
}
주 생성자에서 매개변수에 val, var키워드를 붙여서 프로퍼티의 선언과 초기화를 동시에 할 수 있습니다.
class Bird(val name: String) {
...
}
주 생성자는 클래스 이름 뒤에 선언 합니다. 따라서 본문을 부 생성자처럼 이름 옆에 바로 붙여 쓸 수 없습니다.
따라서 클래스 내부에 init 키워드를 사용하여 주 생성자의 본문을 작성할 수 있습니다.
class Bird constructor(name: String) {
val name: String
val age: Int
init {
this.name = name
this.age = 0
}
}
init 블럭은 클래스 내부에 여러개 존재할 수 있고 위에서부터 차례대로 실행됩니다.
함수에 디폴트 매개변수를 지정했던것처럼 프로퍼티에도 기본값을 지정할 수 있습니다.
class Bird(val name: String = "") {
val age: Int
init {
this.age = 0
}
이렇게 기본값을 지정하면 해당 매개변수를 생략하여 호출할 수 있습니다.
val bird = Bird()
이제 위에서 만든 Bird 클래스를 사용하여 하위 클래스 Parrot 클래스를 만들어보겠습니다.
상속을 하기 위해서는 기반 클래스 앞에 open 키워드를 붙여줘야 합니다. 상속을 위한 기본 구조는 다음과 같습니다.
open class 클래스 이름 {
...
}
class 클래스 이름 : 상속 할 클래스 이름() {
...
}
이제 Bird클래스를 상속받는 파생 클래스 Parrot을 정의해보겠습니다.
oopen class Bird(name: String, var age: Int) {
val name: String
init {
this.name = name
}
constructor(name: String): this(name, 0)
fun fly() = println("i can fly")
}
class Parrot(name: String, age: Int) : Bird(name, age) {
var language = ""
constructor(name: String, age: Int, language: String): this(name, age) {
this.language = language
}
fun speak() = println("i can speak $language")
}
fun main() {
val parrot = Parrot("parrot", 10, "Korean")
println("name: ${parrot.name}, age: ${parrot.age}")
parrot.speak()
parrot.fly()
}
Bird 클래스를 상속 받은 Parrot 클래스는 Bird 클래스의 프로퍼티와 메서드를 공유합니다. 따라서 name과 age 프로퍼티를 사용할 수 있고 fly메서드도 사용 가능합니다.
또한 Parrot 클래스에서 새로운 프로퍼티나 메서드를 정의할 수 있습니다.
같은 클래스 내에서 동일한 이름의 메서드를 인자만 다르게 구현하는 것을 오버로딩이라고 합니다. 이를 통해 같은 행위를 다양한 인자에 대해서 동작하게 만들 수 있습니다.
fun add(x: Int, y: Int): Int {
return x + y
}
fun add(x: Double, y: Double): Double {
return x + y
}
fun add(x: Int, y: Int, z: Int): Int {
return x + y + z
}
오버라이딩은 기반 클래스에서 상속 받은 메서드를 파생 클래스에서 재정의 하는것을 의미합니다.
코틀린에서는 오버라이딩을 하려면 해당 메서드에 기반 클래스에서는 open 키워드를 파생 클래스에는 override 키워드를 붙여줘야 합니다.
open class Bird(name: String, var age: Int) {
val name: String
init {
this.name = name
}
constructor(name: String): this(name, 0)
open fun fly() = println("i can fly")
}
open class Ostrich(name: String, age: Int) : Bird(name, age) {
override fun fly() {
println("i can't fly")
}
}
Bird 클래스의 fly() 메서드를 Ostrich 클래스에서 오버라이딩 했습니다. Ostrich 클래스의 인스턴스에서 fly 메서드를 실행하면 재정의 된 메서드가 실행되어 i can't fly가 출력됩니다.
오버라이딩 된 메서드가 더 이상 재정의 되는것을 막을수도 있습니다. 그러기 위해서는 override 키워드 앞에 final 키워드를 붙여주면 됩니다. final 키워드를 붙이면 하위 클래스에서 더이상 그 메서드를 재정의 하지 못합니다.
open class Ostrich(name: String, age: Int) : Bird(name, age) {
final override fun fly() {
println("i can't fly")
}
}
class BigOstrich(name: String, age: Int): Ostrich(name, age) {
override fun fly() {
// compile error
}
}
super 키워드를 사용하여 상위 클래스에 접근할 수 있습니다.
super | this |
---|---|
super.프로퍼티: 상위 클래스의 프로퍼티 참조 | this.프로퍼티: 현재 클래스의 프로퍼티 참조 |
super.메서드: 상위 클래스의 메서드 참조 | this.메서드: 현재 클래스의 메서드 참조 |
super(): 상위 클래스의 생성자 참조 | this(): 현재 클래스의 생성자 참조 |
class Bird(name: String, var age: Int) {
val name: String
init {
this.name = name
}
constructor(name: String): this(name, 0)
open fun fly() = println("i can fly")
}
open class Ostrich(name: String, age: Int) : Bird(name, age) {
fun superFly() {
super.fly() // super 키워드로 상위 객체에 접근하여 메서드 호출
}
final override fun fly() {
println("i can't fly")
}
}
Ostrich().fly() // i can't fly
Ostrich().superFly() // i can fly
같은 클래스 내에 주 생성자가 존재하고, 또 다른 부 생성자가 존재한다면 부 생성자는 주 생성자에게 위임해야만 합니다.
이러한 위임은 this 키워드를 사용하여 이루어집니다.
class Person(var name: String, var age: Int) {
init { println("main constructor called") }
constructor(name: String): this(name, 0) {
println("sub constructor called")
}
}
fun main() {
Person("seongjki")
}
위 코드를 호출하면 부 생성자가 가장 먼저 호출됩니다. 거기서 생성자가 위임된 것을 확인하고 위임받은 주 생성자가 호출이 되어 전달된 인자로 프로퍼티 name과 age가 seongjki, 0으로 초기화 됩니다. 그 후 주 생성자의 본문 "main constructor called"가 호출되고 마지막으로 "sub constructor called"가 호출됩니다.
아래의 예제는 주 생성자 없이 부 생성자만 존재할 때, 위임이 이루어지는 예제입니다.
open class Person {
constructor(firstName: String) {
println("[Person] firstName: $firstName")
}
constructor(firstName: String, age: Int) {
println("[Person] firstName: $firstName, $age")
}
}
class Developer: Person {
constructor(firstName: String): this(firstName, 10) {
println("[Developer] $firstName")
}
constructor(firstName: String, age: Int) {
println("[Developer] $firstName, $age")
}
}
val dev = Developer(Sean)
/*
[Person] firstName: Sean, 10
[Developer] Sean, 10
[Developer] Sean
*/
클래스 내부에 또 클래스를 선언하는 것이 가능합니다. 이러한 클래스를 inner class라고 합니다. inner class에 대한 자세한 내용은 이후에 알아보고 여기서는 inner class에서 바깥 클래스를 참조하는 법을 알아봅니다.
inner class에서 바깥 클래스를 호출하려면 qualified this 문법을 사용하면 됩니다.
this@label // qualified this syntax
바깥 클래스의 이름을 label로 사용하면 inner class에서 바깥 클래스의 this로 접근 할 수 있습니다.
class Person(var name: String, var age: Int) {
init { println("main constructor called") }
constructor(name: String): this(name, 0) {
println("sub constructor called")
}
inner class Heart {
val person = this@Person
}
}
코틀린은 자바와 마찬가지로 클래스의 다중 상속을 지원하지 않습니다. 하지만 인터페이스는 다중 상속이 가능합니다.
상속 받은 인터페이스에 이름과 파라미터가 모두 똑같은 함수가 있다면 어떻게 될까요? 컴파일러는 어떤 함수를 호출해야 할지 알 수가 없습니다.
따라서 이런 모호함을 해결하기 위해 <>를 사용하여 접근하려는 클래스나 인터페이스의 이름을 지정해줘야 합니다.
open class A {
open fun introduce() {
println("A")
}
}
interface B {
fun introduce() {
println("B")
}
}
class C: A(), B {
override fun introduce() {
super.introduce() // compile error
super<A>.introduce() // A
super<B>.introduce() // B
}
}
캡슐화는 일반적으로 관련 있는 데이터와 함수를 묶는 것을 의미합니다.(class)
이렇게 캡슐화 된 모든 데이터와 함수에 사용자가 아무런 제한 없이 접근 할 수 있으면 제작자의 의도와 다르게 작동할 가능성이 커진다.
따라서 사용자에게 필요한 부분만을 보여주고 그렇지 않은 부분은 숨기는 것을 정보 은닉이라고 한다.
코틀린은 가시성 지시자를 사용하여 정보를 은닉한다. 코틀린의 기본 가시성 지시자는 public이다.
private: 이 요소는 외부에서 접근할 수 없다.
public: 이 요소는 어디서든 접근이 가능하다. (기본값)
protected: 외부에서 접근할 수 없지만, 하위 상속 요소에서는 접근 가능하다.
internal: 같은 모듈 내에서는 접근이 가능하다.
가시성 지시자가 선언되는 위치는 다음과 같다.
[가시성 지시자] 전역변수
[가시성 지시자] 함수
[가시성 지시자] 클래스 이름 {
[가시성 지시자] 생성자
[가시성 지시자] 프로퍼티
[가시성 지시자] 메서드
}
생성자에 가시성 지시자를 사용하는 경우, constructor 키워드를 생략할 수 없습니다.
이제 각 가시성 지시자들을 사용한 예제를 살펴보겠습니다.
private은 외부에서 접근할 수 없도록 만드는 지시자입니다.
다른 파일에서 private으로 선언된 클래스의 객체를 생성할 수 없습니다. 같은 파일 내에서는 객체를 생성할 수 있습니다.
클래스 내부에 private으로 선언된 메서드와 프로퍼티는 클래스 외부에서 접근할 수 없습니다.
다른 클래스에서 Private 객체를 프로퍼티를 가지려면, 그 클래스도 private으로 선언되었거나 프로퍼티가 private으로 선언되어야 합니다.
private class Private {
private var name = "private"
private fun privateFunc() = println(name)
fun access() = privateFunc()
}
private class Other {
val p = Private()
}
// class Other {
// private val p = Private()
// }
fun main() {
val p = Private()
p.name // Cannot access 'name': it is private in 'Private'
p.privateFunc() // Cannot access 'privateFunc': it is private in 'Private'
p.access()
}
protected는 최상위에 선언된 요소(전역변수, 최상위 함수 등)에는 지정될 수 없고 클래스나 인터페이스에만 지정할 수 있습니다.
open class Base {
protected var name = "base"
protected fun printBaseName() = println(name)
fun access() = printBaseName()
}
class Derived: Base() {
fun test() {
println(super.name) // 접근 가능
printBaseName() // 접근 가능
access() // 접근 가능
}
}
fun main() {
val base = Base()
base.access() // 접근 가능
base.name // Cannot access 'name': it is protected in 'Base'
base.printBaseName() // Cannot access 'printBaseName': it is protected in 'Base'
}
internal 지시자는 자바와 다르게 새롭게 정의된 이름입니다. 자바는 package의 이름이 같으면 접근을 허용하는 방식을 사용했습니다. 하지만 코틀린은 package로 제한하지 않고 같은 모듈을 대변하는 internal을 사용합니다.
프로젝트에 모듈이 하나라면 internal은 public과 동일합니다.
책 내용과 검색 내용의 차이로 잘 정리된 벨로그를 참고해주세요
코틀린 공식 문서
@khyunjiee 벨로그
객체지향 프로그래밍의 캡슐화, 상속, 다형성
황영덕,[do it 코틀린 프로그래밍], 이지스퍼블리싱(2021)