Kotlin in Action 공부 이어서..
클래스 이름 바로 뒤 소괄호()에서 설정한게 바로 주 생성자가 된다. val isMarried: Boolean=true 처럼 디폴트 값도 설정이 가능하다.
class Person(val name: String, val age: Int, val isMarried: Boolean=true) {
fun printInfo() {
println("Name: $name, Age: $age, Married: $isMarried")
}
}
fun main() {
val person = Person("Alice", 25)
person.printInfo()
}
// 출력 : Name: Alice, Age: 25, Married: true
클래스명 뒤에 () 소괄호 없이 클래스를 생성하면 주생성자 없이도 만들 수 있다.
생성자의 매개변수는 기본적으로 생성자에서만 사용할 수 있는 지역변수로 클래스 안의 다른 함수에서는 다른 과정을 거치지 않으면 사용할 수 없다.
간단하게 해결하려면 주 생성자의 매개변수의 앞에 val이나 var로 선언하면 클래스의 멤버 변수가 되어 다른 함수에서도 사용 가능하다.
상속시 주 생성자
기반 클래스가 있으면 주 생성자에서 기반 클래스의 생성자를 호출하는데, 기반 클래스의 이름 뒤에 괄호를 치고 생성자 인자를 넘겨준다.
// 기반 클래스
open class Person(val name: String, val age: Int) {
// 주 생성자의 일부로부터 초기화 가능한 프로퍼티
}
// 상속받는 자식 클래스
class Student(name: String, age: Int, val studentId: String) : Person(name, age) {
// 주 생성자에서 상속받은 프로퍼티와 추가된 프로퍼티
}
class Secretive private constructor(){}
init
class User(name: String, count: Int) {
init {
println("I'm init)
}
// 본문..
}
일반적으로 코틀린에서는 디폴트 파라미터 값 설정 덕에 여러 생성자를 만들 일이 적다.
하지만 그럼에도 생성자를 여럿 만들어야 할 일이 있다.
constructor
키워드로 설정할 수 있다. 필요에 따라 얼마든지 생성 가능하다.class Person(val name: String, val age: Int) {
constructor(name: String) : this(name, 0) {...}
constructor(name: String, age: Int, city: String) : this(name, age) {
println("Person created in $city")
//...
}
// ...
}
주생성자와 부생성자가 모두 있을 경우, 반드시 주 생성자가 실행되어야 한다.
부생성자에선 this
를 키워드를 통해 주 생성자를 호출해서, 부 생성자의 매개변수를 주생성자의 매개변수와 연결시켜 주 생성자도 같이 실행시키는 과정을 거쳐야 한다.
부생성자와 부생성자끼리 연결시킬수도 있다. 이 과정에서도 다른 부생성자를 통해 주 생성자는 실행되어야 한다.
그리고 자바에서처럼 super
키워드를 통해 상위 클래스의 생성자를 호출할 수 있다.
코틀린에선 인터페이스에 추상 프로퍼티 선언을 넣을 수 있다.
interface User {
val nickname: String
}
// 1. 주 생성자 안에 프로퍼티를 직접 선언
class PrivateUser(override val nickname: String) : User
// 2. 커스텀 게터로 프로퍼티 설정. 필드에 저장하지 않고 매번 값을 계산한다.
class SubscribingUser(val email: String) : User {
override val nickname: String
get() = email.substringBefore("@")
}
// 3. 초기화 식으로 nickname 초기화. 저장한 값을 불러온다.
class FacebookUser(val accountId: Int) : User{
override val nickname = getFacebookName(accountId)
private fun getFacebookName(accountId: Int): String {
// ...
}
}
자바에선 클래스가 equals, hashCode, toString 등의 메서드를 직접 구현해야 한다.(아니면 IDE가 자동으로 구현)
하지만 코틀린에서는 컴파일러가 이런 메서드를 기계적으로 생성해주는 기능이 있는데, 이를 보이지 않는 곳에서 해줘 소스코드를 깔끔하게 유지해준다.
data
제어자를 붙이면 컴파일러가 자동으로 만들어주고, 이를 데이터 클래스라 한다.data class Client(val name: String, val postalCode: Int)
fun main() {
val client1 = Client("kim", 123)
val client2 = Client("kim", 123)
if (client1 == client2) println("Equal!") // Equal!
println(client1.toString()) // Client(name=kim, postalCode=123)
println(client1.hashCode()) // 3292040
println(client2.hashCode()) // 3292040
}
==
를 참조타입 비교에도 쓸 수있다)data 클래스의 모든 프로퍼티가 val일 필요는 없지만 val로 읽기전용으로 만들어 데이터 클래스를 불변 클래스로 만드는 것을 권장한다.
copy() 메서드는 객체를 복사해 일부 프로퍼티를 바꿀 수 있게 해주는 메서드이다.
이 메서드를 통해 var을 사용하는 대신 객체 자체를 카피해 프로퍼티를 변경해줄 수 있다.
val client3 = client1.copy(postalCode = 456)
println(client3) // Client(name=kim, postalCode=456)
코틀린에선 기본적으로 클래스가 final로 상속을 허용하지 않는다.
이때 클래스에 새로운 동작을 추가하기 위해 일반적으로 데코레이터 패턴을 사용한다.
기존의 클래스(상속x) 대신 새로운 데코레이터를 만들되 기존 클래스와 같은 인터페이스를 데코레이터가 제공하게하고, 기존 클래스를 데코레이터 내부에 필드로 유지하는 것이다.
위의 데코레이터 방식은 준비 코드가 상당히 많이 필요하다는 단점이 있다.
하지만 코틀린에선 인터페이스를 구현할 때 by
키워드를 통해 간단하게 구현을 다른 객체에 위임한다는 사실을 명시할 수 있다.
즉, 상속할 수 없는 final 상태의 기본 클래스를 클래스 위임을 통해 해당 클래스의 기능을 재사용할 수 있는 기능이다.
class C : A by B
: A에서 정의하는 모든 B의 메서드를 C에게 위임한다.
즉, C는 B가 갖고있는 모든 A의 메서드를 구현하지 않아도 가질 수 있다.
아래와 같이 위임 하면 Car 클래스는 엔진의 세부 구현을 몰라도 되며, 이미 만들어진 ElectricEngine 클래스를 재사용하여 엔진을 사용할 수 있다.
interface Engine {
fun start()
}
class ElectricEngine : Engine { // 전기 엔진 클래스가 엔진을 구현
override fun start() {
println("전기 엔진 시작")
}
}
// 차가 엔진 인터페이스를 전기엔진에게 위임받음
class Car(private val engine: Engine) : Engine by engine
fun main() {
val electricEngine = ElectricEngine()
val myCar = Car(electricEngine)
myCar.start() // "전기 엔진 시작" 출력
}
object MySingleton {
fun doSomething() {
println("It is Singleton Object")
}
}
위와 같이 object
로 객체 선언을 하면 클래스 선언 + 단일 인스턴스 선언
이 되어 인스턴스가 하나인 싱글톤으로 사용할 수 있다.
객체 선언문이 있는 위치에서 생성자 호출 없이 즉시 만들어지므로 생성자를 쓸 수 없다.
일반 클래스처럼 다른 클래스의 상속이나 인터페이스의 구현도 가능하다.
자바에서 사용하려면 INSTANCE
필드를 사용하면 된다.
ex) MySingleton.INSTANCE.doSomeThing();
코틀린 클래스는 static을 지원하지 않아 정적인 멤버가 없다.
대신 패키지 수준의 최상위 함수와 객체 선언을 활용하면 되는데, 대부분 최상위 함수를 사용한다.
하지만 최상위 함수는 클래스의 private 멤버에 접근할 권한이 없다.
동반 객체는 클래스 내부에 선언되며, 그 클래스와 연결된 싱글톤 객체를 나타낸다. 클래스의 인스턴스를 만들지 않고도 해당 클래스의 멤버에 접근하거나 메서드를 호출할 수 있는 기능을 제공함으로써 static 멤버를 가지는 것과 비슷한 기능을 한다.
클래스 안에 companion object{...}
로 선언하면 된다.
class MyClass {
companion object {
// 동반 객체의 멤버들
fun myFunction() {
println("This is a function in companion object")
}
val myProperty: Int = 42
}
}
// 동반 객체의 멤버에 접근
MyClass.myFunction() // "This is a function in companion object"
println(MyClass.myProperty) // 42
동반객체는 둘러싼 클래스의 private에 접근할 수 있어 최상위 함수와 차별점을 가진다.
이로써 특정 클래스의 인스턴스를 생성하지 않아도 접근할 수 있기 때문에, 정적(static) 멤버와 유사한 효과를 얻을 수 있으면서도 객체 지향 프로그래밍의 특성을 유지할 수 있다.
팩토리 메서드 패턴을 구현하기에 가장 적합한 방식이다.
동반객체는 클래스 안에 정의된 일반객체로, 이름을 붙이거나 인터페이스를 구현하거나 안에 확장함수와 프로퍼티를 정의할 수도 있다.(이름을 안붙이면 Companion이라는 이름으로 자바에서 접근이 가능하다)
클래스 안에 빈 동반객체를 선언해두고, 클래스 밖에서 Companion에 대한 확장함수를 선언해 클래스의 멤버함수처럼 사용할 수 있다.
함수 내부에서 간단한 객체를 정의하고 사용할 때 유용하다.
해당 함수 내에서만 유효한 임시 객체를 생성할 수 있다.
fun someFunction() {
val obj = object {
val x: Int = 10
val y: Int = 20
}
println("x: ${obj.x}, y: ${obj.y}")
}
익명 객체는 주로 인터페이스를 구현하거나 클래스를 상속받을 때 사용된다.
한 번만 사용할 목적으로 만들어지며, 그 자체로는 재사용되지 않는다.
interface MyInterface {
fun doSomething()
}
val myObject: MyInterface = object : MyInterface {
override fun doSomething() {
println("Anonymous Object is doing something")
}
}