2장에서 다루는 내용

  • 함수, 변수, 클래스, enum, 프로퍼티를 선언하는 방법
  • 제어 구조
  • 스마트 캐스트
  • 예외 던지기와 예외 잡기

2.1 기본 요소: 함수와 변수

함수

ex)
// 본문이 중괄호로 둘러싸인 함수를
// 블록이 본문인 함수
fun max(a: Int, b: Int): Int {
	return if ( a > b ) a else b
}

--------------------------------------
println(max(1,2))
> 2

// 조금 더 간결하게 표현 가능
// return을 제거
// 등호와 식으로 이뤄진 함수를 
// 식이 본문인 함수
fun max(a: Int, b: Int): Int = if ( a > b ) a else b

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

// 조금 더 간결하게 표현 가능
fun max(a: Int, b: Int) = if ( a > b ) a else b

- ⚡반환 타입을 생략할 수 있는 이유는 ?
-> 코틀린은 정적 타입 지정 언어이므로 컴파일 시점에 모든 식의 타입을 지정해야 하지만 ?
-> 식이 본문인 함수의 경우 굳이 적지 않아도
컴파일러가 함수 본문 식을 분석해서 식의 결과 타입을 함수 타입으로 정해준다.

문(statement)과 식(expression)의 구분
kotlin에서 if 는 식이지 문이 아니다.

  • 식(expresstion) :
    값은 만들어 내며 다른 식의 하위 요소로 계산에 참여할 수 있음
  • 문(statement) :
    자신을 둘러싸고 있는 가장 안쪽 블록의 최상위 요소로 존재,
    아무런 값을 만들어내지 않는다

루프를 제외한 대부분의 제어 구조는 식이다.


변수

  • 코틀린에서는 키워드로 변수 선언을 시작하는 대신 변수 이름 뒤에 타입을 명시하거나 생략하게 허용한다.
val answer = 42

// 원하면 타입을 명시해도 된다.
val answer : Int = 42
  • 타입을 지정하지 않으면 컴파일러가 초기화 식을 분석해서
    초기화 식의 타입을 변수 타입으로 지정한다.
//초기화 식이 없다면 변수에 저장될 값에 대해 아무 정보가 없기 때문에
//컴파일러가 타입을 추론할 수 없다. 그런 경우 반드시 지정해야 한다!

val answer : Int
answer = 24

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

  • val ( 값을 뜻하는 value )
    변경 불가능한(immutable) 참조를 저장하는 변수다.
    val로 선언된 변수는 일단 초기화하고 나면 재대입이 불가능
    -> 자바에서는 final 변수에 해당

  • var ( 변수를 뜻하는 variable )
    변경 가능한(mutable) 참조다.
    -> 자바의 일반 변수에 해당

기본적으로는 모든 변수를 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" <- Error: type mismatch

2.2 클래스와 프로퍼티

시작하기 위해 간단한 Person 클래스를 정의
Person에는 name이라는 프로퍼티만 만듦

class Person(val name: String)

↪ 이런 유형의클래스를 값 객체라 부름

2.2.1 프로퍼티

  • 클래스라는 개념의 목적은 데이터를 캡슐화

  • 캡슐화한 데이터를 다루는 코드를 한 주체 아래 가두는 것

  • 클래스는 자신을 사용하는 클라이언트가 그 데이터에 접근하는 통로로 쓸 수 있는 접근자 메소드 를 제공한다.

  • 코틀린 프로퍼티는 자바의 피드와 접근자 메소드를 완전히 대신한다.

  • 클래스에서 프로퍼티를 선언할 때는 앞에서 살펴본 변수를 선언하는 방법과 마찬가지로 val이나 var를 사용한다.

// 클래스 안에서 변경 가능한 프로퍼티 선언하기
class Person (
	val name: String, 
    // 읽기 전용 프로퍼티로, 필드와 게터를 만들어낸다.
    var isMarried: Boolean
    // 쓸 수 있는 프로퍼티로, 필드와 게터/세터를 만들어낸다.
)

// Java에서 Person 클래스를 사용하는 방법
Person person = new Person("Bob", true);
System.out.println(person.getName());
>>> Bob
System.out.println(person.isMarried());
>>> true

// Kotlin에서 Person 클래스 사용하는 방법
val person = Person("Bob", true) // new 키워드를 사용하지 않고 생성자를 호출한다.
println(person.name)
>>> Bob
println(person.isMarried)
>>> true

// 프로퍼티 이름을 직접 사용해도 코틀린이 자동으로 게터를 호출해준다.

↪ 변경 가능한 세터로 수정할 땐
person.isMarried = false를 사용

2.2.2 커스텀 접근자

  • 프로퍼티의 접근자를 직접 작성하는 방법

  • 단순히 값을 반환하는 접근자가 아니라 특정 연산을 수행한 결과를 반환해야 하는 경우라면, 접근자를 직접 작성할 수 있다.

calss Rectangle(val height: Int, val width: Int) {
	val isSquere: Boolean
    	get() {
        	return height == width
		}
}

val rect = Rectangle(41, 43)
println(rect.isSquare)
>>> false

isSquare 처럼
호출될 때 마다 계산해서 수행 결과를 반환하는 방법이 있고,
초기화시, 그리고 종속 변수 값이 변경될 때마다 변수 값을 다시 계산해두고 호출될 때는 단순히 계산 결과를 반환하기만 하도록 구성하는 방법이 있는데

전자는 호출 오버헤드가, 후자는 메모리 오버헤드가 발생한다.

🔥 특별히 많이 호출되지 않는다면 전자로 짜는게 더 보기 좋을 듯
💣 lateinit 은 커스텀 접근자를 사용할 수 없으니 주의!

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

// 킅래스와 함수 선언을 패키지에 넣기
package gemetry.shapes
import java.util.Random

Class Rectangle(val height: Int, val width: Int) {
	val isSquare: 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)
}

↪ 패키지 이름 뒤에 .* 를 추가하면 패키지 안의 모든 선언을 임포트 가능

↪ ⚡패키지 안에 있는 모든 클래스뿐 아니라 최상위에 정의된 함수나 프로퍼티까지 모두 불러온다는 점에 유의하자⚡
ex) import geometry.shapes.*


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

// 간단한 enum 클래스 정의하기
enum class Color {
	RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}
  • 코틀린에서 enum은 소프트 키워드라 부르는 존재

  • enum은 class 앞에 있을 때는 특별한 의미를 지니지만 다른 곳에서는 이름에 사용할 수 있음

  • class는 키워드이다

  • enum은 단순히 값만 열거하는 존재가 ❌
    -> enum 클래스 안에 프로퍼티나 메소드를 정의 가능 ✔️

// 프로퍼티와 메소드가 있는 enum 클래스 선언하기
enum class Color (
	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
    // enum 클래스 안에서 메소드를 정의
}

>>> println(Color.BLUE.rgb())
255
  • enum 에서도 일반적인 클래스와 마찬가지로 생성자와 프로퍼티를 선언

  • 각 enum 상수를 정의할 때는 그 상수에 해당하는 프로퍼티 값을 지정해야 함

2.3.2 when으로 enum 클래스 다루기

  • 자바의 switch에 해당하는 코틀린 구성 요소는 when이다

  • if와 마찬가지로 when도 값을 만들어내는 이다
    -> 식이 본문인 함수에 when을 바로 사용 가능

// when을 사용해 올바른 enum 값 찾기
fun getMnemonic(color: Color) =
	when(color) {
    	Color.RED -> "R"
        Color.ORANGE -> "O"
        Color.YELLOW -> "Y"
        Color.GREEN -> "G"
        Color.BLUE -> "B"
        Color.INDIGO -> "I"
        Color.VIOLET -> "V"
    } // 자바와 달리 break를 넣지 않아도 됨
>>> println(getMnemonic(color.BLUE))
B

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

// 한 when 분기 안에 여러 값 사용하기
fun getMnemonic(color: Color) =
	when(color) {
    	Color.RED, Color.ORANGE, Color.YELLOW -> "warm"
        Color.GREEN -> "neutral"
        Color.BLUE, Color.INDIGO, Color.VIOLET -> "cold"
    }
>>> println(getMnemonic(color.ORANGE))
warm

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

// enum 상수 값을 임포트해서 enum 클래스 수식자 없이 enum 사용하기
import ch02.colors.Color.*

fun getWarmth(color: Color) = when(color) {
	RED, ORANGE, YELLOW -> "warm"
    GREEN -> "neutral"
    BLUE, INDIGO, VIOLET -> "cold"
}

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

// when의 분기 조건에 여러 다른 객체 사용하기
fun mix(c1: Color, c2: Color) =
	when(setOf(c1, c2)) {
    	setOf(RED, YELLOW) -> ORAGNE
        setOf(YELLOW, BLUE) -> GREEN
        setOf(BLUE, VIOLET) -> INDIGO
        else -> thorw Exception("Dirty color")
    }
>>> println(mix(BLUE, YELLOW))
GREEN

/*
c1, c2가 RED, YELLOW ( or YELLOW, RED) 라면 ORANGE 이다.
이를 구현하기 위해 집합 비교를 사용한다.

집합인 Set 객체로 만드는 setOf라는 함수는 원소가 모여 있는 컬렉션으로,
원소의 순서는 중요하지 않다.
/*
  • when 식은 인자 값과 매치하는 조건 값을 찾을 때까지 각 분기를 검사한다.

  • setOf(c1, c2)와 분기 조건에 있는 개체 사이를 매치할 때
    동등성 을 사용한다

  • when의 분기 조건 부분에 식을 넣을 수 있기 때문에 많은 경우 코드를 더 간결하고 아름답게 작성 가능

2.3.4 인자 없는 when 사용

setOf를 사용한 함수가 자주 호출된다면 불필요한 가비지 객체가 늘어난다
-> 인자가 없는 when 식을 사용하면 불필요한 객체 생성을 막을 수 있다
-> 코드가 약간 읽기 어려워지지만 성능을 더 향상시키기 위해 비용 감수해야 함

//인자가 없는 when
fun mixOptimized(c1: Color, c2: Color) =
	when {
    	(c1 == RED && c2 == YELLOW) ||
        c1 == YELLOW && c2 == RED) ->
        ORAGNE
        
        ...
        
        else -> thorw Exception("Dirty color")
    }
>>> println(mixOptimized(BLUE, YELLOW))
GREEN

-> when에 아무 인자도 없으려면 각 분기의 조건이 불리언 결과를 계산하는 식이여야 함


2.4 대상을 이터레이션: while / for loop

2.4.1 while loop

  • while / do - while loop 가 존재, 자바와 다르지 않음
while(조건) { // 조건이 참인 동안 본문을 반복 실행
	...
}

do { // 맨 처음 무조건 본문을 한 번 실행한 다음
	... 
} while(조건) // 조건이 참인 동안 본문을 반복 실행

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

  • 범위는 기본적으로 두 값으로 이뤄진 구간

  • 보통은 두 값은 정수 등의 숫자 타입이며,
    .. 연산자로 시작 값과 끝 값을 연결해서 만든다
    -> 양끝을 포함하는 구간

ex) val onetoTen = 1..10

// 증가 값을 갖고 범위 이터레이션하기
for( i in 100 downTo 1 step 2 ) {
	print( i ) 
}

↪ 증가 값 step을 갖는 수열에 대한 이터레이션한다
↪ 증가 값을 음수로 만들면 역방향 수열도 가능

  • 끝 값을 포함하지 않는 반만 닫힌 범위(반폐구간 or 반개구간)
    -> until 연산자 사용

ex) for( i in 1 until 11)
1,2, ... , 10

2.4.3 맵에 대한 이터레이션

// 맵을 초기화하고 이터레이션하기

// 키에 대해 정렬하기 위해 TreeMap을 사용
val binaryReps = TreeMap<Char, String>() 

// A ~ F까지 문자의 범위를 사용해 이터레이션한다
for(c in 'A'..'F') {
	// 아스키 코드를 2진 표현으로 바꾼다
	val binary = Integer.toBinaryString(c.toInt())
   
   binaryReps[c] = binary // c를 키로 c의 2진 표현을 맵에 넣는다
}

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

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

// 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

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

// when에서 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"
}
>>> println(recognize('8'))
It's a digit

2.5 코틀린의 예외 처리

  • 코틀린의 예외 처리는 자바나 다른 언어의 예외 처리와 비슷

  • 함수는 정상적으로 종료할 수 있지만 오류가 발생하면 예외를 던질 수 있다

  • 함수를 호출하는 쪽에서는 그 예외를 잡아 처리 가능

  • 발생한 예외를 함수 호출 단에서 처리하지 않으면 함수 호출 스택을 거슬러 올라가면서 예외를 처리하는 부분이 나올 때까지 예외를 다시 던진다

val precentage = 
	if (number in 0..100) 
    	number
	else
    	throw IllegalArgumentException( // throw는 식이다
        "A percentage value must be between 0 and 100: $number")

2.5.1 try, catch, finally

// 자바와 마찬가지로 try 사용하기
fun readNumber(reader: BufferReader): Int? {
	try {
    	val line = reader.readLine()
        return Integer.parseInt(line)
    }
    catch( e: NumberFormatException) {
    	return null
    }
    finally {
    	reader.close()
	}
}

↪ 자바에서는 함수를 작성할 때 함수 선언 뒤에 throw IOException을 붙여야 함

IOException이 체크 예외 이기 때문

자바에서는 체크 예외를 명시적으로 처리해야 한다

// try를 식으로 사용하기
fun readNumber(reader: BufferReader) {
	val number = try {
    	Integer.parseInt(reader.readLine()) // 이 식의 값이 try 식의 값이 된다.
    } catch (e: NumberFormatException) {
    	return
    }
    println(number)
}
>>> val reader = BufferReader(StringReader("not a number")
>>> readNumber(reader)
아무것도 출력 x

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

// catch에서 값 반환하기
fun readNumber(reader: BufferReader) {
	val number = try {
    	Integer.parseInt(reader.readLine()) // 예외가 발생안하면 이 값 사용
    } catch (e: NumberFormatException) {
    	null
    }
    println(number)
}
>>> val reader = BufferReader(StringReader("not a number")
>>> readNumber(reader)
null

⭐2.6 정리

  • 함수를 정의할 때 fun 키워드를 사용
    val = 읽기 전용 변수 , var = 변경 가능한 변수

  • 문자열 템플릿을 사용하면 문자열을 연결하지 않아도 되므로 코드가 간결해짐
    -> ex) $식 , ${식}

  • 값 객체 클래스를 아주 간결히 표현 가능

  • if 는 코틀린에서 식이며, 값을 만들어 냄

  • 코틀린의 when 은 자바의 switch와 동등하거나 그 이상

  • 어떤 변수의 타입을 검사하고 나면 굳이 그 변수를 캐스팅하지 않아도 검사한 타입의 변수처럼 사용 가능

  • for, while, do-while 루프는 자바가 제공하는 같은 키워드의 기능과 비슷

  • 1..5와 같은 식은 범위를 만들어 냄

  • 코틀린의 예외 처리는 자바와 비슷
    -> 다만 코틀린에서는 함수가 던질 수 있는 에외를 선언하지 않아도 됨


📌 참고자료

kotlin 의 enum에 대해 자세히알고 싶다면 ( Click )

profile
개발자희망자

0개의 댓글