본문 바로가기

코틀린

코틀린 정리 - (3)

안드로이드 개발하는 kancho입니다

이번 포스팅에서는 코틀린의 클래스와 객체에 대해 정리하려고 합니다.

코틀린의 객체지향 프로그래밍을 하기 위한 가장 기초적이면서 중요한 부분이라고 생각한다.

'코틀린 완벽 가이드' 책을 참고하였습니다

 

Class

어떠한 것에 대해 정의하고 분류하는 설계도

 

/*
User -> 클래스 이름
firstName, lastName, age, email -> 프로퍼티(Property)
fullName() -> 함수
클래스의 접근 제한자는 기본적으로 public
Java -> class가 package-private(패키지 내부)
*/
class User {
    var firstName: String = ""
    var lastName: String = ""
    var age: Int = 30
    var email: String = "kancho@tistory.com"
    
    fun fullName() = "$firstName $lastName"
}

// user -> 수신객체(receiver), User의 property에 접근하기 위해 사용
fun showAge(user: User) = println(user.age)
fun changeEmail(user: User) {
    user.email = "123@1234.com"
}


// 클래스 내부에서의 receiver -> this (default, 생략가능)
class User {
    var firstName: String = ""
    var lastName: String = ""
    var age: Int = 30
    var email: String = "kancho@tistory.com"
    
    fun fullName() = "${this.firstName} ${this.lastName}"
    fun setEmail(email: String) {
        this.email = email
    }
}

fun main() {
    val user = User()	// User클래스 instance 생성

    user.firstName = "kancho"
    user.lastName = "park"
    user.email = "kancho@tistory.com"

    println(user.fullName())    // result -> kancho park
}

 

생성자

주생성자 (primary constructor)

 

/*
클래스 이름 옆의 괄호 부분을 주 생성자라고 한다
주 생성자 파라미터는 property들의 초기화, init 블록 안에서만 사용 가능하다
모든 property가 초기화 되었는지 컴파일러가 확인한다
*/
class User(firstName: String, lastName: String, email: String) {
    val userInfo = "$firstName $lastName, $email"	// 생성자 parameter
    val age: Int

    // init -> 초기화블록, 클래스가 초기화될때 먼저 실행된다
    init {
        println("first init : $userInfo")
    }

    // 복수의 init이 가능하며 순서대로 실행
    // age 프로퍼티 초기화
    init {
        println("second init : $userInfo")
        age = 30
    }
    
    fun showUserInfo() {
        println("$firstName $lastName, $email") // error
    }
}

/*
result -> 
first init : kancho park 123@123.com
second init : kancho park 123@123.com
*/
fun main() {
	val user = User("kancho", "park", "123@123.com")
}


// 주 생성자를 통해 멤버 property 정의 및 초기화
class User2(firstName: String, lastName: String, email: String) {
    val firstName = firstName
    val lastName = lastName
    val email = email

    // firstName, lastName, email 은 멤버 property
    fun showUserInfo() {
        println("$firstName $lastName, $email")
    }
}

 

주 생성자를 통해 멤버 프로퍼티를 초기화시켜 사용해도 되지만 코틀린에서는 더 간단하게 만들 수 있다

/*
val, var을 사용해 
직접 주 생성자 파라미터를 멤버 property로 만들 수 있다
*/
class User3(val firstName: String, val lastName: String, val email: String) {
    val userInfo = "$firstName $lastName, $email"
    fun showUserInfo() {
        println("$firstName $lastName, $email")
    }
}

// 본문 생략도 가능
class User4(val firstName: String, val lastName: String, val email: String)

 

 

부생성자 (secondary constructor)

 

/*
주 생성자 이외에도 생성자를 만들 수 있다
constructor 키워드 사용
부 생성자에는 val,var을 사용하지 못한다
주 생성자가 있다면 모든 부 생성자는 주 생성자에게 위임을 하거나 다른 부생성자에게 위임을 해야한다
*/
class User(val firstName: String, val lastName: String, val email: String) {

    // init은 언제나 부 생성자보다 먼저 호출
    init {
        println("init")
    }

    constructor(firstName: String) : this(firstName, "park") {
        println("constructor1")
    }

    // 주 생성자가 있는 경우 부 생성자들은 모두 this() 생성자를 이용해
    // 생성자 위임 호출을 사용함으로써 결론적으로 주 생성자에 위임해야 한다
    constructor(firstName: String, lastName: String) : this(firstName, lastName, "123@123.com") {
        println("constructor2")
    }
}

fun main() {
    val user = User("kancho")
}

/*
result ->
init
constructor2
constructor1
*/

 

 

멤버 가시성

접근제한자(visibility modifiers)라고도 한다

클래스 멤버마다 지정가능

클래스 내부를 캡슐화해서 구현의 세부사항을 분리한다

 

4가지 제한자

  • public(공개) -> 어디서나 볼 수 있다. default
  • internal(모듈 내부) -> 해당 클래스가 포함된 컴파일 모듈 내부
  • protected(보호) -> 해당 클래스와 속한 클래스의 모든 하위 클래스
  • private(비공개) -> 클래스 내부
class User(val email: String)
class User2(private val email: String)

fun main() {
    val user = User("123@123.com")
    val user2 = User2("123@123.com")
    println(user.email)
    println(user2.email)	// error : private
}

class User3 private constructor(private val email: String)

// 유일한 생성자가 private 이므로 error
fun main() {
    val user = User("123@123.com") // error
}

 

 

Nested class

클래스 내부에서 다른 클래스도 멤버로 가질 수 있다

자바와의 차이점은 코틀린에서는 inner 가 붙는다는 것. 자바 클래스는 디폴트로 내부 클래스이며 내부클래스가 외부 클래스 인스턴스와

연관되길 원치 않으면 static을 붙여야 한다

코틀린의 inner가 없는 내포된 클래스는 외부 클래스 인스턴스와 연관되지 않는다

class User(val firstName: String, val lastName: String, val email: Email) {
    class Email(val email: String) 

    fun printUserInfo() {
        println(email.email)
    }
}

fun main() {
    val email = User.Email("123@123.com")
    val user = User("kancho", "park", email)
    user.printUserInfo()	// result : "123@123.com"
}


/*
기본적으로 내포된 class에 inner가 없으면 외부 클래스에 접근할 수 없다
Email class는 외부의 User class의 함수에 접근할 수 없다
*/
class User(val firstName: String, val lastName: String, val email: Email) {
    class Email(val email: String) {
        fun print() {
            printUserInfo() // error
        }
    }
    
    fun printUserInfo() {
        println(email.email)
    }
}

// inner를 붙여 외부 클래스 property와 함수에 접근 가능
class User(private val firstName: String, private val lastName: String) {
    inner class Email(val email: String) {
        fun print() {
            printUserInfo()
        }
    }

    fun printUserInfo() {
        println("$firstName $lastName")
    }
}

fun main() {
    val user = User("kancho", "park")
    val email = user.Email("123@123.com")
    email.print()	// result : "kancho park"
}

 

지역클래스

클래스 내부나 최상위에서 클래스를 정의하지 않고 함수 본문에서 클래스를 정의할 수 있다

지역 클래스는 자신을 둘러싼 코드 안에서만 사용 가능하다

fun main() {
    var temp = 0

    // 자신을 둘러싼 코드의 선언에 접근할 수 있다. temp
    class User(val age: Int, val email: String) {
        fun plus() {
            temp += 1
        }
    }

    println(temp)
    User(30, "").plus()
    println(temp)
}

 

 

Nullable 타입

null은 아무것도 참조하지 않는 값이며, 어떠한 객체도 가리키지 않는다

자바에서는 모든 reference 타입이 null이 될 수 있지만 코틀린에서는 NPE를 방지하기 위해 nullable한 타입이 따로 있다

해당 타입에 물음표(?)를 붙이면 Nullable한 타입이 된다

 

또한, Nothing? 은 가장 작고 Any? 는 가장 큰 null이 될 수 있는 타입이다

앞서 iterator() 함수를 지원하기만 하면 반복문을 실행할 수 있다고 했는데, nullable한 타입에는 iterator 함수가 없어서 반복문을 실행할 수 없다

 

가변 property에는 smart cast를 사용할 수 없다

 

class User(val firstName: String, val lastName: String) {
    fun printUserInfo() {
        println("$firstName $lastName")
    }
}

fun main() {
    val user = User("kancho", null)	// error. lastName 이 non-null type이기 때문에
}


/*
Nullable 타입은 non-null 타입의 상위 타입
여기에서는 String? 이 String의 상위타입이다
*/
class User(val firstName: String?, val lastName: String?) {
    fun printUserInfo() {
        println("$firstName $lastName")
    }
}

fun main() {
    val user = User("kancho", null)
    
    val nullableStr: String? = "my name is kancho"
    val notNullableStr: String = nullableStr // error. 
}

fun main() {
    val nullableStr: String? = "my name is kancho"

    if (nullableStr == null) return
    
    // smart cast 때문에 compile 된다
    // nullableStr의 값이 null인지 아닌지 확실하게 알 수 있다
    // 컴파일러가 nullableStr을 non-null 타입으로 type cast한다
    if (nullableStr.isEmpty()) { } 
    
    if (outerProperty == null) return
    if (outerProperty.isEmpty()) {} // error. outerProperty는 외부로부터 변화될 수 있어서
}

/*
not-null assertion. KotlinNullPointerException(NPE의 하위)을 발생시킬 수 있다
컴파일러가 null, non-null을 구분하지 못해 smart cast 불가능
safe call을 사용하면 에러를 줄이면서 안전하게 할 수 있다
*/
fun main() {
    notNullAssertion(null)
}

fun notNullAssertion(nullableStr: String?) {
    println(nullableStr.uppercase())	// error.
    println(nullableStr?.uppercase())	// 안전한 호출 연산자(safe call)
    println(nullableStr!!.uppercase())	// 널 아님 단언(not-null assertion)
}

// 같은 결과, 다른 표현
// nullableStr이 null이면 uppercase()를 실행하지 않고 null 반환
// null이 아니면 uppercase() 실행
fun notNullAssertion(nullableStr: String?) {
    if (nullableStr == null) null else nullableStr.uppercase()
    nullableStr?.uppercase()
}


fun main() {
    notNullAssertion(null)
    notNullAssertion("abcd")
}

// Elvis operator(엘비스 연산자) -> ?: 를 사용한다
// null을 대신할 default 값을 지정할 수 있다
fun notNullAssertion(nullableStr: String?) {
    val uppercase = nullableStr?.uppercase()

    println(uppercase ?: "null")
}

/// return 사용하면서 식으로 사용
fun notNullAssertion(nullableStr: String?): String =
    nullableStr?.uppercase() ?: "null"

 

 

최상위 프로퍼티

// util.kt
var outerProperty: String? = "outerProperty" // 최상위 가변 프로퍼티. val은 불변

fun main() {
    println(outerProperty)
}

// 다른 곳에서 import 가능
import util.outerProperty

fun main() {
    println(outerProperty)
}

 

늦은 초기화

/*
lateinit -> 늦은 초기화
property가 변경될 수 있으므로 가변(var)으로로 정의
property 타입은 non-null이며 primitive 타입이 아니어야 한다

lateinit 단점
프로퍼티가 초기화되지 않고 사용할 경우 UninitializedPropertyAccessException
*/
class User {
    var firstName: String? = null
    lateinit var lastName: String

    fun initFirstName(firstName: String) {
        this.firstName = firstName
    }

    fun initLastName(lastName: String) {
        this.lastName = lastName
    }
}

fun main() {
    val user = User()
    user.initFirstName("kancho")
    val firstNameLength = user.firstName?.length ?: 0	// firstName이 null이 아님에도 널 가능성을 처리
    val lastNameLength = user.lastName.length	// lateinit으로 인해 lastName은 null 체크를 하지 않는다
}

 

커스텀 접근자

프로퍼티 값을 읽거나 쓸 때 커스텀하게 정의할 수 있다

커스텀 접근자를 통해 변수와 함수의 동작을 한 선언안에 조합

 

// custom getter
// getter의 return 타입은 property 타입과 같아야 한다
class User(val firstName: String, val lastName: String) {
    val name1: String
        get(): String {
            return "$firstName $lastName"
        }

    val name2
        get(): String {
            return "$firstName $lastName"
        }

    val name3: String
        get() {
            return "$firstName $lastName"
        }

    // 타입 추론
    val name4
        get() = "$firstName $lastName"
}

 

 

backing field가 없기 때문에 클래스 인스턴스에서 메모리를 차지하지 않는다.

backing field는 조건이 필요하다

property에 명시적으로 field를 사용하는 디폴트 접근자나 커스텀 접근자가 하나라도 있으면 backing field가 생성

불변 프로퍼티의 접근자는 read 뿐이므로 위에서는 backding field를 참조하지 않으며 없다

 

backing field가 없으면 필드를 초기화할 수 없다

'코틀린' 카테고리의 다른 글

[코틀린] Scope function (영역 함수)  (0) 2022.10.10
[코틀린] 확장 (Extension)  (0) 2022.10.10
[코틀린] Lazy 초기화  (0) 2022.10.09
코틀린 정리 - (2)  (0) 2022.09.30
코틀린 정리 - (1)  (0) 2022.09.18