본문 바로가기

안드로이드 Android

안드로이드 개발 (42) Kotlin In Action 정리 - 6

(이전편 다시보기)

https://gift123.tistory.com/77

 

안드로이드 개발 (41) Kotlin In Action 정리 - 5

(이전편 다시보기) 안드로이드 개발 (40) Kotlin In Action 정리 - 4 (이전편 다시보기) 안드로이드 개발 (38) Kotlin In Action 정리 - 2 (이전 편 다시보기) 안드로이드 개발 (37) Kotlin In Action 정리 - 1 현업에서

gift123.tistory.com

어느덧 6장 요약 이네요

참고로, kotlin in action을 전체 요약본이 아니라 개인적으로 필요한 부분을 위주로 정리했습니다.

 

6장 코틀린 타입 시스템

kotlin 만의 타입 시스템은 타입이 널 허용하는지 안하는지에 따른 안전성과 가변과 불변 타입으로 나누거나 자바에서 신경써야 했던 래퍼 타입과 원시 타입의 구분이 스마트하게 된다는점 등등의 특징을 가지고 있습니다.

*Loner 생각: kotlin은 위같은 장점으로 굉장히 좋은 안전성을 보여줍니다.*

 

1) 널 가능성

- 안전한 호출 연산자?. 를 잘쓰면 문법적으로 깔끔하게 널일 경우를 처리할 수 있다.

예시로 if(s != null) s.toUpperCase() else null를 s?.toUpperCase() 간결하게 표현이 가능하다. 

 

//?. 처리가 없다면 아래와 같은 코드로 해야한다.
if(s != null) s.toUpperCase() else null

//?. 를 사용한다면 아래처럼 매우 간결한 코드로 null 처리를 매우 효율적으로 할 수 있다.
s?.toUpperCase()

 

- T? 에서 T가 null 일 경우에 null를 return 하지 않고 다른 값을 return하고 싶다면 엘비스 연산자 ?: 를 사용할 수 있다. 

 

- T as? R 은 안전한 캐스트로 T가 R 타입으로 바꿀 수 없다면 null 를 return 한다.

//아래 두개의 코드는 같은 동작을 한다.

//1)
foo as?Type

//2)
if(foo is Type) foo as Type else null

- 그외에 null처리는 let?.{} , lateini, 확장함수를 이용한 방식 ex: Collection<T>.isNullOrEmpty()  등등이 있다.

 

- 모든 타입 파라미터는 기본적으로 null 이 될 수 있다.

 

fun <T>test(t:T){
//위에서 T? 를 명시하지 않았음에도 불구하고 T는 될 수 있어서
// 안전한 호출자 ?. 를 사용해도 Ide의 조언이 없다.
    t?.hashCode()
}

 

- 타입 파라미터가 널이 될 수 없도록 하려면 널 허용이 되지 않는 타입으로 타입 상한을 지정해야한다.

 

//T에 Any로 타입 상한을 하면 T는 null이 될 수 없으므로, 안전한 호출자 ?. 를 쓸 이유가 없다.
fun <T:Any>test(t:T){
    t.hashCode()
}

 

 

*Loner 생각: 문법적으로 null 관리에 있어서 매우 뛰어나다고 생각합니다. 특히 저는 엘비스 연산자는 책에서 짦게 언급 되었지만 현업에서 실용적으로 잘 사용하고 있습니다.*

 

- 플랫폼 타입은 kotlin이 null 관련 정보를 알 수 없는 타입이기 때문에 굉장히 위험하기 때문에 본래 플랫폼 타입에 null 지정 여부 처리를 하던가(Java로 치면 @Nullable 애노테이션이 예시) 문서를 정리해서 공유해야 할 것이다.

 

count:Int! 임에도 불구하고 null을 넣어도 컴파일 상 문제가 없다..

 

- kotlin이 플랫폼 타입을 모두 null 허용 타입으로 처리하지 않고 플랫폼 타입이라는 개념을 사용한 이유는 외부에서 오는 모든 타입이 null 이라면 null을 검사하는 비용이 상당히 발생하기 때문에 플랫폼 타입이라는 가능성을 열어둔 것 이다. 대표적으로 ArraryList<String?>? 를 사용할 때 배열의 원소를 접근할 떄마다 null 검사 및 안전한 캐스트를 일일히 수행해야 할 것이다.

*Loner 생각: Java와 Kotlin을 혼용한 프로젝트에서 눈에 띄게 실수 할 수 있는 문제라서 두개의 언어를 혼용하여 사용한다고 하면 플랫폼 타입에 대한 관리를 철저히 해야할 것 입니다.*

 

- Kotlin에서 자바 메서드를 override 할 때 메서드의 파라미터나 반환 타입을 null 허용할 타입으로 선언할지 아닌지 결정할 수 있다.

 

//아래와 같은 자바 클래스가 있다면
public abstract class JavaTest {
    public abstract Integer addValue (Integer addValue);
}
class JavaTestImpl() : JavaTest() {
//1) addVlue의 파라미터가 널을 허용하지 않도록 지정 가능
    override fun addValue(addValue: Int): Int {
        return addValue + 1
    }
}


class JavaTestImpl() : JavaTest() {
//2) addVlue의 파라미터가 널을 허용하도록 지정 가능
    override fun addValue(addValue: Int?):Int {
        return addValue + 1
    }
}

class JavaTestImpl() : JavaTest() {
//3) addVlue의 return 타입이 널을 허용하도록 지정 가능 
    override fun addValue(addValue: Int):Int? {
        return addValue + 1
    }
}


// 등등.. 상속받은 자바 클래스의 파라미터 나 리턴 타입을 언제든지 널 허용/비허용을 바꿀 수 있다.

 

2) 코틀린의 원시 타입

kotlin은 개발자가 직접 원시 타입과 참조 타입을 구분하지 않는다. 문법 상황에 따라 원시 타입을 쓸지, 참조 타입을 쓸 지 Kotlin 컴파일러가 스마트하게 내부적으로 상황에 맞게 사용한다.

- Kotlin으로 개발하는 개발자는 Java처럼 원시타입 대신 참조 타입을 써야할지 고민할 필요가 없이 항상 한가지 타입만 사용하면 된다. 

 

//코틀린에서 아래와 같이 사용한다면,, 
val i:Int = 1
val list:List<Int> = listOf(1,2,3)

 

 // 자바로 변환하면 아래와 같이 변한다. 
 // 아래는 원시 타입인 int를 사용하고
 int i = true;
 // Collection<T> 와 같이 참조 타입이 필요한 경우 Integer 타입을 사용하는것을 확인할 수 있다.
 List list = CollectionsKt.listOf(new Integer[]{1, 2, 3});

 

 

위와 같이 상황에 따라 원시 타입 , 참조 타입을 내부 컴파일러가 구분해서 쓰기 때문에 개발자는 구분할 필요가 없는 것이다.

 

- Java 원시 타입을 코틀린에서 사용할 때 (플랫폼 타입이 아니라) 널이 될 수 없는 타입으로 취급이 가능하다.

- null 이 될 수 있는 Kotlin 타입은 자바 원시 타입으로 표현할 수 없기 때문에 kotlin 에서 null 이 될 수 있는 원시 타입을 사용하게 되면 그 타입은 Java의 래퍼 타입으로 컴파일 된다. 

- Java와 Kotlin 모두 제네릭 클래스는 박스 타입을 사용하는 공통점이 있다.

 

- kotlin의 잊을만한 원시 타입 리터럴은 double 타입에서 1.2e10, 1.2e-10 표현이 있다.

- kotlin의 잊을만한 원시 타입 리터럴은 0x나 0X 접두사가 붙은 16진 리터럴: 0xCAFEBABE, 0xbcdL

- kotlin의 잊을만한 원시 타입 리터럴은 0b나 0B 접두사가 붙은 2진 리터럴: 0b000000101

- kotlin는 숫자 리터럴 중간에 밑줄을 넣을 수 있다.(1_123, 1_000_000)

 

- Any는 Kotlin의 널이 될 수 없는 타입의 최상위 타입이다. (null을 허용하려면 Any?) 원시 타입이던, 참조 타입이던 모든 타입의 조상 타입이다.

 

//Any는 아래 3가지 메서드를 가지고 있다.
public open class Any {

    public open operator fun equals(other: Any?): Boolean

    public open fun hashCode(): Int

    public open fun toString(): String
}

 

- Java에서 Any와 비슷한 것은 Object 인데, 참조타입만 Object가 정점으로 하는 타입 계층이 된다.

 

- Unit 타입은 void와 비슷하지만 다르다. Unit은 타입인자로 사용할 수 있다. 코틀린의 함수는 반환하는 타입을 명시하지 않으면 묵시적으로  Unit을 반환하고 있고, 별도의 return 을 명시할 필요 없이 Unit을 반환 한다.

//Unit은 싱글톤 으로 구현 되어 있음
public object Unit {
    override fun toString() = "kotlin.Unit"
}

 

- Nothing은 정상적으로 끝나지 않음을 의미 한다.

 

public class Nothing private constructor()

 

/*Loner 생각: Nothing 은 정말 위 한줄이 끝입니다. class로 만들었는데, 생성자가 private이라서 생성할 수가 없습니다. 내부에 companion object가 있는것도 아니라서 호출할 내부 인스턴스도 없으며, object가 아닌 class라서 단일 인스턴스로 사용할 방법이 없습니다.  그래서 함수에 반환 타입으로 사용하면 Nothing 새롭게 생성하거나 호출할 단일 인스턴스가 없어서, 함수내에 오직 throw와 같은 프로그램의 종료를 적는 수밖에 없습니다.*/

 

fun nothingTest(): Nothing {
    throw RuntimeException("이 함수의 끝에 throw말고 적을 수 있는게 없다..")
}

 

3) 컬렉션과 배열

- null 가능성에 관해서 크게 4가지 경우가 있다. Collection<T?>? , Collection<T?> , Collection<T>?, Collection<T> 어떤 null 가능성을 사용하는 가에 따라 널 검사 하는 빈도나 처리하는 문법 등등이 달라진다.

/*Loner 생각: 개인적으로 Collection<T>? 와 같이 Collection이 아예 빈 경우를 나타낼거라면 대안으로 Collection<T>로 타입을 지정한 뒤 초기 값으로 emptyList()을 활용하는 경우가 많은 것 같습니다.  ?. 에 따른 널 검사 빈도 횟수도 있고, 빈 컬렉션임을 더 명시적으로 나타낸다 생각되기 때문 입니다.*/

 

- Collection 타입은 각각 Collection과 MutableCollection 으로, 읽기 전용 혹은 변경 가능한 컬렉션으로 나뉘어 있고, 상황에 따라 효율적으로 변경 가능, 읽기 전용을 사용해서 변경할 지점과 읽기 지점을 확실히 명시 할 수 있습니다. 

 

/*Loner 생각: 아래 코드는 Kotlin.collections 최상위 파일에 적혀있는 Collection의 핵심이 되는 코드들 입니다. */

 

package kotlin.collections

import kotlin.internal.PlatformDependent

public interface Iterable<out T> {
    public operator fun iterator(): Iterator<T>
}

public interface MutableIterable<out T> : Iterable<T> {
    override fun iterator(): MutableIterator<T>
}

public interface Collection<out E> : Iterable<E> {
    public val size: Int

    public fun isEmpty(): Boolean

    public operator fun contains(element: @UnsafeVariance E): Boolean

    override fun iterator(): Iterator<E>

    public fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
}


public interface MutableCollection<E> : Collection<E>, MutableIterable<E> {
    override fun iterator(): MutableIterator<E>
    
    public fun add(element: E): Boolean

    public fun remove(element: E): Boolean

    public fun addAll(elements: Collection<E>): Boolean

    public fun removeAll(elements: Collection<E>): Boolean

    public fun retainAll(elements: Collection<E>): Boolean

    public fun clear(): Unit
}

public interface List<out E> : Collection<E> {

    override val size: Int
    override fun isEmpty(): Boolean
    override fun contains(element: @UnsafeVariance E): Boolean
    override fun iterator(): Iterator<E>

    override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
    
    public operator fun get(index: Int): E

    public fun indexOf(element: @UnsafeVariance E): Int
    public fun lastIndexOf(element: @UnsafeVariance E): Int

    public fun listIterator(): ListIterator<E>

    public fun listIterator(index: Int): ListIterator<E>

    public fun subList(fromIndex: Int, toIndex: Int): List<E>
}

public interface MutableList<E> : List<E>, MutableCollection<E> {

    override fun add(element: E): Boolean
    override fun remove(element: E): Boolean
    override fun addAll(elements: Collection<E>): Boolean
    public fun addAll(index: Int, elements: Collection<E>): Boolean

    override fun removeAll(elements: Collection<E>): Boolean
    override fun retainAll(elements: Collection<E>): Boolean
    override fun clear(): Unit

    public operator fun set(index: Int, element: E): E

    public fun add(index: Int, element: E): Unit
    
    public fun removeAt(index: Int): E

    override fun listIterator(): MutableListIterator<E>

    override fun listIterator(index: Int): MutableListIterator<E>

    override fun subList(fromIndex: Int, toIndex: Int): MutableList<E>
}

public interface Set<out E> : Collection<E> {
    override val size: Int
    override fun isEmpty(): Boolean
    override fun contains(element: @UnsafeVariance E): Boolean
    override fun iterator(): Iterator<E>

    override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
}

public interface MutableSet<E> : Set<E>, MutableCollection<E> {

    override fun iterator(): MutableIterator<E>

    override fun add(element: E): Boolean

    override fun remove(element: E): Boolean

    override fun addAll(elements: Collection<E>): Boolean
    override fun removeAll(elements: Collection<E>): Boolean
    override fun retainAll(elements: Collection<E>): Boolean
    override fun clear(): Unit
}

public interface Map<K, out V> {

    public val size: Int

    public fun isEmpty(): Boolean

    public fun containsKey(key: K): Boolean

    public fun containsValue(value: @UnsafeVariance V): Boolean

    public operator fun get(key: K): V?

    @SinceKotlin("1.1")
    @PlatformDependent
    public fun getOrDefault(key: K, defaultValue: @UnsafeVariance V): V {
        return null as V
    }
    
    public val keys: Set<K>
    public val values: Collection<V>
    public val entries: Set<Map.Entry<K, V>>
    public interface Entry<out K, out V> {
        public val key: K
        public val value: V
    }
}

public interface MutableMap<K, V> : Map<K, V> {

    public fun put(key: K, value: V): V?

    public fun remove(key: K): V?
    
    @SinceKotlin("1.1")
    @PlatformDependent
    public fun remove(key: K, value: V): Boolean {
        return true
    }

    public fun putAll(from: Map<out K, V>): Unit

    public fun clear(): Unit

    override val keys: MutableSet<K>

    override val values: MutableCollection<V>

    override val entries: MutableSet<MutableMap.MutableEntry<K, V>>

    public interface MutableEntry<K, V> : Map.Entry<K, V> {
        public fun setValue(newValue: V): V
    }
}

/*Loner 생각: 모든 Collection 타입은 인터페이스로 구현이 되어있고, Iterable로 부터 시작해 Collection에서 구현하기 시작해서 계층구조를 만들어 왔다는 것을 확인할 수 있습니다. 사용 되는 class는 따로 있습니다. 대표적인 예시로 ArrayList, HashSet, LinkedHashMap 입니다. ArrayList 은 최종적으로 MutableList<T>를 구현하고 HashSet은 MutableSet<T> 를 최종 구현하고 LinkedHashMap는 MutableMap<T>를 구현합니다. */

 

//MutableList를 구현 한다.
expect class ArrayList<E> : MutableList<E>, RandomAccess {
 /*...*/
}

//MutableSet을 구현 한다.
expect class HashSet<E> : MutableSet<E> {
/*...*/
}

//MutableMap을 구현 한다.
expect class LinkedHashMap<K, V> : MutableMap<K, V> {
/*...*/
}

 

/*Loner 생각: 그렇기 때문에 Kotlin이 Java 와 똑같은 Collection을 쓸 수 있는 이유가 위와 같이 인터페이스로 정의한 타입에 따라 필요한 기능만 쓰면서 제한을 두거나 사용하기 때문에 Java와 같은 Collection 을 쓰면서 Kotlin만의 특징을 살릴 수 있었던 이유가 Collection 전용 확장함수와 함께 위 같은 계층을 가졌기 때문 입니다.*/

 

- 읽기 전용 컬렉션이 항상 멀티스레딩에서 안전하다는 보장은 없다. kotlin의 collection은 읽기 전용과 변경 가능 전용 타입을 사용하는데 상황에 따라 같은 컬렉션 객체를 읽기전용과 변경 가능 전용 타입이 동시에 참조한다면 문제가 발생할 수 있다.

예를 들어, 읽기 전용 컬렉션을 통해 사용하다가 변경 가능 컬렉션에서 컬렉션 내용을 변경하게 되면 멀티 스레딩으로 인한 오류가 발생할 가능성이 있다. 

 

- Java와 kotlin을 같이 쓰는 프로젝트에서 Java는 읽기 전용, 변경 가능 개념이 없기 때문에 Java 메서드 인자에 Kotlin의 Collection을 넘길때 이를 유의해야한다. (kotlin에서 읽기 전용 컬렉션으로 Java 메서드에 인자를 넘겼지만, Java 메서드에서 변경 가능으로 쓰고 있으면 해당 읽기 전용 컬렉션 내용이 변경된다.)

 

- 플랫폼 타입에서 Java 상위 클래스를  Kotlin이 구현해서 메서드를 override해서 구현 할때 collection 타입인 파라미터도 읽기 전용이나 변경 가능 전용으로 개발자가 원하는 의도대로 바꿀 수 있다. 

 

- 배열에서 원시 타입을 쓰고 싶다면 IntArray , ByteArrary, CharArray 등등 원시타입을 사용할 수 있도록 하는 배열들을 지원한다.

 

 

이전에는 스쳐 지나갔던 부분들이 다시 훍어보니 보이네요. 베이직이 탄탄할수록 든든한 기분이 듭니다.