# 코틀린의 특징

# 클래스

1. 데이터 클래스

  • 자료를 저장하는 클래스를 만드는 과정을 단순하게 해준다.
  • 자료를 구성하는 프로퍼티만 선언하면 컴파일러가 equlas(), hashcode(), toString(), copy() 함수를 자동으로 생성해준다.
  • 코틀린에서 ==는 자바의 equals() 메소드와 같은 동작을 한다. 즉, 값을 비교한다.
1
2
3
4
5
6
7
8
// 주 생성자에서 데이터 클래스에 포함되는 프로퍼티를 함께 선언한다. 
data class Person(val name: String, val age: Int)

fun main(args: Array<String>){
val lee = Person("lee", 26)
val park = Person("park",23)
val lim = Person("lee",26)
}

2. 한정 클래스

  • 한정 클래스(seald class)는 enum 클래스를 확장한 개념이다.
  • 이를 상속하는 클래스는 한정 클래스로 정의되는 여러 종류 중 하나로 취급된다.
  • 한정 클래스를 상속하는 클래스는 일반적으로 클래스 내에 중첩하여 선언한다. 외부에 선언할 수도 있다.
  • 한정 클래스로 정의된 클래스의 종류에 따라 다른 작업을 처리해야 할 때 유용하다.
1
2
3
4
5
6
7
8
9
10
11
12
sealed class MobileApp(val os: String){
class Android(os: String, val packageName: String) : MobileApp(os)

class iOS(os: String, val bundleId: String) : MobileApp(os)
}


fun whoAmI(app: MobileApp) = when(app){
is MobileApp.Android -> println("${app.os}")
is MobileApp.iOS -> println("${app.os}")
// 모든 경우를 처리했으므로 else를 쓰지 않아도 된다.
}

한정 클래스에 새로운 클래스를 추가했고, 한정 클래스를 상속한 클래스의 종류에 따라 다른 동작을 처리해야 한다고 가정해보자. 새로 추가된 유형인 WindowsMobile 클래스를 처리하지 않으면 컴파일 에러가 발생하므로 새로운 유형에 대한 처리가 누락되는 것을 방지할 수 있다. 따라서 동작을 처리하는 것의 누락을 방지할 수 있다는 이점을 가지고 있다.

3. 프로퍼티의 사용자 getter/setter

  • 프로퍼티에는 내부에 저장된 필드의 값을 가져오거나 설정할 수 있도록 getter/setter를 내부적으로 구현하고 있다. 이는 단순히 필드의 값을 반환하거나 설정하도록 구현되어 있다.
  • 사용자 지정 getter/setter의 구현을 원하는대로 변경할 수 있으며, 특정 객체의 값에 따른 다양한 정보를 속성 형태로 제공할 때 유용.
  • get(), set(value) 사용.
1
2
3
4
5
6
7
// 나이에 따른 성인 여부를 속성 형태로 제공하는 예시.
class Person(val age: Int, val name: String){

val adult : Boolean
get() = age>=19
// 19세 이상이면 성인으로 간주한다.
}

사용자 지정 setter를 사용하면 프로퍼티 내 필드에 설정되는 값을 제어할 수 있으나, 읽고 쓰기가 모두 가능한 프로퍼티(var)에서만 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
class Person(val age: Int, val name: String){

val adult : Boolean
get() = age>=19

var address: String = ""
set(value){
field = value.subString(0..9)
}
// 사용자 지정 setter를 사용해 인자로 들어온 문자열의 앞 10자리만 필드에 저장한다.
}

# 함수

1. 명명된 인자

  • 명명된 인자(named parameter)를 사용함으로써 함수를 호출할 때 매개변수의 순서와 상관없이 인자를 전달할 수 있다.
  • 또한, 매개변수의 수가 많아지더라도 각 인자에 어떤 값이 전달되는지 쉽게 구분할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
fun drawCircle(x: Int, y: Int, radius: Int){
// 생략
}

fun main(args: Array<String>){
drawCircle(x = 10,y = 5,radius = 25)

// 순서를 바꿔도 명명된 인자를 통해서 순서를 바꿔도 동일하게 호출할 수 있다.
drawCircle(y = 5,x = 10,radius = 25)

// 인자 중 일부에만 사용할 수도 있다.
drawCircle(10,5,radius = 25)
}

2. 매개변수

  • 함수의 매개변수에 기본값을 지정할 수 있으며, 이때 지정하는 값을 기본 매개변수라고 한다.
  • 유용하게 사용할 수 있다.
1
2
3
4
5
6
7
8
9
// 반지름의 기본값으로 25를 갖는다.
fun drawCircle(x: Int, y: Int, radius:Int = 25){
// 생략.
}

fun main(args:Array<String>){
// 반지름을 지정하지 않았으므로 원의 반지름은 기본 값인 25로 지정된다.
drawCircle(10,5)
}

3. 단일 표현식 표기

  • Unit 타입을 제외한 타입을 반환하는 함수라면 함수의 내용을 단일 표현식을 사용하여 정의할 수 있다.
1
2
3
4
5
6
7
8
9
10
// 기본 형태.
fun sum(a: Int, b: Int) : Int {
return a+b
}

// 단일 표현식.
fun sum(a: Int, b: Int) : Int = return a+b

// 반환 타입도 생략 가능.
fun sum(a: Int, b: Int) = return a+b

3. 확장 함수

  • 확장 함수를 사용하여 상속 없이 기존 클래스에 새로운 함수를 추가할 수 있다.
  • 확장 함수를 추가할 대상 클래스는 리시버 타입(receiver type)이라 부르며, 이 리시버 타입 뒤에 점(.)을 찍고 그 뒤에 원하는 함수의 형태를 적는 방식으로 정의한다.
  • 확장 함수 구현부에서는 this를 사용하여 클래스의 인스턴스에 접근할 수 있으며 이를 리시버 객체(receiver object)라 부른다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun main(args: Array<String>){
val foo = "Foo"

val foobar = foo.withBar()
println(foobar)
}

// String 클래스에 withPostfix() 함수 추가.
// this를 사용하여 인스턴스에 접근할 수 있다.
private fun String.withPostfix(postFix: String) : String{
return "$this$postFix"
}

// this를 사용하여 인스턴스에 접근할 수 있으므로, 앞에서 정의한 확장 함수를 사용할 수 있다.
fun String.withBar() = this.withPostfix("Bar")

// 결과
FooBar
  • 확장 함수를 호출하는 모습이 클래스 내 정의된 함수의 경우와 똑같다 할지라도, 이는 클래스 외부에 정의하는 함수이다.
  • 리시버 객체에서는 클래스 내 public으로 정의된 프로퍼티나 함수에만 접근할 수 있다.
  • 확장 함수는 리시버 타입에 직접 추가되는 함수가 아니다. 리시버 타입과 확장 함수의 인자를 인자로 받는 새로운 함수를 만들고, 확장 함수를 호출하면 이 새로운 함수가 호출되는 형태이다.

4. 연산자 오버로딩

  • 사용자 정의 타입에 한해 연산자 오버로딩을 지원한다.
  • 각 연산자별로 사전 정의된 함수를 재정의하는 방식으로 연산자 오버로딩을 사용할 수 있다.
  • operator 키워드를 사용하며, 기존의 연산자를 재정의하는 것만 허용된다.
  • 연산자 재정의는 방법이 동일하기 때문에 사용자가 원하는 형태를 직접 구현하면 된다. 아래에 단항 연산자를 기준으로 예를 들어보겠다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Volume(var left: Int, var right: Int){

// 단항 연산자 '-'를 재정의한다.
operator fun unaryMinus(): Volume{
this.left = -this.left
this.right = -this.right
return this
}
}

var voulme = Volume(50,50)

// Volume 클래스 내 left, right 값이 반전되어 할당된다.
var v1 = -volume

주의할 점은 비교 연산자의 경우에는 다른 연산자와 달리 각 연산자가 모두 동일한 함수에 할당된다. 따라서 해당 함수가 반환하는 값의 크기에 따라 해당 연산자의 참, 거짓 여부를 판단한다. comparTo 함수의 반환형은 항상 Int 어야 한다.

# 람다 표현식

  • 람다 표현식을 통해 훨씬 간편하고 직관적인 문법을 사용할 수 있다.
  • 특히 익명 클래스를 간결하게 표현할 때 유용하게 사용할 수 있다.
  • 중괄호를 사용하여 앞뒤를 묶어준다.
1
2
3
4
5
// 람다 표현식을 사용한 리스너 선언.
button.setOnClickListener({v: View -> doSomething()})

// 인자 타입 생략 가능.
button.setOnClickListener({v -> doSomething()})
  • 람다 표현식에서 하나의 메소드만 호출한다면 멤버 참조를 이용해 더 간략하게 표현할 수 있다.
1
2
3
4
5
6
7
8
9
fun doSomething(v: View){
// 생략.
}

// doSomething() 함수 하나만을 호출하고 있다.
button.setOnClickListener({v -> doSomething(v)})

// 멤버 참조를 사용해 doSomething() 함수에 바로 대입할 수 있다.
button.setOnClickListener(::doSomething)

코틀린에서는 프로퍼티도 멤버 참조를 지원한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person(val name: String, val age: Int){

// 성인 여부를 표시하는 프로퍼티
val adult = age>19
}

fun printAdults(people: List<Person>){

// 필터링 조건을 람다 표현식을 사용해 대입.
people.filter({people -> people.adult})
.forEach{ println("Name= ${it.name}) }

// 멤버 참조를 사용해 adult 프로퍼티를 바로 대입한다.
people.filter(Person::adult)
.forEach{ println("Name= ${it.name}) }
}

람다 표현식의 유용한 기능

  • 함수가 단 하나의 함수 타입 매개변수를 가질 경우, 인자 대입을 위한 괄호를 생략하고 바로 람다 표현식을 사용할 수 있다.
1
2
3
4
5
// setOnClickListener의 마지막 인자로 함수 타입을 대입한다.
button.setOnClickListener({v -> doSomething()})

// 다른 인자가 없으므로, 괄호 없이 바로 외부에 람다 표현식을 사용할 수 있다.
button.setOnClickListener { v -> doSomething() }
  • 또한, 람다 표현식 내 매개변수의 개수가 하나인 경우 매개 변수 선언을 생략할 수 있으며, 참조가 필요한 경우 it을 사용할 수 있다.
1
button.setOnClickListener{ doSomething(it) }
  • 여러 개의 매개 변수를 갖는 람다 표현식에서 사용하지 않는 매개변수는 이름 대신 _를 사용하여 사용하지 않는 매개변수라는 것을 명시할 수 있다.
1
2
3
4
5
var dialog = AlertDialog.Builder(this)

// dialog 매개 변수를 사용하지 않아 _로 표시
.setNegativeButton("Cancel") {_, which -> doCancel(which)}
.create()

인라인 함수

  • 람다 표현식을 사용하면, 함수를 인자로 넘길 수 있는 고차 함수(higher-order function)에 들어갈 함수형 인자를 쉽게 표현할 수 있다.
  • 인라인 함수를 사용하면 함수의 매개변수로 받는 함수형 인자의 본체를 해당 인자가 사용되는 부분에 그대로 대입하므로 성능 하락을 방지할 수 있다.
1
2
3
4
5
6
7
8
9
// 인자로 받은 함수를 내부에서 실행하는 함수.
inline fun doSomething(body: () -> Unit){
println("onPreExecute()")
body()
println("onPostExecute()")
}

// 인라인 함수 호출.
doSomething{ println("do Something()") }

인라인 함수는 컴파일 과정에서 아래와 같이 변환된다.

1
2
3
4
println("onPreExecute()")
// 인자로 전달된 함수 본체의 내용이 그대로 복사된 것을 확인할 수 있다.
println("do Something()")
println("onPostExecute()")

# 여타 특징

타입 별칭

  • 복잡한 구조로 구성된 타입을 간략하게 표현할 수 있다.
  • typealias를 사용한다.
  • 클래스나 함수와 마찬가지로 타입을 인자로 받을 수도 있으며, 함수형 타입에도 타입 별칭을 지정할 수 있다.
1
2
3
4
5
6
7
8
// List<Person>을 PeopleList라는 이름을 갖는 타입 별칭으로 선언.
typealias PeopleList = List<Person>

fun sendMessage(people: PeopleList){
people.forEach{
// 메시지 전송.
}
}

타입 별칭을 사용해 새롭게 선언한다고 해서 이 타입에 해당하는 새로운 클래스가 생성되는 것은 아니다. 타입 별칭으로 선언된 타입은 컴파일 시점에 모두 원래 타입으로 변환되므로 실행 시점의 부하가 없다는 장점이 있다.

분해 선언

  • 각 프로퍼티가 가진 자료의 값을 한번에 여러 개의 값(val) 혹은 변수(var)에 할당할 수 있다. 이 기능을 분해 선언이라고 부른다.
1
2
3
4
5
6
data class Person(val age: Int, val name: Strig)

val person = Person("Lee",26)

// 사람 객체에 포함된 필드의 값을 한번에 여러 값에 할당한다.
val (ageOfPerson, nameOfPerson) = person

분해 선언은 프로퍼티가 가진 자료의 값을 어떻게 전달할까? 이를 알아보기 위해 해당 코드가 어떻게 컴파일되는지 아래에서 알아보자.

1
2
val ageOfPerson: Int = person.component1()
val nameOfPerson: String = person.component2()

이처럼 분해 선언을 사용하면 내부적으로 각 값에 component1(), component2() 함수의 반환값을 할당한다. 프로퍼티의 수가 늘어나면 3,4, … 와 같이 함수 뒤의 숫자가 증가하는 형태, 즉 componentN() 형태의 함수를 추가로 사용하게 된다.

분해 선언을 사용하려면 클래스에 프로퍼티의 수만큼 componentN() 함수가 있어야 하며, 이 함수들을 포함하고 있는 클래스에만 분해 선언을 사용할 수 있다. 아래는 분해 선언을 기본으로 제공하는 클래스들이다.

  • data class로 선언된 클래스
  • kotlin.Pair
  • kotlin.Triple
  • kotlin.collections.Map.Entry

특히, 맵 자료구조를 사용할 때 유용하다.

1
2
3
4
5
6
7
8
9
10
11
12
val cities: Map<String, String> = ... // 도시 정보를 저장하고 있는 맵

// 맵 내 각 항목의 키와 값을 별도로 선언하여 사용한다.
// 따라서 keySet()과 같은 함수가 필요없어진다.
for((cityCode, name) in cities){
println("$cityCode = $name")
}

// 람다 표현식 내 매개변수에서도 분해 선언을 사용할 수 있다.
cities.forEach{ cityCode, name ->
println("$cityCode = $name")
}

분해 선언을 지원하는 클래스를 제외한 개발자가 작성한 클래스에서 분해 선언을 사용하고 싶다면, 해당 클래스 내에 별도로 componentN() 함수를 프로퍼티의 선언 순서 및 타입에 알맞게 추가해줘야 한다.

componentN() 함수를 선언할 때는 앞에 operator를 붙여 줘야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person(val age: Int, val name: String){
// 첫 번째 프로퍼티의 값 반환.
operator fun component1() = this.age
// 두 번째 프로퍼티의 값 반환.
operator fun component2() = this.name
}

val person = Person("lee",26)

// 분해 선언 사용.
val (age,name) = person

// 사용하지 않는 변수 혹은 값은 _로 표시한다.
// 따라서 아래는 name 만 사용하는 경우이다.
val (_, name) = person