본문 바로가기
Kotlin in Action

2. kotlin 기본요소, 스마트 캐스트, 예외처리 등

by 히예네 2024. 7. 13.
728x90
반응형

1. 기본 요소 : 함수와 변수

(1) 함수

a. 코틀린 문법의 특성

fun main(args: Array<String>){
    println("Hello world")
}
  • fun 키워드로 함수를 선언한다.
  • 파라미터는 (파라미터 변수명: 타입) 로 선언한다.
  • 함수를 최상위 수준에서 정의할 수 있다.
  • 배열도 일반적인 클래스와 같다. 자바처럼 배열처리를 해줄 필요가 없다.
  • println()같은 래퍼(Wrapper)를 제공한다.
    • Wrapper : 여러가지 표준 자바 라이브러리 함수를 간결하게 사용할 수 있게 코틀린 표준 라이브러리에서 감싸준다.
      • System.out.println → println
  • ; (세미클론) 붙이지 않아도 된다.

b. 함수

fun main(args: Array<String>){
    println(max(1,2))
}

fun max(a: Int, b: Int) : Int {
    return if (a > b) a else b 
    // 코틀린에서 if는 식이라서 값을 만들어내며, 그래서 리턴값에 들어갈 수 있다.
}
  • 코틀린에서 if는 식(expression)이지 문(장)(statement)이 아니다.
    • 식 : 값을 만들어내며, 다른 식의 하위요소로 계산에 참여할 수 있다. (리턴값에 대입가능, 식이니까!)
    • 문 : 자신을 둘러싸고 있는 가장 안쪽 블록의 최상위 요소, 아무런 값을 만들어내지못한다.
      • if를 문으로 쓰면 이렇게 된다. 조건에 따라 a,b를 선택 한 것일뿐 만들어낸게 아니다.
      if (a > b) {
          a // a가 b보다 크면 a를 그대로 사용
      } else {
          b // 그렇지 않으면 b를 사용
      }
      
  • 식이 본문인 함수
    • 등호를 제거했다.
    • 식이 본문인 함수에서만! 반환타입인 Int도 생략 가능하다.
      • 코틀린은 정적타입 언어 → 컴파일 단계에서 타입추론을 해주기 때문에!
//식이 본문인 함수
fun max(a: Int, b: Int) : Int = if (a > b) a else b

//블록이 본문인 함수
fun max(a: Int, b: Int) : Int {
    return if (a > b) a else b 
}

(2) 변수

a. 변경 가능한 변수와 변경 불가능한 변수

<aside> 📌 권장사항 기본적으로 모든 변수를 val 키워드를 사용해 불변 변수를 선언하자. 필요시 var로 변경. → 변경 불가능한 참조와 변경 불가능한 객체를 부수효과가 없는 함수와 조합해 사용하면 코드가 함수형코드에 가까워진다.

</aside>

  • 변수 선언 키워드
    • var : 변경 가능한 참조. 자바의 일반변수에 해당.
    • val : 변경 불가능한 참조. 자바의 final 변수에 해당. 초기화 후 재대입 불가.
      • val로 선언했어도, 그 참조가 가르키는 객체의 내부값은 변경될 수 있다.
      fun main(args: Array<String>){
          val language = arrayListOf("java") //불변 참조를 선언
          language.add("kotlin") //참조를 가리키는 객체 내부를 변경한다. 
          print(language) // [java, kotlin]
      }
      

b. 문자열 템플릿

  • 문자열 리터럴 안에 변수를 넣을 수 있다. ${변수}
    • 자바의 문자열 접합 연산과 동일한 기능 , 좀 더 효율적이다.
    • 코틀린은 정적언어, 즉 컴파일 단계에서 해당 변수가 파악되지 않으면 에러가 난다.
      • 컴파일된 코드는 StringBuilder를사용하고, 문자열 상수와 변수의 값을 append로 문자열 빌더뒤에 추가한다.
fun main(args: Array<String>) {
    val name = if (args.size > 0) args[0] else "Kotlin"
    print("Hello ${name}")
}

2. 클래스와 프로퍼티

자바 코드로 간단한 Person class를 만들어보자.

//JAVA
public class Person {
    private final String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

코틀린으로 변환하면?

  • 필드 대입 로직을 훨씬 더 적은 코드로 작성가능하다.
    • 이런 유형의 클래스를 값 객체라 부른다
      • 값 객체 (value object) : 코드 없이 데이터만 저장하는 클래스
//Kotlin - 1줄로 끝난다. 
class Person(val name: String)

(1) 프로퍼티

<aside> 📌 클래스를 만드는 이유는? 데이터를 캡슐화하고, 캡슐화한 데이터를 다루는 코드를 한 class 파일 아래 가두는것이다. 클래스는 자신을 사용하는 개발자가 그 데이터에 접근하는 통로로써 접근자 메소드를 제공한다. (getter, setter)

</aside>

a. 프로퍼티란?

  • 코틀린 프로퍼티는 자바의 필드와 접근자 메소드를 완전히 대신한다.
    • var, val로 선언한다.
class Person(
    val name: String, // 읽기전용 프로퍼티 : private 필드와 필드를 읽는 단순한 public getter를 만들어낸다.
    var isMarried: Boolean // 쓸 수 있는 프로퍼티 : private 필드와 public  getter/setter를 만들어낸다.
)
  • 게터와 세터의 예외 규칙
    • getter : 필드를 읽게 해줌
    • setter : 필드 변경을 허용하게 해줌
    • 자바
    • //getisMarried가 아니다. System.out.println(person.isMarried); //setter를 이용할 땐 is를 빼고 setMarried()메소드를 사용한다. person.setMarried(false)
    • 코틀린
    • println(person.isMarried) //자바와 달리 코틀린은 대입. 직관적이다. person.isMarried = false

b. 커스텀 접근자

  • 대부분의 프로퍼티에는 그 프로퍼티의 값을 저장하기 위한 필드가 있다.
    • 이를 프로퍼티를 뒷받침하는 필드(backing field)라고 부른다.
    • 커스텀 게터 : 하지만 커스텀 게터를 만들면, 프로퍼티값을 그때그때 계산할 수 있다.
      • isSquare 프로퍼티에는 자체 값을 저장하는 필드가 필요없다. 자체 구현을 제공하는 게터만 존재한다.
        • 개발자가 접근 할 때 마다 게터가 프로퍼티 값을 매번 다시 계산한다.
    • class Rectangle(val height: Int, val width: Int) { val isSquare: Boolean get() { //프로퍼티 게터 선언 return height == width } }

c. 코틀린 소스코드 구조 : 디렉터리와 패키지

  • 자바 : 모든 클래스를 패키지 단위로 관리한다.
  • 코틀린 : 모든 코틀린 파일 맨 앞에 package 문을 넣을 수 있다.
    • 그 파일안에 들어있는 클래스, 함수, 프로퍼트 등등이 패키지 안에 들어간다.
    • 같은 패키지안에 있다면 다른 파일에서 정의한 선언일지라도 직접 사용할 수 있다.
      • 다른 패키지 안에 있으면 import로 가져 올 수 있다.

(2) 선택 표현과 처리 : enum과 when

a. enum 클래스 정의

enum class Color {
    RED, ORAGNE, YELLOW, GREEN, BLUE, INDIGO, VIOLET,
}
  • 자바 선언보다 코틀린 선언에 더 많은 키워드를 써야하는 흔치 않은 예
    • 코틀린은 enum class이고 자바에서는 enum을 사용한다.
  • enum은 class 앞에 있을때만 특별한 의미를 지닌다. (다른곳에서는 이름으로 사용가능) → 소프트 키워드 특징 : class 앞이 아니면 다른곳에서도 쓸수있다.
    • 반면, class는 키워드라서 다른곳에서 사용불가
  • 프로퍼티, 메소드도 정의 가능하다.
    • 코틀린에서 유일하게 ; 찍는 곳 : enum class에서 프로퍼티와 메소드 사이에 구분자로 찍어준다.
enum class Colors(val r: Int, val g: Int, val b: Int) {
    RED(255, 0, 0), ORAGNE(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
}

fun main(){
    println(Colors.BLUE.rgb())
    println(Colors.GREEN.rgb())
}

b. when으로 enum 클래스 다루기

  • when도 if와 마찬가지로 값을 만들어내는 식이다.
fun main() {
    println(getMnemonic(Colors.RED)) //Richard
}

fun getMnemonic(color: Colors) =
    when (color) {
        Colors.RED -> "Richard"
        Colors.ORAGNE -> "Of"
        Colors.YELLOW -> "York"
        Colors.GREEN -> "Gave"
        Colors.BLUE -> "Battle"
        Colors.INDIGO -> "In"
        Colors.VIOLET -> "Vain"
    }
  • 한 when 분기안에 여러 값 사용하기
fun getWarmth(color: Colors) =
    when (color) {
        Colors.RED, Colors.ORAGNE, Colors.YELLOW -> "warm"
        Colors.GREEN, Colors.BLUE, Colors.INDIGO -> "neutral"
        Colors.VIOLET -> "cold"
    }
  • 다른 class에서 import해서 가져오기
//ImportColorsTest class를 따로 만들어서 실행
import com.anehta.athenakotlinlab.Colors.*

fun getWarmth2(colors: Colors) = when (colors) {
    RED, ORAGNE, YELLOW -> "warm" //import한 상수를 이름만으로 사용가능
    GREEN, BLUE -> "neutral"
    INDIGO, VIOLET -> "cold"
}

c. when과 임의의 객체를 함께 사용

  • 자바의 switch 분기조건에는 enum상수나 숫자리터럴만 사용할 수 있지만, 코틀린 when의 분기조건은 임의의 객체를 허용한다.
//두 색을 혼합했을 때 미리 정해지 팔레트에 들어있는 색이 될 수 있는지 알려주는 함수
fun mix(c1: Colors, c2: Colors) =
    when (setOf(c1,c2)) {
        setOf(RED, YELLOW) -> ORAGNE
        setOf(YELLOW, BLUE) -> GREEN
        setOf(BLUE, VIOLET) -> INDIGO
        else -> throw Exception("Dirty color")
    }

fun main() {
    println(mix(RED,YELLOW))
}
  • setOf() 기능 : 인자로 전달 받은 여러 객체를, 그 객체들을 포함하는 집합인 Set 객체로 만들어준다.
    • ex : setOf(RED, YELLOW)은 RED와 YELLOW로 구성된 Set 컬렉션을 만든다. 순서는 중요하지 않음.
  • setOf(c1,c2)과 i. setOf(RED, YELLOW)을 비교하고 아니면 다음 분기로 넘어간다.
    • setOf(c1,c2)와 ii. setOf(YELLOW, BLUE)를 비교하고 아니면 다음 분기로 넘어간다…. 계속….
  • 하지만! 이 방법은 굉장히 비효율적이다. 두 색을 비교하기 위해 Set 인스턴스를 생성한다. 가비지 객체가 늘어난다.

d. 인자 없는 when 사용

  • 인자가 없으면 불필요한 객체 생성을 막을 수 있다. 하지만 코드를 읽기 어려워질 수도 있다.
  • when에 아무 인자도 없으려면 각 분기의 조건이 불리언 결과를 계산하는 식이어야한다.
  • fun mix 처럼 추가객체를 만들지 않지만, 가독성이 떨어진다.
fun mixOptimized(c1: Colors, c2: Colors) =
    when {
        (c1 == RED && c2 == YELLOW) || (c1 == YELLOW && c2 == RED) -> ORAGNE
        (c1 == BLUE && c2 == YELLOW) || (c1 == BLUE && c2 == YELLOW) -> GREEN
        (c1 == BLUE && c2 == VIOLET) || (c1 == VIOLET && c2 == BLUE) -> INDIGO
        else -> throw Exception("Dirty color")
    }

(3) 스마트 캐스트 : 타입 검사와 타입 캐스트를 조합

a. (num1 + num2) + num4 와 같은 간단한 산술식을 계산하는 함수를 만들자.

식을 트리 구조로 저장하자. SUM{ (NUM(1)+NUM(2)) + NUM(4) }

interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr
// Expr` 인터페이스는 아무 메소드도 선언하지 않으며, 
// 단지 여러 타입의 식 객체를 아우르는 **공통 타입 역할**만 수행한다.
fun eval(e: Expr): Int {
    if (e is Num) { // is로 Num 인지 검사한 다음부터 컴파일러는 e를 Num으로 해석한다. 
									  // 그래서 Num에 있는 value를 명시적 캐스팅없이 e.value로 바로 사용 가능하다. 
        val n = e as Num
        return n.value
    }
    if (e is Sum) {
        return eval(e.right) + eval(e.left)
    }
    throw IllegalArgumentException("Unknown Expression")
}
  • 코틀린에서는 프로그래머 대신 컴파일러가 캐스팅 해준다.
    • 자바에서는 어떤 변수의 타입을 instanceof로 확인한 다음, 그 타입에 속한 멤버에 접근하기 위해서는 명시적으로 변수 타입을 캐스팅해야한다. 이런 멤버접근을 여러번 수행해야한다면, 변수에 따로 캐스팅한 결과를 저장한 후 사용해야한다.
  • SmartCastIDE는 스마트캐스트라는것을 배경색깔로 표시해준다.
    • 어떤 변수가 원하는 타입인지 일단 is로 검사하고 나면 굳이 변수를 원하는 타입으로 캐스팅하지 않아도 마치 처음부터 그 변수가 원하는 타입으로 선언된 것처럼 사용할 수있다. (실제로는 컴파일러가 캐스팅을 수행함)
    • 스마트 캐스트는 is로 변수에 든 값의 타입을 검사한 다음에 그 값이 바뀔 수없는 경우에만 작동한다.
      • 즉, Num에 들어있는 value는 val로 선언해야한다.
      • 커스텀 접근자도 불가. (항상 같은 값을 내놓지 않는다.)
    • 명시적 캐스팅은 as 키워드를 사용한다.

b. (num1 + num2) + num4를 좀 더 코틀린스럽게! if를 when으로!

  • 코틀린 if와 자바 if의 차이점
    • 코틀린 if (a > b) a else b 와 자바 삼항연산자 int result = (a > b) ? a : b; 는 같다.
    • 코틀린의 if는 식이므로 삼항연산자가 따로 없다.
fun eval(e: Expr): Int =
    if (e is Num) { //is로 Num 타입인지 확인 한 이후로 컴파일러는 e를 Num타입으로 해석한다.
        e.value //
    } else if (e is Sum) {
        eval(e.left) + eval(e.right)
    } else {
        throw IllegalArgumentException("Unknown Expression")
    }
// 좀 더 코틀린스럽게 when을 이용하자.
fun eval(e: Expr): Int =
    when (e) {
        is Num -> e.value
        is Sum -> eval(e.left) + eval(e.right)
        else -> throw IllegalArgumentException("Unknown Expression")
    }

c. if와 when의 분기에서 블록 사용

  • eval 함수에 로그를 추가해보자. 각 분기는 블록처리 후, 블록의 맨 마지막에 그 분기의 결과값을 위치시키면 된다.
fun main(){
    println(evalWithLogging(Sum(Sum(Num(1),Num(2)),Num(4))))
}

num : 1
num : 2
sum : 3
num : 4
sum : 7
7

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 IllegalStateException("Unknown Expression")
    }
  • 식이 본문인 함수는 블록을 가질 수 없다.
  • 블록이 본문인 함수는 리턴값이 반드시 필요하다.

(4) 이터레이션 Iteration : while과 for 루프

  • 이터레이션 : 결과를 내기위해 프로세스를 반복하는 작업. “그 문장의 블록은 이터레이션 되고 있다” 말한다.

a. while , do-while 루프

while (조건) {
본문 // 조건이 참인 동안 반복실행
}
--------------------
do {
본문
}
while (조건) {
} //일단 무조건 1회는 본문을 실행한다. 그 뒤에 조건이 맞으면 반복실행.

b. 수에 대한 이터레이션 : 범위와 수열

  • 자바의 for 루프 (int i=0;i<=10;i++)을 사용하고, 코틀린에서는 범위를 사용한다.
  • 코틀린 폐구간은 .. 연산자로 나타낸다. val oneToTen = 1..10
  • 어떤 범위에 속한 값을, 일정한 순서대로 이터레이션 하는 것을 수열이라한다.

<aside> 💡 피즈버즈게임

  • 3으로 나누어 떨어지는 숫자는 “피즈”
  • 5로 나누어 떨어지는 숫자는 “버즈”
  • 3,5 둘다 나누어 떨어지는 숫자는 “피즈버즈”를 외친다.

</aside>

fun main() {
    for (i in 1..10) {
        println(fizzBuzz(i))
    }
    println("--------")
    for (i in 30 downTo 1 step 2) {
        println(fizzBuzz(i))
    }
}

fun fizzBuzz(i: Int) = when {
    i % 15 == 0 -> "FizzBuzz"
    i % 3 == 0 -> "Fizz"
    i % 5 == 0 -> "Buzz"
    else -> "${i}" // 셋 다 아니면 그 수 자체를 반환한다.
}

//출력값
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
--------
FizzBuzz
28
26
Fizz
22
Buzz
Fizz
16
14
Fizz
Buzz
8
Fizz
4
2

c. 맵에 대한 이터레이션

fun main() {
    val binaryReps = TreeMap<Char, String>()

    for (c in 'A'..'F') { //문자의 범위를 사용해 이터레이션한다.
        val binary = Integer.toBinaryString(c.toInt())
        //정수를 2진으로 바꿔준다.
        binaryReps[c] = binary
    }

    for ((letter, binary) in binaryReps) {
        println("${letter} = ${binary}")
    }
}

// 출력값
A = 1000001
B = 1000010
C = 1000011
D = 1000100
E = 1000101
F = 1000110

d. in으로 컬렉션이나 범위의 원소 검사

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
fun recognize(c: Char) = when (c) {
    in '0'..'9' -> "It's digit!"
    in 'a'..'z', in 'A'..'Z' -> "It's a letter!"
    else -> "I don't know!"
}
println(recognize('C')) //It's a letter!
  • 비교 가능한 클래스라면(java.lang.Comparable 인터페이스를 구현한 클래스), 그 클래스의 인스턴스객체를 사용해 범위를 만들 수 있다.

(5) 코틀린의 예외 처리

  • 함수는 정상적으로 종료할 수 있지만, 오류가 발생하면 예외(throw)를 던질 수 있다.
    • 함수를 호출하는쪽에서 예외를 잡아 처리한다.
    • 만약 발생한 예외를 함수호출단에서 처리하지 않으면..?
      • 함수 호출 스택을 거슬러 올라가면서 예외를 처리하는 부분이 나올때까지 다시 던진다. (rethrow)
val percentage = if (number in 0..100) number else throw IllegalArgumentException("sorry")
//조건에 들어가면 초기화, 아니면 예외를 던진다.

a. try, catch, finally

fun readNumber(reader: BufferedReader): Int? {
    try {
        val line = reader.readLine() // 1.입력받는다
        return Integer.parseInt(line) // 2.입력값을 정수로 바꿔서 리턴한다.
    } catch (e: NumberFormatException) {
        return null //이때 예외가 발생하면 catch 블록이 실행된다. null을 리턴한다.
    } finally {
        reader.close() //항상실행하는 블록. 예외가 발생해도 실행된다. BufferedReader를 닫아준다.
    }
}
  • 자바처럼 throws 절 코드가 없다.
    • 자바는 throws IOException을 붙여야한다.
    • IOException : 체크 예외, 자바에서는 체크예외를 명시적으로 처리해야한다.
  • 코틀린은 체크예외와 언체크예외를 구별하지 않는다. (최신 JVM 언어 특징)

b. try를 식으로 이용해서 코틀린스럽게!

fun main() {
    val reader = BufferedReader(StringReader("333333"))
    readNumber(reader) //333333

    val reader2 = BufferedReader(StringReader("hello"))
    readNumber(reader2) //null

}

fun readNumber(reader: BufferedReader) {
    val number = try {
        Integer.parseInt(reader.readLine())
    } catch (e: NumberFormatException) {
        null // 예외처리로 여겨지면 null을 리턴한다.
    }
    println(number)
}

</aside>

1. 기본 요소 : 함수와 변수

(1) 함수

a. 코틀린 문법의 특성

fun main(args: Array<String>){
    println("Hello world")
}
  • fun 키워드로 함수를 선언한다.
  • 파라미터는 (파라미터 변수명: 타입) 로 선언한다.
  • 함수를 최상위 수준에서 정의할 수 있다.
  • 배열도 일반적인 클래스와 같다. 자바처럼 배열처리를 해줄 필요가 없다.
  • println()같은 래퍼(Wrapper)를 제공한다.
    • Wrapper : 여러가지 표준 자바 라이브러리 함수를 간결하게 사용할 수 있게 코틀린 표준 라이브러리에서 감싸준다.
      • System.out.println → println
  • ; (세미클론) 붙이지 않아도 된다.

b. 함수

fun main(args: Array<String>){
    println(max(1,2))
}

fun max(a: Int, b: Int) : Int {
    return if (a > b) a else b 
    // 코틀린에서 if는 식이라서 값을 만들어내며, 그래서 리턴값에 들어갈 수 있다.
}
  • 코틀린에서 if는 식(expression)이지 문(장)(statement)이 아니다.
    • 식 : 값을 만들어내며, 다른 식의 하위요소로 계산에 참여할 수 있다. (리턴값에 대입가능, 식이니까!)
    • 문 : 자신을 둘러싸고 있는 가장 안쪽 블록의 최상위 요소, 아무런 값을 만들어내지못한다.
      • if를 문으로 쓰면 이렇게 된다. 조건에 따라 a,b를 선택 한 것일뿐 만들어낸게 아니다.
      if (a > b) {
          a // a가 b보다 크면 a를 그대로 사용
      } else {
          b // 그렇지 않으면 b를 사용
      }
      
  • 식이 본문인 함수
    • 등호를 제거했다.
    • 식이 본문인 함수에서만! 반환타입인 Int도 생략 가능하다.
      • 코틀린은 정적타입 언어 → 컴파일 단계에서 타입추론을 해주기 때문에!
//식이 본문인 함수
fun max(a: Int, b: Int) : Int = if (a > b) a else b

//블록이 본문인 함수
fun max(a: Int, b: Int) : Int {
    return if (a > b) a else b 
}

(2) 변수

a. 변경 가능한 변수와 변경 불가능한 변수

<aside> 📌 권장사항 기본적으로 모든 변수를 val 키워드를 사용해 불변 변수를 선언하자. 필요시 var로 변경. → 변경 불가능한 참조와 변경 불가능한 객체를 부수효과가 없는 함수와 조합해 사용하면 코드가 함수형코드에 가까워진다.

</aside>

  • 변수 선언 키워드
    • var : 변경 가능한 참조. 자바의 일반변수에 해당.
    • val : 변경 불가능한 참조. 자바의 final 변수에 해당. 초기화 후 재대입 불가.
      • val로 선언했어도, 그 참조가 가르키는 객체의 내부값은 변경될 수 있다.
      fun main(args: Array<String>){
          val language = arrayListOf("java") //불변 참조를 선언
          language.add("kotlin") //참조를 가리키는 객체 내부를 변경한다. 
          print(language) // [java, kotlin]
      }
      

b. 문자열 템플릿

  • 문자열 리터럴 안에 변수를 넣을 수 있다. ${변수}
    • 자바의 문자열 접합 연산과 동일한 기능 , 좀 더 효율적이다.
    • 코틀린은 정적언어, 즉 컴파일 단계에서 해당 변수가 파악되지 않으면 에러가 난다.
      • 컴파일된 코드는 StringBuilder를사용하고, 문자열 상수와 변수의 값을 append로 문자열 빌더뒤에 추가한다.
fun main(args: Array<String>) {
    val name = if (args.size > 0) args[0] else "Kotlin"
    print("Hello ${name}")
}

2. 클래스와 프로퍼티

자바 코드로 간단한 Person class를 만들어보자.

//JAVA
public class Person {
    private final String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

코틀린으로 변환하면?

  • 필드 대입 로직을 훨씬 더 적은 코드로 작성가능하다.
    • 이런 유형의 클래스를 값 객체라 부른다
      • 값 객체 (value object) : 코드 없이 데이터만 저장하는 클래스
//Kotlin - 1줄로 끝난다. 
class Person(val name: String)

(1) 프로퍼티

<aside> 📌 클래스를 만드는 이유는? 데이터를 캡슐화하고, 캡슐화한 데이터를 다루는 코드를 한 class 파일 아래 가두는것이다. 클래스는 자신을 사용하는 개발자가 그 데이터에 접근하는 통로로써 접근자 메소드를 제공한다. (getter, setter)

</aside>

a. 프로퍼티란?

  • 코틀린 프로퍼티는 자바의 필드와 접근자 메소드를 완전히 대신한다.
    • var, val로 선언한다.
class Person(
    val name: String, // 읽기전용 프로퍼티 : private 필드와 필드를 읽는 단순한 public getter를 만들어낸다.
    var isMarried: Boolean // 쓸 수 있는 프로퍼티 : private 필드와 public  getter/setter를 만들어낸다.
)
  • 게터와 세터의 예외 규칙
    • getter : 필드를 읽게 해줌
    • setter : 필드 변경을 허용하게 해줌
    • 자바
    • //getisMarried가 아니다. System.out.println(person.isMarried); //setter를 이용할 땐 is를 빼고 setMarried()메소드를 사용한다. person.setMarried(false)
    • 코틀린
    • println(person.isMarried) //자바와 달리 코틀린은 대입. 직관적이다. person.isMarried = false

b. 커스텀 접근자

  • 대부분의 프로퍼티에는 그 프로퍼티의 값을 저장하기 위한 필드가 있다.
    • 이를 프로퍼티를 뒷받침하는 필드(backing field)라고 부른다.
    • 커스텀 게터 : 하지만 커스텀 게터를 만들면, 프로퍼티값을 그때그때 계산할 수 있다.
      • isSquare 프로퍼티에는 자체 값을 저장하는 필드가 필요없다. 자체 구현을 제공하는 게터만 존재한다.
        • 개발자가 접근 할 때 마다 게터가 프로퍼티 값을 매번 다시 계산한다.
    • class Rectangle(val height: Int, val width: Int) { val isSquare: Boolean get() { //프로퍼티 게터 선언 return height == width } }

c. 코틀린 소스코드 구조 : 디렉터리와 패키지

  • 자바 : 모든 클래스를 패키지 단위로 관리한다.
  • 코틀린 : 모든 코틀린 파일 맨 앞에 package 문을 넣을 수 있다.
    • 그 파일안에 들어있는 클래스, 함수, 프로퍼트 등등이 패키지 안에 들어간다.
    • 같은 패키지안에 있다면 다른 파일에서 정의한 선언일지라도 직접 사용할 수 있다.
      • 다른 패키지 안에 있으면 import로 가져 올 수 있다.

(2) 선택 표현과 처리 : enum과 when

a. enum 클래스 정의

enum class Color {
    RED, ORAGNE, YELLOW, GREEN, BLUE, INDIGO, VIOLET,
}
  • 자바 선언보다 코틀린 선언에 더 많은 키워드를 써야하는 흔치 않은 예
    • 코틀린은 enum class이고 자바에서는 enum을 사용한다.
  • enum은 class 앞에 있을때만 특별한 의미를 지닌다. (다른곳에서는 이름으로 사용가능) → 소프트 키워드 특징 : class 앞이 아니면 다른곳에서도 쓸수있다.
    • 반면, class는 키워드라서 다른곳에서 사용불가
  • 프로퍼티, 메소드도 정의 가능하다.
    • 코틀린에서 유일하게 ; 찍는 곳 : enum class에서 프로퍼티와 메소드 사이에 구분자로 찍어준다.
enum class Colors(val r: Int, val g: Int, val b: Int) {
    RED(255, 0, 0), ORAGNE(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
}

fun main(){
    println(Colors.BLUE.rgb())
    println(Colors.GREEN.rgb())
}

b. when으로 enum 클래스 다루기

  • when도 if와 마찬가지로 값을 만들어내는 식이다.
fun main() {
    println(getMnemonic(Colors.RED)) //Richard
}

fun getMnemonic(color: Colors) =
    when (color) {
        Colors.RED -> "Richard"
        Colors.ORAGNE -> "Of"
        Colors.YELLOW -> "York"
        Colors.GREEN -> "Gave"
        Colors.BLUE -> "Battle"
        Colors.INDIGO -> "In"
        Colors.VIOLET -> "Vain"
    }
  • 한 when 분기안에 여러 값 사용하기
fun getWarmth(color: Colors) =
    when (color) {
        Colors.RED, Colors.ORAGNE, Colors.YELLOW -> "warm"
        Colors.GREEN, Colors.BLUE, Colors.INDIGO -> "neutral"
        Colors.VIOLET -> "cold"
    }
  • 다른 class에서 import해서 가져오기
//ImportColorsTest class를 따로 만들어서 실행
import com.anehta.athenakotlinlab.Colors.*

fun getWarmth2(colors: Colors) = when (colors) {
    RED, ORAGNE, YELLOW -> "warm" //import한 상수를 이름만으로 사용가능
    GREEN, BLUE -> "neutral"
    INDIGO, VIOLET -> "cold"
}

c. when과 임의의 객체를 함께 사용

  • 자바의 switch 분기조건에는 enum상수나 숫자리터럴만 사용할 수 있지만, 코틀린 when의 분기조건은 임의의 객체를 허용한다.
//두 색을 혼합했을 때 미리 정해지 팔레트에 들어있는 색이 될 수 있는지 알려주는 함수
fun mix(c1: Colors, c2: Colors) =
    when (setOf(c1,c2)) {
        setOf(RED, YELLOW) -> ORAGNE
        setOf(YELLOW, BLUE) -> GREEN
        setOf(BLUE, VIOLET) -> INDIGO
        else -> throw Exception("Dirty color")
    }

fun main() {
    println(mix(RED,YELLOW))
}
  • setOf() 기능 : 인자로 전달 받은 여러 객체를, 그 객체들을 포함하는 집합인 Set 객체로 만들어준다.
    • ex : setOf(RED, YELLOW)은 RED와 YELLOW로 구성된 Set 컬렉션을 만든다. 순서는 중요하지 않음.
  • setOf(c1,c2)과 i. setOf(RED, YELLOW)을 비교하고 아니면 다음 분기로 넘어간다.
    • setOf(c1,c2)와 ii. setOf(YELLOW, BLUE)를 비교하고 아니면 다음 분기로 넘어간다…. 계속….
  • 하지만! 이 방법은 굉장히 비효율적이다. 두 색을 비교하기 위해 Set 인스턴스를 생성한다. 가비지 객체가 늘어난다.

d. 인자 없는 when 사용

  • 인자가 없으면 불필요한 객체 생성을 막을 수 있다. 하지만 코드를 읽기 어려워질 수도 있다.
  • when에 아무 인자도 없으려면 각 분기의 조건이 불리언 결과를 계산하는 식이어야한다.
  • fun mix 처럼 추가객체를 만들지 않지만, 가독성이 떨어진다.
fun mixOptimized(c1: Colors, c2: Colors) =
    when {
        (c1 == RED && c2 == YELLOW) || (c1 == YELLOW && c2 == RED) -> ORAGNE
        (c1 == BLUE && c2 == YELLOW) || (c1 == BLUE && c2 == YELLOW) -> GREEN
        (c1 == BLUE && c2 == VIOLET) || (c1 == VIOLET && c2 == BLUE) -> INDIGO
        else -> throw Exception("Dirty color")
    }

(3) 스마트 캐스트 : 타입 검사와 타입 캐스트를 조합

a. (num1 + num2) + num4 와 같은 간단한 산술식을 계산하는 함수를 만들자.

식을 트리 구조로 저장하자. SUM{ (NUM(1)+NUM(2)) + NUM(4) }

interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr
// Expr` 인터페이스는 아무 메소드도 선언하지 않으며, 
// 단지 여러 타입의 식 객체를 아우르는 **공통 타입 역할**만 수행한다.
fun eval(e: Expr): Int {
    if (e is Num) { // is로 Num 인지 검사한 다음부터 컴파일러는 e를 Num으로 해석한다. 
									  // 그래서 Num에 있는 value를 명시적 캐스팅없이 e.value로 바로 사용 가능하다. 
        val n = e as Num
        return n.value
    }
    if (e is Sum) {
        return eval(e.right) + eval(e.left)
    }
    throw IllegalArgumentException("Unknown Expression")
}
  • 코틀린에서는 프로그래머 대신 컴파일러가 캐스팅 해준다.
    • 자바에서는 어떤 변수의 타입을 instanceof로 확인한 다음, 그 타입에 속한 멤버에 접근하기 위해서는 명시적으로 변수 타입을 캐스팅해야한다. 이런 멤버접근을 여러번 수행해야한다면, 변수에 따로 캐스팅한 결과를 저장한 후 사용해야한다.
  • SmartCastIDE는 스마트캐스트라는것을 배경색깔로 표시해준다.
    • 어떤 변수가 원하는 타입인지 일단 is로 검사하고 나면 굳이 변수를 원하는 타입으로 캐스팅하지 않아도 마치 처음부터 그 변수가 원하는 타입으로 선언된 것처럼 사용할 수있다. (실제로는 컴파일러가 캐스팅을 수행함)
    • 스마트 캐스트는 is로 변수에 든 값의 타입을 검사한 다음에 그 값이 바뀔 수없는 경우에만 작동한다.
      • 즉, Num에 들어있는 value는 val로 선언해야한다.
      • 커스텀 접근자도 불가. (항상 같은 값을 내놓지 않는다.)
    • 명시적 캐스팅은 as 키워드를 사용한다.

b. (num1 + num2) + num4를 좀 더 코틀린스럽게! if를 when으로!

  • 코틀린 if와 자바 if의 차이점
    • 코틀린 if (a > b) a else b 와 자바 삼항연산자 int result = (a > b) ? a : b; 는 같다.
    • 코틀린의 if는 식이므로 삼항연산자가 따로 없다.
fun eval(e: Expr): Int =
    if (e is Num) { //is로 Num 타입인지 확인 한 이후로 컴파일러는 e를 Num타입으로 해석한다.
        e.value //
    } else if (e is Sum) {
        eval(e.left) + eval(e.right)
    } else {
        throw IllegalArgumentException("Unknown Expression")
    }
// 좀 더 코틀린스럽게 when을 이용하자.
fun eval(e: Expr): Int =
    when (e) {
        is Num -> e.value
        is Sum -> eval(e.left) + eval(e.right)
        else -> throw IllegalArgumentException("Unknown Expression")
    }

c. if와 when의 분기에서 블록 사용

  • eval 함수에 로그를 추가해보자. 각 분기는 블록처리 후, 블록의 맨 마지막에 그 분기의 결과값을 위치시키면 된다.
fun main(){
    println(evalWithLogging(Sum(Sum(Num(1),Num(2)),Num(4))))
}

num : 1
num : 2
sum : 3
num : 4
sum : 7
7

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 IllegalStateException("Unknown Expression")
    }
  • 식이 본문인 함수는 블록을 가질 수 없다.
  • 블록이 본문인 함수는 리턴값이 반드시 필요하다.

(4) 이터레이션 Iteration : while과 for 루프

  • 이터레이션 : 결과를 내기위해 프로세스를 반복하는 작업. “그 문장의 블록은 이터레이션 되고 있다” 말한다.

a. while , do-while 루프

while (조건) {
본문 // 조건이 참인 동안 반복실행
}
--------------------
do {
본문
}
while (조건) {
} //일단 무조건 1회는 본문을 실행한다. 그 뒤에 조건이 맞으면 반복실행.

b. 수에 대한 이터레이션 : 범위와 수열

  • 자바의 for 루프 (int i=0;i<=10;i++)을 사용하고, 코틀린에서는 범위를 사용한다.
  • 코틀린 폐구간은 .. 연산자로 나타낸다. val oneToTen = 1..10
  • 어떤 범위에 속한 값을, 일정한 순서대로 이터레이션 하는 것을 수열이라한다.

 💡 피즈버즈게임

  • 3으로 나누어 떨어지는 숫자는 “피즈”
  • 5로 나누어 떨어지는 숫자는 “버즈”
  • 3,5 둘다 나누어 떨어지는 숫자는 “피즈버즈”를 외친다.
fun main() {
    for (i in 1..10) {
        println(fizzBuzz(i))
    }
    println("--------")
    for (i in 30 downTo 1 step 2) {
        println(fizzBuzz(i))
    }
}

fun fizzBuzz(i: Int) = when {
    i % 15 == 0 -> "FizzBuzz"
    i % 3 == 0 -> "Fizz"
    i % 5 == 0 -> "Buzz"
    else -> "${i}" // 셋 다 아니면 그 수 자체를 반환한다.
}

//출력값
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
--------
FizzBuzz
28
26
Fizz
22
Buzz
Fizz
16
14
Fizz
Buzz
8
Fizz
4
2

c. 맵에 대한 이터레이션

fun main() {
    val binaryReps = TreeMap<Char, String>()

    for (c in 'A'..'F') { //문자의 범위를 사용해 이터레이션한다.
        val binary = Integer.toBinaryString(c.toInt())
        //정수를 2진으로 바꿔준다.
        binaryReps[c] = binary
    }

    for ((letter, binary) in binaryReps) {
        println("${letter} = ${binary}")
    }
}

// 출력값
A = 1000001
B = 1000010
C = 1000011
D = 1000100
E = 1000101
F = 1000110

d. in으로 컬렉션이나 범위의 원소 검사

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
fun recognize(c: Char) = when (c) {
    in '0'..'9' -> "It's digit!"
    in 'a'..'z', in 'A'..'Z' -> "It's a letter!"
    else -> "I don't know!"
}
println(recognize('C')) //It's a letter!
  • 비교 가능한 클래스라면(java.lang.Comparable 인터페이스를 구현한 클래스), 그 클래스의 인스턴스객체를 사용해 범위를 만들 수 있다.

(5) 코틀린의 예외 처리

  • 함수는 정상적으로 종료할 수 있지만, 오류가 발생하면 예외(throw)를 던질 수 있다.
    • 함수를 호출하는쪽에서 예외를 잡아 처리한다.
    • 만약 발생한 예외를 함수호출단에서 처리하지 않으면..?
      • 함수 호출 스택을 거슬러 올라가면서 예외를 처리하는 부분이 나올때까지 다시 던진다. (rethrow)
val percentage = if (number in 0..100) number else throw IllegalArgumentException("sorry")
//조건에 들어가면 초기화, 아니면 예외를 던진다.

a. try, catch, finally

fun readNumber(reader: BufferedReader): Int? {
    try {
        val line = reader.readLine() // 1.입력받는다
        return Integer.parseInt(line) // 2.입력값을 정수로 바꿔서 리턴한다.
    } catch (e: NumberFormatException) {
        return null //이때 예외가 발생하면 catch 블록이 실행된다. null을 리턴한다.
    } finally {
        reader.close() //항상실행하는 블록. 예외가 발생해도 실행된다. BufferedReader를 닫아준다.
    }
}
  • 자바처럼 throws 절 코드가 없다.
    • 자바는 throws IOException을 붙여야한다.
    • IOException : 체크 예외, 자바에서는 체크예외를 명시적으로 처리해야한다.
  • 코틀린은 체크예외와 언체크예외를 구별하지 않는다. (최신 JVM 언어 특징)

b. try를 식으로 이용해서 코틀린스럽게!

fun main() {
    val reader = BufferedReader(StringReader("333333"))
    readNumber(reader) //333333

    val reader2 = BufferedReader(StringReader("hello"))
    readNumber(reader2) //null

}

fun readNumber(reader: BufferedReader) {
    val number = try {
        Integer.parseInt(reader.readLine())
    } catch (e: NumberFormatException) {
        null // 예외처리로 여겨지면 null을 리턴한다.
    }
    println(number)
}
728x90
반응형

'Kotlin in Action' 카테고리의 다른 글

4. kotlin 클래스, 객체, 인터페이스  (0) 2024.07.13
3. kotlin 컬렉션, 확장함수, 정규식  (0) 2024.07.13
1. 코틀린이란?  (0) 2024.07.13