6. 코틀린 타입 시스템

2023. 4. 30. 21:24·CS/코틀린 인 액션

제일 큰 차이는 null를 다루는지 여부. 이것만 알아도 90%는 먹고 들어감

 

6.1. 널 가능성

어느 타입이 null이 될 수 있으면 ?가 붙고, 명확하게 null이 될 수 없다면 붙지 않는다. 즉,

Type? = Type + null

이 되는거

 

그래서 만약 내가 다루고자 하는 타입이 널이 될 수 있다면 널인지 아닌지 체크해줘야 한다.

fun strLenSafe(s: String?): Int = 
    if (s != null) s.length else 0
    
val x: String? = null
println(strLenSafe(x)) // 0

println(strLenSafe("abc")) // 3

그럼 ?가 있을 때마다 이렇게 해야되냐? 그래서 타입 ? 을 만들면서 널을 다루는 전용 연산자들을 만들었다(마음에 드는 철학).

 

 

안전한 호출 연산자 ?. 은 해당 변수가 null이 아니면 그대로 함수를 실행하고, null이면 null을 반환한다.

fun printAllCaps(s: String?) {
    val allCaps: String? = s?.uppercase(Locale.getDefault())
    println(allCaps)
}

printAllCaps("abc") // ABC
printAllCaps(null) // null

 

엘비스 연산자 ?:는 만약 null이면 다른걸 대입하라는 거다.

이름이 엘비스인 이유는 저 기호가 엘비스 가수 머리모양이랑 비슷해서

 

http://t0.gstatic.com/licensed-image?q=tbn:ANd9GcTr4NoZ0FtPzrZ1Ya_MSKuBYEFOi8LQ4kXcfo_kmCrKvcFCH-0ddrEwy9Czr4BkTiJl

fun strLenSafe(s: String?): Int =
    s?.length ?: 0

 

안전한 캐스트 as? 는 변환을 시도해서 되면 하고 아니면 null

class Person(val firstName: String, val lastName: String) {
    override fun equals(other: Any?): Boolean {
        val otherPerson = other as? Person ?: return false

        return otherPerson.firstName == firstName &&
            otherPerson.lastName == lastName
    }

    override fun hashCode(): Int =
        firstName.hashCode() * 37 + lastName.hashCode()
}

val p1 = Person("John", "Smith")
val p2 = Person("John", "Smith")

println(p1 == p2) // true
println(p1.equals(p2)) // true

println(p1.equals(42)) // false

 

널 아님 단언 !! 은 코드 작성자가 얘는 null이 될 수 없다고 변환시켜주는 것. 물론 에러뜨면 사용자 책임

fun ignoreNulls(s: String?) {
    val sNotNull: String = s!!
    println(sNotNull.length)
}

fun main(args: Array<String>) {
    ignoreNulls(null)
}

/*
Exception in thread "main" java.lang.NullPointerException
	at MainKt.ignoreNulls(Main.kt:2)
	at MainKt.main(Main.kt:7)
*/

그래도 좋은 건 컴파일러가 검사해주기 때문에 함수 실행 시점에 에러를 뜨게 하는 것. 그래서 막 깊숙히 이것저것 다 실행하고 난 뒤에 에러 떠서 찾기 힘든게 아니라 내 코드에서 명확히 알 수 있음.

 

let 함수는 해당 함수가 null이 아니면 실행, null이면 아무 일도 없었다

fun sendEmailTo(email: String) {
    println("Sending email to $email")
}

fun main(args: Array<String>) {
    var email: String? = "yole@example.com"
    email?.let { sendEmailTo(it) } // Sending email to yole@example.com
    email = null
    email?.let { sendEmailTo(it) }
}

 

나중에 초기화하는 lateinit도 있는데, null하다가 생기는 NullPointerException보단 "lateinit property ~~ has not been initialized"라는 명확한 에러문이 떠서 좋음.

class MyTest {
    private lateinit var myService: MyService
    
    @Before fun setUp() { // setUp을 해 줘야 초기화됨
        myService = MyService()
    }
    
    @Test fun testAction() {
        Assert.assertEquals("foo",
            myService.performAction())
    }
}

 

 

널이 될 수 있는 타입 확장은 null까지 고려해서 작성하는 거. string에서 null이나 ""이나 그게 그거 아닐까

fun verifyUserInput(input: String?) {
    if (input.isNullOrBlank()) {
        println("Please fill in the required fields")
    }
}

fun main(args: Array<String>) {
    verifyUserInput(" ")
    verifyUserInput(null)
    verifyUserInput("test")
}

isNullOrBlank는 다음 확장 함수라고 생각하면 됨

fun String?.isNullOrBlank(): Boolean {
    return this == null || this.isBlank()
}

 

let은 안쓰냐 하지만, 다시 보면 let도 널 안전 연산자 ?.를 붙여써서 안의 it은 null인지 아닌지 검사 안함

val person: Person? = ...
person.let { sendEmailTo(it) } // ERROR: Type mismatch: inferred type is Person? but Person was expected
person?.let { sendEmailTo(it) }

 

타입 파라미터의 널 가능성은 꺽쇠 <>로 알려줄 수 있음

fun <T> printHashCode(t: T) {
    println(t?.hashCode())
}

fun main(args: Array<String>) {
    printHashCode(null) // Any?로 추론됨 // null
}
fun <T: Any> printHashCode(t: T) {
    println(t.hashCode())
}

fun main(args: Array<String>) {
    // printHashCode(null) // Kotlin: Null can not be a value of a non-null type TypeVariable(T)
    printHashCode(42)
}

 

 

하지만 자바는 null이니 아니니 그런 타입이 없다. 그래서 추가 데코레이터로 @Nullable, @NotNull을 지원하기도 하고 코틀린도 이걸 읽어서 타입 변환할 줄 안다.

@Nullable + Type = Type?

@NotNull + Type = Type

 

하지만 저런게 없을 경우, 코틀린은 다 고려해서 작성해야 한다. 이를 플랫폼 타입이라 함.

Type = Type? 또는 Type

 

// 자바
public class Person {
    private final String name;
    
    public Person(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
}
fun yellAt(person: Person) {
    println(person.name.toUpperCase() + "!!!")
}


fun main(args: Array<String>) {
    yellAt(Person(null)) // java.lang.IllegalArgumentException: Parameter specified as non-null is null: method ~~
}
fun yellAtSafe(person: Person) {
    println((person.name ?: "Anyone").toUpperCase() + "!!!")
}


fun main(args: Array<String>) {
    yellAtSafe(Person(null))
}

 

// 자바
interface StringProcessor {
    void process(String value);
}
class StringPrinter: StringProcessor {
    override fun process(value: String) {
        println(value)
    }
}

class NullableStringPrinter: StringProcessor {
    override fun process(value: String?) {
        if (value != null) {
            println(value)
        }
    }
}

코틀린은 두개 다 받아들인다.

 

 

6.2. 코틀린의 원시 타입

코틀린의 컬렉션은 자바의 컬렉션을 그대로 쓰는 것 처럼, 다른 int나 boolean 같은 것들도 자바의 것을 가져다 쓴다.

하지만 타입도 원시 타입과 참조 타입이 있다. 사실 자바가 콜렉션에 Int 넣을 때 Int를 쌩으로 넣는게 아닌 래퍼타입을 넣는데, 쌩 Int에 메서드를 사용하거나 컬렉션에 원시 타입을 담을 순 없기 때문. 원시 타입(primitive type)(int 등)의 변수에는 그 값이 직접 들어가지만, 참조 타입(reference type)(string 등)의 변수에는 메모리상의 객체 위치가 들어간다.

코틀린은 무슨 타입을 사용할지를 실행 시점에 결정한다. 원시 타입이 계산이 빠르기 때문에 가능한 경우엔 원시 타입을 사용하다가 컬렉션에 넣을 땐 래퍼 타입을 사용하는 방식.

널이 될 수 없는 타입의 경우, 자바에서 원시 타입은 널이 될 수 없기 때문에 코틀린에서 그대로 쓸 수 있고, 코틀린에서도 널이 될 수 없는 타입은 자바에서도 널이 될 수 없기때문에 서로 그대로 가져다 쓰면 된다.

 

그럼 널이 될 수 있는 타입(Int?, Boolean? 등)은? 래퍼인 참조 타입 쓴다.

 

숫자 간 타입 변환이 가능한데, 코드 작성 시 실수를 줄이도록 메서드를 작성하여 변환하도록 명시하고 있다.

val i = 1
// val l:Long = i // Error: Type mismatch
val l:Long = i.toLong()
println(l) // 1
val m = 1L
println(m) // 1
val o = 1.0f
println(o) // 1.0

 

Any 같은 경우, 자바의 모든 클래스의 조상인 java.lang.Object와 대응된다.

val answer:Any = 42
println(answer)
println(answer::class) // class java.lang.Integer (Kotlin reflection is not available)

 

Unit은 자바의 void와 대응.

 

Nothing은 이 함수가 정상적으로 끝나지 않는다는 표시다.

fun fail(message: String): Nothing {
    throw IllegalStateException(message)
}

fun main(args: Array<String>) {
    fail("Error occurred") // Exception in thread "main" java.lang.IllegalStateException: Error occurred

}

Nothing을 작성하면 컴파일러는 이 함수가 결코 정상 종료되지 않음을 알고 그 함수를 호출하는 코드를 분석할 때 사용한다.

라고 한다.

 

 

6.3 컬렉션과 배열

리스트 자체가 널이 될 수 있는지, 원소가 널이 될 수 있는건지, 둘 다 인지 헷갈리지 말기

 

코틀린의 컬렉션에서 중요한 성질이 있는데, 읽기 전용 컬렉션 인터페이스랑 변경 가능한 컬렉션(Mutable) 인터페이스를 분리했다는 것이다. 그래서 읽기 전용 컬렉션은 변경할 수 있는 메서드 자체가 없다.

fun <T> copyElements(source: Collection<T>,
                     target: MutableCollection<T>) {
    for (item in source) {
        target.add(item)
    }
}

fun main(args: Array<String>) {
    val source: Collection<Int> = arrayListOf(3, 5, 7)
    val target: MutableCollection<Int> = arrayListOf(1)
    copyElements(source, target)
    println(target)
    
    val source2: Collection<Int> = arrayListOf(3, 5, 7)
    val target2: Collection<Number> = arrayListOf(1.0)
//    copyElements(source2, target2) // Error: Type mismatch
}

하지만 이 악물고 바꿀라고 하면 바꿀 수 있다. 변경 가능한 것을 참조할 수도 있기 때문.

그래서 완전 안심하지는 말자. 즉 읽기 전용 컬렉션이 항상 스레드 안전(thread safe)하지는 않다는 점을 명심해야 한다.

 

 

자바랑은

코틀린의 기본 구조는 java.util 패키지에 있는 자바 컬렉션 인터페이스의 구조를 그대로 옮겨 놓았다.

자바의 컬렉션은 코틀린이 자신의 것을 상속한 것 처럼 취급한다. 하지만 자바엔 읽기 전용, 변경 가능 그런게 없기 때문에 자바와 공유할 땐 사용자가 잘 짜야 한다.

컬렉션 타입 읽기 전용 타입 변경 가능 타입
List listOf mutableListOf, arrayListOf
Set setOf mutableSetOf, hashSetOf, linkedSetOf, sortedSetOf
Map mapOf mutableMapOf, hashMapOf, linkedMapOf, sortedMapOf

'CS > 코틀린 인 액션' 카테고리의 다른 글

8. 고차 함수: 파라미터와 반환 값으로 람다 사용  (0) 2023.05.05
7. 연산자 오버로딩과 기타 관례  (0) 2023.05.03
5. 람다로 프로그래밍  (0) 2023.04.30
4. 클래스, 객체, 인터페이스  (0) 2023.04.28
3. 함수 정의와 호출  (0) 2023.04.24
'CS/코틀린 인 액션' 카테고리의 다른 글
  • 8. 고차 함수: 파라미터와 반환 값으로 람다 사용
  • 7. 연산자 오버로딩과 기타 관례
  • 5. 람다로 프로그래밍
  • 4. 클래스, 객체, 인터페이스
용나리
용나리
  • 용나리
    티스토리 블로그
    용나리
  • 전체
    오늘
    어제
    • 분류 전체보기 (334)
      • 과거의 것들 (93)
        • AI Tech boostcamp (92)
      • 생각정리(고찰) (2)
      • 기술 글 (1)
      • 코딩테스트 (4)
        • C++ (0)
        • Python (4)
      • CS (121)
        • 컴퓨터 시스템 (4)
        • 코틀린 인 액션 (13)
        • 김영한 스프링 강의 (104)
      • 일지 남기기용 (11)
        • 운동 (10)
      • 개발 배포 해보기 (1)
      • 프로그래밍 언어 및 기타 (32)
        • Spring Boot (9)
        • Python (9)
        • Kotlin (1)
        • Flutter (2)
        • SQL (4)
        • Docker (3)
        • 공통 (4)
      • os (4)
        • Linux (4)
      • 기술 (17)
        • PyTorch (6)
        • Computer Vision (6)
        • NLP (1)
        • 기타 (4)
      • 제품 후기 (0)
      • 게임 (0)
        • Human Resource Machine (0)
      • 강의 (26)
        • fullstackdeeplearning_sprin.. (9)
        • 부캠 안드로이드 학습정리 (17)
      • 개인 메모 (10)
      • IT 기타 (5)
      • 논문 읽기 연습 (5)
        • Computer Vision (1)
        • NLP (0)
        • 공통 (2)
        • 그냥 메모 (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    pip install killed
    파이썬 실행경로
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
용나리
6. 코틀린 타입 시스템
상단으로

티스토리툴바