[Kotlin] 코틀린 기초
Kotlin in Action 2장 정리 내용
함수와 변수
간단한 예제인 "Hello, world"를 보며 정리해보자.
//자바의 경우
public static void main(String[] args) {
System.out.println("Hello, World!")
}
// 코틀린의 경우
fun main(args: Array<String>) {
println("Hello, World!")
}
- 함수를 선언할 때 fun을 사용한다.
- 파라미터 이름 뒤에 해당 파라미터의 타입을 쓴다.
- 자바와 달리 클래스 안에 함수를 넣을 필요 없이 최상위 수준에서 정의할 수 있다.
- 배열도 일반적인 클래스처럼 사용된다. 자바와 달리 배열 처리를 위한 문법이 따로 존재하지 않는다.
- System.out.println을 사용하지않고 println만으로 함수를 간결하게 사용할 수있다.
- 줄 끝에 ; (세미콜론)을 붙이지 않아도 된다.
함수
//1번
fun max(a: Int, b: Int): Int {
return if (a > b) a else b
}
// 2번 식이 본문인 함수
//위에 함수에서 return을 제거하고 등호(=)를 식 앞에 붙이면 간결하게 함수표현 가능하다.
fun max(a: Int, b: Int): Int = if (a > b) a else b
// 3번
//더 간략하게 반환타입을 제거할 수 있다.
fun max(a: Int, b: Int) = if (a > b) a else b
함수 선언은 fun 키워드로 시작한다.
순서는 다음과 같다.
fun 함수이름(파라미터) : 반환타입 { return 본문}
위에 예제를 보면 점점 함수가 간결해지며 3번에서는 반환타입을 생략한 것을 볼 수있다.
반환타입을 생략할 수 있는 이유는 무엇인가?
코틀린은 정적타입지정언어라 컴파일 시점에 모든 식의 타입을 지정해야한다.
하지만 식이 본문인 함수는 굳이 사용자가 반환타입을 적지 않아도 컴파일러가 함수 본문 식을 분석해서 식의 결과타입을 반환타입으로 지정해주기에 생략이 가능하다. (식이 본문인 함수만 반환타입 생략가능)
식(Expression)과 문(Statement)의 차이
식(Expression)
정의: 식은 값을 생성하거나 계산하는 코드. 식은 실행 시 결과값을 반환함
예시:
val a = 5 + 3 // '5 + 3'는 식이다.
val isAdult = age > 18 // 'age > 18'은 식이다.
// java에서 if는 문이지만 코틀린에서는 식으로 사용된다.
if(a>b) ? a else b
- 리터럴: 5, "Hello"
- 변수 참조: x, name
- 수식: a + b, x * 2
- 제어문: if, while, switch (코틀린의 경우 for문을 제외한 대부분의 제어문은 식에 해당한다. 자바에서는 모든 제어문이 문이다.)
- 함수 호출: Math.max(3, 7), getUserName()
- 논리 연산: x && y, a || b
특징:
- 값을 반환한다.
- 함수 호출이나 연산 등에서 사용된다.
- 다른 식이나 문 내에서 사용될 수 있다.
문(Statement)
정의: 문은 프로그램의 동작을 수행하는 코드의 기본 단위, 문은 특정 작업을 수행하지만, 그 자체로는 값을 반환X
예시:
val x = 5 // 변수 선언 및 초기화 문
if (x > 3) { // if 문
println("x is greater than 3") // 출력 문
}
var y = 10
y = y + 1 // 할당문
- 변수 선언 및 초기화: int x = 10;
- 할당문: x = 5;
- 제어문: for (코틀린의 경우 for만 해당)
- 함수 정의: void printHello() { System.out.println("Hello"); }
- 반환문: return x;
특징:
- 특정 작업을 수행한다.
- 값을 반환하지 않지만, 프로그램의 상태를 변경할 수 있다.
- 다른 문을 포함할 수 있다.
변수
//자바
int answer = 42;
//코틀린
val answer: Int = 42 //타입지정
val answer = 42 // 타입생략
//초기화식이 아닌 경우
val answer: Int
answer = 42
자바에서는 변수 선언 시 타입이 맨 앞에 온다. 코틀린에서는 타입을 생략하는 경우가 많지만
초기화식을 사용하지 않을 때는 타입을 명시해줘야한다.
- val(값을 뜻하는 value에서 따옴) : 변경 불가능한 참조를 저장하는 변수로 자바로는 final변수에 해당한다.
- var(변수를 뜻하는 variable에서 따옴) : 변경 가능한 참조이다. 자바에서는 일반함수이다.
기본적으로는 모든 변수를 val 키워드를 사용해 불변 변수로 선언하고, 나중에 꼭 필요할 때에만 var로 변경하자.
val 변수는 블록을 실행할 때 정확히 한 번만 초기화돼야 한다. 하지만 조건에 따라 val 값을 다른 여러 값으로 초기화할 수도 있다.
val message: String
if (canPerformOperation()) {
message = "Success"
// ... 연산을 수행한다.
} else {
message = "Failed"
}
val 참조 자체는 불변일지라도 그 참조가 가리키는 객체의 내부 값은 변경될 수 있다.
val languages = arrayListOf("Java")
languages.add("Kotlin")
var 키워드를 사용하면 변수의 값을 변경할 수 있지만 변수의 타입은 고정돼 바뀌지 않는다.
var answer = 42
answer = "no answer" // 컴파일 오류 발생
문자열 템플릿
$ 문자를 문자열에 넣고 싶으면 println("\$x")와 같이 \를 사용해 $를 이스케이프시켜야 한다.
fun main(args: Array<String>) {
println("Hello, ${if (args.size > 0) args[0] else "Kotlin"}!")
}
클래스 및 프로퍼티
//자바
public class Person {
private final String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
//코틀린
class Person ( val name: String)
같은 내용의 Java 클래스와 코틀린 클래스이다.
코틀린의 기본 가시성은 public으로 이런경우 변경자를 생락해도 된다.
class Person (
val name: String, //읽기전용 프로퍼티, 코틀린은 비공개 필드와 필드를 읽는 Getter를 생성
var isMarried:Boolean//쓸 수 있는 프로퍼티, 코틀린은 비공개필드, 공개 Getter, 공개 Setter를 생성
)
자바에서는 필드와 접근자(getter, setter)를 한데 묶어 프로퍼티라고 부르며, 프로퍼티라는 개념을 활용하는 프레임워크가 많다. 코틀린은 프로퍼티를 언어 기본 기능으로 제공하며, 코틀린 프로퍼티는 자바의 필드와 접근자 메소드를 완전히 대신한다.
enum
//간단한 enum class정의
enum class Color {
RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}
//프로퍼티와 메서드가 있는 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);
//enum 클래스 안에 메소드를 정의할땐 반드시 enum 상수 목록과 메소드 정의 사이에 세미콜론을 넣어야 한다.
fun rgb() = (r * 256 + g) * 256 + b //메서드 정의
}
enum에서도 일반적이 클래스와 마찬가지로 생성자와 프로퍼티를 선언한다.
각 enum 상수를 정의할 때는 그 상수에 해당하는 프로퍼티 값을 지정해야만한다.
코틀린에서는 거의 유일하게 세미콜론이 필수 인 경우가 있는데 바로 enum 클래스 안에 메소드를 정의할땐 반드시 enum 상수 목록과 메소드 정의 사이에 세미콜론을 넣어야 한다.
when
when은 자바의 switch를 대치하되 더 강력하며, 자주 사용되는 프로그래밍 요소이다.
자바의 switch와 달리 when 절에서는 각 분기의 끝에 break를 넣지 않아도 된다
//when을 통해 올바른 enum값 찾기
fun getMnemonic(color: Color) = // 함수의 반환값으로 when식을 바로 사용
when (color) {
Color.RED -> "Richard"
Color.ORANGE -> "Of"
Color.YELLOW -> "York"
Color.GREEN -> "Gave"
Color.BLUE -> "Battle"
Color.INDIGO -> "In"
Color.VIOLET -> "Vain"
}
//when 분기 안에 여러 값 사용
fun getMnemonic(color: Color) =
when (color) {
Color.RED, Color.ORANGE, Color.YELLOW -> "warm"
Color.GREEN, Color.BLUE, Color.INDIGO -> "In"
Color.VIOLET -> "cold"
}
분기 조건에 상수만을 사용할 수 있는 자바 switch와 달리 코틀린 when의 분기 조건은 임의의 객체를 허용한다.
예제는 아래와 같다.
//1번 함수: when 조건에 여러 다른 객체 사용
fun mix(c1: Color, c2: Color) =
when (setOf(c1, c2)) {
setOf(Color.RED, Color.YELLOW) -> Color.ORANGE
setOf(Color.YELLOW, Color.BLUE) -> Color.GREEN
setOf(Color.BLUE, Color.VIOLET) -> Color.INDIGO
else -> throw Exception("Dirty Color")
}
println(mix(Color.BLUE, Color.YELLOW))
// GREEN
//2번 함수: 인자없는 when 사용
fun mixOptimized(c1: Color, c2: Color) =
when {
(c1 == RED && c2 == YELLOW) ||
(c1 == YELLOW && 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")
}
1번 함수는 인자로 전달받은 여러 객체를 포함하는 집합인 Set객체로 만든 setOf라는 함수를 사용한다.
set은 원소가 모여 있는 컬렉션으로 원소의 순서는 중요하지않다. setOf(RED, YELLOW)가 아닌 setOf(YELLOW, RED)가 같다는 말이다.
편하긴 하지만 약간 비효율적이다.
호출될 때마다 함수 인자로 주어지는 두색이 when 분기 조건에 있는 다른 색과 같은지 비교하기위해 여러 set인스턴스를 생성할 것이다. 보통은 이런 비효율성이 크게 문제 되지 않지만 1번 함수가 자주 호출된다면 불필요한 가비지 객체가 늘어나는 것을 방지하기 위해 고치는 편이 좋다.
2번 함수는 1번의 함수를 보완한다.
인자가 없는 when식을 사용하면 추가객체를 만들지 않는다는 장점이 있지만 가독성이 떨어지는 단점이 있다.
스마트 캐스트: 타입 검사와 타입 캐스트를 조합
interface Expr
class Num (val value: Int) : Expr
// value라는 프로퍼티만 존재하는 단순한 클래스로 Expr 인터페이스를 구현
class Sum (val left: Expr, val right: Expr) : Expr
// Expr 타입의 객체라면 어떤 것이나 Sum함수의 인자가 될 수 있다. Num이나 다른 Sum이 인자로 올 수 있다.
fun eval(e: Expr): Int {
if(e is Num) { // 변수타입 검사
val n = e as Num //원하는 타입으로 명시적변경
return n.value
}
if(e is Sum) {
return eval(e.right) + eval(e.left)
}
throw IllegalArgumentException("unknown expression")
}
코틀린에서는 프로그래머 대신 컴파일러가 캐스팅을 해준다. 어떤 변수가 원하는 타입인지 일단 is로 검사하고 나면 굳이 변수를 원하는 타입으로 캐스팅하지 않아도 마치 처음부터 그 변수가 원하는 타입으로 선언된 것처럼 사용할 수 있다.
이를 스마트 캐스트라 부른다.
원하는 타입으로 명시적으로 타입 캐스팅을 하려면 as키워드를 사용한다.
if를 when으로 리팩토링할 수 있다.
//코틀린에서는 if가 값을 만들어내기 때문에 자바와 달리 3항 연산자가 따로 없다.
fun eval(e: Expr): Int =
if (e is Num) {
e.value
} else if (e is Sum) {
eval(e.right) + eval(e.left)
} else {
throw IllegalArgumentException("Unknown expression")
}
//if -> when식으로 변경
fun eval(e: Expr): Int =
when (e) {
is Num -> //인자 타입검사
e.value
is Sum -> //인자 타입검사
eval(e.right) + eval(e.left)
else ->
throw IllegalArgumentException("Unknown expression")
}
이터레이션: while과 for 루프
while루프코틀린에는 while과 do-while이 있다. 두 루프 문법은 자바와 동일하다.
while (조건) {
println("테스트 중")
}
do {
// 무조건 본문을 한번 실행 후 조건이 맞을때까지 본문 반복
} while (조건)
fun main(args: Array<String>) {
for(i in 1..100){ // 1~100 범위의 정수에 대해 반복진행
println(fizzBuzz(i))
}
for (i in 100 downTo 1 step 2) { //100 downTo 1은 역방향 수열
println(fizzBuzz(i))
}
}
fun fizzBuzz(i: Int) = when {
i % 15 == 0 -> "fizzBuzz "
i % 3== 0 -> "fizz "
i % 5 == 0 -> "buzz "
else -> "$i"
}
맵 / 컬렉션에 대한 이터레이션
//맵예제
val binaryReps = TreeMap<Char, String>()
for(c in 'A'..'F') { //A부터 F까지 문자 범위를 사용해서 반복
val binary = Integer.toBinaryString(c.toInt()) //아스키코드를 2진으로
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()) {
println("$index = $element")
}
in으로 원소검사
in 연산자를 사용해 어떤 값이 범위에 속하는지 검사할 수 있다. 반대로 !in을 사용하면 어떤 값이 범위에 속하지 않는지 검사할 수 있다.
fun recognize(c: Char) = when (c) {
in '0'..'9' -> "It's a digit!"
in 'a'..'z', in 'A'..'Z' -> "It's a letter!"
else -> "I don't know…"
}
예외처리
코틀린의 예외처리는 자바나 다른 언어의 예외 처리와 비슷하다. 함수는 정상적으로 종료할 수 있지만 오류가 발생하면 예외를 던질 수 있다.
if(percentage !in 0..100) {
throw IllegalArgumentException ("0~100사이의 percentage여야한다: $percentage")
}
val percentage =
if(number in 0..100)
number
else
throw IllegalArgumentException("0~100사이의 percentage여야한다: $percentage")
//throw는 식이다.
try, catch, finally
자바 코드와 가장 큰 차이는 throws절이 코드에 없다는 점이다. 자바에서는 함수를 작성할 떄 함수 선언 뒤에 throws IOException을 붙여야 한다. IOExption이 체크 예외이기 때문이다.
try를 식으로 사용하기
// 1번
fun readNumber(reader: BufferedReader) {
val number = try {
Integer.parseInt(reader.readLine())
} catch (e: NumberFormatException) {
return //예외가 발생하게 된 경우 다음 코드를 실행하지 않는다.
}
}
// 2번
fun readNumber(reader: BufferedReader) {
val number = try {
Integer.parseInt(reader.readLine())
} catch (e: NumberFormatException) {
null //예외가 발생하면 null값을 사용한다.
}
}