코틀린을 공부하면 정리하는 포스팅.

# 코틀린의 특징

간결한 문법

  • 문장 끝에 세미 콜론(;)을 넣지 않는다.
  • new 키워드를 사용하지 않고 객체를 생성한다.
  • 타입 추론을 지원하므로 타입을 명시하지 않아도 된다.

널 안정성

  • 널 값의 허용 여부를 명확히 구분하며 컴파일 단계에서 검사한다.
1
2
3
4
5
var a : String? = null
// 널 값 허용.

var b : String = "b"
// 널 값 허용하지 않음.

가변/불변 구분

  • val(값) : 값을 한번 할당하고 나면 그 후에 값을 변경할 수 없는 변수. 자바의 final 변수와 유사.
  • var(변수) : 할당된 값을 자유자재로 바꿀 수 있는 변수.
  • 변수의 가변/불변과 유사하게 컬렉션 자료형에 대해서도 가변/불변 여부를 구분한다.
    • 객체에 할당된 값이 아닌 컬렉션 내에 포함된 자료들을 추가,삭제할 수 있는지 여부를 구분한다.
    • 불변의 경우 삽입,삭제,수정을 위한 함수가 없다.
1
2
3
4
5
6
7
8
9
10
11
var a : String = "a"
val b : String = "b"

a = "aa" // 가변이므로 가능.
b = "bb" // 불변이므로 불가능.

val immutable : List<String> = listOf("a","b") // 불변 리스트.
val mutable : MutableList<String> = mutableListOf("a","b") // 가변 리스트.

immutable.add("c") // add() 함수가 정의되어 있지 않음.
mutable.add("c") // add()와 같은 자료를 수정할 수 있는 함수가 정의되어 있음.

람다표현식 지원

  • 람다 표현식을 기본으로 제공.
1
2
3
view.setOnClickListener{
Toast.makeText(it.context, "Click", Toast.LENGTH_SHORT).show()
}

스트림API 지원

  • 코틀린 표준 라이브러리를 통해 컬렉션 내 자료를 다루는 데 유용한 스트림 API를 지원한다.

완벽한 자바 호환성

  • 자바에서 코틀린을 사용할 수 있고, 반대로 코틀린에서 자바 코드를 사용할 수 있다.

# 주요 문법

값 및 변수 선언

1
2
3
4
5
6
7
8
9
10
var a : String = "a" 
// String 타입의 a 선언.

var b = "b"
// 타입을 명시하지 않아도 타입을 추론한다.

var c : String
// 선언 시 초기화를 하지 않으면 타입을 꼭 붙여야 한다.
c = "c"
// 자료 할당.

함수 선언

  • Unit은 자바의 void와 유사하다. 반환 타입이 없음을 의미한다. 생략 가능하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
fun sum(a : Int, b : Int) : Int{
return a+b
}


fun greet(str : String) : Unit{
println(str)
}

// Unit 반환 타입을 생략한 경우.
fun greet(str : String) {
println(str)
}

클래스 및 인터페이스 선언

1
2
3
4
5
6
7
8
9
10
11
12
13
// Class
class V{
var a : Stnig = "a"

fun A(){

}
}

// interface
interface I{
fun C()
}

조건문

1
2
3
4
5
6
7
8
9
10
11
12
// if-else
if(a>b){
return a
}else{
return b
}

// when. 자바의 switch문과 같음.
when(count){
1 -> println("$count item")
else -> println("else item")
}

반복문

  • for-each만은 지원한다.
  • while, do-while문은 자바와 사용법이 똑같다.
1
2
3
4
val items = listOf("a","b")
for(item in items){
println(item)
}

# 기본 자료형

  • 코틀린은 모든 타입을 객체로 표현.
    • 원시 타입과 래퍼 클래스를 구분하지 않는다.

원시 타입을 모두 객체로 처리하면 비효율적일 수 있다.
코틀린에서는 코드를 작성하는 시점에 원시 타입과 래퍼를 구분하지 않지만, 컴파일 단계를 거치면서 가장 효율적인 타입으로 변환된다.

  • 값이나 변수의 타입으로 사용되는 경우 : 원시 타입으로 변환
  • 컬렉션의 타입 인자로 사용되는 경우 : 래퍼로 변환
1
2
3
4
5
6
7
var a : Int = ...
var b : List<Int> = ...

// java에서는 아래와 같이 바뀐다.

int a = ...
List<Integer> b = ...

숫자

  • 숫자를 표현하는 모든 자료형은 Number 클래스를 상속한다.
  • 현재 숫자에 해당하는 문자를 반환하는 toChar() 함수를 추가로 제공한다.
  • Long 타입은 혼동 방지를 위해 대문자 리터럴 표기만 지원한다.

문자

  • 자바에서는 문자에 해당하는 아스키 코드를 char에 숫자 형태로 대입할 수 있었지만, 코틀린에서는 문자만 대입할 수 있으며, 숫자를 대입할 경우 컴파일 에러가 발생한다.
  • toChar()를 사용해서 다른 자료형의 값을 문자 자료형에 대입한다.
1
2
3
4
val code : Int = 65

// code에 해당하는 문자를 할당.
val ch : Char = code.toChar()

논리

  • 자바와 동일하게 사용하며, Boolean을 사용한다.

문자열

  • 자바와 거의 동일하게 사용된다.
  • 문자열의 특정 문자에 접근하기 위해 charAt() 대신 get() 혹은 대괄호([])와 인덱스를 사용해서 접근한다.
  • 문자열 템플릿을 사용해 템플릿 문자열 내에 직접 인자를 대입한다.
    • 인자로 값이나 변수 대신 표현식을 넣을 경우 표현식을 중괄호로 구분하면 된다.
1
2
3
4
5
6
7
8
9
10
val foo : String = "Victory"

val ch1 : Char = foo.get(0) // V

val ch2 : Char = foo[1] // i

// 문자열 템플릿.
val length : Int = 300
val lengthText : String = "Length : $length meters"
val text : String = "Length : ${foo.lenght} !"

배열

  • 배열은 타입 인자를 갖는 Array 클래스로 표현한다.
  • arrayOf()는 코틀린 표준 라이브러리에 포함되어 있으며, 입력받은 인자로 구성된 배열을 생성.
  • 자바의 원시 타입은 코틀린 배열 클래스의 타입 인자로 사용할 수 없다.
  • 자바 원시 타입을 인자로 갖는 배열을 표현하기 위해서 각 원시 타입에 대응하는 특수한 클래스를 제공한다.
1
2
3
4
5
6
7
val words : Array<String> = arrayOf("a","b","c","d")

// 원시 타입의 배열.
val intArr : IntArray = intArrayOf(1,2,3,4,5)

// 래퍼 타입 배열.
val IntArr : Array<Int> = arrayOf(1,2,3,4,5)
  • 가변 인자에 배열을 전달하는 경우에 스프레드 연산자(*)를 사용한다.
1
2
3
4
5
6
fun go(varag args : String){

}

val strArr : Array<String> = arrayOf("a","b","c")
go(*strArr) // 스프레드 연산자 사용.

# 컬렉션

  • JVM을 기반으로 하는 코틀린에서 컬렉션은 자바에서 제공하는 클래스들을 그대로 사용한다.
  • 이때, 타입 별칭을 사용해 컬렉션 내 다른 클래스와의 일관성을 유지한다.
  • 컬렉션 내 자료의 수정 가능 여부에 따라 컬렉션의 종류를 구분하며 인터페이스를 통해 사용 가능한 함수를 제한하는 방식으로 구현되어 있다.

Collection, List 인터페이스에는 자료를 조회하는 함수만 포함되어 있으므로 자료가 할당되면 수정이 불가능하다. 그 대신, 각 인터페이스를 상속한 MutableCollection, MutableList 인터페이스에 자료를 수정하는 함수가 포함되어 있다.

Set, Map도 동일한 규칙이 지정되며 아래 표와 같다.

자료구조 자료 수정 불가 자료 수정 가능
List kotlin.collections.List kotlin.collections.MutableList
Map kotlin.collections.Map kotlin.collections.MutableMap
Set kotlin.collections.Set kotlin.collections.MutableSet
  • 다음은 컬렉션을 쉽게 생성하는 함수를 나타낸다.
  • listOf, setOf, mapOf는 자료의 수정이 불가능하다.(불변)
함수명 자료 수정 가능 여부 반환 타입(실제 타입)
listOf() X kotlin.collections.List
arrayListOf() O kotlin.collections.ArrayList(java.util.ArrayList)
SetOf() X kotlin.collections.Set
hashSetOf() O kotlin.collections.HashSet(java.util.HashSet)
linkedSetOf() O kotlin.collections.LinkedHashSet(java.util.LinkedHashSet)
sortedSetOf() O kotlin.collections.TreeSet(java.util.TreeSet)
mapOf() X kotlin.collections.Map
hashMapOf() O kotlin.collections.HashMap(java.util.HashMap)
linkedMapOf() O kotlin.collections.HashMap(java.util.LinkedHashMap)
sortedMapOf() O kotlin.collections.LinkedHashMap(java.util.SortedMap)
  • 배열의 특정 원소에 접근하는 방법과 동일하게 컬렉션 내 항목에 접근할 수 있다.
1
2
3
4
5
6
val immutableList : List<String> = listOf("a","b")

// get(0)과 동일.
val firstItem : String = immutableList[0]
// 불변이므로 컴파일 에러 발생.
immutableList[0] = "c"
  • 맵은 숫자 대신 키 값을 넣어 항목에 접근할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
val immutableMap : Map<String, Int> = mapOf(Pair("A",65), Pair("B",66))

// 키 "A"에 해당하는 값 - get("A")와 동일
val code : Int = immutableMap["A"]
// 불변이므로 값 할당시 컴파일 에러 발생.
immutableMap["C"] = 67

val mutableMap : HashMap<String, Int> = hashMapOf(Pair("A",65),Pair("B",66))
// 가변이므로 가능.
mutableMap["C"] = 67

// 조금 더 간단하게 Pair 표현
val map : Map<String, Int> = mapOf("A" to 65, "B" to 66)

# 클래스 및 인터페이스

클래스 및 인터페이스 선언

  • 클래스 및 인터페이스를 선언하는 방식은 자바와 거의 유사하다.
  • 접근 제한자를 지정하지 않는 경우 기본은 public이다.
  • 선언된 내용이 없으면 클래스와 인터페이스는 몸체만 선언할 수 있다.
  • new를 사용하지 않고 인스턴스를 생성한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A{

}

class B // class 몸체.

interface C{

}

interface D // interface 몸체.

// new 키워드 사용 없이 인스턴스 생성.
val foo : Foo = Foo()

val bar : Bar = Bar(1)
  • 추상 클래스는 자바와 동일한 선언 방법이지만, 인스턴스를 생성하는 형태가 다르다.
  • 인터페이스를 선언하거나, 인터페이스의 인스턴스를 만드는 방법은 추상 클래스와 매우 유사하다.
  • 추상 클래스에서는 인스턴스를 생성 시 생성자를 사용하지만, 생성자가 없는 인터페이스는 인스턴스 이름만 사용한다.
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
26
27
28
29
// 추상 클래스의 선언.
abstract class Foo{

abstract fun bar()
}

// 추상 클래스의 인스턴스 생성.
// object: [생성자] 형태로 선언.
val foo = object: Foo(){

override fun bar(){
// 함수 구현.
}
}

// 인터페이스 선언.
interface Bar{

fun baz()
}

// 인터페이스의 인스턴스 생성.
// object: [인터페이스 이름] 형태로 선언.
val bar = object: Bar{

override fun baz(){
// 함수 구현.
}
}

프로퍼티

  • 프로퍼티는 자료를 저장할 수 있는 필드와 이에 상응하는 getter/setter 메소드를 함께 제공하며, 자바의 필드와 유사한 형태로 선언한다.
  • 클래스의 멤버로 사용하는 프로퍼티는 초기값을 명시적으로 지정해야 한다. 그렇지 않을 경우 컴파일 에러 발생.
    • 단, 생성자에서 프로퍼티의 값을 할당한다면 선언 시 값을 할당하지 않아도 된다.
  • 프로퍼티 선언 시점이나 생성자 호출 시점에 값을 할당할 수 없는 경우에는 lateinit 키워드를 사용하여 프로퍼티의 값이 나중에 할당될 것임을 명시한다.
  • lateinit 키워드는 var 프로퍼티에만 사용 가능하다.
    • 반드시 초기화 작업을 해야 한다.
  • 프로퍼티에 초기값을 할당하는 시점에서 해당 프로퍼티의 타입을 추론할 수 있다면 타입 선언 생략 가능.
1
2
3
4
5
6
7
8
9
class Person{
val name : String? = null // 값을 읽을 수만 있는 val, getter만 있음.

var address : String? = null // 값을 읽고 쓰는게 모두 가능한 var, getter/setter 모두 있음.

lateinit var gender : String? // 선언 시점에 값을 할당하지 않아도 컴파일 에러가 발생하지 않음.

var age = 20
}
  • lateinit 키워드를 사용했는데 초기화를 하지 않으면 Uninitialized PropertyAccessException 예외가 발생하한다. 컴파일 단계에서는 확인이 불가능하므로 lateinit 키워드를 사용할 경우 반드시 초기화 여부를 확인하는 것이 좋다.
1
2
3
4
5
6
7
8
lateinit var name : String

// lateinit 키워드를 사용한 프로퍼티가 초기화 되었는지 확인할 수 있다.
if(::name.inInitialized){
println("초기화 O")
}else{
println("초기화 X")
}

접근 제한자

  • 제한자가 없으면 public으로 간주한다.
  • internal 접근 제한자를 제공한다.
    • 단순히 같은 패키지에 있으면 접근이 가능했던 자바의 패키지 단위 제한과 달리 internal 접근 제한자는 동일 모듈 내에 있는 클래스들로의 접근을 제한한다.
    • 외부 모듈에서는 이 접근 제한자로 선언된 요소에 접근할 수 없다.
  • 제한하는 모듈의 범위
    • IntelliJ IDEA 모듈
    • Maven / Gradle 프로젝트
    • 하나의 Ant 태스크 내에서 함께 컴파일 되는 파일들.