ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Kotlin In Action] 코틀린 타입 시스템
    프로그래밍 언어/kotlin 2021. 8. 6. 13:53
    728x90
    반응형

    코틀린 원시 타입 소개와 자바 타입과 코틀린 원시 타입의 관계

     

    코틀린 컬렉션 소개와 자바 컬렉션과 코틀린 컬렉션의 관계

     

    타입 시스템

    - 널이 될 수 있는 타입

    - 읽기 전용 컬렉션

     

    6.1 널 가능성

    널이 될 수 있는지 여부를 타입 시스템에 추가함으로써 컴파일러가 여러 가지 오류를 컴파일 시 미리 감지해서 실행 시점에 발생할 수 있는 예외의 가능성을 줄일 수 있다.

     

    널이 될 수 있는 타입

    널이 될 수 있는 타입을 명시적으로 지원한다.

    어떤 변수가 null이 될 수 있다면, 그 변수에 대해 메소드를 호출하면 NullPointerException이 발생할 수 있으므로 안전하지 않다. 

    어떤 타입 이름 뒤에 물음표를 붙이면 그 타입의 변수나 프로퍼티에 null 참조를 저장할 수 있다는 뜻.

     

    널이 될 수 있는 타입의 변수에 대해 변수.메소드()처럼 메소드를 직접 호출할 수 없다.

    널이 될 수 있는 값을 널이 될 수 없는 타입의 변수에 대입할 수 없다.

    널이 될 수 있는 타입의 값을 널이 될 수 없는 타입의 파라미터를 받는 함수에 전달할 수 없다.

     

    타입의 의미

    타입 : 분류로, 타입은 어떤 값들이 가능한지와 그 타입에 대해 수행할 수 있는 연산의 종류를 결정한다.

    널이 될 수 있는 타입과 될 수 없는 타입을 구분하면 각 타입의 값에 대해 어떤 연산이 가능하고, 실행 시점에 예외를 발생시킬 수 있는 연산을 금지시킬 수 있다.

     

    안전한 호출 연산자: ?.

    s?.toUpperCase() // 식의 결과 타입은 String?
    // 위와 아래는 같은 의미이다.
    if (s != null) s.toUpperCase() else null

    널이 아닌 값에 대해서만 메소드를 호출한다.

    만약 s가 널이라면, 메소드 호출은 무시되고 null이 결과값이 된다. (안전한 호출의 결과 타입도 널이 될 수 있는 타입이다)

     

    엘비스 연산자: ?:

    좌항 값이 널이 아니면 좌항 값을 결과로 하고 좌항 값이 널이면 우항 값을 결과로 한다

    fun foo(s: String?) {
    	val t: String = s ?: ""
    }

    우항에 return, throw 등의 연산을 넣을 수 있다. ->  전제 조건을 검사하는 경우 유용

     

    안전한 타입 캐스트 연산자: as?

    어떠한 값을 지정한 타입으로 캐스트한다. 값을 대상 타입으로 변환할 수 없으면 null을 반환한다.

     

    널 아님 단언: !!

    어떤 값이든 널이 될 수 없는 타입으로 바꿀 수 있다. 값이 null 일때 NPE이 던져진다.

    액션 클래스 안에는 그 액션의 상태를 변경하는 메소드와 실제 액션을 실행하는 메소드가 있다.

    update 메소드 안에서 검사 조건을 만족하지 않으면 execute 메소드는 호출될 수 없다.

     

    let 함수

    let 함수와 안전한 호출 연산자를 함께 사용하면, 원하는 식을 평가해서 결과가 널인지 검사한 다음, 그 결과를 변수에 넣는 작업을 간단한 식을 사용해 한꺼번에 처리할 수 있다. 결과가 널일 경우 아무일도 일어나지 않는다.

    널이 될 수 있는 값을 넣이 아닌 값만 인자로 받는 함수에 넘기는 경우 많이 사용한다.

    자신의 수신 객체를 인자로 전달받은 람다에게 넘긴다. 널이 될 수 있는 값에 대해 안전한 호출 구문을 사용해 let을 호출하되 널이 될 수 없는 타입을 인자로 받는 람다를 let에 전달한다. 

    email?.let { email ->
        sendEmailTo(email)
    }

    람다 안에서는 널이 될 수 없는 타입으로  email(수신 객체)을 사용할 수 있다.

     

    여러 값이 널인지 검사해야 한다면 let 호출을 중첩시켜서 처리할 수 있지만, 코드가 복잡해진다. 이럴 경우는 일반적인 if를 사용해 모든 값을 한꺼번에 검사하는게 낫다.

     

    나중에 초기화할 프로퍼티

    객체 인스턴스를 일단 생성한 다음, 나중에 초기화시키는 프레임워크가 많다.

    안드로이드에서는 onCreate 에서 액티비티를 초기화한다.

    하지만, 코틀린에서는 일반적으로 생성자에서 모든 프로퍼티를 초기화해야 한다. 코틀린에서 클래스 안의 널이 될 수 없는 프로퍼티를 생성자 안에서 초기화하지 않고 특별한 메소드 안에서 초기화 할 수는 없다. 

    또한, 널이 될 수 없는 타입이라면 반드시 널이 아닌 값으로 프로퍼티 초기화 해야한다. 

    class MyService {
        fun performAction(): String = "foo"
    }
    
    class MyTest {
        private var myService: MyService? = null
    
        @Before fun setUp() {
            myService = MyService()
        }
    
        @Test fun testAction() {
            Assert.assertEquals("foo",
                myService!!.performAction())
        }
    }

    프로퍼티를 여러번 사용해야 하면 코드가 못생겨진다. 이를 해결하기 위해 나중에 초기화할 수 있다.

     

    널이 될 수 있는 타입 확장

    어떤 메소드를 호출하기 전에 수신 객체 역할을 하는 변수가 널이 될 수 없다고 보장하는 대신, 직접 변수에 대해 메소드를 호출해도 확장 함수인 메소드가 알아서 널을 처리해준다. 

    안전한 호출 없이널이 될 수 있는 수신 객체 타입에 대해 선언된 확장 함수 호출이 가능하다. 

    실제, String? 타입의 수신 객체에 대해 호출할 수 있는 isNullOrEmpty이나 isNullOrBlank 메소드가 있다.

    if (input.isNullOrBlank()) {
        println("Please fill in the required fields")
    }

    널이 될 수 있는 타입에 대한 확장을 정의하면 널이 될 수 있는 값에 대해 그 확장 함수를 호출할 수 있다.

    fun String?.isNullOrBlank(): Boolean =
        this == null || this.isBlank() // 두 번째 this엔 스마트 캐스트 적용

    널이 될 수 있는 타입의 확장 함수 안에서는 this가 null이 될 수 있다. (자바는 메소드 안의 this는 그 메소드가 호출된 수신 객체를 가리키므로 항상 널이 아님)

     

    타입 파라미터의 널 가능성

    fun <T> printHashCode(t: T) {
        println(t?.hashCode()) // t가 null이 될 수 있으므로 안전한 호출 사용해야 한다.
    }
    
    fun <T: Any> printHashCode2(t: T) { // 타입 상한 지정 -> 널이 될 수 있는 값을 거부
        println(t.hashCode())
    }
    
    fun main(args: Array<String>) {
        printHashCode(null) // t의 타입은 Any?로 추론
    }

     

    널 가능성과 자바

    자바의 @Nullable String 은 코틀린에서 String?과 같다.

    자바의 @NotNull String 은 코틀린에서 String과 같다.

    이런 널 가능성 에노테이션이 소스코드에 없는 경우, 자바의 타입은 코틀린의 플랫폼 타입이 된다.

    * 플랫폼 타입 : 코틀린이 널 관련 정보를 알 수 없는 타입. 

    이 타입은, 널이 될 수 있는 타입으로 처리해도 되고, 널이 될 수 없는 타입으로 처리해도 된다. 

     

    코틀린 컴파일러는 공개 가시성인 함수의 널이 아닌 타입인 파라미터수신 객체에 대한 널 검사를 추가해준다.

    따라서 함수 호출 시점에 파라미터 값 검사가 이루어지기 때문에, 널 값을 사용하면 함수 호출 시점에 즉시 예외가 발생한다.

     

    * 상속 

    코틀린에서 자바 메소드를 오버라이드 할 때 그 메소드의 파라미터와 반환타입을 널이 될 수 있는 타입으로 선언할지, 널이 될 수 없는 타입으로 선언할 지 결정해야 한다. 

    코틀린 컴파일러는 널이 될 수 없는 타입으로 선언한 모든 파라미터에 대해 널이 아님을 검사하는 단언문을 만들어준다. 따라서 해당 메소드에게 자바 코드가 널 값을 넘기면 예외가 발생한다.

    6.2 코틀린의 원시 타입

    원시 타입: Int, Boolean 등

     

    널이 될 수 있는 원시 타입: Int?, Boolean? 등

    널이 될 수 있는 타입은 자바의 원시 타입으로 표현할 수 없다. 따라서, 코틀린의 널이 될 수 있는 원시 타입자바의 래퍼 타입으로 컴파일 된다. 

    제네릭 클래스의 경우 래퍼타입을 사용한다.

    자바에서 JVM은 타입 인자로 원시 타입을 허용하지 않기 때문에 박스 타입을 사용한다. 따라서 코틀린에서도 어떤 클래스의 타입 인자로 원시 타입을 넘기면 그 타입에 대한 박스 타입을 사용한다.

    원시 타입으로 이루어진 대규모 컬렉션을 효율적으로 저장해야 한다면, 원시 타입으로 이루어진 효율적인 컬렉션을 제공하는 서드파티 라이브러리를 사용하거나 배열을 이용해야 한다.

     

    숫자 변환

    코틀린은 한 타입의 숫자를 다른 타입의 숫자로 자동변환하지 않는다. 결과 타입이 허용하는 숫자 범위가 원래 타입의 범위보다 넓은 경우 조차 자동 변환이 불가능하다.

    val i = 1
    val l: Long = i // 컴파일 오류
    val l: Long = i.toLong()

    코틀린은 모든 원시 타입에 대한 변환 함수를 제공한다. 

    박스 타입간의 equals 메소드는 그 안에 들어있는 값이 아닌, 박스 타입 객체를 비교한다. 

    new Integer(42).equals(new Long(42)) // false
    val x = 1
    val list = listOf(1L, 2L, 3L)
    x in list // false
    x.toLong() in listOf(1L, 2L, 3L) // true

    원시 타입 리터럴 사용 or 타입이 알려진 변수에 대입/함수의 인자로 넘길 경우 -> 변환 함수 호출할 필요 없음

    산술 연산자는 적당한 타입의 값을 받아들일 수 있에 오버로드 되어있다.

    val b: Byte = 1
    val l = b + 1L

     

    Any, Any?: 최상위 타입

    Any: 모든 널이 될 수 없는 타입의 조상 타입

    자바에서는 참조 타입만 Object를 정점으로 하는 타입 계층에 포함되며, 원시 타입은 그런 계층에 들어있지 않다.

    따라서, Object 타입의 객체가 필요한데 int 같은 원시 타입은 java.lang.Integer 같은 래퍼 타입으로 감싸야한다.

    Any 타입은 내부에서 Object 타입에 대응한다.

     

    Unit

    자바의 void와 같은 기능

    관심을 가질만한 내용을 전혀 반환하지 않는 함수의 반환 타입으로 사용할 수 있다.

    fun f(): Unit { ... }
    fun f() { ... }
    // 둘이 같은 의미

    코틀린 함수의 반환 타입이  Unit이고, 그 함수가 제네릭 함수를 오버라이드 하지 않음 -> 자바 void 함수로 컴파일됨


    Unit 을 타입 인자로 사용할 수 있다.

    Unit 타입의 함수는 unit을 묵시적으로 반환한다.

    -> 제네릭 파라미터를 반환하는 함수를 오버라이드 하면서 반환 타입으로 Unit을 사용할 때 유용하다.

    interface Processor<T> {
        fun process(): T
    }
    
    class NoResultProcessor : Processor<Unit> { // Unit을 반환하지만 타입 지정할 필요 없다. 
        override fun process() {
            // 업무 처리 코드 
        } // 리턴 명시할 필요 없음. 컴파일러가 묵시적으로 return Unit 넣어줌
    }

     

    Nothing

    함수가 정상적으로 끝나지 않는 경우를 표현할 때 유용

    함수의 반환 타입이나 반환 타입으로 쓰일 타입 파라미터로만 쓸 수 있다.

    val address = company.address ?: fail("No address")
    println(address.city)

     

    6.3 컬렉션과 배열

    널 가능성과 컬렉션

    컬렉션 안에 넣 값을 넣을 수 있는지 여부 고려 필수

    fun readNumbers(reader: BufferedReader): List<Int?> {
        val result = ArrayList<Int?>()
        for (line in reader.lineSequence()) {
            try {
                val number = line.toInt()
                result.add(number)
            }
            catch(e: NumberFormatException) {
                result.add(null)
            }
        }
        return result
    }

    List<Int?> 리스트 안의 각 값이 널이 될 수 있다. 리스트 자체는 널이 될 수 없다.

    List<Int>? 전체 리스트가 널이 될 수 있다.

    List<Int?>?

     

    읽기 전용과 변경 가능한 컬렉션

    코틀린에서는 컬렉션 안의 데이터를 접근하는 인터페이스와 컬렉션 안의 데이터를 변경하는 인터페이스를 분리했다.

    ex)

    kotlin.collections.Collection

    [읽기] 원소를 이터레이션, 컬렉션 크기 얻기, 어떤 값이 컬렉션 안에 들어가는지 검사, 컬렉션에서 데이터를 읽는 여러 다른 연산 ..

    kotlin.collections.MutableCollection ( Collection 인터페이스를 확장 )

    [쓰기] 원소 추가/제거/삭제 등... 

     

    가능하면 읽기 전용 인터페이스를 사용하는게 좋다. 변경이 필요할 때만 변경 가능한 버전을 사용하자.

     

    읽기 전용 컬렉션이라고 해서 꼭 변경 불가능한 컬렉션일 필요는 없다.

    같은 인스턴스를 가리키는 변경 가능한 인터페이스 타입의 참조도 있을 수 있다.

    이런 상황에서 이 컬렉션을 참조하는 다른 코드를 병렬실행한다면, 컬렉션 사용 도중 다른 컬렉션이 그 컬렉션의 내용을 변경하는 상황이 생길 수 있다.(ConcurrentModificationException 발생) 따라서, 읽기 전용 컬렉션이 항상 스레드 안전하지 않다.

    그 데이터를 적절히 동기화 하거나 동시 접근을 허용하는 데이터 구조를 활용해야 한다.

     

    코틀린 컬렉션과 자바

    변경 가능한 컬렉션, 읽기 전용 컬렉션을 어떻게 구분하나?

    코틀린은 모든 자바 컬렉션 인터페이스마다 읽기 전용 인터페이스변경 가능한 인터페이스라는 두가지 표현을 제공한다

    변경 가능한 인터페이스는 자신과 대응하는 읽기 전용 인터페이스를 확장(상속) 한다.

     

    컬렉션을 파라미터로 받는 자바 메소드가 있다면, Collection이나 MutableCollection 값을 인자로 넘길 수 있다.

    이러면, 자바에서 읽기 전용으로 선언된 컬렉션도, 변경 가능할 수 있게된다. 널이 아닌 원소로 이루어진 컬렉션을 자바 메소드로 넘겼는데, 자바쪽에서 널을 컬렉션의 원소로 넣을 수도 있다. 이를 코틀린 컴파일러가 막을 수는 없다. 따라서 주의를 기울여야 한다. 코틀린 쪽 타입이 적절히 자바 쪽에서 컬렉션에게 가할 수 있는 변경의 내용을 반영(널 가능성이나 불변성)하게 해야한다. 

     

    컬렉션을 플랫폼 타입으로 다루기

    자바 쪽에서 선언한 컬렉션 타입의 변수를 코틀린에서는 플랫폼 타입으로 본다. 

    컬렉션 타입이 시그니처에 들어간 자바 메소드 구현을 오버라이드하려는 경우, 읽기 전용 컬렉션과 변경 가능 컬렉션의 차이가 문제된다.

    • 컬렉션이 널이 될 수 있는가?
    • 컬렉션의 원소가 널이 될 수 있는가?
    • 오버라이드하는 메소드가 컬렉션을 변경할 수 있는가?
    /* 자바 */
    interface FileContentProcessor {
        void processContents(File path, byte[] binaryContents, List<String> textContents);
    }
    
    /*코틀린으로 구현*/
    class FileIndexer : FileContentProcessor {
        override fun processContents(path: File, 
        binaryContents: ByteArray?,
        textContents: List<String>?) {
            // ...
        }
    }

    파일에 들어있는 텍스트를 처리하는 인터페이스를 코틀린으로 구현할 경우

    • 일부 파일은 이진 파일이며, 때문에 파일 안의 내용이 텍스트로 표현할 수 없는 경우가 있다 -> List는 널이 될 수 있다
    • 파일의 각 줄은 널일 수 없다 -> List의 원소는 널이 될 수 없다
    • 리스트는 파일의 내용을 표현하며, 바꿀 필요가 없다 -> List는 읽기 전용

     

    객체의 배열과 원시 타입의 배열

    fun array1() {
        // 람다는 배열의 원소의 인덱스를 인자로 받아서 배열의 해당 위치에 들어갈 원소를 반환
        val letters = Array<String>(26) { i -> ('a' + i).toString() }
        println(letters.joinToString(""))
    }
    
    fun array2() {
        val strings = listOf("a", "b", "c")
        // 배열을 인자로 받는 자바 함수를 호풀하거나 vararg 파라미터를 받는 코틀린 함수를 호출하기 위해 배열을 만든다.
        // 데이터가 이미 컬렉션에 들어있다면, 배열로 변환해야 한다. -> toTypedArray() 사용
        println("%s/%s/%s".format(*strings.toTypedArray())) // vararg 인자를 넘기기 위해 스프레드 연산자(*)를 써야 한다.
    }

    원시 타입의 배열을 만드는 방법

    • 각 배열 타입의 생성자는 size 인자를 받아서 해당 원시 타입의 디폴트 값으로 초기화된 size 크기의 배열을 반환한다.
    • 팩토리 함수는 여러 값을 가변 인자로 받아서 그런 값이 들어간 배열을 반환한다.
    val fiveZeros = IntArray(5)
    val fiveZerosToo = intArrayOf(0, 0, 0, 0, 0)
    • 크기와 람다를 인자로 받는 생성자를 사용한다.
    fun array3() {
        /*
        배열 타입의 타입 인자도 항상 객체 타입이 되서, Array<Int> 를 선언하면 박싱된 정수의 배열로 된다.
        원시 타입의 배열이 필요하다면 특별한 배열 클래스를 써야한다.
        Int 타입의 경우에는 IntArray.
        코틀린은 ByteArray, CharArray, BooleanArray 등 다양하게 제공
        각각 byte[], char[], bool[] 등으로 컴파일
        */
        val squares = IntArray(5) { i -> (i+1) * (i+1) }
        println(squares.joinToString())
    }

    toIntArray 등의 변환 함수를 사용하면 박싱된 값이 들어있는 컬렉션을 박싱하지 않는 값이 들어있는 배열로 변환할 수 있다.

     

    코틀린에서는, 배열 기본 연산과 더불어 컬렉션에서 사용할 수 있는 모든 확장 함수를 배열에 제공한다. 단, 해당 확장 함수가 반환하는 값은 배열이 아니라 리스트이다.

    val strings = arrayOf("a", "b", "c", 1, 3)
    val test = strings.filter{ it is String } // 리스트 타입

     

     

    728x90

    댓글

Designed by Tistory.