코틀린1 - 코틀린 기초
1장. 코틀린이란
더 적은 코드로 더 편하게
- 코틀린의 주목적은 현재 자바가 사용되고 있는 모든 용도에 적합하면서도 더 간결하고 생산적이며 안전한 대체 언어를 제공하는 것
- 안드로이드 공식언어
- 서버, 모바일, 웹, 프론트엔드까지 코틀린언어 하나로 작성가능
컴파일 과정
- Kotlin Tutorials
kotlinc <소스파일 or 디렉터리> -include-runtime -d <jar 이름>
- 코틀린 - *.kt -> 코틀린 컴파일러 -> *.class -> *.jar -> (코틀린 런타임)애플리케이션
- 자바 - *.java -> 자바 컴파일러 -> *.class -> *.jar -> 애플리케이션
코틀린 컴파일러로 컴파일한 코드는 코틀린 런타임 라이브러리에 의존 런타임 라이브러리에는 코틀린 자체 표준 라이브러리 클래스와 코틀린에서 자바 API의 기능을 확장한 내용이 들어있다. 코틀린으로 컴파일한 애플리케이션을 배포할 때는 런타임 라이브러리도 함께 배포해야 한다.
2장. 코틀린 기초
Hello World
fun main(args: Array<String>){
println("Hello, world!")
}
- 함수를 최상위 수준에 정의할 수 있다! 꼭 클래스 안에 함수를 넣어야 할 필요가 없다.
- 코틀린에는 자바와 달리 배열 처리를 위한 문법이 따로 존재하지 않는다.
- System.out.println 대신 println이라고 쓴다. 표준 자바라이브러리 함수를 감싼 Wrapper
- 세미콜론을 붙이지 않아도 된다!
함수
fun max(a:Int, b:Int): Int{
return if(a > b) a else b
}
- 코틀린 if는 값을 만들어내지 못하는 문장이 아니고 결과를 만드는 식이다. 코틀린에서는 루프를 제외한 대부분의 제어 구조가 식이다.
boolean a = false;
if (a = false){
// 자바) 자바의 if는 값을 가지지 않은 문장이다. a == false라고 써야하는데 대입식으로 잘못 사용한 경우
// 에러가 나지 않고 예상과 다른 결과를 내보낼 수 있다.
System.out.println(a); //런타임 에러
}
// 코틀린) 앞선 함수의 식이 하나라면 중괄호와 return을 제거해서 식이 본문인 함수로 만들 수 있다.
fun max(a:Int, b:Int) = if(a > b) a else b
- 반환타입을 생략할 수 있는 이유 - 컴파일러가 함수 본문 식을 분석해 식의 결과 타입을 추론할 수 있기 때문.
- 식이 본문인 함수의 반환타입만 생략가능. 블록이 본문인 함수는 return문을 여러번 사용할 수 있기 때문.
변수
- 코틀린에서는 타입 지정을 생략하는 경우가 흔하기 때문에 변수 이름 뒤에 타입을 명시하는 편이 자연스럽다
val question = "이것은 타입을 명시하지 않아도 스트링"
val question2:String = "이것은 타입을 명시한 스트링"
val answer = 42
val answer2:Int = 42
val answer3: Int //선언을 먼저하고
answer3 = 42 //나중에 할당할 수도 있다.
// 이 경우 변수타입을 반드시 명시해야한다.
- 변경가능한 변수 변경 불가능한 변수
val value = "변경 불가능한 변수"
value = "변경" //에러
var variable = "변경 가능한 변수"
variable = "변경" //OK
-
변경불가능한 참조를 저장하는 변수 val은 일단 초기화하고 나면 재대입이 불가능하다.
-
기본적으로는 모든 변수를 val키워드를 사용해 불변 변수로 선언하고, 나중에 꼭 필요할 때에만 var로 변경하라.
-
val 참조 자체는 불변일지라도 그 참조가 가리키는 객체의 내부 값은 변경될 수 있다 (중요)
val languages = arrayListOf("Java")
languages.add("Kotlin")
- var는 변수의 값을 변경할 수 있지만 변수의 타입은 고정돼 바뀌지 않는다.
var answer = 42
answer = "no answer" // 컴파일 오류 type mismatch
- 문자열 템플릿
fun template(name: String){
println("Hello, $name")
}
template("Jonny")
//출력 - Hello, Jonny
fun printArray(arr: Array<String>){
println("arr.size = ${arr.size}")
}
- 문자열 템플릿은 컴파일시 StringBuilder를 사용하고 문자열 상수와 변수의 값을 append로 문자열 빌더 뒤에 추가한다.
- 자바에서 + 연산으로 문자열과 변수를 붙일때와 같다.
클래스
// 자바
public class Person{
private final String name; //필드
public Person(String name){ //생성자
this.name = name;
}
public String getName(){ //게터
return name;
}
}
- 필드가 늘어날 수록 생성자의 대입문 수도 늘어난다. 코틀린에서는 필드 대입 로직을 훨씬 적은 코드로 작성할 수 있다.
// 코틀린
class Person(val name: String)
- 끝. 깔끔하다.
- 코드 없이 데이터만 저장하는 클래스를 값 객체(value object-VO)라 부른다.
- public이 기본
프로퍼티
- 클래스의 목적은 데이터를 캡슐화하고 그 데이터를 다루는 코드를 한 주체 아래 가두는 것
- 자바에서는 데이터를 필드에 저장하며 가시성은 기본이 비공개(private)다.
- 클래스는 자신을 사용하는 클라이언트가 그 데이터에 접근하는 통로로 쓸 수 있는 접근자 메소드(accessor method)를 제공한다. 게터랑 세터
- 자바에서는 필드와 접근자를 묶어 프로퍼티라 부른다.
class Person(
val name:String, // 읽기 전용 프로퍼티 private필드와 getter생성
var isMarried: Boolean // 읽고 쓸 수 있는 프로퍼티 private필드와 getter, setter생성
)
val person = Person("Bob", true)
println(person.name) //프로퍼티 이름을 사용하면 코틀린이 getter를 호출
println(person.isMarried)
person.isMarrie = false //setter를 호출하여 값을 주입. 필드는 private임에 주의
커스텀 게터
class Rectangle(val height: Int, val width: Int){
val isSquare: Boolean get() = height == width // 커스텀 게터 선언
}
val rect = Rectangle(30, 30)
println(rect.isSquare) //true
val rect = Rectangle(30, 29)
println(rect.isSquare) //false
- 프로퍼티 값을 그때그때 계산할 수 있다.
디렉터리와 패키지
package geometry.shapes // 패키지 선언
import java.util.Random // 표준 자바 라이브러리 클래스 임포트
class Rectangle(val height: Int, val width: Int){
val isSqure: Boolean get() = height == width
}
fun createRandomRectangle() : Rectangle {
val random = Random()
return Rectangle(random.nextInt(), random.nextInt())
}
package geometry.example
import geometry.shapes.createRandomRectangle // 다른 패키지의 함수 임포트
fun main(args: Array<String>){
println(createRandomRectangle().isSquare)
}
- 클래스 임포트와 함수 임포트에 차이가 없다.
- 자바에서는 패키지의 구조와 일치하는 디렉터리 계층 구조를 만들고 클래스의 소스코드를 그 클래스가 속한 패키지와 같은 디렉터리에 위치시켜야 한다.
//자바 패키지 구조
geometry
ㄴexample
ㄴㄴMain
ㄴshapes
ㄴㄴRectangle
ㄴㄴRectangleUtil
- 코틀린에서는 여러 클래스를 한 파일에 넣을 수 있고 파일의 이름도 마음대로 정할 수 있다.
- 코틀린에서는 디스크상의 어느 디렉터리에 소스코드 파일을 위치시키든 관계없다.
- 따라서 원하는 대로 소스코드를 구성할 수 있다.
//코틀린 구조
geometry
ㄴexample.kt
ㄴshapes.kt
- 대부분의 경우 자바와 같이 패키지별로 디렉터리를 구성하는 편이 낫다
- 자바의 방식을 따르지 않으면 자바 클래스를 코틀린 클래스로 마이그레이션할 때 문제가 생길 수도 있다.
- 하지만각 클래스를 정의하는 소스코드 크기가 작은 경우 여러 클래스를 한 파일에 넣는 것을 주저해서는 안된다.
enum과 when
- 코틀린의 when은 자바의 switch를 대치하되 강력하다.
enum class Color {
RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}
-
코틀린에서 enum은 소프트 키워드이다.
-
enum키워드가 class앞에 있을 때만 특별한 의미를 지닌다. 따라서 enum을 일반 클래스의 이름으로 사용할 수 있다.
-
반면 class는 키워드다. 따라서 클래스의 이름으로 class는 사용할 수 없다.
-
코틀린의 enum은 단순히 값만 열거하는 존재가 아니라 프로퍼티나 메소드를 정의할 수 있다.
enum class Color(
val r:Int, val g:Int, val b:Int // 상수의 프로퍼티 정의
) {
RED(255, 0, 0), // 각 상수를 생성할 때 그에 대한 프로퍼티 값을 지정한다.
ORANGE(255, 165, 0),
YELLOW(255, 255, 0),
GREEN(0, 255, 0),
BLUE(0, 0, 255),
INDIGO(75, 0, 130),
VIOLET(238, 130, 238); // 여기서는 반드시 세미콜론을 사용해야 한다.
fun rgb() = (r * 256 + g) * 256 + b //enum 클래스 안에서 메소드를 정의한다.
}
when으로 enum 클래스 다루기
fun getMnemonic(color: Color) =
when (color) {
// when도 값을 만들어내는 식이다. -> 여러 줄 식을 본문으로 하는 함수
Color.RED -> "Richard"
Color.ORANGE -> "Of"
Color.YELLOW -> "York"
Color.GREEN -> "Gave"
Color.BLUE -> "Battle"
Color.INDIGO -> "In"
Color.VIOLET -> "Vain"
}
//println(getMnemonic(Color.BLUE))
-
break를 넣지 않아도 된다.
-
상수값을 임포트하면 더 간단하게 사용할 수 있다.
import ch02.colors.Color // 다른 패키지에서 정의한 Color 클래스 임포트
import ch02.colors.Color.* // 짧은 이름으로 사용하기 위해 enum 상수를 모두 임포트
fun getWarmth(color: Color) =
when (color) {
RED, ORANGE, YELLOW -> "warm" // 임포트한 enum 상수를 이름만으로 사용
GREEN -> "netural"
BLUE, INDIGO, VIOLET -> "cold"
}
- 코틀린 when의 분기 조건은 임의의 객체를 허용한다.
fun mix(c1: Color, c2: Color) =
when (setOf(c1, c2)){ // when 식의 인자로 아무 객체나 사용할 수 있다.
setOf(RED, YELLOW) -> ORANGE
setOf(YELLOW, BLUE) -> GREEN
setOf(BLUE, VIOLET) -> INDIGO
else -> throw Exception("Dirty color")
}
인자가 없는 when
- 비효율 개선하기
- 위 예제의 함수는 호출될 때마다 함 수 인자로 주어진 두 색이 when의 분기 조건에 있는 다른 두 색과 같은이 비교하기 위해 여러 Set인스턴스를 생성한다.
- 불필요한 가비지 객체가 늘어나는 것을 방지하기 위해 함수를 고쳐 쓰는 편이 낫다.
- 인자가 없는 when 식을 사용하면 불필요한 객체생성을 막을 수 있다.
- 코드는 약간 읽기 어려워지지만 성능을 위해 그 정도 비용을 감수해야 하는 경우도 자주 있다.
fun mixOptimized(c1: Colir, c2: Color) =
when {
(c1 == RED && c2 == YELLO) ||
(c1 == YELLO && c2 == RED) -> ORANGE
(c1 == YELLOW && c2 == BLUE) ||
(c1 == BLUE && c2 == YELLOW) -> GREEN
(c1 == BLUE && c2 == VIOLET) ||
(c1 == VIOLET && c2 == BLUE) -> INDIGO
else -> throw Exception("Dirty color")
}
- when에 아무 인자도 없으려면 각 분기의 조건이 불리언 결과를 계산하는 식이어야 한다.
스마트 캐스트: 타입 검사 + 타입 캐스트
- (1 + 2) + 4와 같은 간단한 산술식을 계산하는 함수 만들기
- 식 -> Sum이거나 Num -> 인터페이스
- Sum -> 자식이 둘 있는 중간 노드
- Num -> 항상 terminal 노드
interface Expr
class Num(val value: Int) : Expr // Int형의 인자만 올 수 있는 Expr 인터페이스 구현 클래스이다.
class Sum(val left: Expr, val right: Expr) : Expr // Sum의 인자는 Expr인터페이스를 구현한 누구든 올 수 있다. 즉 Sum의 인자로 다른 Sum이나 Num모두 올 수 있다.
// 작은 클래스, 인터페이스들을 한 파일에 모으는 것을 주저하지 말자.
println(eval(Sum(Sum(Num(1), Num(2)), Num(4)))) //eval구현하기 전임
// 7
-
Expr 인터페이스에는 두 가지 구현 클래스가 존재한다. 따라서 식을 평가하려면 두 가지 경우를 고려해야 한다.
-
어떤 식이 수라면 그 값을 반환한다.
-
어떤 식이 합계라면 좌항과 우항의 값을 계산한 다음에 그 두 값을 합한 값을 반환한다.
-
자바스타일 코드
//코틀린언어지만 자바스타일 코드
fun eval(e: Expr): Int {
if (e is Num) { // 변수 타입 검사 -> 자바의 instanceof와 비슷
val n = e as Num // 명시적 캐스팅
return n.value
}
if (e is Sum) {
val n = e as Sum
return eval(n.left) + eval(n.right)
}
throw IllegalArgumentException("Unknown expression")
}
//명시적 캐스팅은 필요없음
fun eval(e: Expr): Int {
if (e is Num) { // 변수 타입 검사 + 캐스팅
return e.value
}
if (e is Sum) {
return eval(e.left) + eval(e.right)
}
throw IllegalArgumentException("Unknown expression")
}
println(eval(Sum(Sum(Num(1), Num(2)), Num(4))))
// 7
- if를 when으로 변경
fun eval(e: Expr): Int =
if(e is Num) {
e.value
} else if (e is Sum) {
eval(e.left) + eval(e.left)
} else {
throw IllegalArgumentException("Unknown expression")
}
println(eval(Sum(Sum(Num(1), Num(2)), Num(4))))
// 7
fun eval(e: Expr): Int =
when(e){
is Num -> e.value
is Sum -> eval(e.left) + eval(e.right)
else -> throw IllegalArgumentException("Unknown expression")
}
- 블록 사용하는 경우 블록의 마지막 문장이 블록 전체의 결과가 된다.
fun evalWithLogging(e: Expr): Int =
when(e){
is Num -> {
println("num: ${e.value})
e.value //블록의 마지막 식 반환
}
is Sum -> {
val left = evalWithLogging(e.left)
val right = evalWithLogging(e.right)
println("sum: $left + $right")
left + right //블록의 마지막 식 반환
}
else -> throw IllegalArgumentException("Unknown expression")
}
println(evalWithLogging(Sum(Sum(Num(1), Num(2)), Num(4))))
// num: 1
// num: 2
// sum: 1 + 2
// num: 4
// sum: 3 + 4
// 7
이터레이션: 범위와 수열
-
while은 자바와 동일
-
for
-
for(int i=0; i<5; i++){} 형태의 for는 코틀린에 존재하지 않는다.
-
대신 범위를 사용
val oneToTen = 1..10
-
순열(progression) - 어떤 범위의 속한 값을 일정한 순서로 이터레이션 하는 것
-
fizzBuzz게임
fun fizzBuzz(i: Int) = when {
i % 15 -> "FizzBuzz"
i % 5 -> "Buzz"
i % 3 -> "Fizz"
else -> "$i"
}
for (i in 1..100){
print(fizzBuzz(i))
}
// 1 2 Fizz 4 Buzz Fizz 7 ...
// 100부터 거꾸로 세되 짝수만
for (i in 100 downTo 1 step 2){
print(fizzBuzz(i))
}
// 1부터 99까지만 세고 싶지만 100을 명시하는 게 더 편할 때
val size = 100
for (i in 1 until size){
print(fizzBuzz(i))
}
맵에 대한 이터레이션
val binaryReps = TreeMap<SChar, String>()
for (c in 'A'..'F'){ // 문자 범위
val binary = Integer.toBinaryString(c.toInt())
binaryReps[c] = binary // 자바 - binaryReps.put(c, binary) 값 설정
}
for((letter, binary) in binaryReps){ // 맵에 대한 이터레이션 (구조 분해 구문)
println("$letter = $binary")
}
- 컬렉션에 대한 이터레이션
val list = arrayListOf("10", "11", "1001")
for((index, element) in list.withIndex()){ // withIndex는 3장에서
println("$index: $element")
}
- in 키워드
- for문 안에서 사용. 컬렉션이나 범위에 대해 이터레이션
- 어떤 값이 범위나 컬렉션에 들어있는지 확인
fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
fun isNotDigit(c: Char) = c !in '0'..'9'
println(isLetter('q')) // true
println(isNotDigit('x')) // true
c in 'a'..'z'
-> 'a' <= c && c <= 'z'
로 변환된다. (표준 라이브러리 범위 클래스 구현 안에 감쳐줘 있다.)
- 범위는 비교가 가능한 클래스(java.lang.Comparable 인터페이스를 구현한 클래스)라면 그 클래스의 인스턴스 객체를 사용해 범위를 만들 수 있다.
- Comparable을 사용하는 범위의 경우 그 범위 내의 모든 객체를 항상 이터레이션하지는 못한다.
- ‘Java’와 ‘Kotlin’사이의 모든 문자열을 이터레이션 불가
- 하지만 in 연산자를 사용하면 값이 범위 안에 속하는지 항상 결정할 수 있다.
println("Kotlin" in "Java".."Scala")
- String에 있는 Comparable 구현이 두 문자열을 알파벳 순서로 비교하기 때문에 in 검사에서도 문자열을 알파벳 순서로 비교할 수 있다.
println("Kotlin" in setOf("Java", "Scala"))
- 컬렉션에서도 in 연산을 사용할 수 있다.
예외처리
- 코틀린의 예외 처리는 자바나 다른 언어의 예외처리와 비슷하다.
- 함수는 정상적으로 종료할 수 있지만 오류가 발생하면 예외를 던질 수 있다.
- 함수를 호출하는 쪽에서는 그 예외를 잡아 처리할 수 있다.
- 발생한 예외를 함수 호출 단에서 처리하지 않으면 함수 호출 스택을 거슬러 올라가면서 예외를 처리하는 부분이 나올 때까지 예외를 다시 던진다.
- 자바와 달리 코틀린의 throw는 식이므로 다른 식에 포함될 수 있다.
val percentage =
if(number in 0..100) number
else throw IllegalArgumentException("A percentage value must be between 0 and 100: $number")
try, catch, finally
fun readNumber(reader: BufferedReader):Int?{
// 함수가 던질 수 있는 예외를 명시할 필요가 없다.
try {
val line = reader.readLine()
return Integer.parseInt(line)
} catch (e:NumberFormatException){
return null
} finally {
reader.close()
}
}
-
자바에서는 함수를 작성할 때 함수 선언 뒤에 throws IOException을 붙여야 한다.
-
이유는 IOException이 체크 예외이기 때문이다.
-
자바에서는 체크 예외를 명시적으로 처리해야 한다.
-
어떤 함수가 던질 가능성이 있는 예외나 그 함수가 호출한 다른 함수에서 발생할 수 있는 예외를 모두 catch로 처리해야 하며, 처리하지 않은 예외는 throws 절에 명시해야 한다.
-
코틀린에서는 체크 예외와 언체크 예외를 구별하지 않는다.
-
코틀린에서는 함수가 던지는 예외를 지정하지 않고 발생한 예외를 잡아내도 되고 잡아내지 않아도 된다.
-
위 예제에서 NumberFormatException은 체크 예외가 아니다.
-
따라서 자바 컴파일러는 NumberFormatException을 잡아내게 강제하지 않는다.
-
그에 따라 실제 실행 시점에 NumberFormatException이 자주 발생한다.
-
하지만 입력 값이 잘못된 경우는 흔히 있는 일이므로 그런 문제가 발생한 경우 부드럽게 다음 단계로 넘어가도록 프로그램을 설계해야하기 때문에 굉장히 불편하다.
-
BufferedReader.close는 IOException을 던질 수 있는데, 그 예외는 체크 예외이므로 자바에서는 반드시 처리해야 한다.
-
하지만 실제 스트림을 닫다가 실패하는 경우 클라이언트 프로그램이 취할 수 있는 의미있는 동작은 없다.
-
그러므로 이 IOException을 잡아내는 코드는 그냥 불필요하다.
-
try를 식으로 활용
fun readNumber(reader: BufferedReader){
val nuber = try{
Integer.parseInt(reader.readLine())
} catch(e: NumberFormatException){
return
}
println(number)
}
요약
- 함수를 정의할 때 fun 키워드를 사용. val은 읽기 전용 변수, var는 변경 가능한 변수 선언
- 문자열 템플릿 “$변수” ”${식}”
- 코틀린에서는 값 객체 클래스를 간결하게 표현할 수 있다.
- if는 코틀린에서 식이며, 값을 만들어낸다.
- 코틀린의 when은 자바의 switch와 비슷하지만 더 강력하다.(객체를 인자로..)
- 변수의 타입을 검사하고 나면 그 변수를 캐스팅하지 않아도 검사한 타입의 변수처럼 사용할 수 있다. (컴파일러가 스마트 캐스팅)
- 코틀린의 for는 맵이나 컬렉션을 이터레이션할 때 더 편리하다.
- 1..5와 같은 식은 범위를 만들어낸다. 범위와 수열은 코틀린에서 같은 문법을 사용하며, for 루푸에 대해 같은 추상화를 제공한다. 어떤 값이 범위 안에 들어 있거나 들어있지 않은지 검사하기 위해서 in이나 !in을 사용한다.
- 코틀린의 예외 처리는 자바와 비슷하지만 코틀린에서는 함수가 던질 수 있는 예외를 선언하지 않아도 된다.
참고자료
- Kotlin in Action
- http://try.kotl.in/
- https://kotlinlang.org/docs/tutorials/