Programming Language/Kotlin

코틀린 기초 - 클래스 기초

TwinParadox 2024. 3. 24. 20:05
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
  • 적용 전 체크할 사항
    1. 프로퍼티가 코드에서 변경될 수 있는 지점이 여러 곳일 수 있어 var 이어야 함
    2. 프로퍼티의 타입이 nullable이 아니어야 하고, 원시 값을 표현하는 타입도 아니어야 함
    3. 프로퍼티를 정의하면서 동시에 초기화하는 대입을 하지 않아야 한다.(당연하게도 필요 없는 부분임)
728x90
728x90

'Programming Language > Kotlin' 카테고리의 다른 글

코틀린 기초 - 기초 문법  (1) 2024.03.17