Kotlin in Action 정리 (1)

Kotlin: what and why

Kotlin’s primary traits

  • Target: 서버, 안드로이드, JVM
  • 타임 추론을 바탕으로 한 정적 타입
  • Functional, OOP 둘다 지원
  • 자바 코드 쉽게 활용 가능

코틀린 설계 철학

  • Pragmatic 실용적인
    • 프로덕션 용이고, 다른 언어들에서 가져온 익숙한 기능들을 도입
    • 특정 프로그래밍 패러다임을 강요하지 않음
    • IDE 제공
  • Concise 간략한
    • Boilerplate 제거, 풍부한 라이브러리 제공
    • 람다 제공, 특수문자 연산자 오버로딩 미제공
  • Safe 안전한
    • JVM 위이므로, 타입사용, 동적 메모리 관리 면에서 안전함
    • null을 명시적으로 표현, 컴파일 시에 null에러 조기 차단
    • 런타임 타입 체크가 더 값싸고, 쉽게 가능
  • Interoperable 상호 운용 가능한
    • Java-to-Kotlin 정적 변환, 자동 리펙토링 가능
    • Java의 라이브러리 활용 그대로 가능
    • Collections 라이브러리는 자바에 완전 의존

코틀린 툴 사용

  • kotlinc로 컴파일, kt파일을 class로 변환
    • 컴파일 시, 의존성 지닌 jar을 포함시함
  • 실제 프로젝트에서는 Maven, Gradle, Ant 등의 빌드 툴을 사용함

Kotlin Basics

기초 요소들: 함수

1
2
3
4
5
6
7
8
9
10
11
12
// 함수, 인자, 리턴 없음
fun main(args: Array<String>) {}

// 함수, 인자, 리턴있음, 삼항연산자, Block Body
fun max(a: Int, b: Int): Int {
  return if (a > b) a else b
}

// Expression Body
fun max(a: Int, b: Int): Int = if (a > b) a else b
// Expression Body, omit return type
fun max(a: Int, b: Int) = if (a > b) a else b

기초 요소들: val, var

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// immutable reference variable
val answer = 42
val answer: Int = 42
val yearsToCompute = 7.5e6

// immutable reference, 리스트 객체에 요소 추가는 가능
val languages = arrayListOf("Java")
languages.add("Kotlin")

// 재할당이 불가능한 것이기 때문에, 선언과 첫 할당의 분리가 가능
val answer: Int
answer = 42

// 단 한번만 할당되는것이 분명하다면, control flow에 따른 분리 가능
val message: String
if (canPerformOperation()) { message = "Success" }
else { message = "Failed" }

string template

1
2
3
4
5
val name = "Kotlin"
// string template with variable
println("Hello, $name!")
// string template with expression
println("Hello, ${args[0]}!")

Classes and properties

  • Java에서는 private 변수에 getter와 setter를 선언해 사용
  • Kotlin에서는 property에 기본적으로 getter setter가 선언됨
    • primary 생성자 인자에 val, var 붙이면 자동으로 프로퍼티 생성됨
    • 필요하면 getter, setter 바꿔서 선언할 수 있음
    • private set으로 선언 가능
  • backing field: 프로퍼티뒤에 숨겨진 필드
    • Kotlin에서는 필드에 직접 접근이 불가능함(프로퍼티는 필드가 아님)
    • setter에서 값을 대입할 때, field 를 통해서 backing field에 대입
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 반복적인 생성자 축약 + 프로퍼티 선언
class Person(
  val name: String,
  var isMarried: Boolean
)

// custom accessor, not using backing field
class Rectangle(val height: Int, val width: Int) {
  val isSquare: Boolean
    get() = height == width
}

// private setter
class LengthCounter {
  var counter: Int = 0 private set
}

디렉토리, 패키지 관리

  • 클래스 별로 파일을 나누는 java
  • Kotlin에서는 꼭 그럴필요는 없음
    • 패키지를 하나의 파일로 해도 되고
    • 패키지를 폴더로 만들어도 무방하다.

when과 enum

  • enum class: Java enum의 확장
    • enum class는 프로퍼티를 가질 수 있음
    • enum class의 첫 문장으로 enum constant선언 가능
    • constant 선언 후에 세미콜론 찍어야함(Kotlin 내 유일한 세미콜론 사용)
    • 세미 콜론 이후에 멤버 함수를 선언 가능
  • when: switch의 확장
    • 조건과 결과가 ->로 대응되어서 나열됨
    • 각 조건 끝에 세미콜론이 생략된 것이라 생각하는게 편함
    • When에 argument를 준 경우 다음과 같은 형태가 가능
      • 값 -> 값 or {람다}
      • 값 -> 값 or {람다}
      • 값,값 -> 값 or {람다}
      • ArbitaryObject -> 값 or {람다}
      • is Type -> 값 or {람다}
      • else -> 값 or {람다}
    • When에 argument를 주지 않은 경우
      • Boolean -> 값 or {람다}

Smart cast

  • 타입 캐스팅은 as, 타입체크는 is
  • explicit casting: e as Num
  • smart casting: if (e is Sum) { return e.right }
    • 타입을 체크한 이후에는 캐스팅이 필요없다.
    • 값이 변하는 경우는 스마트 캐스팅을 적용할 수 없다.
  • when is Type 구문에서도 스마트 캐스팅이 적용됨

반복문, in 연산자

  • while은 Java랑 같음
  • for는 range based만 있음
    • in연산자를 사용
    • 1..5: 1, 2, 3, 4, 5
    • 1 until 5: 1, 2, 3, 4
    • 1 until 5 step 2: 1, 3
    • 5 downTo 1: 5, 4, 3, 2, 1
    • 5 downTo 1 step 2: 5, 3, 1
  • map을 반복할 경우
    • for ((k, v) in treeMap)
  • in 연산자
    • 반복문 외에 포함을 체크할 때 사용 가능,
    • 문자: c in 'a'..'z'
    • 문자열: "Kotlin" in "Java".."Scala"

예외처리

  • Java와 달리 new 필요없음, 호출할 예외 명시 필요 없음
  • throw는 expression의 일부가 될 수 있음
  • try를 expression으로 쓸 수 있음, 람다 형태
  • 중괄호는 생략 불가
  • try, catch, finally 사용 가능
  • try-with-resource 구문은 없음
1
2
3
4
5
6
7
8
9
10
11
// throw as expression
val x = if (cond) 10 else throw Exception()

// try as expression
val number = try {
  Integer.parseInt(reader.readLine())
} catch (e: IOException) {
  null
} catch (e: NumberFormatException) {
  return
}

Defining and calling functions

함수 호출을 더 쉽게 만드는 기능들

  • Named arguments
    • 단 Java7 이전 코드들에서는 지원 안됨
  • Default parameters
    • overloading 수를 줄여줌
    • 기본 인자는 Callee에 포함됨. 빌드시에 유의할 것
  • @JvmOverloaded
    • Java에서 호출하기 위해, 모든 가능한 Signature를 오버로딩 해줌
    • Default param 지정시 개수만큼 오버로딩된 함수가 생김
  • 최상위 함수
    • 함수를 최상위(package 아래) 레벨에 class 없이 선언할 수 있음
    • 컴파일시 파일 이름 클래스가 자동생성(join.kt = class JoinKt)
    • Java에서 import시 Filename class를 써줘야함
  • 최상위 property
    • Java에서 getter/setter로 접근 가능
    • const로 선언시, Java에서 static final 취급, 직접 접근 가능

Extension functions

  • 기존 라이브러리에 함수 및 프로퍼티 추가 가능
    • Java나 다른 JVM 코드(java class로 컴파일 가능한)도 확장 가능
    • 확장 대상을 Reciever type이라고 함
  • 명시적 import가 필요
    • extension 자체를 명시적으로 import 해야함
    • Reciever를 import와 무관
    • (참고) as로 import된 대상의 이름 변경 가능
  • 가시성이 제한됨
    • private, protected 멤버는 접근 불가
    • 다른 extension function은 호출 가능
  • Java에서는 Reciever를 첫 인자로 받는 정적함수로 취급됨
  • Java에서의 Util함수를 extension으로 사용하면 유용함
    • Util함수는 상태를 저장하지 않는 함수들
  • 오버로딩이 불가능함
    • 실제로는 정적함수에 불과함
    • 즉 런타임 타입에 무관하게, 컴파일 타임에 호출될 함수가 결정됨
    • 멤버 함수와 충돌시, 무조건 멤버함수가 우선시됨
1
2
3
4
5
6
7
8
// Java 클래스의 확장
fun String.lastChar(): Char = this.get(this.length - 1)

// 위 코드와 같음 (this 없이 사용)
fun String.lastChar(): Char = get(length - 1)

// Java
char c = StringUtilKt.lastChar("Java");

Extension properties

  • 상태를 가지지 않는 조건 하에서 확장 가능
  • Java 코드 상으로는 정적 getter/setter가 추가되는 것
  • getter가 무조건 있어야함 (backing field가 없으므로)
  • val로 선언하면, getter만 있으면 됨
  • var로 선언하면, getter/setter 둘다 있어야함
1
2
3
4
5
6
7
8
var StringBuilder.lastChar: Char
  get() = get(length - 1)
  set(value: Char) {
    this.setCharAt(length - 1, value)
  }

// in java
StringUtilKt.getLastChar("Java")

Java Collections 확장과 그 원리들

  • Java에 비해 Collections의 함수가 확장되어 있음
    • Factory: hashSetOf, arrayListOf, hashMapOf
    • Utility: last(), max()
  • 가변 인자 vararg
    • Java의 int... 대신 타입 앞에 vararg 붙임
    • *args: 배열을 가변인자로 이용 가능
    • Collection의 생성자들에 사용
  • map 선언시의 to
    • 1.to("one") == 1 to "one"
    • infix로 선언된 함수는 위 표기 가능
  • destructuring declaration
    • C++에서의 unstructured binding
    • component1() 등의 형태로 선언된 메소드가 있으면 사용 가능
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 가변 인자 선언
fun listOf<T>(vararg values: T): List<T> { ... }

// 리스트 확장 가번 인자
val list = listOf("args: ", *args)

// infix 함수 예시
infix fun Any.to(other: Any) = Pair(this, other)
val map = mapOf(1 to "one", 7 to "seven")

// destructuring declaration
val name = person.component1()
val age = person.component2()
val (name, age) = person

정규식 및 문자열 처리

  • 정규식 타입 따로 존재
    • Java는 정규식 타입이 따로 없어서 매우 혼란스러움
    • Kotlin은 정규식 타입이 따로 존재, String.toRegex로 생성
  • 정규식 타입의 기능들
    • match, find등 일반적 컨셉 제공
    • destructured를 통해서 분리 선언 가능
  • """: 여러줄 문자열
    • trimMargin("."): 들여쓰기 처리
    • escape 문자는 지원 안함
    • string template은 가능
1
2
3
4
5
6
7
8
9
10
11
12
// java split with regex
"12.3-4".split("[.]") == ["12", "3-4"]
"12.3-4".split(".") == []

// kotlin
"12.34".split(".") == ["12", "3-4"]
"12.34".split(".", "-") == ["12", "3", "4"]
"12.3-4".split("[.-]".toRegex()) == ["12", "3", "4"]

// kotlin regex, match, destructed
val matchResult = regex.matchEntire(path)
val (directory, filename, extension) = matchResult.destructured

로컬 함수 및 클로저

  • 함수 내에서 함수를 선언하는것이 허용됨
  • 클로저 역시 지원함, 람다/로컬함수 내에서 변수를 수정하는게 가능

Classes, objects, and interfaces

Interface

  • Java8과 유사
  • 함수들은 open이 기본, 구현체에서 overide 필요
  • 인터페이스 default 함수 사용 가능
    • default 키워드 필요 없음
    • 다중 상속 충돌 시 컴파일 에러 - 명시적 선언 필요
  • 프로퍼티도 선언 가능
    • 구현시 override 붙여서 구현 해주어야함
    • 프로퍼티의 getter/setter도 default 함수처럼 구현부를 가질 수 있음
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
interface Clickable {
  fun click() // 추상 메소드
  fun showOff() = println("I'm clickable!") // 디폴트 메소드
}

// 구현시에 콜론(:) 사용
class Button : Clickable, Focusable {
  override fun click() = println("I was clicked")

  // 명시적 충돌 해소, super의 사용법
  override fun showOff() {
    super<Clickable>.showOff()
    super<Focusable>.showOff()
  }
}

// 인터페이스 프로퍼티와 클래스를 통한 구현
interface User { val nickname: String }
class PrivateUser(override val nickname: String) : User

// default 프로퍼티 getter/setter
interface User {
  val email: String
  val nickname: String get() = email.substringBefore('@')
}

Class

  • 제어자: final이 기본, open은 명시적으로 부여
    • 자바의 경우는 open이 기본
    • open class: 상속 가능
    • abstract class: 인스턴스화 불가능
    • open fun: override 가능
    • override fun: overriding 함수, open으로 간주
    • final override fun: overriding 함수, 명시적 final 부여
    • abstract fun: subclass에서 구현이 강제됨, open으로 간주
  • 접근 제어자: public 기본
    • 자바의 경우는 package-priaate가 기본
    • Kotlin은 package 대신 module을 접근 제어의 단위로 사용
  • class 접근 제어
    • public class: 전체 공개
    • internal class: 모듈 내 공개
    • private class: 파일 내 공개
  • 멤버 함수 접근 제어
    • public member: 전체 공개
    • internal member: 모듈 내 공개
    • protected member: 서브클래스에 공개, 자바와 달리 package내 공개 아님
    • private member: 클래스 내 공개

inner, nested, sealed class

  • inner vs nested class
    • Kotlin은 Serialize문제를 피하기 위해 nested class를 기본값으로 씀
    • inner 클래스가 가진 Outer 인스턴스 참조가 Serialize에 문제를 일으킴
  • sealed class
    • enum 대신, 클래스 계층 구조를 when(switch) 구문의 인자로 쓰게 해줌
    • nested class 대상이 제한(sealed)됨
    • 컴파일러가 else의 빠짐을 체크하는 등 실수를 줄여줌
1
2
3
4
5
6
7
8
9
class Outer {
  // outer 인스턴스 접근 가능
  class InnerClass // Java
  inner class InnerClass // Kotlin

  // outer 인스턴스 접근 불가
  static class NestedClass // Java
  class NestedClass // Kotlin
}

class 초기화

  • Primary 생성자와 init block
    • constructor 키워드로 primary 생성자 선언 가능, 키워드 생략 가능
    • init block은 primary 생성자를 위한 실행 block임
    • primary 생성자 인자에, val, var을 부여해 프로퍼티 생성 가능
  • constructor의 존재 의의 = private constructor
    • companion 오브젝트(정적 멤버) 내의 유틸리티 함수에서 생성 가능
    • Factory 혹은 Singleton 패턴을 강제하고 싶을때 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 아래 3개의 클래스 생성은 모두 동치
// 1. 모든 식별자를 다 명시적으로 쓴 경우
class User constructor(_nickname: String) {
  val nickname: String
  init { nickname = _nickname }
}

// 2. constructor 키워드와 init block 생략
class User(_nickname: String) {
  val nickname = _nickname
}

// 3. 프로퍼티 자동 생성, curly brace 생략
class User(val nickname: String)

// private constructor
class Secretive private constructor() {}
  • secondary constructor
    • 여러 개의 생성자가 필요할 때 사용
    • primary가 없으면
      • this를 통한 명시적 호출 필요 없음
      • 상속 받았을 경우 secondary에서 super 호출해야함
    • primary와 함께 쓰면
      • 상속 받았을 경우 primary에서는 반드시 부모 생성자 호출해야함
      • secondary는 this를 명시적 호출 필요
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// primary 없으면(1) : this 위임 호출 불필요
open class View {
  constructor(ctx: Context) {}
}

// primary 없으면(2) : 상속 시 super 호출 필요
class MyButton : View {
  constructor(ctx: Context): super(ctx) {}
}

// primary 있으면(1) : this 위임 호출 필요
open class View(ctx: Context) {
  constructor(ctx: Context, attr: AttributeSet): this(ctx) {}
}

// primary 있으면(2) : 상속 시에도 반드시 this 호출 필요
class MyButton(var data: Data) : View(data) {
  // this를 명시적으로 호출
  constructor(ctx: Context): this(ctx.data) {}
  // 다른 생성자를 명시적으로 호출(연쇄)
  constructor(wrp: Wrapper): this(wrp.ctx) {}
}

data class

  • Kotlin 클래스는 아래 3가지를 Any로부터 상속받아 가지고 있음
    • toString, equals, hashCode
  • 필드들을 고려해서 class에 4가지 기능을 추가 혹은 재정의 해줌
    • toString, equals, hashCode, copy
    • hash 및 동일성 계산에는 Primary 생성자의 프로퍼티만 고려
  • immutable: val property만 사용하는게 바람직함
    • read-only이어야만 HashMap의 키로 사용할 수 있음
    • FP의 side-effect 제거에도 유용
    • 변경이 필요할 경우 copy를 사용하여 해결 가능

Class delecation: “by” keyword

  • decorator pattern
    • D가 A 인스턴스를 멤버로 들고, 해당 객체의 기능을 확장시키는 패턴
    • A 인스턴스의 멤버 함수는, D의 멤버함수로 모두 등록됨
  • 위의 패턴을 boilerplate 없이 by 키워드로 해결해줌
  • why delegation(composition)
    • 객체 지향에서 상속이 가지는 유연성 부족의 문제를 해결하기 위해
    • 합성 혹은 위임을 사용
    • 합성 혹은 위임은, 캡슐화를 유지한채로 인터페이스만 사용함
    • 상속(is-a) 관계를 합성(has-a) 관계로
class DelegatingCollection<T>(
  innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList {}

“object” 키워드

  • singleton: object
    • class 대신 object로 만들면 됨
    • 생성자 선언 불가, 정의하는 즉시 인스턴스 생김
    • thread-safe creation
    • call은 thread-safe하지 않으므로, 대비책을 구현해줘야함
  • companion object
    • 클래스의 정적 멤버들을 담는 오브젝트, 싱글톤과 속성 비슷
    • 클래스당 한 개만 선언 가능
    • 클래스의 private 멤버에도 접근 가능
    • private 생성자와 함께 Factory를 강제하는데 쓰일 수 있음
    • 상속되지 않음
    • 이름을 가질 수 있음
    • 인터페이스를 구현할 수도 있음
    • 확장도 가능: fun ClassName.Companion.func() = ...
  • 익명 내부 객체: object {}
    • 자바의 익명 클래스와 비슷한 기능
    • 함수 내에서 클래스를 정의하게 됨
    • Java와 다르게 익명 객체가 여러개의 Interface를 구현하는건 안됨
    • Closure 사용 가능함
    • 또 Java와 다르게 Closure로 참조하는 변수들의 값을 고칠 수 있음

Programming with lambdas

  • why?
    • verbosity를 낮추기 위한 방안
    • 익명 클래스를 대체
  • curly brace를 통해서 람다 표현
    • 5단계로 단계적 축약 가능 (코드 참조)
  • 람다는 변수에 대입할 수 있음
  • member reference는 Java8과 유사
    • top-level 함수도 참조 가능
    • 인스턴스에 대해서도 참조 가능
  • mutable 변수들에 closure로서 접근 가능
    • 익명 object와 동일
    • Java에서는 final로 취급되어야했음
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 람다 기본 형태
val sum = { x: Int, y: Int -> x + y }
// 직접 호출 방식
{ println(42) }()
run { println(42) }

// 아래 5 개 모두 같은 결과
val people = listOf(Person("Alice", 29), Person("Bob", 31))
// (1) 기본 형태
people.maxBy( { p: Person -> p.age } )
// (2) 람다가 함수의 마지막 인자일 경우 밖으로 뺄 수 있음
people.maxBy() { p: Person -> p.age }
// (3) 인자가 람다만 있을 경우, 빈 괄호를 제거할 수 있음
people.maxBy { p: Person -> p.age }
// (4) 타입 추론
people.maxBy { p -> p.age }
// (5) 인자가 하나인 람다는 "it"를 인자 대신해서 사용하고 파라미터 생략
people.maxBy { it.age } 

// (+) member reference 사용
people.maxBy(Person::age)

Functional APIs for collections

  • Collection 자체에서 제공하는 API들
    • filter: 해당 Collection에 속한것만 남김
    • map: Collection을 돌면서 매핑하고, ArrayList로 묶어서 리턴
    • flatMap: map하되, 대상 컬렉션의 원소를 까서 ArrayList로 묶어서 리턴
    • groupBy: 그룹화 한 Map 리턴
    • all, any: Boolean 리턴
    • find, count: Element / 개수 리턴
  • Sequence
    • Java8의 Stream의 하위호환, 병렬실행 기능은 제공 안함
    • Lazy, 파이프라이닝 등 Stream의 기능과 완전히 동일하다
    • Stream의 특징, 기능들은 Modern Java in Action에서 더 자세하게 공부함

Functional interface

  • SAM 인터페이스는 Java에서 함수(람다)를 대입받기 위해 사용
  • Java와의 호환을 위해서 지원
  • fun interface로 선언, 람다와의 자동 변환을 지원해줌
  • Java와 달리 interface에는 추상함수가 1개이더라도 람다를 대입 불가
    • Kotlin에서는 함수를 위한 타입이 존재함
    • 그러므로 interface를 람다를 대입받기 위한 용도로 사용할 필요 없음

with, apply 함수

  • 확장(Extension) 함수와 유사한 기능을 하도록 람다를 사용
  • 단 클래스가 아니라, 객체를 대상으로 호출
  • 확장처럼 람다에서, this를 쓰거나, 내부 함수들을 호출할 수 있다
  • with: Reciever와 람다를 받아 실행 결과를 반환
  • apply: Reciever의 멤버함수, 람다를 받아 실행 후, Reciever를 재반환
  • run, also, let등이 유사한 기능을 함, 참고
1
2
3
4
5
6
val r: R = T().run { foo(); toR() }
val r: R = T().let { it.foo(); it.toR() }
val r: R = with(T()) { foo(); toR() }

val r: T = T().apply { foo() }
val r: T = T().also { it.foo() }

The Kotlin type system

Nullability

  • 모든 타입은 기본적으로 null을 참조할 수 없음
  • String?과 같이 물음표로 null 참조 가능을 표시
  • nullable 타입에 . 접근시 컴파일 에러 발생
  • 대입 가능 여부
    • String?String에 대입 불가능
    • <T> 제너릭에는 Nullable 대입 가능, <T: Any?>로 간주
    • <T: Any>로 명시한 경우에는 Nullable 대입 불가
  • 스마트 캐스트처럼 검사 한 이후에는, Not-null 타입으로 변경됨
    • if문으로 명시적으로 검사하거나
    • ?. 연산자로 접근해야 접근이 허용됨
  • ?. safe call operator: 대상이 null이면, null을 값으로 내보냄
  • ?: elvis operator: 대상이 null이면 다른 값을 내보냄
    • 값을 내보내지 않고, 리턴하거나, 예외를 던질수도 있음
  • as? safe cast: 타입 변경시 null이면 null을 내보냄
  • !! not-null assert: null이면 강제 예외 발생
  • let을 이용해서 null체크의 가독성을 높일 수 있음
    • 한개 변수에 대한 null 체크를 수행할 때 가장 유용함
1
2
3
4
5
6
7
8
9
10
// 한줄로 여러개의 null 체크
fun Person.countryName() = company?.address?.country ?: "Unknown"

// null이면 예외 던지기 혹은 리턴
val address = person.company?.address ?: return
val address = person.company?.address ?: throw ..

// if null체크 vs let 함수 이용
if (email != null) sendEmailTo(email)
email?.let { sendEmailTo(it) }
  • lateinit 멤버를 나중에 초기화 할 수 있게 한다.
    • nullable 타입의 사용을 줄여줌
    • 초기화 전에 접근하면 예외가 발생한다.
  • Nullable 타입을 명시적으로 확장할 수 있다.
    • 함수 스스로가 “null을 책임지겠다” 라고 말하는 의미
  • Java 코드 실행시 호환성: annotation 있는 경우
    • @Nullable, @NotNull -> Type?, Type
  • Java 코드 실행시 호환성: annotation 없는 경우
    • Type으로도 받을 수 있지만, 예외 발생 가능성이 생김
    • Type?으로 받으면 Null 관련 확인을 꼭해줘야함
    • 타입 없이 받으면, Type?, Type 둘 다로 사용 가능한 Platform Type
    • ., .? 모두로 사용 가능하고, Warning도 나오지 않음
    • 참고로 Type.?를 쓰면 Warning이 나옴
1
2
3
4
5
6
7
8
9
10
11
// Nullable 타입의 명시적 확장
fun String?.isNullOrBlank(): Boolean = this == null || this.isBlank()

// Platform 타입 예시
val str: String = Utility.returnStringJava() // Not-Null
val str: String? = Utility.returnStringJava() // Nullable
val str = Utility.returnStringJava() // Platform Type

println(str.length)

println(str?.length)

Primitive types

  • Kotlin은 value type이 없음
    • Int는 컴파일러가 최대한 int로 최적화
    • Int?의 경우는, null 가능성 때문에 Integer가 될수도 있음
    • Generic으로 쓰이는 경우는 무조건 Boxed됨
  • Type casting
    • 정수, 실수의 암시적 타입변환이 없음
    • Java가 Boxed Type을 다르게 취급하는 것과 같은 맥락
    • 무조건 변환 함수를 불러줘야함. toLong, toInt
    • 1L1은 다르게 취급
  • 다만 Literal 간의 연산에서는 암시적 타입 변환을 지원
1
2
3
4
5
6
7
// explicit type conversion required
1 in listOf(1L, 2L, 3L) == False
new Integer(42).equals(new Long(42)) == False

// implicit type conversion on literal
val b: Byte = 1 // int to byte
val l = b + 1L  // byte + long = long

Special types

  • Any, Any? 모든 타입의 부모. the root types
  • Unit: void를 일부 대체
    • “의미 없는 기본 단위를 자동으로 리턴함” 이라는 의미를 가짐
    • Unit 인스턴스는 싱글톤임
    • void 대비 Unit은 Generic에 사용할 수 있다는 장점이 있음
    • Java의 Void도 유사하지만 리턴을 해야한다는 제한이 있음
  • Nothing: void를 일부 대체
    • “아예 리턴하지 않음”을 의미
    • 컴파일러가 리턴 실수를 적극적으로 감지해 줄 수 있음
1
2
3
4
5
6
interface Processor<T> { fun process(): T }

// generic에 Unit 사용 가능
class NoResultProcessor : Processor<Unit> {
  override fun process() { } // 리턴 필요 없음
}

Nullability and collections

  • Nullable 타입을 Generic에도 쓸 수 있다.
  • List<Int?>List<Int>?는 다르다.
  • Collection은 기본적으로 read-only
    • 즉 값 변경, 추가를 위한 메소드를 제공해주지 않는다.
    • 하지만 데이터가 immutable이라는것은 아님, 컴파일러가 보호해주지않음
    • Java는 read-only가 없기 때문에, Java단에서 값이 변경될 수 있음
    • Thread safe하지 않다.
    • listOf, setOf, mapOf
  • MutableCollection
    • Collection을 상속받아 만든다.
    • 값 변경, 추가, 제거를 위한 메소드들을 제공해준다.
    • List: mutableListOf, arrayListOf
    • Set: mutableSetOf, hashSetOf, linkedSetOf, sortedSetOf
    • Map: mutableMapOf, hashMapOf, linkedMapOf, sortedMapOf
  • Array 자바의 []에 해당
    • 팩토리 함수로 생성하거나, 생성자+람다로 생성
    • spread operator로 가변인자에 활용 가능
    • filter, map 등의 추가 기능을 Kotlin 단에서 제공
  • Primitive의 배열은 따로 타입을 할당해 최적화 가능
    • intArrayOf(0, 0, 0, 0, 0)
    • boxing 오버헤드가 없어서 훨씬 빠르다
1
2
3
4
5
6
// 생성자 + 람다
var arr = Array<String>(26) { ('a' + it).toString() }
// 팩토리
var arr = arrayOf("a", "b", "c")
// spread operator
println("%s/%s/%s".format(*strings.toTypedArray()))