본문 바로가기
발전로그/책장로그-개발

kotlin in action 02

by 4lleycat 2022. 1. 24.

2장 코틀린 기초

2.1 기본요소

- 함수

반환값이 없는 경우와 함수의 기본

fun main(args: Array<String>, i: Int) {
    println("Hello, world!!!")
}

>>> Hello, world!!!
// 일반적인 함수본문
fun main() {
    println(max(1,2))
}


fun max(a: Int, b: Int): Int {
    return if (a > b) a else b
}

>>> 2

// 식을 함수본문으로 변환사용 한경우
fun main() {
    println(max(1,2))
}


fun max(a: Int, b: Int): Int = if (a > b) a else b

>>> 2

// 반환 타입 생략
fun main() {
    println(max(1,2))
}


fun max(a: Int, b: Int): Int = if (a > b) a else b

>>> 2

  • 함수 선언은 fun 키워드를 사용한다
  • 파라미터 이름뒤에 파리미터의 타입을 쓴다
    • 변수도 동일
    • args : 파라미터 이름
    • Array<String> : 파라미터 타입
  • 파이썬처럼 함수를 최상위 수준에 정의할 수 있다.
  • 배열처리를 위한 문법이 존재하지 않는다
    • Array<String> 바로 사용가능
  • println 같은 자바 라이브러리를 간결할게 사용할 수 있는 래퍼를 제공
  • 세미콜론 없음!!!
  • 반환값 없는 함수에 void 선언 안해도 된다
  • 코틀린에서는 식(expression)이 본문(statement)인 함수가 많이 쓰인다
    • 인텔이제이에서 변환 지원 (🍯🍯🍯 )
    • 식을 본문으로 사용할경우 컴파일러가 타입추론으로 반환 타입을 자동으로 분석반환한다.
      • 주의 : 반대로 블록이 본문인경우는 꼭 반환타입을 써줘야한다

- 변수

// 타입 미지정, 초기화 변수 선언
val question = "삶, 우주, 그리고 모든것에 대한 궁극적인 질문"
val answer = 42

// 타입 지정, 초기화 변수 선언
val answer: Int = 42

// 초기화 하지 않는경우 타입 선언 필수
val answer: Int

// 부동소수점의 경우 Double 타입으로 컴파일러가 인식
val yearsToComputer = 7.5e6

// 값을 초기화 하지 않음
val message: String
// 조건에 따른 val 값의 초기화, 이후 변수 값 변경 불가
if (canPerFormOperation()) {
    message = "success"
} else {
    message = "fail"
}

// val 객체 내부값은 변경 가능하다
val languages = arrayListOf("java")
languages.add("kotlin")

// var 같은 타입의 값으로만 변경 가능하다
// 아래의 경우 type mismatch 에러발생
var message = "success"
message = 0
  • val
    • val로 지정된 불변 타입 변수는 초기에 값을 할당되면 나중에 값을 변경할 수 없으며 값을 변경하게 되면 컴파일 에러가 발생
    • Java 의 final
    • 블록 실행시 한번만 초기화 해야한다, 다만 블록이 실행될때 하나의 초기화 문장만 실행됨을 컴파일러가 확인 할 수 있다면
      조건에 따른 값으로 초기화가 가능하다
    • 객체의 내부값은 변경이 가능하다
  • var
    • 초기화 이후에도 값변경 가능
    • 단 변수의 타입은 변경 불가하다
      • 형변환이 필요
      • 컴파일러는 변수 선언시점의 초기화식으로 타입을 추론하기 때문
  • var 보다는 val 변수를 권장 
    • 프로그래밍에서 대부분의 경우 변수의 값을 변경할 필요가 없고, 변수를 불변으로 하는 경우 여러 면에서 유리하다는 것을 알게 되어서.
    • 변수를 사용할때 최초 값 대입 이후로 굳이 값을 변경하지 않는 경우가 많다. 특히 임시적으로 사용되는 지역변수, 함수의 파라미터와 같은 경우 대부분 값을 변경하지 않는다
    • 반대로 불변으로 선언할 경우, '메모리,멀티쓰레드 안전성, 함수형 코드'등 얻을수 있는 이점이 많다.

 

- 문자열 템플릿

// 문자열 템플릿
// 책 예제가 문제인지 args.size 1이 나온다 빈string이...
fun main(args: Array<String>) {
    val name = if (args.size > 0) args[0] else "kotlin"
    println(args[0] is String) //true?
    println("hello, $name!")
}

// 복잡한식도 중괄호로 감싸서 사용가능
fun main(args: Array<String>) {
    println("hello, ${if (args.size > 1) args[0] else "kotlin"}!")
}

//문자열 그대로 $name 을 출력할경우 이스케이프처리
fun main(args: Array<String>) {
    val name = if (args.size > 1) args[0] else "kotlin"
    println("hello, \$name!")
}

 

2.2 클래스와 프로퍼티

- 클래스

//java VO
public class Person {
   private final string name;
   public Person(String name) {
      this.name = name
   }
   
   public string getName() {
       return name;
   }
}

//kotlin 변환
class Person (val name: String)
  •  코틀린의 기본 가시성 public

- 프로퍼티

class Person(
    // getter만 제공 
    val name: String,
    
    // getter, setter 제공
    var isMarried: Boolean
)

fun main() {
   val person = Person("bob", true) // new 를 쓰지않고 생성가능
   
   //get 메소드 대신 intance.property 로 접근가능
   println(person.name)
   println(person.isMarried)
   
   //setter대신 직접 프로퍼티에 대입 (자꾸 파이썬의 향기가)
   person.isMarried = false
   println(person.isMarried)
}

>>>bob
>>>true
>>>false
  • 자바에서 선언한 클래스를 코틀린 문법을 사용할 수 있다.
    • getter를 val 프로퍼티로 사용
    • getter, setter 다있는경우  var 프로퍼티로 사용

- 커스텀 접근자

class Rectagle(
    val height: Int,
    
    val width: Int
){
    // isSquare get 할때 getter에서 계산된 값이 반환
    // java 에서 호출시 isSquare()
    val isSquare: Boolean
        get() {
            return height == width
        }
    val sumHw: Int
    	get() {
            return height + width
        }
}

fun main() {
   val rectangle = Rectagle(40, 40)
   
   println(rectangle.isSquare)
   println(rectangle.sumHw)
   
}

>>>true
>>>80
  • 커스텀 getter 를 사용하면 프로퍼티의 값을 그때 그때 계산하는 backing field 로 만들 수 있음

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

package geometry.shapes // 패키지 선언

import java.util.Random // 자바 라이브러리 import

class Rectangle(
        val height: Int,
        val width: Int
){
    val isSquare: Boolean
        get() = height == width
}

fun createRectangle() : Rectangle {
    val random = Random() //자바 랜덤 클래스 사용
    return Rectangle(random.nextInt(), random.nextInt())
}

----------------
//위에 만든 패키지를 다른파일에서 import
import geometry.shapes.createRectangle

fun main() {
    val rectangle = createRectangle()
    println(rectangle.isSquare)
}

>>>false
  • 코틀린 파일의 맨앞에 package 선언하면 파일안에 모든 클래스, 함수등이 패키지로 들어감
  • import package.* 패키지 하위 모두 import
  • 자바 패키지 처럼 계층구조를 강제하진 않지만 자바방식을 따르는게 좋다.
    • 마이그레이션등에 문제가 생길수 있음

2.3 선택 표현과 처리 : enum 과 when 

- enum 클래스의 정의

// enum 의 기본 선언
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); //이경우 세미콜론필요
	
    // 상수에 프로퍼티 값을 커스텀 반환가능
    fun rgb() = (r * 256 + g) * 256 + b
}

fun main() {
    println(Color.BLUE.rgb())
}

>>>255

- when으로 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), BLACK(0, 0, 0);

    fun rgb() = (r * 256 + g) * 256 + b
}

fun getMnemonic(color: Color) =
    when (color) {
        Color.RED -> "Richard"
        Color.ORANGE -> "Of"
        Color.YELLOW -> "York"
        Color.GREEN -> "Grave"
        Color.BLUE -> "Battle"
        Color.INDIGO -> "In"
        Color.VIOLET, Color.BLACK -> "Vain"
	}

fun main() {
    println(getMnemonic(Color.VIOLET))
}

>>>Vain
  • when : java의  switch를 대체
  • break 넣지 않아도 됨
  • 복수 매칭은 콤마구분
package ch02.colors

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), BLACK(0, 0, 0);

    fun rgb() = (r * 256 + g) * 256 + b
}


----------
import ch02.colors.color

fun getMnemonic(color: Color) =
    when (color) {
        Color.RED -> "Richard"
        Color.ORANGE -> "Of"
        Color.YELLOW -> "York"
        Color.GREEN -> "Grave"
        Color.BLUE -> "Battle"
        Color.INDIGO -> "In"
        Color.VIOLET, Color.BLACK -> "Vain"
	}

fun main() {
    println(getMnemonic(Color.VIOLET))
}

>>>Vain
  • 상수값 import 사용 가능
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
}

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") //예외 값
    }

fun main() {
    println(mix(Color.YELLOW, Color.RED)) //set 객체에 넣으므로 순서는 중요치 않음
}

>>>ORANGE

---------------------

// when 인자없이 사용
fun mix(c1: Color, c2: Color) =
    when {
        (c1 == Color.RED && c2 == Color.YELLOW) ||
        (c2 == Color.RED && c1 == Color.YELLOW) -> Color.ORANGE
        (c1 == Color.YELLOW && c2 == Color.BLUE) ||
        (c2 == Color.YELLOW && c1 == Color.BLUE) -> Color.GREEN
        (c1 == Color.BLUE && c2 == Color.VIOLET) ||
        (c2 == Color.BLUE && c1 == Color.VIOLET) -> Color.INDIGO
        else -> throw Exception("dirty")
    }

fun main() {
    println(mix(Color.VIOLET))
}
  • 분기 조건어 여러 객체를 조합해서 사용가능 (예제 참고)
  • setOf : set 객체 생성

- 스마트 캐스트 타입 : 타입검사와 타입 캐스트를  조합

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){
        	val n = e as Num // 중복 캐스팅
            n.value
        } else if (e is Sum) {
            eval(e.right) + eval(e.left) //스마트 캐스트
        } else {
            throw IllegalArgumentException("unknown expression")
        }

fun main() {
    println(eval(Sum(Num(1), Num(2))))
}
  • 코틀린에서는 is type 을 이용해 타입검사를 한다
  • is 로 검사하고 나면 자동 캐스팅 되는데 이를 스마트 캐스트라고 한다
  • 스마트캐스트는 타입 검사이후 값이 바뀔 수 없는 경우 작동
    • val 변수
    • val 프로퍼티
  • 타입 캐스팅 하려면 as 키워드 사용 

- 리팩토링 : if를 when으로 변경

interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr

fun eval (e: Expr): Int =
        when (e) {
            is Num -> {
            	// 분기 코드 블록
                e.value //스마트 캐스트 적용
            }
            is Sum -> {
                eval(e.right) + eval(e.left)
            }
            else -> {
                throw IllegalArgumentException("unknown expression")
            }
        }

fun main() {
    println(eval(Sum(Num(1), Num(2))))
}
  • fun eval if 식을 when 으로 변경
  • if, when 모두 분기에 블록을 사용할 수 있다

2.4 대상을 이터레이션 : while과 for루프

- while 루프

  • 자바와 동일 문법이다

- 수에 대한 이터레이션 : 범위와 수열 (for)

// 지정 범위만큼 이터레이션
fun fizzBuzz(i: Int) = when {
    i % 15 == 0 -> "FizzBuzz"
    i % 3 == 0 -> "Fizz"
    i % 5 == 0 -> "Buzz"
    else -> "$i"
}

        
fun main() {
    for (i in 1..15) { // .. 연산자 시작과 끝값을 연결해서 범위를 만든다
        println(fizzBuzz(i))
    }
}
>>>1
2
Fizz
4
......

---------------------------

// 증가 값을 가지고 이터레이션
fun fizzBuzz(i: Int) = when {
    i % 15 == 0 -> "FizzBuzz"
    i % 3 == 0 -> "Fizz"
    i % 5 == 0 -> "Buzz"
    else -> "$i"
}


fun main() {
    for (i in 20 downTo 1 step 2) {//20 부터 2씩 감소
        println(fizzBuzz(i))
    }
}

>>>Buzz
Fizz
16
14
Fizz
Buzz
  • downTo 역방향 수열
    • 1..20 step 2 : 2씩 증가
  • 끝값이 특정 값이 아닐때 (특정 크기 만큼)
    • utill 함수 사용 (x in 0 util size)

- 맵에 대한 이터레이션 (foreach)

import java.util.*

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

    for (c in 'a'..'f') {
        val binary = Integer.toBinaryString(c.code)
        binaryReps[c] = binary
    }

    for ((letter, binary) in binaryReps) { // foreach!
        println(letter)
        println(binary)
    }
}

>>a
1100001
b
1100010
c
1100011
d
1100100
e
1100101
f
1100110

- in으로 컬렉션이나, 범위에 원소 검사

// 값이 범위에 속하는지 검사
fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
fun isNotDigit(c: Char) = c in '0'..'9'

fun main() {
    println(isLetter('q'))
    println(isNotDigit('x'))
}

>>true
false

-------------------------
// when에서 조건으로 사용
fun recognize(c: Char) = when (c) {
    in '0'..'9' -> "digit"
    in 'a'..'z', in 'A'..'Z' -> "letter"
    else -> "i dont know"
}
fun main() {
    println(recognize('q'))
}

>>letter
  • 비교가 가능한 클래스라면(comparable인터페이스 구현)범위를 만들고 비교가능
  • 모든 객체를 이터레이션 할순 없지만  in을 사용하여 속해있는지 비교는 가능하다

2.5 코틀린의 예외처리

import java.io.BufferedReader
import java.io.StringReader

// 자바와 크게 다른 부분 없는 예외
fun readNumber(reader: BufferedReader): Int? { // ? 함수가 null 반환가능함 명시
  try {
      val line = reader.readLine()
      return Integer.parseInt(line)
  } catch (e: NumberFormatException) {
      return null
  }finally {
      reader.close()
  }
}

fun main() {
    val reader = BufferedReader(StringReader("239"))
    println(readNumber(reader))
}

--------------
// 식으로 예외 사용

import java.io.BufferedReader
import java.io.StringReader

fun readNumber(reader: BufferedReader) {
    val number =  try {
        Integer.parseInt(reader.readLine())
    } catch (e: NumberFormatException) {
        null
    }
    println(number)
}

fun main() {
    val reader = BufferedReader(StringReader("not a number"))
    readNumber(reader)
}

>>>null
  • 식으로 예외를 받아서 처리할수 있는 부분이 중요한 차이!