기본 요소: 함수와 변수
함수
1 | fun {함수이름}({매개변수 이름}: {매개변수 타입}) : {반환값 타입} { |
반환값이 없는 함수 예시
1 | fun main(args: Array<String>) { |
반환값이 있는 함수 예시
1 | fun max(a: Int, b: Int) : Int { |
문(statement)과 식(expression)의 구분
코틀린의if
는 식이지 문이 아니다. 반환값이 있는 함수 예시를 보면 반환값에 if식이 들어 간 것을 확인할 수 있다. 식은 값을 만들어내며 다른 식의 하위 요소로 계싼에 참여할 수 있다. 반면 문은 자신이 둘러싸고 있는 가장 안쪽 블록의 최상위 요소로 존재하며 아무런 값을 만들어 내지 않는다. 자바는 모든 제어 구조가 문인 반면, 코틀린은 루프를 제외한 모든 제어 구조가 식이다.
식이 본문인 함수
max
함수를 좀 더 간결하게 줄여보자.
먼저 본문이 식 하나인 블록으로 구성되어 있어서 이를 식으로 대체할 수 있다.
1 | fun max(a: Int, b: Int) : Int = if (a > b) a else b |
위 처럼 등호와 식으로 이뤄진 함수를 식이 본문인 함수라고 하고, 본문이 중괄호로 쌓인 함수를 블록이 본문인 함수라고 한다.
그리고 식이 본문인 함수인 경우 반환 타입을 생략할 수 있다. 식이 본문인 경우 컴파일러가 함수 본문 식을 분석해서 식의 결과 타입을 함수 반환 타입으로 정해준다. 이렇게 컴파일러가 타입을 분석해 프로그래머 대신 프로그램 구성 요소의 타입을 정해주는 기능을 타입추론 이라고 부른다.
1 | fun max(a: Int, b: Int) = if (a > b) a else b |
변수
코틀린에서는 변수를 초기화할 때 타입을 생략할 수 있다. 타입을 생략하는 경우 타입추론을 활용한다.
1 | val statement = "중요한 문장입니다." |
초기화 식을 사용하지 않고 변수를 선언하려면 반드시 타입을 적어줘야 한다. 변수에 저장될 값에 대한 정보가 없어서 타입추론을 할 수 없기 때문이다.
1 | val value : Int |
변경 가능한 변수와 변경 불가능한 변수
1 | // 값을 뜻하는 value - 초기화 이후 변경 불가 |
이때 재밌는 점은 자바 final은 선언과 동시에 초기화해줘야 하지만 코틀린의 val
은 한번만 초기화된다. 즉 다음과 같은 제어 구조를 구성할 수 있다.
1 | fun doMessage(input : String) { |
문자열 템플릿
1 | fun stringTemplate(input: String) { |
문자열 리터럴 안에서 변수를 사용할 수 있도록 하는 방법이다. $
를 앞에 붙여주고 변수명을 적어주면 상요할 수 있다.
중괄호를 써주면 변수명이 아닌 간단한 식을 넣어줄 수도 있다.
클래스와 프로퍼티
자바빈 클래스인 Person
을 자바와 코틀린으로 구현해보자.
자바
1 | public class Person { |
코틀린
1 | class Person(val name: String) |
와우! 어떻게 이렇게 된 것일까?
코틀린에서는 생성자를 통해 필드 대입 로직을 묵시적으로 생략해도 된다. 그리고 가시성 변경자가 public 인 경우 생략할 수 있다.
프로퍼티
클래스라는 개념의 목적은 데이터를 캡슐화 하고 캡슐화한 데이터를 다루는 코드를 한 주체 아래 가두는 것이다.
자바에서는 데이터를 필드에 저장하고 해당 데이터를 접근할 수 있는 접근자 메서드를 제공한다. (보통 게터, 세터)
자바에서는 이런 필드와 접근자 메서드를 묶어서 프로퍼티라고 정의한다.
코틀린은 프로퍼티를 언어 기본 기능으로 제공한다. 코틀린 프로퍼티는 자바의 필드 + 접근자 메서드를 완전히 대체한다.
1 | class Person( |
변수 선언과 비슷하게 val
과 var
키워드로 선언할 수 있다. 이때 두 키워드에 따라 프로퍼티 유형이 달라진다.
val
: 읽기 전용 프로퍼티, 비공개 필드 + 게터var
: 쓸 수 있는 프로퍼티, 비공개 필드 + 게터 + 세터
is
가 붙은 변수명의 게터 세터isMarried
와 같이 변수명이is
로 시작하는 경우 게터가get
이 붙지 않고 원래 이름 그대로 사용한다.
그리고 세터는is
부분을set
으로 바꿔 사용한다.isMarried
는setMarried
가 될 것이다.
그렇다면 자바와 코틀린의 클래스 활용법을 비교해보자.
자바
1 | Person person = new Person("yang", false); |
코틀린
1 | val person = Person("yang", false) |
커스텀 접근자
프로퍼티는 그 값을 저장하기 위해 프로퍼티를 뒷받침하는 필드가 존재한다. 그런데 필요하면 프로퍼티 값을 그때그때 계산할 수도 있다. 커스텀 게터를 사용하면 그런 프로퍼티를 구현할 수 있다.
1 | class Rectangle(val height: Int, val width: Int) { |
직사각형이 정사각형인지를 굳이 필드로 저장하지 않고 커스텀 접근자로 구현한 예시이다. 본문이 식인 경우와 블록인 경우 모두 가능하다. 식이 복잡하면 블록으로 구현할 수 있다.
선택 표현과 처리: enum과 when
enum 클래스 정의
1 | enum class Color { |
코틀린에서 enum
은 소프트 키워드이다. 소포트 키워드는 특정 조건에서만 특정한 의미를 갖는 키워드이다. 소프트 키워드는 변수 명 같이 다른 이름으로 사용할 수 있다. 하지만 class
는 키워드이고 다른 곳에서 이름으로 사용할 수 없다.
코틀린 enum도 프로퍼티와 메서드를 추가해줄 수 있다.
1 | enum class Color( |
일반적인 클래스와 비슷하게 생성자를 통해 프로퍼티를 선언할 수 있다. 각 상수마다 프로퍼티 값을 정의해준다.
그리고 중요한 점은 enum에 메서드가 존재하는 경우 마지막 enum 상수 끝에 세미콜론을 넣어줘야 한다!
when으로 enum 클래스 다루기
자바의 switch
가 있다면 코틀린에는 when
이 있다. 각 색깔에 맞는 과일 이름을 반환하는 메서드를 when
을 활용해서 구현할 수 있다.
1 | fun getFruitNameOf(color: Color) = |
코틀린의 when
은 if
와 마찬가지로 값을 반환하는 식이다. 그래서 식이 본문인 함수로 구현할 수 있다. 자바 switch
와는 다르게 매번 break
을 넣어주지 않아도 된다.
when과 임의의 객체를 함께 사용
when
은 분기 조건에 임의의 객체도 지원해서 상수(enum 상수나 숫자 리터럴)만 허용하는 switch
보다 강력하다.
1 | fun mix(c1: Color, c2: Color) = |
분기 조건에 두 색을 가진 집합 객체를 받아서 사용할 수 있다. 이때 임의의 객체를 분기 조건으로 사용하면 해당 분기가 맞는지는 동등성을 통해 확인한다.
하지만 위 코드는 매번 메서드를 실행할 때마다 여러 Set 객체를 만들어서 비교한다. 메서드 호출이 굉장히 많을 경우 불필요한 가비지 객체가 많아짐을 의미한다.
인자 없는 when 사용
인자가 없는 when
식 사용하면 불필요한 객체 생성을 막을 수 있다. 코드는 장황해지만 성능 상의 이점을 얻을 수 있다.
1 | fun mixOptimized(c1: Color, c2: Color) = |
when
식에 인자가 없으려면 매 분기가 참 거짓을 판별하는 식이어야 한다.
스마트 캐스트: 타입 검사와 타입 캐스트를 조합
(1 + 2) + 4
와 같이 덧셈을 계산하는 함수를 만들어보자.
우선 식을 인코딩하는 방법을 생각해본다. 우리는 식을 트리 구조로 저장한다. 노드는 Num
(값)과 Sum
(합) 두가지 형식을 가지고, 최하단 노드는 항상 Num
이고 Sum
은 자식을 가진 중간 노드다.
Expr
인터페이스를 선언하고 Sum
과 Num
모두 구현하도록 하자.
1 | interface Expr |
그렇다면 (1 + 2) + 4
라는 식은 Sum(Sum(Num(1), Num(2)), Num(4))
이런 객체로 표현할 수 있다.
그렇다면 해당 Expr
객체의 값을 반환하는 eval
메서드를 통해 값을 구한다고 하면 다음과 같은 코드가 된다.println(eval(Sum(Sum(Num(1), Num(2)), Num(4))))
Expr
객체가 Sum
과 Num
에 따라 eval
이 다르게 작동해야 한다.
Num
: 그 값을 반환Sum
: 좌항과 우항을 계산한 다음 두 값을 합한 값을 반환
이런 분기를 처리하는 코드를 짜보는데, Java 방식으로 Kotlin 코드를 짜보자.
1 | fun eval(e: Expr): Int { |
is
는 Java의 instancof
와 비슷하고, as
는 캐스팅하는 역할을 한다.
Kotlin에서는 is
로 검사하고 나면 해당 변수는 컴파일러가 검사했던 타입으로 캐스팅해준다. 이를 스마트 캐스트라고 부른다.
스마트 캐스트를 적용하면 캐스팅하던 코드가 사라진다.
1 | fun eval(e: Expr): Int { |
리팩토링: if를 when으로 변경
Kotlin의 if
는 값을 만들어내는 식임을 더 활용해보자.
1 | fun eval(e: Expr): Int = |
식이 본문인 함수로 변경됐다. 하지만 if
를 when
으로 변경해서 더 다듬을 수 있다.
1 | fun eval(e: Expr): Int = |
분기 조건에 값 동등성 조건 대신 다른 기능을 활용하였다. 이 경우에도 스마트 캐스트가 작동한다.
if와 when의 분기에서 블록 사용
if
와 when
의 분기에서 복잡한 로직을 실행하려면 블록을 사용한다. 그리고 각 블록은 반환하려는 값을 맨 마지막에 작성하면 된다. 즉 블록의 마지막 식이 블록의 결과이다.
1 | fun evalWithLogging(e: Expr): Int = |
대상을 이터레이션: while과 for 루프
while
코틀린의 while
은 자바와 별반 다르지 않다.
1 | while (조건) { |
수에 대한 이터레이션: 범위와 수열
전통적인 for문을 코틀린에서 제공하지 않는다. 이를 대신하기 위해 코틀린에서는 범위를 사용한다.
범위는 val oneToTen = 1..10
이런 방식으로 만든다. 코틀린의 범위는 폐구간 혹은 양 끝을 포함하는 구간이다.
이때 역방향으로 수열을 만들고 싶으면 100 downTo 1
이런 식으로 구현할 수 있다. 이때 감소폭을 다루고 싶으면 step
키워드를 붙이면 된다. 즉 100 downTo 1 step 2
이런 식으로 구현할 수 있다.
코틀린은 기본적으로 개발자가 작성한 마지막 끝 점을 포함한 수열을 만든다. 만약 닫힌 구간을 구현하고 싶으면 until
키워드를 사용하자. 0 until 101
은 0..100
과 같다.
맵에 대한 이터레이션
1 | for ((key, value) in someMap) { |
이런 형태로 맵 객체를 구조 분해해서 사용할 수 있다.
물론 굳이 맵이 아니더라도 구조 분해할 수 있다.
1 | for ((index, element) in someList.withIndex()) { |
in으로 컬렉션이나 범위의 원소 검사
in 연산자로 순회 뿐만아니라 원소가 포함됐는지 검사할 수 있다.
1 | fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z' |
when
절에서도 분기를 구분할 때 사용할 수 있다.
1 | fun recognize(c: Char) = when(c) { |
in
절로 원소 겁사할 수 있는 범위는 비교가 가능한 클래스(Comparable
을 구현한 클래스)면 무엇이든 된다.
예외 처리
코틀린의 예외 발생은 자바와 거의 비슷하다. 다만 코틀린에서는 throw
키워드가 식을 만든다는 점만 알고 있자. 그래서 다른 식 내부에서 사용될 수 있다.
1 | fun main(args: Array<String>) { |
try, catch, finally
자바와 마찬가지로 try, catch, finally를 사용한다. 다만 throws IOException
이 없다는 점을 주목하자. 자바는 체크 예외를 반드시 어떻게든 처리해줘야 하는데 코틀린은 체크 예외와 언체크드 예외를 구분하지 않는다. 체크 예외가 발생한다고 해서 클라이언트 프로그램이 취할 수 있는 의미있는 동작이 마땅하지 않은 경우가 많기 때문에 의미 없는 예외를 다시 던지거나 예외를 잡고 처리하지 않도록 구현하는 경우가 많기 때문이다.
1 | fun readNumber (reader: BufferedReader): Int? { |
try를 식으로도 사용할 수 있다.
1 | fun readNumber(reader: BufferedReader) { |
이 경우 catch 블록을 보면 return 문을 통해 메서드를 종료시키고 있다. 이런 방식은 catch 됐을 때 메서드를 종료시키고 싶은 경우 적절하다. 하지만 다른 값을 반환해야 하는 경우는 다르게 작성할 수 있다.
1 | fun readNumber(reader: BufferedReader) { |
이렇게 마지막줄에 블록의 결과를 넣는 규칙에 따라 null을 결과값으로 반환되도록 구현했다.