Convention

코틀린의 관례에 대해 다음 순으로, 정리한다.

  1. 산술 연산자
  2. 비교 연산자
  3. 컬렉션, 범위
  4. 구조 분해 선언

관례란, 언어 기능과 미리 정해진 이름의 함수를 연결해주는 기법이다.

산술 연산자 오버로딩

자바에서는,

  1. 원시 타입에 대해서 산술 연산자를 사용할 수 있으며
  2. String 에 대해 + 연산자를 사용할 수 있다.

코틀린에서는, 다른 클래스에도 산술 연산자를 사용할 수 있다.

이항 산술 연산 오버로딩

연산자를 오버로딩하는 함수 앞에는 operator 키워드가 붙어야한다.
다음을 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
fun main() {
val p1 = Point(10, 20)
val p2 = Point(30, 40)

println(p1 + p2)
}

data class Point(val x: Int, val y: Int) {

operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
}
Read more

컬렉션과 배열

코틀린의 컬렉션과 자바의 컬렉션 간의 관계에 대해 정리한다.

널 가능성과 컬렉션

타입 인자로 쓰인 타입에 ? 표시를 붙이면 널을 저장할 수 있다.
아래 예제에서는, List<Int?> 는 Int 나 null 을 저장할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun readNumbers(reader: BufferedReader): List<Int?> {

val result = ArrayList<Int?>()

for (line in reader.lineSequence()) {
try {
val number = line.toInt()
result.add(number)
} catch (e: NumberFormatException) {
result.add(null)
}
}

return result
}

“어떤 변수 타입의 널 가능성” 과 “타입 파라미터로 쓰인 타입의 널 가능성” 은 차이가 있다.
예를 들어,

  1. List<Int?> : 리스트 안의 각 값이 널이 될 수 잇다.
  2. List? : 전체 리스트가 널이 될 수 있다.

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

코틀린 컬렉션과 자바 컬렉션의 중요한 차이 중 하나는,
코틀린에서는 컬렉션 안의 데이터에 접근하는 인터페이스와 변경하는 인터페이스가 분리되었다는 점이다.

  1. kotlin.collections.Collection : 컬렉션 안의 데이터에 접근
  2. kotlin.collections.MutableCollection : 데이터 변경 (kotlin.collections.Collection 를 확장)

아래 코드에서, target 에 해당하는 인자로 읽기 전용 컬랙션을 넘길 수 없다.

Read more

Primitive Type

코틀린의 원시 타입을 정리한다.

원시 타입 : Int, Boolean …

자바에서는 원시 타입과 참조 타입을 구분한다.
원시 타입의 변수에는 그 값이 들어가고, 참조 타입의 변수에는 메모리상의 객체 위치가 들어간다.

코틀린에서는 원시 타입과 참조 타입을 구분하지 않는다.
아래 코드에서는, 정수를 표현하기 위해 Int 를 사용한다.

1
2
val i: Int = 1
val list: List<Int> = listOf(1, 2, 3)

그리고, 코틀린에서는 아래처럼 원시 타입의 값에 대해 메서드 호출이 가능하다.

1
2
3
4
fun showProgress(progress: Int){
val percent = progress.coerceIn(0, 100)
println("progress : $percent")
}

그런데, 원시 타입과 참조 타입 같으면 항상 객체로 표현되는 걸까? 아니다.
실행 시점에 가장 효츌적인 방식으로 표현된다.
예를 들어, 대부분의 경우 코틀린의 Int 타입은 자바 int 타입으로 컴파일 된다.

널이 될 수 있는 원시 타입 : Int?, Boolean? …

자바에서는, null 참조를 참조 타입의 변수에만 대입할 수 있다.
그래서, 코틀린에서 널이 될 수 있는 원시 타입을 사용하면 자바의 래퍼타입으로 컴파일 된다.
아레 예제에서, age 프로퍼티의 값은 java.lang.Integer 로 저장된다.

Read more

Null

코틀린에서 null 을 어떻게 처리하는지 정리한다.

nullability

nullability (널 가능성) 은, NPE 를 피할 수 있도록 돕기 위한 코틀린 타입 시스템의 특성이다.
널이 될 수 있는지의 여부를 타입 시스템에 추가해서,
컴파일러가 오류를 미리 감지해 실행 시점에 발생할 수 있는 가능성을 줄인다.

널이 될 수 있는 타입

다음 자바 함수를 보자.

1
2
3
int stringLength(String s) {
return s.length();
}

이 함수의 파라미터로 null 을 전달하면, NPE 가 발생한다.
이 함수를 코틀린으로 작성하면,

1
fun stringLength(s: String) = s.length

이 함수의 파라미터로 null 을 전달하면, 컴파일 시 오류가 발생한다.
그래서, 실행 시점에 NPE 가 발생하지 않는다고 확신할 수 있다.

만약 위 함수가 널과 문자열을 받을 수 있게 하려면, ? 를 명시해야한다.

Read more

람다

코틀린의 람다에 대해 정리한다.

람다 소개

아래와 같은 내용을 코드로 적용하려면 어떻게 해야할까 ?

  • 이벤트가 발생하면 이 Handler 를 실행하자
  • 모든 원소에 이 연산을 적용하자

자바에서는 무명 내부 클래스를 사용해, 클래스를 선언하고 그 클래스의 인스턴스를 함수에 넘길 수 있다.
함수형 프로그래밍에서는 함수를 직접 다른 함수에 전달할 수 있다.

자바의 다음 예를 보자.

1
2
3
4
5
6
button.setOnClickListener(new onClickListener(){
@Override
public void onClick(View view){
// event when clicked
}
});

무명 내부 클래스를 선언해서, 코드가 번잡하다.
코틀린에서는, 자바 8 과 마찬가지로 람다를 사용할 수 있다.

1
button.setOnClickListener { /* event when clicked */ }

람다와 컬렉션

Read more

지연 계산 컬렉션

lazy collection 연산에 대해 정리한다.

지연 계산 (lazy) 켈렉션 연산

다음 예를 보자.

1
2
3
people
.map(Person::name)
.filter { it.startsWith("A") }

filter 와 map 은 리스트를 반환한다.
즉 위 코드는, 한 리스트는 filter 의 결과를 담고 다른 리스트는 map 의 결과를 담는다.
원소의 개수가 수백만 개라면 효율이 떨어질 수 있다.

그래서, 각 연산이 컬렉션을 직접 사용하는 대신 시퀀스를 사용하게 만들어 효율을 높일 수 있다.

1
2
3
4
people.asSequence()     // 원본 컬렉션을 시퀀스로 변환
.map(Person::name)
.filter { it.startsWith("A") }
.toList() // 결과 시퀀스를 다시 리스트로 변환

시퀀스 연산 실행 : 중간 연산, 최종 연산

자바 8의 스트림과 코틀린의 시퀀스의 개념은 같다.

중간 연산은 다른 시퀀스를 반환한다.
최종 연산은 결과를 반환한다.

Read more

표준 라이브러리

컬렉션을 다루는 코틀린 표준 라이브러리를 정리한다.

  • filter, map
  • all, any, count, find
  • groupBy
  • flatMap, flatten
  • with, apply

filter, map

filter 는 컬렉션에서 원치 않는 원소를 제거한다.

1
2
val list = listOf(1, 2, 3, 4)
println(list.filter { it % 2 == 0 })

결과는,
컬렉션의 원소 중에, 주어진 술어 (==predicate, 참/거짓을 반환하는 함수) 를 만족하는 원소로 이루어진 새로운 컬렉션이다.

map 은 원소를 변환한다.

1
2
val list = listOf(1, 2, 3, 4)
println(list.map { it * it })

결과는,
원본 리스트와 원소 개수는 같지만 각 원소는 주어진 함수에 따라 변환된 새로운 컬렉션이다.

all, any, count, find

Read more

object 키워드

object 키워드가 사용되는 경우인, 다음 내용을 정리한다.

  • 객체 선언
  • 동반 객체
  • 객체 식

객체 선언 : 싱글턴 객체 생성

인스턴스가 하나만 필요한 경우가 있다.
자바에서는 다음과 같이 singleton pattern 으로 구현할 수 있다.

  1. 클래스의 생성자를 private 으로 제한하고,
  2. 정적인 플드에 그 클래스의 유일핸 객체를 저장

코틀린은 ‘객체 선언’ 기능을 통해, 싱글턴 패턴을 언어에서 기본 지원한다.
( 객체 선언 = 클래스 선언 + 그 클래스에 속한 단일 인스턴스 선언 )

object keyword 를 사용해서,
클래스를 정의하고 그 클래스의 인스턴스를 만들어서 변수에 저장하는 모든 작업을 한 문장으로 처리한다.
일반 클래스의 객체와 달리, 싱글턴 객체는 객체 선언문이 있는 위치에서 생성자 호출없이 즉시 만들어진다.

1
2
3
4
5
6
7
8
9
10
11
12
13
object Payroll {
val allEmployees = arrayListOf<Person>()

fun calculateSalary() {
for (person in allEmployees) {
//..
}
}
}

// usage
Payroll.allEmployees.add(Person(name = "jko", isMarried = false))
Payroll.calculateSalary()

클래스 안에 객체로 선언할 수도 있다.

1
2
3
4
5
6
7
8
9
10
data class Person(val name: String) {

object NameComparator : Comparator<Person> {
override fun compare(o1: Person, o2: Person): Int = o1.name.compareTo(o2.name)
}
}

// usage
val persons = listOf(Person("jko"), Person("junhee-ko"))
println(persons.sortedWith(Person.NameComparator))
Read more

데이터 클래스

다음 내용을 정리한다.

  • 데이터 클래스 : toString(), equals(), hashCode(), copy()
  • 클래스 위임 패턴 : by

모든 클래스가 정의해야하는 메서드

간단한 클래스를 작성해보자.

1
class Client(val name: String, val postCode: Int)

이 클래스에 대해서,
toString(), equals(), hashCode() 메서드들이 어떻게 사용되는지 보자.

toString()

기본 제공되는 객체의 문자열 표현은 다음과 같은 형식이다 : Client@5e9f23bf
기본 구현을 바꾸려면 toString 메서드를 오버라이드해야한다.

1
2
3
4
class Client(val name: String, val postCode: Int) {

override fun toString(): String = "Client(name='$name', postCode=$postCode)"
}

equals()

Read more

생성자

다음 내용을 정리한다.

  • 주 생성자, 부 생성자
  • 인터페이스의 프로퍼티
  • field 키워드
  • 접근자 가시성

클래스 초기화 : 주 생성자, 초기화 블록

클래스 이름 뒤에 괄호로 둘러싸인 코드가 주 생성자이다.

1
class User(val nickname: String)

같은 목적을 이룰 수 있는 다은 형태의 코드를 보자.

1
2
3
4
5
6
7
class User constructor(_nickname: String) {
val nickname: String

init {
nickname = _nickname // _ : 프로퍼티와 생성자 파라미터를 구분하기 위해 사용
}
}

constructor 키워드는, 주 생성자나 부 생성자를 정의할 때 사용한다.
init 키워드는, 초기화 블록을 시작한다. 초기화 블록은, 클래스의 객체가 만들어질 때 실행된다.

위 코드에서,

  1. nickname 프로퍼티를 초기화 하는 코드를, nickname 프로퍼티 선언에 포함 가능
  2. 주 생성자 앞에 다른 annotation 이나 가시성 변경자 없다면, constructor 키워드 생략 가능
Read more