2장에서는 프로그램의 필수 요소인 변수, 함수, 클래스 등을 코틀린에서 선언하는 방법, 프로퍼티 개념, 여러가지 제어구조, 스마트 캐스트, 예외처리 를 살펴본다.
함수
//블록이 본문인 함수
fun max1(a: Int, b: Int): Int{
return if(a > b) a else b
}
- 함수 선언은 fun 키워드로 시작
- fun 키워드 다음에는 함수 이름
- 함수 이름 다음 괄호는 파라미터 목록이 온다
- 함수 반환 타입과 파라미터 목록은 :(콜론) 으로 구분
- 블록으로 함수를 감싸고 있기 때문에, '블록이 본문인 함수'라고 함 (참고로 코틀린에서는 루프를 제외한 대부분의 제어구조가 식)
식이 본문인 함수
//식이 본문인 함수
fun max(a: Int, b: Int): Int = if(a > b) a else b
- 등호와 식으로 이뤄진 함수를 '식이 본문인 함수' 라고 함
//식이 본문인 함수(리턴 타입 생략도 가능, 단 식이 본문인 함수에만 해당 됨)
//코틀린 컴파일러가 본문 식을 분석해 함수 반환 타입을 정해줄 수 있음(타입 추론)
fun max3(a: Int, b: Int) = if(a > b) a else b
- 식이 본문인 함수에서만 리턴 타입 생략도 가능
- 코틀린 컴파일러가 본문 식을 분석해 함수 반환 타입을 정해줄 수 있음 (타입 추론)
변수
코틀린에서는 자바와 다르게 키워드로 변수 선언을 시작하는 대신 변수 이름 뒤에 타입을 명시하거나 생략할 수 있다.
val answer2: Int = 42
- 변수 키워드, 변수명, :(콜론)으로 구분, 타입명시, 그리고 값을 할당 하는 형태
val question = "삶, 우주, 그리고 모든 것에 대한 궁극적인 질문"
val answer = 42
- 타입을 명시하지 않았지만, 컴파일러가 알아서 타입을 추론한다. (question 은 String, answer 는 Int)
여기서 잠깐, 코틀린에서는 초기화 식을 사용하지 않고 변수를 선언하려면 반드시 타입을 명시적으로 선언해야 함.
val answer3: Int
answer3 = 42
- 초기화 식이 없다면 변수에 저장될 값에 대해 아무 정보가 없기 때문에 컴파일러 조차 타입추론이 불가능하다. 따라서 변수에 대한 값 초기화가 없는 경우엔 무조건 타입을 명시적으로 선언하자.
변경 가능한 변수와 변경 불가능한 변수
val (변경 불가능한 변수)
- val 로 선언된 변수는 일단 초기화하고 나면 재대입이 불가능함. 자바로 치면, final 변수라 볼 수 있음
- val 변수는 여러번 초기화 불가능, 딱 한 번만 초기화해야 함
val languages = arrayListOf("Java")
languages.add("Kotlin")
- val 참조 자체는 불변일지라도 그 참조가 가르키는 객체의 내부 값의 변경은 가능하다.
- languages 변경 불가능한 변수에 list 객체를 참조하게 하고, 참조가 가르키는 객체를 add를 통해 내부를 변경해도 잘 동작한다.
var (변경 가능한 변수)
- var 로 선언된 변수는 값 변경이 가능한 함수이며, 자바의 일반 변수에 해당함
var answer = 42
answer = "no answer" //Error Type mismatch (컴파일 오류)
- var 키워드를 사용하면 값 변경은 가능하지만, 타입 변경은 불가능 하다
코틀린에서는 기본적으로 val 변수를 사용하고, 필요시에만 var 변수를 사용한다. 이러한 이유는 변경 가능한 var 변수를 많이 허용하면 불필요한 부수효과(부작용)를 야기할 수 있기 때문, 또한 변경 불가능한 변수를 사용해야 함수형 코드 구현이 가능함
문자열 템플릿
문자열 템플릿 기능은 변수를 문자열 안에 사용할 수 있도록 해준다. 문자열 리터럴의 필요한 곳에 변수를 넣되 앞에 $ 를 추가해주면 된다.
fun main(args:Array<String>){
val name = if(args.size > 0) args[0] else "kotlin"
println("Hello, $name!")
if(args.size > 0){
println("Hello, ${args[0]}!")
}
println("Hello, ${if(args.size > 0) args[0] else "someone"}!")
}
- 컴파일 시점에 문자열안에 사용한 변수가 존재하지 않는 변수면 컴파일 오류 발생
- 특수문자를 사용하고 싶으면 '\' 를 사용해 이스케이프 시킬 수 있음
- 식 자체를 중괄호 {} 로 감싸서 문자열 템플릿으로 사용 할 수 있음
클래스
우선 일반적인 자바 클래스 모습
public class Person {
private final String name;
public Person(String name){
this.name = name;
}
public String getName(){
return name;
}
}
name 말고 다른 필드가 생기면, 생성자에서 인자를 받는 부분 및 코드도 수정해줘야 한다. 물론 lombok을 사용하면 직접 수정할 필요는 없지만, 코틀린은 lombok 도움 없이도 자동으로 더 스마트하게 심플하게 만들어준다.
코틀린으로 변환한 모습
class Person(val name: String)
- 엄청나게 심플해졌다...
- 이런 유형의 클래스(코드가 없이 데이터만 저장하는 클래스)를 '값 객체' 라고 부른다.
- 코틀린에서는 public 키워드를 생략해도 무방하다.
프로퍼티
자바에서는 필드 + 접근자를 한데 묶어 프로퍼티라고 부르며, 코틀린 프로퍼티는 자바의 필드와 접근자 메서드를 완전시 대신할 수 있다.
fun main(arg:Array<String>){
val person = Person("limky", true) //new 키워드 사용하지 않아도 된다.
println(person.name) // 프로퍼티 이름을 직접 사용해도 알아서 getter 호출해준다.
println(person.isMarried) // 프로퍼티 이름을 직접 사용해도 알아서 getter 호출해준다.
}
class Person(
val name: String, //변경 불가능한 변수
var isMarried: Boolean //변경 가능한 변수
)
- name은 읽기 전용 프로퍼티로, 코틀린은 (비공개) 필드와 필드를 읽는 단순한 (공개) getter 를 만들어낸다.
- isMarried는 쓰기가 가능한 프로퍼티로, 코틀린은 (비공개) 필드와 (공개) getter, (공개) setter 를 만들어낸다.
- new 키워드 없이 객체를 생성할 수 있음
- 프로퍼티 이름을 직접 사용하면, 알아서 getter 를 호출해줌
커스텀 접근자
프로퍼티 접근자를 직접 작성하는 방법도 있다.
fun main(arg:Array<String>){
val rectangle = Rectangle(10, 10)
println(rectangle.isSquare)
}
class Rectangle(val h:Int, val w:Int){
val isSquare: Boolean
get() = h == w //프로퍼티 게터 직접 구현
}
- isSquare 프로퍼티에는 자체 값을 저장하는 필드가 필요 없다.
- 프로퍼티에 접근할 때 마다 게터가 프로퍼티 값을 매번 다시 계산한다.
디렉터리와 패키지
자바와 동일하며, 굳이 다른점이 있다면 자바는 클래스별로 파일을 구성하나, 코틀린에서는 한파일에 여러 클래스를 넣을 순 있다. 하지만 자바와 코틀린을 함께 사용하는 프로젝트에서는 자바 방식을 따르는게 중요함.
enum과 when
코틀린에서도 enum이 있다. (자바와 크게 다르지 않아 보임..)
fun main(arg:Array<String>){
println(Color.BLUE.rgb())
}
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 안에서 메서드 정의 가능
}
- class 키워드 앞에 enum 소프트 키워드를 붙여서 선언 할 수 있음
- 값을 열거할 수 있음
- enum 안에 프로퍼티나 메서드를 정의할 수 있음
when은 자바에서 switch 문과 같으며, if와 마찬가지로 값을 만들어 내는 식이다.
fun getMnemonic(color: Color) =
when (color) {
Color.RED->"Richard"
Color.ORANGE->"Of"
Color.YELLOW->"York"
Color.GREEN->"Gave"
Color.BLUE->"Battle"
Color.INDIGO->"In"
Color.VIOLET->"Vain"
}
- 자바와 달리 분기의 끝에 break를 넣지 않아도 된다.(break 누락으로 인한 오류 가능성 제로)
- 성공적으로 매치되는 분기를 찾으면 그 분기를 실행
import ch02.Color.*
fun getWarmth(color: Color) =
when (color) {
RED, ORANGE, YELLOW->"warm"
GREEN -> "neutral"
BLUE, INDIGO, VIOLET->"cold"
}
- 한 분기 안에서 여러 값을 매치 패턴으로 사용 가능 (,콤마로 구분)
- enum 클래스를 import 하면, import한 enum 상수를 이름만으로 사용할 수 있음
fun mix(c1: Color, c2: Color) =
when (setOf(c1, c2)){ //인자로 받은 객체가 각 분기 조건에 있는 개체와 같은지 테스트
setOf(RED, YELLOW) -> ORANGE //두 색을 혼합해서 다른 색을 만들 수 있는 경우를 열거
setOf(YELLOW, BLUE) -> GREEN
setOf(BLUE, VIOLET) -> INDIGO
else -> throw Exception("Dirty color") //매치되지 않으면 exception 발생
}
- 분기조건에 상수 값만 사용할 수 있는 자바와 달리 코틀린에서는 분기 조건에 임의의 객체를 허용할 수 있다.
- 코틀린 표준 라이브러리에는 인자로 전달받은 여러 객체를 그 객체들을 포함하는 집합인 Set 객체로 만드는 setOf 라는 함수가 있음
- when 분기 조건 부분에 식을 넣을 수 있기 때문에 많은 경우 코드를 간결하게 작성할 수 있음
- set 컬렉션을 사용했기 때문에 순서에 상관이 없다.
- 단, 위 함수가 자주 호출 될 수록, set 인스턴스가 생성되기 때문에 가비지 객체가 늘어나는 부분은 성능적인 측면에서 아쉽다.
인자로 넘어온 객체를 가지고 set 컬렉션을 구성하지 않고, 인자 없이 처리하여 성능부분에 대한 이슈를 해결해보자
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")
}
- when 에 인자가 없으려면 각 분기의 조건이 boolean 결과를 계산하는 식이어야 함
- 가독성은 떨어지나, 성능적인 부분에 대한 이슈를 해결했음.
스마트 캐스트 (타입 검사와 타입 캐스트를 조합)
코틀린에서는 is를 사용해 변수 타입을 검사한다. is 검사는 자바의 instanceof와 비슷하지만, 자바에서 변수의 타입을 instanceof로 확인한 다음에 그 타입에 속한 멤버에 접근하기 위해 명시적으로 변수 타입을 캐스팅해야 한다. 하지만 코틀린에서는 컴파일러가 이를 알아서 해준다.
interface Expr
class Num (val value: Int) : Expr
class Sum (val left:Expr, val right:Expr) : Expr
fun eval(e: Expr): Int {
if (e is Num) { //is 검사와 동시에 e 타입을 추론함
val n = e as Num //명시적으로 Num 타입으로 명시하지만 이는 불필요한 중복처리
return n.value
}
if (e is Sum) {
return eval(e.right) + eval(e.left)
}
throw IllegalArgumentException("Unknown expression")
}
- is 검사와 동시에 e 인자의 타입을 추론함
- is 검사를 거치면, e as Num 과 같이 명시적으로 타입을 정의할 필요없이 이미 e 는 Num 클래스 타입으로 타입추론 처리됨
- 스마트캐스트를 사용한다면, 프로퍼티는 val 이어야 하며 커스컴 접근자를 사용하면 안된다.
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의 분기에 식이 하나밖에 없다면 중괄호 생략가능
- if분기에 블록을 사용하는 경우 그 블록 마지막 식이 해당 분기의 결과 값임
when 식을 앞에서 살펴본 값 동등성 검사가 아닌 다른 기능에도 쓸 수 있는데, 바로 타입검사를 하는 when 분기 처리이다.
fun eval(e: Expr): Int =
when (e) {
is Num -> //인자 타입을 검사하는 when 분기
e.value //스마트 캐스트 발동
is Sum ->
eval(e.right) + eval(e.left)
else ->
throw IllegalArgumentException("Unknown expression")
}
- if 문 내 is 검사 처럼 when 분기 처리시 타입 검사를 진행하고, 스마트 캐스트까지 발동됨을 확인 할 수 있다.
이터레이션 (while 과 for 루프)
코틀린의 while 문은 자바와 동일하다.
단 코틀린에서 for 문은 자바의 for-each 루프 형태만 존재한다.
fun main(args: Array<String>) {
for(i in 1..100){ //1 ~ 100까지 증가
print(fizzBuzz(i))
}
for (i in 100 downTo 1 step 2) { //100 ~ 1까지 2개씩 감소
print(fizzBuzz(i))
}
}
fun fizzBuzz(i: Int) = when{
i % 15 == 0 -> "FizzBuzz"
i % 3 == 0 -> "Fizz"
i % 5 == 0 -> "Buzz"
else -> "$i"
}
- in 1..100 : 1 ~ 100까지 증가
- 역으로 감소 하고 싶다면, downTo 사용
- 증가폭 조절을 위해 step 사용
in으로 컬렉션이나 범위의 원소 검사
fun main(arg:Array<String>){
println(isLetter('q')) //결과 true
println(isNotDigit('x'))//결과 true
println(recognize('3'))//결과 It's a digit!
}
fun isLetter(c:Char) = c in 'a'..'z' || c in 'A'..'Z'
fun isNotDigit(c:Char) = c !in '0'..'9'
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…"
}
코틀린 예외처리
자바와 마찬가지로 try,catch,finally를 사용할 수 있고 큰 차이는 없음.
자바와 가장 큰 차이는 throws 절이 코드에 없다는 점
코틀린에서는 함수가 던지는 예외를 지정하지 않고 발생한 예외를 잡아도 되고 잡아내지 않아도 된다.
kotlin에서의 try는 if나 when과 마찬가지로 식이다.
따라서 try 값을 변수에 할당하는 것이 가능하다.
댓글