클래스 정의하기
클래스 내부 구조
코틀린에서 클래스는 class 키워드와 클래스이름으로 정의합니다.
클래스는 내부에서 변수와 함수를 가질 수 있습니다.
class Person {
var firstName: String = ""
var familyName: String = ""
var age: Int = 0
fun fullName() = "$firstName $familyName"
fun showMe() {
println("${fullName()}: $age")
}
}
Person 클래스이고 firstName, familyName, age라는 프로퍼티가 있고 fullName()과 showMe()라는 함수가 있습니다.
클래스 내부에서 본인 자체에 접근할 때는 this라는 키워드를 사용하고 이를 수신객체라고 말합니다.
보통은 수신객체를 생략해서 사용합니다.
fun fullName() = "${this.firstName} ${this.familyName}"
fun showMe() {
println("${this.fullName()}: $age")
}
하지만 아래와 같이 파라미터의 이름과 클래스 프로퍼티의 이름이 같은 경우에 this를 사용합니다.
class Person {
var firstName: String = ""
var familyName: String = ""
fun setName(firstName: String, familyName: String) {
this.firstName = firstName
this.familyName = familyName
}
}
생성자
위 예제에서는 생성자를 지정하지 않아서 객체를 생성하면 기본 생성자를 사용해서 객체를 만들게 됩니다.
class Person(firstName: String, familyName: String) {
val fullName = "$firstName $familyName"
}
fun main() {
val person = Person("석준", "정")
println(person.fullName) // 석준 정
}
클래스 이름 뒤에 만드는 생성자를 주생성자라고 부릅니다.
주생성자는 block이 없어서 생성과 동시에 어떤 작업을 할 수 없습니다. 그래서 init { }을 사용해서 초기화 시점에 작업을 진행 할 수 있습니다.
class Person(firstName: String, familyName: String) {
val fullName = "$firstName $familyName"
init {
println("Created new Person instance: $fullName")
}
}
생성자 파라미터 자체를 클래스 프로퍼티로 사용하고 싶다면 val/var를 붙이면 됩니다.
class Person(val firstName: String, val familyName: String) {
val fullName = "$firstName $familyName"
}
함수와 마찬가지로 디폴트값을 사용할 수 있고 vararg도 사용 가능합니다.
class Person(val firstName: String, val familyName: String = "") {
fun fullName() = "$firstName $familyName"
}
class Room(vararg val persons: Person) {
fun showNames() {
for (person in persons) {
println(person.fullName())
}
}
}
fun main() {
val room = Room(Person("석준"), Person("디노", "정"))
room.showNames()
}
constructor 키워드를 사용하면 부생성자(secondary constructor)를 만들 수 있는데 부생성자는 여러개를 만들 수 있습니다.
class Person {
val firstName: String
val familyName: String
constructor(firstName: String, familyName: String) {
this.firstName = firstName
this.familyName = familyName
}
constructor(fullName: String) {
val names = fullName.split(" ")
if (names.size != 2) {
throw IllegalArgumentException("Invalid name: $fullName")
}
firstName = names[0]
familyName = names[1]
}
}
: 키워드를 사용해서 부생성자에서 부생성자를 호출 할 수도 있고, 부생성자에서 주생성자를 호출 할 수 있습니다.
만약 주생성자가 있다면 부생성자는 주생성자를 꼭 호출 해야만 합니다.
class Person {
val fullName: String
constructor(firstName: String, familyName: String) : this("$firstName $familyName")
constructor(fullName: String) {
this.fullName = fullName
}
}
class Person(val fullName: String) {
constructor(firstName: String, familyName: String) : this("$firstName $familyName")
}
내포된 클래스(Nested Class)
클래스는 함수, 프로퍼티, 생성자 외에 내부에 클래스를 가질 수 있습니다.
class Person(val id: Id, val age: Int) {
class Id(val firstName: String, val familyName: String)
fun showMe() {
println("${id.firstName} ${id.familyName}, $age")
}
}
fun main() {
val id = Person.Id("석준", "정")
val person = Person(id, 31)
person.showMe() // 석준 정, 31
}
inner 키워드를 사용하면 자신을 둘러싼 외부 클래스의 현재 인스턴스에 접근할 수 있습니다.
class Person(val firstName: String, val familyName: String) {
inner class Possession(val description: String) {
fun showOwner() = println(fullName()) // Person 클래스의 fullName() 함수 접근
}
private fun fullName() = "$firstName $familyName"
}
fun main() {
val person = Person("석준", "정")
val wallet = person.Possession("Wallet")
wallet.showOwner() // 석준 정
}
this 뒤에 @ 를 사용해서 상위 스코프의 인스턴스에 접근 할 수 있습니다.
class Person(val firstName: String, val familyName: String) {
inner class Possession(val description: String) {
fun getOwner() = this@Person
}
}
지역 클래스
지역클래스는 코드 블럭 안에서만 사용할 수 있는 클래스입니다.
fun main() {
class Point(val x: Int, val y: Int) {
fun shift(dx: Int, dy: Int): Point = Point(x + dx, y + dy)
override fun toString() = "($x, $y)"
}
val p = Point(10, 10)
println(p.shift(-1, 3)) // (9, 13)
}
지역함수와 동일하게 지역클래스도 코드블럭에 값을 포획(capture) 할 수 있고 변경할 수도 있습니다.
fun main() {
var x = 1
class Counter {
fun increment() {
x++
}
}
Counter().increment()
println(x) // 2
}
널 가능성
코틀린에서 참조 값에는 아무것도 참조하지 않는 경우를 나타내는 null 이라는 값이 있습니다.
null인 참조에서 어떤 프로퍼티나 어떤 함수에 접근하면 NPE(Null Pointer Exception)이 발생하고 이것 은 런타임 오류라고 합니다.
코틀린 타입에서는 null을 참조할 수 있는 타입과 없는 타입을 구분해서 사용하기 때문에 NPE 발생를 많이 방지할 수 있습니다.
널이 될 수 있는 타입
기본적으로 모든 타입은 널을 참조 할 수 없는 타입입니다. 널을 참조 할 수 없는 타입에 null 값을 넣게 되면 컴파일 에러가 발생합니다.
fun isLetterString(s: String): Boolean {
if (s.isEmpty()) return false
for (ch in s) {
if (!ch.isLetter()) return false
}
return true
}
fun main() {
println(isLetterString("abc"))
println(isLetterString(null)) // Null can not be a value of a non-null type String
}
널을 참조하게 만들려면 타입 뒤에 ? 키워드를 사용합니다.
?가 붙은 타입이 ?가 붙지 않은 타입보다 상위 타입이라서 nullable한 타입을 non-null 타입에 대입할 수 없습니다.
fun isBooleanString(s: String?) = s == "false" || s == "true"
fun main() {
println(isBooleanString(null))
val s: String? = "abc"
/**
Type mismatch.
Required: String
Found: String?
*/
val ss: String = s
}
널 가능성과 스마트 캐스트
널이 될 수 있는 값을 처리하는 가장 직접적인 방법은 조건물을 사용해 null과 비교하면 됩니다.
fun isLetterString(s: String?): Boolean {
if (s == null) return false
if (s.isEmpty()) return false
for (ch in s) {
if (!ch.isLetter()) return false
}
return true
}
함수의 파라미터는 val로 불변이기 때문에 null검사를 마치고 난 후에 파라미터는 non-null로 인지 됩니다.
var인 값은 변경 될 수 있기 때문에 null 검사를 하더라도 스마트 캐스트가 되지 않아 nullable 타입으로 인지 될 수 있습니다.
fun main() {
var s: String? = readLine()
if (s != null) {
s = readLine()
println(s.length) // Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
}
}
널 아님 단언 연산자
nullable한 타입에서 개발자는 타입이 null이 아님을 단언하도록 하는 것이 !! 연산자 입니다.
이 연산자는 실제로 값이 null일 때 NPE를 발생 시켜서 프로그램이 강제종료 될 수 있으니 조심히 사용해야 합니다.
실제로는 사용하지 않는 것이 좋습니다.
fun main() {
var name: String? = null
fun initialize() {
name = "석준"
}
fun sayHello() {
println(name!!.uppercase())
}
initialize() // 이 코드가 sayHello()보다 먼저 호출 하지 않으면 NPE 발생
sayHello()
}
안전한 호출 연산자
nullable한 타입에서 null이 아닐 때 호출 하도록 하는 safe call하는 방식이 있습니다.
safe call은 null이 아니면 동작을 수행하고 null이면 null을 리턴합니다.
fun main() {
val n = readLine()?.toInt()
if (n != null) {
println(n + 1)
} else {
println("No value")
}
}
엘비스 연산자
널을 다루는 유용한 연산자로 ?: 가 있습니다.
fun sayHello(name: String?) {
println("Hello, ${name ?: "Unknown"}")
}
fun main() {
sayHello("석준") // Hello, 석준
sayHello(null) // Hello, Unknown
}
엘비스 연산자는 기본값을 지정할 때 유용하고 특정 값이 null일 때 함수를 종료 할 수도 있습니다.
fun main() {
val n = readLine()?.toInt() ?: 0
println("입력한 숫자는 ${n}입니다.")
}
fun showName(firstName: String?, familyName: String?) {
firstName ?: return
familyName ?: return
println("$firstName $familyName")
}
단순한 변수 이상인 프로퍼티
최상위 프로퍼티
클래스나 함수와 마찬가지로 최상위 수준에 프로퍼티를 정의할 수 있습니다.
val prefix = "Hello, "
fun main() {
val name = readLine() ?: return
println("$prefix$name")
}
늦은 초기화
클래스를 인스턴스로 만들 때 프로퍼티를 무조건 초기화 해야만 합니다.
하지만 초기화 되는 것이 특정 시점 이후여야만 하는 경우가 생기는데 이 때 늦은 초기화를 합니다.
늦은 초기화는 lateinit var로 프로퍼티를 선언합니다.
class Person {
lateinit var firstName: String
lateinit var familyName: String
fun parseName(fullName: String) {
val names = fullName.split(",")
this.firstName = names[0]
this.familyName = names[1]
}
fun showMe() {
println("$firstName $familyName")
}
}
fun main() {
val person = Person()
person.showMe()
}
초기화 하지 않고 사용하면 UninitializedPropertyAccessException 에러가 발생합니다.
초기화여부를 확인하기 위해서 isInitialized를 사용합니다.
fun showMe() {
if (::firstName.isInitialized && ::familyName.isInitialized) {
println("$firstName $familyName")
}
}
커스텀 접근자
지금까지 클래스 내부에 프로퍼티는 단순 값을 저장하기 위한 목적으로 사용했습니다.
하지만 값을 저장할 때 특별한 로직이 들어가거나 값을 가져올 때 특별한 로직이 들어가게 할 수 있습니다.
커스텀 접근자 블럭에서 실제 프로퍼티에 접근하기 위해서는 field라는 값을 사용합니다.
class Person(var firstName: String, var familyName: String) {
var fullName: String
get() = "$firstName $familyName"
set(value) {
val names = value.split(" ")
this.firstName = names[0]
this.familyName = names[1]
}
var age: Int = -1
set(value) {
if (value <= 0) {
throw IllegalArgumentException("Invalid age: $value")
}
field = value
}
}
getter는 외부로 노출시키면서 내부에서는 setter만 노출하고 싶을 때는 커스텀 접근자에 가시성을 추가하면 됩니다.
import java.util.*
class Person(name: String) {
var lastChanged: Date? = null
private set
var name: String = name
set(value) {
lastChanged = Date()
field = value
}
}
지연 계산 프로퍼티와 위임
lateinit은 개발자가 특정 시점에 초기화를 해줘야만 에러가 발생하지 않습니다.
지연 계산 프로퍼티인 lazy를 사용하면 특정 시점에 초기화 하는게 아닌 프로퍼티에 접근할 때 초기화가 됩니다.
lazy를 사용하기 위해서는 by라는 키워드를 사용해야 합니다.
val name by lazy {
println("name프로퍼티 lazy로 초기화")
"석준 정"
}
fun main() {
/**
1
name프로퍼티 lazy로 초기화
석준 정
2
석준 정
*/
println("1")
println(name)
println("2")
println(name)
}
name은 1에서 초기화가 되고 2에서는 기존의 값을 그대로 사용합니다.
object
인스턴스가 하나만 존재하는 클래스를 object로 만들 수 있습니다.
object 선언
object Application {
val name = "My Application"
override fun toString(): String = name
fun exit() {}
}
Application라는 object는 프로그램에서 하나뿐인 인스턴스를 가지고 초기화는 object에 접근할 때 됩니다.
companion object
class 내부에 object를 만들어서 인스턴스를 여러개 만들 수도 있고 하나만 존재하도록 할 수 있습니다.
companion object는 class 내부에 한개만 존재할 수 있습니다.
class Application(val name: String) {
fun showMe() {
println(String.format(introductionTemplate, name))
}
companion object {
private const val introductionTemplate = "안녕하세요 이 어플리케이션은 %s 입니다."
}
}
fun main() {
Application("코틀린 완벽 가이드").showMe() // 안녕하세요 이 어플리케이션은 코틀린 완벽 가이드 입니다.
}
object expression
명시적인 선언 없이 객체를 만들 수 있는 object expression을 제공합니다.
fun main() {
fun midPoint(xRange: IntRange, yRange: IntRange) = object {
val x = (xRange.first + xRange.last) / 2
val y = (yRange.first + yRange.last) / 2
}
val midPoint = midPoint(1..5, 2..6) // val midPoint: <anonymous object : Any>
println("${midPoint.x} ${midPoint.y}")
}
하지만 object expression이 최상위 레벨에 있게 되면 Any 타입으로 되기 때문에 위 예제처럼 x나 y는 접근할 수 없습니다.
fun midPoint(xRange: IntRange, yRange: IntRange) = object {
val x = (xRange.first + xRange.last) / 2
val y = (yRange.first + yRange.last) / 2
}
fun main() {
val midPoint = midPoint(1..5, 2..6) // val midPoint: Any
println("${midPoint.x} ${midPoint.y}") // Any라서 x와 y에 접근할 수 없음
}
'개발 > 코틀린' 카테고리의 다른 글
(코틀린) 특별한 클래스 사용하기 (0) | 2022.10.23 |
---|---|
(코틀린) 함수형 프로그래밍 (0) | 2022.10.10 |
(코틀린) 예외처리 (0) | 2022.09.25 |
(코틀린) 조건문, 반복문 (0) | 2022.09.25 |
(코틀린) 함수 (0) | 2022.09.24 |