제일 큰 차이는 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이면 다른걸 대입하라는 거다.
이름이 엘비스인 이유는 저 기호가 엘비스 가수 머리모양이랑 비슷해서


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 |