책/코틀린 쿡북

3장 - 코틀린 객체 지향 프로그래밍

ballde 2022. 7. 18. 11:05

객체 초기화, getter, setter, 나중 초기화, 지연 초기화, 싱글톤, Nothing 이해하기

3.1 const와 val의 차이

런타임 보다는 컴파일 타임에 변수가 상수임을 나타내야 한다.

  • 컴파일 상수(컴파일 시점)에 const 변경자를 사용한다.
  • 실행 시간(런타임)에 val 키워드를 사용한다. (java final)

그렇다면 const를 지원하는 이유는 뭘까?

컴파일 상수

  • 함수나 클래스의 생성자에게도 결코 할당 될 수 없고 오직 문자열이나 기본 자료형으로 할당 되어야함(클래스의 프로퍼티나 지역변수로 할당 할 수 없으며 문자열 또는 래퍼타입)
  • 컴파일 타임 상수는 반드시 객체나 동반 객체 선언의 최상위 속성 또는 멤버여야 한다.(보통 companion objet에 쓰임)
  • getter, setter를 가질 수 없다.
const val DEFAULT_PRIORITY = 3

class Task(
    val name: String,
    _priority: Int = DEFAULT_PRIORITY
) {
    
    companion object {
        const val MIN_PRIORITY = 1
    }
    
}
  • 코틀린에서 val은 키워드지만 const는 private, inline과 같은 변경자 주의!!
  • const는 val과 무조건 같이 쓰임

3.2 사용자 정의 획득자와 설정자 생성하기(getter, setter)

  • 기본적으로 public -
  • 데이터 은닉을 침해하는 것처럼 보임 - 클래스에서 필드는 직접 선언할 수 없음
// name에는 타입이 무조건 들어가야함
class Task2(val name: String) {
	var priority = 3	
}

단점

  • 인스턴스화 할 때 priority에 값을 할당할 수 없다.
Task2("hello").apply { priority = 3 }

// 범위 지정함수 (apply, run, with, let, also)
  • apply블록을 사용해서 priority에 값을 할당 가능
  • 쉽게 사용자 정의 획득자와 설정자를 추가할 수 있다
  • 생성자에서 선언한 속성에는 할당된 기본값이 있더라도 반드시 타입 정의가 들어가야한다.

https://balldev.tistory.com/84

 

코틀린 범위 지정 함수(apply, run, with, let, also)

정의 수신 객체 지정 람다함수 특정 객체에 대한 작업을 블록 안에 넣어 실행할 수 있도록 하는 함수 이름에 따라서 범위 지정이 달라진다. apply, run, with, let, also this: 수신 객체를 람다의 수신 객

balldev.tistory.com

class Task2(val name: String) {
    var priority = 3
    
    val isLowPriority get() = priority < 3
}

task2.isLowPriority 접근 가능
  • 코틀린은 자동으로 지원 필드를 생성한다(get, set)
class Task(
    val name: String,
    _priority: Int = DEFAULT_PRIORITY
) {

    companion object {
        const val MIN_PRIORITY = 1
    }

		// 함수 이름처럼 이름은 마음대로 해도 된다.
    var sdf = validPriority(_priority)
    set(value) {
        field    = validPriority(value)
    }

    private fun validPriority(p: Int) = p.coerceIn(MIN_PRIORITY, 5)

}
  • setter가 실행됨
  • 함수 이름처럼 원하는 이름으로 할 수 있다.

3.3 데이터 클래스 정의하기

  • 데이터를 담는 특정 클래스의 용도를 위해 data 키워드 제공 → 자바 데이터베이스 엔티티와 비슷
  • 일관된 equals, hashCode, toString, copy 함수를 생성
data class Product(
	val name: String,
	val price: Double
)

val p1 = Product("hello", 1.0)
val p2 = Product("hello", 1.0)

println(product1 === product2) // false
println(product1 == product2) // true
// hashCode가 같다. 
println("${product1.hashCode()} == ${product2.hashCode()}   " )
  • 데이터 클래스의 copy 함수 호출하면 얕은 복사가 수행됨
data class Product(var name: String)
data class CopyObject(var product: Product)
val copyObject1 = CopyObject(Product("name"))
val copyObject2 = copyObject1.copy()
println(copyObject1 === copyObject2) // false
println(copyObject1.product === copyObject2.product) // true
  • 내부 Product를 공유하고 있음
  • 데이터 클래스의 copy는 깊은 복사가 아닌 얕은 복사를 수행한다.
  • 하지만 기본형 변수는 깊은 복사가 가능
data class CopyObject2(var name: String)

val copyObject1 = CopyObject2("name")
val copyObject2 = copyObject1.copy()
println(copyObject1 === copyObject2) // false
val p = Product("hello", 1.0)
val (name, price) = p
  • 위처럼 구조분해 가능
  • 코틀린이 생성하는 함수가 포함된 속성을 사용하고 싶지 않다면 주 생성자가 아닌 클래스 몸체에 속성을 추가하자. - 클래스 안에서 private로 생성하기

https://balldev.tistory.com/85

 

동등성, 동일성 연산(==, ===)

동등성 동일성 동등성(equals) 오브젝트 서로가 완전히 동일 값이 같은지 동일성(==) 동일한 정보를 가지고 있는 오브젝트 같은 메모리에 다른 오브젝트가 존재하는 경우 주소값이 같은지 자바 ==

balldev.tistory.com

3.4 지원 속성 기법

클래스의 속성을 클라이언트에게 노출하고 싶지만 해당 속성을 초기화하거나 읽는 방법을 제거해야한다.

class Customer(val name: String) {

    private var _message: List<String>? = null

    val messages: List<String>
        get() {
            if (_message == null) {
                _message = loadMessages()
            }
            return _message!!
        }

    private fun loadMessages(): List<String> =
        mutableListOf(
            "hello",
            "sldkf",
            "slkdf"
        ).also { println("loaded messages") }

}

		val customer = Customer("name").apply { messages }
    val customer2 = Customer("name")
    println(customer.messages.size)

3.5 연산자 중복

자신이 작성하지 않은 클래스에도 연산자와 관련된 함수를 추가할 수 있다.

operator fun Point.unaryMinus() = Point(-x, -y)

operator fun Complex.plus(c: Complex) = this.add(c)
operator fun Complex.plus(d: Double) = this.add(d)
...
  • equals를 제외한 모든 연산자 함수를 재정의 할 때는 operator 키워드는 필수
  • 확장 함수는 자바 클래스의 기존 메서드에 연산을 위임한다.

3.6 나중 초기화 lateinit

생성자 속성 초기화를 위한 정보가 충분하지 않으면 해당 속성을 널 비허용 속성으로 만들고 싶을 경우

  • 꼭 필요한 경우에만 사용
  • apply를 먼저 고려하자
  • lateinit 변경자는 클래스 몸체에서만 선언됨
  • 사용자 정의 획득자와 설정자가 없는 var 속성에서만 사용 가능
  • 널 할당이 불가능한 타입
  • 기본타입에서 사용 불가능
  • 변수가 처음 사용되기 전에 초기화 가능
  • 객체 바깥쪽에서도 초기화 가능
val lateHello = LateHello()
// 오류남(UninitializedPropertyAccessException)
println(lateHello.name)
class LateHello {
    lateinit var name: String

    fun initializeName() {
        println("${::name.isInitialized}") // false
        name = "world"
        println("${::name.isInitialized}") // true
    }
}

lazy

  • 속성에 처음 접근 할 때 수행
  • 초기화 비용이 높은데 lazy를 사용하면 초기화는 반드시 실패
  • lazy는 val 속성에 사용 가능

선언

class LateHello {
    lateinit var name: String
		
		// 처음 호출할 때 한번 호출
    val name2: String by lazy {
			"skdfjs"
		}

fun initializeName() {
println("${::name.isInitialized}")
        name = "world"
println("${::name.isInitialized}")
    }

}

3.7 equals 재정의를 위해 안전 타입 변환, 래퍼런스 동등, 엘비스 사용

동등 - 자바(equals) 코틀린(== 이 자동으로 equals로 비교)

equals

  • 반사성, 대칭성, 추이성, 일관성, null 도 처리 가능해야한다.
  • 동등하다고 판단하여 hashCode 도 같아야한다.

코틀린의 equals 구현 (인텔리제이가 생성해준것도 본질적으로 같음)

override fun equals(other: Any?): Boolean {

	if (this === other) return true
	val otherVersion = (other as? KotlinVersion) ?: return false
	return this.version == otherVersion.version
}
  • === 으로 래퍼런스 동등성 확인
  • 인자를 원하는 타입으로 변환하거나 널을 리턴하는 안전 타입 변환 연산자 as?를 사용
num is Int

// as는 형변환이 가능하지 않으면 예외발생
val x: String = y as String

// y가 null이 아니면 String으로 형변환되어 x에 할당
val x: String? = y as? String
  • 안전 타입 변환 연산자가 널을 리턴하면 같은 클래스의 인스턴스가 아니므로 동등일 수 없기 때문에 ?: false 반환
  • 인스턴스의 version과 other 객체의 version 동등 여부 검사

3.8 싱글톤 생성하기

  • 클래스 하나당 인스턴스는 딱 하나만 존재하게 만들고 싶다.

싱글톤 정의 방법

  • 클래스의 모든 생성자를 private로 정의
  • 해당 클래스를 인스턴스화 하고 그 인스턴스 래퍼런스를 리턴하는 정적 팩토리 메서드를 제공

싱글톤 예시(몇개의 프로세서가 사용 가능한지)

fun main() {

	val processors = Runtime.getRuntime().availableProcessors()
}

Runtime 예제

public class Runtime {
	private static final Runtime currentRuntime = new Runtime();

	public static Runtime getRuntime() {
		return currentRuntime;	
	}
	
	// 인스턴스 생성 못하게
	private Runtime()
}
// 코틀린에서는 class 대신 object 사용
// 디컴파일 해보면 위의 자바코드와 비슷하게 나옴
object MySingleton {
	val myProperty = 3
	fun myFunction() = "HELLO"
}

public final class MySingleton {
	private static final int myProperty = 3;
	public static final MySingleton INSTANCE;
	public final void myFunction() {
		return "HELLO";
	}
}

자바에서 싱글톤 멤버 접근

MySingleton.INSTANCE.myFunction();
MySingleton.INSTANCE.getMyProperty();

코틀린에서 싱글톤 멤버 접근

MySingleton.myFunction()
MySingleton.myProperty

3.9 Noting

절대 리턴하지 않는 함수에 Nothing을 사용한다.

public class Nothing private constructor()
  • 밖에서도 안에서도 인스턴스화 할 수 없다. - 인스턴스는 존재하지 않는다.

대표적 예시

fun doNothing() : Nothing = throw Exception("nothing")

주의점: 자바에서는 예외를 던지든 리턴타입이 변경하지 않음

// 구체적인 타입을 명시하지 않는 경우
val x = null
  • 이것은 Nothing? 이다.
  • Nothing은 모든 타입의 하위타입