규턴의 개발블로그

Kotlin의 Null

Null가능성

  • nullabilityt는 NPE를 피할 수 있게 만들어짐 -> NPE를 컴파일 시점에서 잡아줄 수 있도록 하기 위함
  • 타입뒤에 ? 을 붙여 널 가능성을 표시
fun strLen(s:String) = s.length  -> strLen(null) 호출(컴파일 에러 발생하지 않음)-> 런타임 에러발생
fun strLen(s:String?) : Int = if( s!= null) s.length else 0 -> strLen(null) 호출 -> 컴파일 에러 
  • ?타입 시스템에서는 null체크 이후에만 변수.메서드() 를 호출 할 수 있음

참고

코틀린의 런타임에서는 예를들어 String, String? 모두 같은 타입의 객체임
모든 검사는 컴파일 시점에서 수행된다 -> 런타임 시점에 null값처리에 대한 비용이 들지 않는다.

?. (안전한 호출 연산자)

    val test : String? = s?.toUpperCase()
    if(s != null) s.toUpperCase() else null
  • 다음과 같이 ?.를 통해 null이 아닌경우 -> toUpperCase return
  • 그렇지 않으면 null을 return

?: (Elvis 연산자)

val t: String = s ?: "null입니다"

/*
*?. ?: 혼합사용 예시
*/ 
computer?.memory ?: "memory가 존재하지 않습니다"
  • elvis연산자는 null이 아닌경우 ?: 앞에값을 return, null인경우 ?:의 뒤에값을 return
  • (자바의 3항연산자와 비슷한느낌을 받음)

as? (안전한 캐스트)

class Person(val name){
    override fun equals(o: Any?) : Boolean{
        val otherPerson = o as? Person ?: return false
        return otherPerson.name == this.name
    }

    override fun equals(o: Any?) : Boolean{
        if(o is Person){
            val otherPerson = o as Person
        }else return false

        val otherPerson = o as? Person ?: return false
        return otherPerson.name == this.name;
    }
}
  • 일반적인 as는 "a as String" 와 같이 a를 String 타입으로 변환시킨다.
  • as?는 위의 if문에 작성된 is, as를 사용한것과 같은 효과를 간단한게 나타낼 수 있다.
    • as?에서 Person이면 o를 Person으로 변환시키고, 그렇지 않으면 return false;를 하게 된다.
  • 주로 equals에서 많이 사용한다고 한다.

!! (null 아님 단언선언)

    val value= list.selectedValue !! // null인경우 NPE
    val value=list.selectedValue ? : return List.of()
  • 책에서는 조금 권장되지 않는 방법이라고 말하고 있음
    ( 그 이유는, !!는 컴파일러입장에서 검증하지 않고 강제로 null이 아니야 라고 알려주는것이기에 실제로 코틀린 설계자들도 !!와 같이 못생긴 기호를 택했다고 함 )
fun checkNull(value: String?): String? {
    return value ?: throw NullPointerException()
}

fun main() {
    val value: String? = "String"
    val newValue: String? = checkNull(value)
    val notNullValue: String = newValue!!
}
  • 활용방법은 컴파일러입장에선 notNullValue가 아무리 checkNull()함수에서 null체크를 하고왔다고 해도 newValue가 null아 아니라는것을 모른다. 그렇기에 이때 newValue!!를 사용할 수 있다.

let 함수

  • 함수의 인자로 전달된 람다를 실행한 후 결과를 반환한다.
  • val email: String? = "test@naver.com" //let과 ?.은 혼합하여 사용 email?.let { sendToEmail(it) // it은 email, email이 null일경우 실행되지 않음 } // if문을 작성한경우 if(email!=null) sendToEmail(email)
  • let 함수는 str값이 null이 아닌 경우에만 호출된다. -> ?.(안전한 호출 연산자)와 함께 사용될 수 있다.

late-initialized


class MyService {
    fun performed(): String = "foo"
}

class MyController {
    private lateinit var myService: MyService

    fun setUp() {
        myService = MyService()
    }

    fun action(): Boolean {
        return myService.performed().equals("foo")
    }
}


fun main() {
    val myController: MyController = MyController()
    myController.setUp() //lateinit (MyService 객체 주입)
    print(myController.action()) //true
}
  • 실제로 스프링에서는 생성자 주입이 일어나기에 lateinit과 직접적인 관련이 없음
  • MyController에 MyService객체 주입이 생성사 시점에서 일어나지 않는것을 구현하기 위해 lateinit을 사용할 수 있음

코틀린과 자바(null 다루기) - platform 타입

  • @nullable + Type = Type?
  • @NotNull + Type = Type
  • 플랫폼 타입 = 코틀린이 널 관련 정보를 알 수 없는 타입
  • 코틀린에서는 플랫폼 타입을 선언할 수 없음
    • 자바코드에서 가져온 타입만 플랫폼 타입이 된다.
    • 코틀린은 자바 등 다른 언어에서 넘어온 타입들을 특수하게 다룬다 => 플랫폼 타입

Kotlin의 원시타입

코틀린은 원시타입과 래퍼 타입을 구분하지 않음

코틀린에서의 원시타입

  • 코틀린에서는 실행시점에 가장 효율적인 방법을 선택한다. (원시 vs 래퍼)
    • ex Kotlin's Int는 null이 될수없기때문에 자바의 int(원시)타입으로 컴파일 됨
  • null은 자바의 래퍼 타입에만 대입할 수 있음 -> 자바의 래퍼타입으로 컴파일 된다.
  • 특수하게 제네릭 클래스인 경우에는 래퍼타입을 사용해야한다.

코틀린에서의 숫자

    val x = 1 //Int
    val list = listOf(1L, 2L, 3L)
    print(x in list) //컴파일 에러
    print(x.toLong() in list)
  • 코틀린에서는 서로 다른 타입의 숫자를 자동 변환하지 않음
    • 명시적인 형 변환이 필요하다

Any, Any? (최상위 타입)

  • 자바에서는 Object가 클래스 계층의 최상위 타입 <-> 코틀린에서는 Any, Any?
    • 코틀린 함수가 Any를 사용하면 자바 바이트코드의 Object로 컴파일 된다.
  • toString, equals, hashCode라는 메서드는 Any의 메서드를 상속한것
    • 하지만 Object에 존재하는 (wait, notify)는 Any에서 사용하지 못함

Unit 타입: 코틀린의 void

  • 일반적으로 자바의 void와 같은 기능을 한다. (java의 void는 반환타입이 없기에 생략한다는 의미를 가짐)
  • Unit은 Unit이라는 값 하나만을 가짐(Unit?이 존재하지 않음)

Nothing 타입

fun fail(message: String): Nothing {
    throw IllegalArgumentException(message)
}
fun invokeANothingOnlyFunction() {
    fail("nothing")
    println("hello") // Unreachable code라고 컴파일 단에서 경고
}
  • 함수가 정상적으로 끝나지 않는것을 알려주는 특별한 반환타입
  • Nothing 타입은 아무값도 포함하지 않음.
  • kotlin에서는 throw가 expression 이다. 그래서 이때 throw의 타입이 Nothing 이다.

컬렉션과 배열

Null 가능한 컬렉션

  • List<Int?>를 통해 각 원소에 null이 들어간 list를 만들 수 있다.
  • List<Int?>는 각 원소가 null or Int인것이며 List?는 List자체가 null일수 있다는것을 나타낸다.

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

  • 코틀린에서의 읽기전용은 Collection
    • 원소를 추가하거나 제거하는 메서드가 존재하지 않음
  • 변경 가능한 Collection = MutableCollection
    • Collection 인터페이스를 확장하면서 add, remove,clear등의 함수를 추가하였음
  • 코드레벨에서는 항상 읽기전용을 우선적으로 사용하는것을 권장함
    • 추후 변경이 필요하면 MutableCollection을 사용하는것이 좋음
    • 이를통해 Collection으로 선언되었으면, 변경되지 않는 컬렉션이라고 유츄 가능하다.
  • 하지만 읽기전용 Collection이 항상 안전한것은 아닐 수 있다.
    • List , MutalbleList 모두 같은 list를 참조하고 있을 수 있다.
    • 이러한 경우 병렬적으로 참조되는 list가 변경될 수 있으며ConcurrentModificationException이 발생할 수 있다. 결론적으로 읽기전용 컬렉션은 항상 thread safe한것은 아니다.

코틀린 컬렉션과 자바

  • 코틀린 컬렉션은 자바 컬렉션 인터페이스의 인스턴스
  • 코틀린에서, 자바에서의 ArrayList나 HashSet같은 경우도 MutableList,MutableSet을 상속한것처럼 취급한다
  • 문제는 코틀린에서 Collection(읽기전용)으로 선언하여도 자바코드에서는 해당 컬렉션 객체를 변경가능하다.
    • 이러한 문제에 대한 해결은 책에서는 코틀린에서 MutableList(변경 가능) 컬렉션을 사용하여 개발자로 부터 해당 객체가 변경되었을 수 있다는것을 명시하는것이 좋다고 한다.

코틀린에서의 배열

  • 코틀린에서 배열 생성은 다음과 같은 예시가 존재한다.
     val arrayOf = arrayOf(1, 2, 3) // 1, 2, 3
     val arrayOfNulls = arrayOfNulls<Int>(3) // null, null, null
     val array = Array<Int>(3){it -> it * it} //0, 1, 4
    • 컬렉션에 사용할 수 있는 모든 확장 함수를 배열에서도 제공한다 (ex-filter,map)
      val arrayOf = arrayOf("hello", "world")
      arrayOf.forEachIndexed { index, s ->
         println("$index 번째 문장 : $s")
         //0 번째 문장 : hello
         //1 번째 문장 : world
      }
profile

규턴의 개발블로그

@규턴이

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!