728x90
"코틀린 완벽 가이드"라는 책을 보면서, 필요한 부분만 간추렸습니다.
버전이 달라지면서 변경된 부분이나, 잘못된 부분이 있을 수 있고 혹시 발견하게 되시면 지적은 언제나 환영합니다.
클래스 정의
class Person {
var firstName: String = ""
var familyName: String = ""
var age: Int = 0
fun fullName() = "${this.firstName} ${this.familyName}"
fun showMe() {
println("${this.fullName()}: ${this.age}")
}
}
- 자바는 package private 이었지만, 코틀린 클래스는 기본적으로 public
- 자바와 달리, 코틀린에서는 클라이언트 코드를 바꾸지 않아도 원하는 대로 프로퍼티 구현 변경 가능
생성자
- 자바와 달리 인스턴스 생성에 new 를 사용하지 않는다.
- 생성자에 인자를 넘길 때, named로 사용 가능
주생성자(Primary)
- 생성자에 varargs 도 넣을 수 있음
class Person constructor(firstName: String, familyName: String) {
val fullName = "$firstName $familyName"
}
// 어노테이션이나, 가시성 변경자를 달리하지 않는 경우 constructor는 생략 가능
class Person(firstName: String, familyName: String) {
val fullName = "$firstName $familyName"
}
// 파라미터 이름 앞에 val을 추가하면, 해당 이름을 가지는 프로퍼티 정의 및 초기화 간소화 가능
// 아래 코드는 firstName, familyName 모두 초기화함
class Person(val firstName: String, val familyName: String) {
val fullName = "$firstName $familyName"
}
// 디폴트 파라미터와 varargs를 넣을 수도 있음
class Person(val firstName: String, val familyName: String = "") {
}
class Room(varargs val persons: Person) {
fun showNames() {
for (person in persons) println(person.fullName())
}
}
// 단순 value object 형 클래스라면 이런식으로 본문을 아예 생략할 수 있음
class Person(val firstName: String, val familyName: String = "")
- 생성자 파라미터 지정 + 초기화할 프로퍼티 지정용
부생성자(Secondary)
- 부생성자는 주생성자와 다르게 클래스 내부 본문에서 함수 정의하는 것처럼 정의
- constructor 생략 불가
- Unit 타입을 반환하는 함수와 마찬가지의 형태이며, 부생성자 안에서는 return도 사용 가능
- val , var 파라미터에 사용 불가
- 클래스 주생성자 선언하지 않은 경우,
- 모든 부생성자는 자신의 본문을 실행하기 전에 프로퍼티 초기화 및 init 을 실행한다.
- 클래스 주생성자가 있으면,
- 모든 부생성자는 주생성자에게 위임하거나 다른 부생성자에게 위임해야 함
class Person(fullName: String) {
val firstName: String
val familyName: String
constructor(firstName: String, familyName: String) {
this.firstName = firstName
this.familyName = familyName
}
constructor(fullName: String) {
if (names.size != 2) {
throw IllegalArgumentException("Invalid Name")
}
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
}
}
초기화블록
- 초기화 블록 내부에서는 return 불가
- 클래스 객체 생성 시점에 프로퍼티 초기화와 함께 순서대로 실행됨
- 초기화 블록 안에서도 프로퍼티 초기화 가능
class Person constructor(firstName: String, familyName: String) {
val fullName = "$firstName $familyName"
init {
println("new Person instance: $fullName")
}
}
class Person(fullName: String) {
val firstName: String
val familyName: String
init {
val names = fullName.split(" ")
if (names.size != 2) {
throw IllegalArgumentException("Invalid Name")
}
firstName = names[0]
familyName = names[1]
}
}
가시성
- 자바의 package private 이 코틀린에는 없음
public | 어디에서나, 기본값 |
internal | 멤버를 멤버가 속한 클래스가 포함된 컴파일 모듈 내부에서만 |
protected | 멤버를 멤버가 속한 클래스, 멤버가 속한 하위 클래스 안에서 |
private | 멤버가 속한 클래스 내부에서만 |
Nested Class
class Person (val id: Id, val age: Int) {
class Id(val firstName: String, val familyName: String)
fun showInfo() = println("${id.firstName} ${id.familyName}: $age")
}
- 자바와 달리 바깥 클래스는 Nested 클래스의 비공개 멤버에 접근 불가
- Nested 클래스에 inner를 붙이면, 해당 클래스를 둘러싼 외부 클래스의 현재 인스턴스 접근 가능
- 코틀린과 자바의 Nested 클래스는 상당부분 유사하지만, 내부 클래스 관련해서는
- 자바는 기본적으로 내부 클래스로 처리하고, 아닌 경우 static을 명시
- 코틀린은 inner를 명시해야 내부 클래스로 간주하고, 아닌 경우 외부 클래스와 연관되지 않음
지역 클래스
- 자바처럼 함수 본문에 클래스 정의하여 지역 클래스 활용 가능
- 자바와 달리, 코틀린에서는 클래스 본문 안에서 자신이 접근할 수 있는 값을 캡쳐 및 변경이 가능한데, 비용이 수반됨
- 지역 클래스에는 가시성 설정 불가
커스텀 접근자
- lateinit 프로퍼티는 항상 자동으로 접근자가 생성되어 직접 커스텀 접근자 정의 불가
- 주생성자 파라미터로 선언된 프로퍼티에 대한 접근자도 지원하지 않음
- 생성자 파라미터를 사용하고 클래스 본문 안에 프로퍼티에 그 값을 대입하여 해결
// custom getter
class Person(val firstName: String, val familyName: String) {
val fullName: String
get(): String { // 프로퍼티 읽을 때 자동으로 이것을 호출함
return "$firstName $familyName"
}
}
// 더 간단하게
class Person(val firstName: String, val familyName: String) {
val fullName // 식을 본문으로, 타입 추론까지 활용
get() = "$firstName $familyName"
}
// custom setter
class Person(val firstName: String, val familyName: String) {
var age: Int? = null
set(value) {
if (value != null && value <= 0) {
throw IllegalArgumentException("Invalid Age")
}
field = value // field 키워드 사용 가능
}
}
지연 계산 프로퍼티와 위임
// 함수 바탕의 lazy 활용 예시
val text: String by lazy {
File("sample.txt").readText()
}
fun main() {
while (true) {
when (val cmd = readLine() ?: return) {
"print data" -> println(text)
"exit" -> return
}
}
}
- lazy 프로퍼티는 이를 읽기 전까지 값을 계산하지 않고, 계산된 후에는 저장되어 활용
- lateinit 과 다르게 불변 프로퍼티가 아니므로, 초기화 이후에는 변경되지 않음
- 기본적으로 thread-safe 하여서, 여러 스레드에서 접근해도 궁극적으로는 같은 값임
Object
선언
- 코틀린은 기본적으로 싱글턴을 패턴을 내장한다.
- object 키워드를 이용해서 싱글턴 선언
- static, private 생성자로 대응해야 하는 자바와 달리 많이 심플함
- 초기화는 싱글턴 클래스가 실제 로딩 시점까지 지연됨
object Application {
val name = "Foo"
override fun toString() = name
fun exit() { // something }
}
- 클래스와 마찬가지로 동작하여, 멤버 함수나 프로퍼티, init 블록 사용 가능
- 주생성자, 부생성자가 없음
- object 본문에 있는 클래스는 inner를 붙일 수 없음
- 인스턴스가 하나라 당연히 inner 불필요
- 자바에서는 유틸리티 클래스를 사용했지만, 코틀린에서는 권장하지 않음
- 일단 코틀린은 정적 메서드를 정의 불가
- 자바와 다르게 최상위 선언을 패키지 안에 함께 모아둘 수 있어서 유틸 클래스 선언의 필요가 없음
Companion Object
- 생성자를 직접 활용하지 않고 object 멤버 함수로 팩토리 디자인 패턴 설계가 가능함
- object 로 그냥 만들면, 해당 이름을 계속 명시해야 함
- companion 을 추가하면 이름을 명시하지 않아도 됨
- companion object 는 정의 단계에서도 이름을 생략하는 것을 권장
// companion으로 안한 경우
class Application private constructor(val name: String) {
object Factory {
fun create(args: Array<String>): Application? {
val name = args.firstOrNull() ?: return null
return Application(name)
}
}
}
val app = Application.Factory.create(args) ?: return
// companion 사용
class Application private constructor(val name: String) {
companion object Factory { // 이름도 생략 가능
fun create(args: Array<String>): Application? {
val name = args.firstOrNull() ?: return null
return Application(name)
}
}
}
val app = Application.create(args) ?: return
- 자바의 static 멤버와 동일하게 외부 클래스와 똑같은 전역 상태를 공유하며 모든 멤버에 접근 가능
- 코틀린의 companion object는 인스턴스이므로, 더 유연하다.
- 상위 타입 상속 가능
- 일반 object 처럼 여러 곳에 전달 가능
Object expression
fun main() {
val o = object {
val x = readLine()!!.toInt()
val y = readLine()!!.toInt()
}
println(o.x * o.y)
}
- 명시적 선언 없이 object 바로 선언 가능
- 자바의 익명 클래스와 매우 유사
- 지역 선언이나 비공개 선언에서만 전달 가능
- 최상위 함수로 정의하면 멤버에 접근 시 컴파일 오류
- 지역 함수, 클래스와 마찬가지로 자신을 둘러싼 코드 영역의 변수를 캡쳐 가능하며 본문에서 변경도 가능
Nullable
- 코틀린에서는 null을 포함하는 타입과 그렇지 않은 타입을 시스템이 구분하는 중요한 특징이 있음
- 기본적으로 모든 참조 타입은 null이 될 수 없음
- null이 가능하게 하려면, ? 를 사용하여 nullable type으로 만든다.
fun foo(s: String?) = s == "false" || s == "true"
- 타입 상, nullable type은 그렇지 않은 기본 타입의 상위 타입이다.
println(foo(null)) // pass
val s: String? = "abc" // pass
val st: String = s // error
- Int Boolean 같은 원시 타입에도 nullable이 있지 이런 경우 박싱처리 됨
- 가장 작은 nullable : Nothing?
- 가장 큰 nullable : Any?
- 스마트 캐스트를 이용해서 nullable 의 기본 타입이 가진 기능을 활용할 수 있음
!!
- 이 연산자가 붙은 식의 타입은 원래 타입의 널이 될 수 없는 버전
- 수신 객체가 null이 아니면 무언가를 하고, null이면 null을 반환하는 시나리오
- 안전한 호출 연산자를 연쇄시켜 처리
readLine()?.toInt()?.toString(16)
엘비스 연산자(?:)
- null을 대신할 디폴트 값을 설정할 수 있음
- 자바의 Optional.orElseGet() 같은 느낌..
- 우선순위는 or 등의 중위 연산자와 in !in 사이에 위치
- 비교/동등성 연산자나, || && 대입 보다 우선순위가 높음
val num = raedLine()?.toInt() ?: 0 // null이면 0
단순 변수 이상의 프로퍼티
최상위 프로퍼티
- 전역 변수나 상수와 비슷한 역할
늦은 초기화 lateinit
class Foo {
lateinit var text: String
fun loadFile(file: File) {
text = file.readText()
}
}
fun getContentLength(content: Content) = content.text?.length ?: 0
- 적용 전 체크할 사항
- 프로퍼티가 코드에서 변경될 수 있는 지점이 여러 곳일 수 있어 var 이어야 함
- 프로퍼티의 타입이 nullable이 아니어야 하고, 원시 값을 표현하는 타입도 아니어야 함
- 프로퍼티를 정의하면서 동시에 초기화하는 대입을 하지 않아야 한다.(당연하게도 필요 없는 부분임)
728x90
'Programming Language > Kotlin' 카테고리의 다른 글
코틀린 기초 - 기초 문법 (1) | 2024.03.17 |
---|