대부분의 언어와 마찬가지로 Kotilin에는 흐름에 대한 제어를 위한 표현이 있습니다.

  • return : 수행하던 구문에서 바로 리턴
  • break : 반복수행 하던 구문 중단
  • continue : 반복 구문 계속 수행

예제를 통해 알아보겠습니다.

fun main() {
    println("1에서 10까지의 수 하나를 입력하세요.")
    val value = readln().toInt()

    for(i in 1..10) {
        if(i/value == 1) {
            println("입력하신 값은 $i 입니다.")
            break
        }
    }
}

 

숫자를 입력받아 어떤 수인지 찾는 프로그램이라고 가정했습니다.

물론 입력 받은 value를 바로 출력하면 되지만, value의 값이 미지의 값이라고 가정하고 진행합니다.

for 반복문을 통해 i를 value로 나누어 1이면 같은 수겠지요? 그 수를 출력합니다.

출력했다는 이야기는 목적을 달성했다는 이야기이고 더 남은 반복문을 실행할 필요는 없습니다.

bareak를 통해 for 반복문에서 빠져 나옵니다.

 

위 예제에서 for 문을 별도 function으로 구현해 보겠습니다.

fun main() {
    println("1에서 10까지의 수 하나를 입력하세요.")
    val value = readln().toInt()

    findNumber(value)
}

fun findNumber(value :Int) {

    for(i in 1..10) {
        if(i/value == 1) {
            println("입력하신 값은 $i 입니다.")
            return
        }
        println("입력하신 값은 $i는 아닙니다.")
    }
    println("프로그램을 종료합니다.")
}

위 예제에서는 findNumber라는 function을 통해 value 값을 찾는 프로그램 입니다.

for 반복문을 수행하면서 if문의 조건이 true가 되면 출력을 하고 return합니다.

그럼 실제 실행해보면 결과는 어떨까요?

return 실행 결과

4를 입력해 4라는 것을 찾은 후 반복문에서 빠져나왔습니다.

그런데 여기서 보면 반복문 밖에 있는 출력문이 수행되지 않은 것을 알 수 있습니다.

반복문만이 아니라 function 자체에서 빠져나왔기 때문입니다.

 

그렇다면 어떻게 하면 for 반복문에서만 빠져 나올 수 있을까요?

return을 break로 변경한 실행 결과입니다.

break 실행 결과

break는 for 반복문에서만 빠져나와 아래에 있는 구문이 실행되었습니다.

 

Kotlin 모든 표현식에는 Label(레이블) 표시될 있습니다. 레이블은 abc@ 또는 fooBar@ 같이 식별자 뒤에 @ 기호가 붙는 형식을 갖습니다. 표현식에 레이블을 지정하려면 표현식 앞에 레이블을 추가하면 됩니다.

레이블은 어디에 쓰일까요?

fun main() {
    for(i in 1..3) {
        println("${i} 단입니다.")
        for (j in 1..3) {
            println("$i X $j = ${i*j}" )
            if(j==2) {
                break
            }
        }
    }
}

위 예제는 두개의 중첩 for 반복문이 있습니다. break 는 어디서 빠져나가는 것일까요?

실행결과

안쪽의 for문에서 빠져나왔습니다.

그렇다면 밖의 for 구문에서 빠져나오고 싶다면 어떻게 해야할까요? 이런 경우 사용하는 것이 레이블입니다. 

아래 예제로 확인해보겠습니다. 첫번째 반복문에 firstloop이라는 label을 달았고 break@firstloop라고 명시하여 첫번째 레이블 표현식에서 빠져나가도록 지정하였습니다.

fun main() {
    // 첫번째 for 구문에 firstloop이라는 레이블을 달았습니다.
    firstloop@ for(i in 1..3) {
        println("${i} 단입니다.")
        for (j in 1..3) {
            println("$i X $j = ${i*j}" )
            if(j==2) {
                // 레이블 firstloop에서 빠져나가라고 지시합니다.
                break@firstloop
            }
        }
    }
}

실행결과

 

Returns to labels

Kotlin에서는 함수들을 중첩해서 사용할 수 있습니다. return을 사용하면 외부 함수에서 반화하여 빠져나올 수 있습니다. 

fun foo() {
    listOf(1, 2, 3, 4, 5).forEach {
        if (it == 3) return // 조건을 만족하면 반환합니다. 즉, foo ()함수에서 빠져 나갑니다.
        print(it)
    }
    println("이 문구는 프린트 되지 않습니다.")
}

 

만약 List에 3이 없다면 println이 실행됩니다.

List에 3이 있어도 println을 실행하려면 어떻게 해야할까요?

 

break 구문과 유사하게 label을 사용하여 landa 표현식에서만 빠져나올 수 있습니다.

fun foo() {
    listOf(1, 2, 3, 4, 5).forEach lit@{
        if (it == 3) return@lit // lamda 호출자에 대한 로컬 반환
        print(it)
    }
    println("이제 프린트 됩니다.")
}

 

좀더 편리하게 label을 지정하지 않고 묵시적 label을 사용하기도 합니다. 

fun foo() {
    listOf(1, 2, 3, 4, 5).forEach {
        if (it == 3) return@forEach // local return to the caller of the lambda - the forEach loop
        print(it)
    }
    println(" 이제 프린트 됩니다.")
}

 

또, lamda 표현식을 익명 함수(anonymous function)으로 바꿀 수 있으며, 이때는 익명 함수 자체에서 반환 됩니다. 

fun foo() {
    listOf(1, 2, 3, 4, 5).forEach(fun(value: Int) {
        if (value == 3) return  // local return to the caller of the anonymous function - the forEach loop
        print(value)
    })
    println(" done with anonymous function")
}

이때에도 println 문은 실행됩니다.

 

break 구문에 직접적으로 상응하지는 않지만, 다른 중첩 lamda를 추가하고 로컬이 아닌곳에서 반환하여 시뮬레이션 할 수는 있습니다.

fun foo() {
    run loop@{
        listOf(1, 2, 3, 4, 5).forEach {
            if (it == 3) return@loop // non-local return from the lambda passed to run
            print(it)
        }
    }
    print(" done with nested loop")
}

 

value를 return 할 수도 있습니다.

fun foo() {
    val num = run loop@{
        listOf(1, 2, 3, 4, 5).forEach {
            if (it == 3) return@loop it// non-local return from the lambda passed to run
        }
    }
    print(" $num done with nested loop")
}

 

실행 결과는 아래와 같습니다.

3 done with nested loop

Kotlin에서 반복문은 for 또는 while을 사용하여 구현할 수 있습니다.

for와 while 사용법에 대해 이야기 하기전에 Kotlin에서 제공하는 Ranges의 개념을 이해할 필요가 있습니다.

Ranges

Ranges 반복문에서 몇번을 반복할 것인지에 대한 구현입니다.

// 시작 숫자, 마지막 숫자 포함
1..5 // 1,2,3,4,5
1.rangeTo(5) // 1,2,3,4,5

// 마지막 숫자 포함하지 않음
1..<5 // 1,2,3,4
1 until 5 // 1,2,3,4
1.rangeUntil(5) // 1,2,3,4

// 역으로 반복
(1..5).reversed() //5,4,3,2,1
5 downTo 1 // 5,4,3,2,1

// step를 사용하여 임의의 단계로 숫자를 반복 
0..10 step 2 // 0,2,4,6,8,10
0.rangeTo(10) step 2 // 0,2,4,6,8
0 until 10 // 0,2,4,6,8
0.rangeUntil(10) step 2 //0,2,4,6,8
10 downTo 0 sten 2 // 10,8,6,4,2,0

// char에도 동일하게 적용 가능
tempChar in 'a' until 'f' step 2 //a,c,e

 

Loops

Kotlin에서는 반복 구조를 구현하기 위해 for와 while을 사용할 수 있습니다.

Rages를 사용하여 범위 값안에서 반복하여 작업을 해야하는 경우는 for를 사용하고,

특정 조건이 충족될 때까지 작업을 계속하려면 while를 사용합니다.

 

for

for 반복 구문을 사용해서 구구단 프로그램을 만들어 보겠습니다.

fun main() {
    println("몇단을 외워볼까요?")
    
    // readln()은 입력값을 String으로 반환
    // 계산을 위해 Int로 분석
    val level = readln().toInt
    
    println("$level 단을 외워보겠습니다.")
    
    // 결과 값을 저장할 변수 생성
    var result = 0
    // 1에서 9까지(9포함) for문 구현
    for(num in 1..9) {
    	result = level*num
    	println("$level X $num = $result")
    }
}

실행 결과

 

for 구문의 반복 지정을 위해서 range를 사용하는 방법을 알아보았습니다.

사실 for 구문의 반복 지정은 range를 통해서만 가능한 것은 아닙니다.

Kotlin 공식 문서에 따르면 'iterator(반복자)를 제공하는 어떤 것이던 for 반복문에서 사용할수 있다.' 라고 언급하고 있습니다.

무슨 이야기 인지 예제로 알아보겠습니다.

fun main() {
    // Array 선언
    val array = arrayOf("apple","banana","cherry")

    // iterator는 array의 element 순서대로 반복하는 iterator를 반환 
    for (value in array.iterator())
        print("$value ")
    //apple banana cherry

    // indices는 array의 유효한 인덱스 범위를 반환
    for (index in array.indices)
        print("${array[index]} ")
    //apple banana cherry
    
    // array의 각 element를 index와 element 자체를 포함하는
    // IndexedValue로 래핑하여 Lazy Iterable 반환
    for ((index, value) in array.withIndex())
        println("$index:$value ")
    //0:apple 1:banana 2:cherry 
}

 

while

while 과 do-while 반복 구문이 있습니다. 두 구문은 특정 조건(혹은 상태)가 만족하는 한 { }안의 내용을 연속적으로 반복한다는 것은 같으나 특정 조건(혹은 상태)를 확인하는 시점이 다릅니다.

 

특정 조건(혹은 상태) 확인 시점

while 반복문은 조건을 먼저 확인하고 조건이 맞으면 { }안의 내용을 실행

do-while 반복문은 먼저 실행하고 그 다음 조건을 확인. 따라서 조건과 상관없이 먼저 한번은 실행됩니다.

fun main() {
    var x = 5
    
    // x가 0보다 크면 반복 실행합니다.
    while (x > 0) {
        println("x는 ${x}입니다.")
        x--
    }
    println("do-while 반복문 실행전 x는 ${x}입니다.")
    
    //먼저 실행한 후 x가 0보다 큰지 확인합니다.
    do {
        println("x는 ${x}입니다.")
        x--
    } while (x > 0)
}

위 예제에 대한 결과는 아래와 같습니다.

do-while문 x가 0보다 크지 않아도 실행 됨

결과에서 본것처럼 do-while문 실행전에 x의 값은 0이었습니다. do-while문은 구문을 먼저 실행하고 조건을 확인하기 때문에 먼저 {} 안의 프린트 문을 실행하였고 그 후에 x가 0보다 큰수가 아니기 때문에 반복문을 종료하였습니다.

 

 

 

모든 프로그래밍 언어는 Control flow가 있습니다.

무엇인가를 판단하여 사실인지 아닌지(true or false)를 반환(return)하는 코드입니다.

예를 들어 2에서 1을 빼면 1인가요? 라는 질문에 사실(true)이라고 반환(return)하겠지요. 

이러한 코드들을 Conditional expressions라고 합니다.

 

Conditional expressions

Kotlin은 Conditional expressions(조건)를 확인하기 위해 if와 when을 제공합니다. 

 

If expression

if 표현은 간단합니다.

// 조건이 사실(true)이면 해야할일 수행
// 아니면 아무것도 하지 않음
if(조건) {
	해야할일
}

// 조건이 사실이면(true) 해야할일 수행
// 조건이 사실이 아니면(false) 이면 다른일 수행
if(조건) {
	해야할일
} else {
	다른일
}

// 조건1이 사실이면(true) 해야할일1 수행
// 조건2가 사실이면(true) 해야할일2 수행
// 조건1과 조건2가 모두 사실이 아니면(false) 해야할일3 수행
if(조건1) {
	해야할일1
} else if(조건2) {
	해야할일2
} else {
	해야할일3
}

 

if(조건)은 조건의 사실여부에 따라 값을 반환하며, 그 값은 true 또는 false 입니다.

조건이 사실이면(true)이면, { } 안의 내용을 수행합니다.

조건을 여러개 확인하는 경우에는 else if(조건)을 사용할 수 있습니다. if문의 조건은 순차적으로 확인하여 진행하기 때문에, 앞의 조건이 true이면 그 아래 조건은 무시됩니다. else if(조건)은 계속 추가하여 여러개의 조건을 확인 할 수 있습니다.

Kotlin에는 JVM에 기반하지만 tenary operator, 삼항연산자(condition ? then : else)를 사용하지 않습니다.

대신, Kotlin에서는 ? operator에 Null safety를 위한 주요한 역할을 부여했습니다. 

 

if문에 대한 여러가지 표현식을 살표보겠습니다. ※해야할일 Code가 1 line이면 {}을 생략할 수 있습니다.  

val a = 3
val b = 2

var max = a
// b가 a보다 크면 max를 b로 바꿉니다.
// a와 b중 큰 수가 max값이 됩니다.
if(a < b) max =b

// else 구문 사용
if (a > b) {
	max = a
} else {
    max = b
}

// 간단히 줄이기
max = if(a > b) a else b

 

 

when expression

when은 확인하는 조건이 여러개일때 사용합니다. 

when 구문은 크게 2가지 형태로 표현할 수 있습니다.

when(조건) {} 또는 when {} 형태입니다.

when은 {} 안에 ->를 사용하여 각 컨디션에 대한 분기문을 표현합니다.

else if와 마찬가지로 분기는 순차적으로 확인되며 true가 되는 분기문을 실행하고 나머지는 무시됩니다.

만약 분기문 전체가 true가 아니면 else 구문이 실행됩니다.

fun main() {
	prinln("프로야구팀을 입력하세요.")
	val team = readln() // 입력창으로 부터 읽어드림

	// 조건 구문
	when(team) {
		"LG" -> println("서울")
		"KT" -> println("수원")
		"SSG" -> println("인천")
		"NC" -> println("창원")
		"두산" -> println("서울")
		"KIA" -> println("광주")
		"롯데" -> println("부산")
		"삼성" -> println("대구")
		"한화" -> println("대전")
		"키움" -> println("서울")
		else -> println("모르겠어요.")
	}
    
	// 결과가 같은 경우에는 조건을 합쳐서 표현할 수 있음
	when(team) {
    	// LG 또는 두산 또는 키움인 경우
		"LG","두산","키움" -> println("서울")
		"KT" -> println("수원")
		"SSG" -> println("인천")
		"NC" -> println("창원")
		"KIA" -> println("광주")
		"롯데" -> println("부산")
		"삼성" -> println("대구")
		"한화" -> println("대전")
		else -> println("모르겠어요.")
	}     

	// 표현 구문
	when {
		team == "LG" -> println("서울")
		team == "KT" -> println("수원")
		team == "SSG" -> println("인천")
		team == "NC" -> println("창원")
		team == "두산" -> println("서울")
		team == "KIA" -> println("광주")
		team == "롯데" -> println("부산")
		team == "삼성" -> println("대구")
		team == "한화" -> println("대전")
		team == "키움" -> println("서울")
		else -> println("모르겠어요.")
	}

	//공통된 println을 밖으로...
	val city = when {
		team == "LG" -> "서울"
		team == "KT" -> "수원"
		team == "SSG" -> "인천"
		team == "NC" -> "창원"
		team == "두산" -> "서울"
		team == "KIA" -> "광주"
		team == "롯데" -> "부산"
		team == "삼성" -> "대구"
		team == "한화" -> "대전"
		team == "키움" -> "서울"
		else -> "모르겠어요."
	}
	println(city)
}

 

※ Kotlin 공식 사이트에서는 if문과 when 문 중 하나를 선택해야 하는 경우 when을 사용하도록 권장합니다.

 

 

Kotlin에서는 데이터를 그룹화해서 효율적으로 사용할 수 있도록 Collection을 제공합니다.

 

Collection type 설명
List
MutableList
순서가 있는 값(item)들의 모음
Set
MutableSet
순서가 없는 값(item)들의 모음. Set 내에서 값(item)이 중첩될 수 없음
Map
MutableMap
key-value pair의 모음. key는 Map 내에서 unique한 값으로 오직 하나의 value로 매핑

 

각 Collection은 mutable 또는 read only 분류됩니다. 즉 변경가능한, 변경불가능한 collection입니다.

 

List

List는 순서대로 값을 저장하며, 저장되는 값이 같더라도 허용합니다.

 

읽기만 가능한 List는 listOf, 변경가능한 MutlableList는 mutableListOf로 생성할 수 있습니다.

위의 예제처럼 List는 변경할 수 없으며, 읽기만 가능합니다. 반면, MutalbleList는 값을 삭제하거나, 변경하거나, 더할 수 있습니다.

 

List는 순서가 있는 구조이기 때문에 indexed acess opertor [] 를 사용할 수 있습니다. 또한 first(), last() function을 이용해서 첫번째 혹은 마지막 값(item)에 접근할 수 있습니다. 그리고 count() funtion을 통해 저장된 값(item)의 갯수를 알 수 있습니다.

val readOnlyFruits = listOf("사과", "귤", "포도")
println("첫 번째 과일은 ${readOnlyFruits[0]}입니다.")
//첫번째 과일은 사과입니다.

println("첫 번째 과일은 ${readOnlyFruits.first()}입니다.")
//첫번째 과일은 사과입니다.

println("마지막 과일은 ${readOnlyFruits.last()}입니다.")
//첫번째 과일은 포도입니다.

println("담겨져있는 과일 종류의 수는 ${readOnlyFruits.count()}개입니다.")
//첫번째 과일은 사과입니다.

 

List안에 특정 값(item)이 포함되어있는지 확인할때는 in operator를 사용합니다.

val readOnlyFruits = listOf("사과", "귤", "포도")
println("포도" in readOnlyFruits) //포도가 있으면 true, 없으면 false
//true

 

MutableList에서 값을 더하기 위해선 add(),삭제하기 위해서는 remove()를 사용합니다.

val fruits = mutableListOf("사과", "귤", "포도")
fruits.add("딸기")
println(fruits)
//[사과, 귤, 포도, 딸기]

fruits.remove("귤")
println(fruits)
//[사과, 포도, 딸기]

 

첨언을 하자면 List, MutableList 모두 val로 선언했습니다. 왜 MutableList는 var이 아닌데 변경 가능한지 헷갈릴 수도 있습니다.

MutalbeList의 내부에 값이 변경되는 것이지 MutableList 자체가 변경되는 것이 아납니다. 다시 말해 그릇은 그대로 있고 그 안의 내용물만 바뀌는 겁니다. val로 선언한 것이 바로 그릇 MutableList이고 변경한것은 그 안의 값(item)들입니다.

아래의 예시가 적절한 예제는 아니지만 개념 이해를 돕는것으로는 도움이 될 것 같습니다.

val valFruits = mutableListOf("사과", "귤", "포도")
//valFruits = mutableListOf("사과", "귤", "포도") //ERROR!! val은 한번 생성하면 변경할 수 없는 값입니다.
    
var varFruits = mutableListOf("사과", "귤", "포도")
varFruits = mutableListOf("사과","귤","포도") //var은 변경할 수 있습니다.

 


Set

List와 유사하지만 다른점이 있다면, Set은 순서를 중요시하지 않습니다. 대신에 중첩된 값을 받아들이지 않습니다.

 

읽기만 가능한 Set은 setOf, 변경가능한 MutlableSetd은 mutableSetOf로 생성할 수 있습니다.

중복 item 정리됨

 

Set과 MutableSet은 item의 순서가 없기때문에 indexed access operator []를 사용할 수 없습니다. 주의하세요!!!

count(), add(), remove() funtion과 in operator는 List와 동일하게 사용가능합니다.


Map

Map은 key-value 쌍을 저장하는데 사용합니다. 

key에서 느껴지듯이 key 값은 Map 내에서 유일(unique)해야합니다. value는 중첩될수 있습니다.

key-value 쌍 예를 들어 보면

kbs1, 9

kbs2, 7 

mbc, 11

sbs, 6

이렇게 예를 들 수 있겠네요.

kbs1은 9번 채널, kbs2는 7번 채널, mbc는 11번 채널, sbs는 6번 채널 입니다.

그럼 이걸 어떻게 표현할까요?

Map 생성을 통해 알아보겠습니다.

 

읽기만 가능한 Map은 mapOf, 변경가능한 MutlableMap은 mutableMapOf로 생성할 수 있습니다.

예를 들어 String을 Key로하고 Int를 Value로 하는 경우 < >을 사용하여 data sturcture을 명시할 수 있습니다.

Map<String, Int>, MutableMap<String, Int>

 

가장 쉬운 key-value pair 생성 방식은 to 를 사용하는 방법입니다.

// Read-only map
val readOnlyChannel = mapOf("KBS1" to 9, "MBC" to 11, "SBS" to 5)
println(readOnlyChannel)
// {KBS1=9, MBC=11, SBS=5}

// Mutable map with explicit type declaration
val channel: MutableMap<String, Int> = mutableMapOf("KBS1" to 9, "MBC" to 11, "SBS" to 5)
println(channel)
// {apple=100, kiwi=190, orange=100}

 

List와 동일하게 Map의 value에 접근하는 방법은 indexed access operator []을 사용하는 것입니다. count()의 사용법도 List와 동일합니다. 

 

Map에서는 key-value pair 추가를 위해 put(), 삭제를 위해 remove() function을 사용합니다.

val channel = mutableMapOf("KBS1" to 9, "MBC" to 11, "SBS" to 5)
channel.put("KBS2", 7)
println(channel)
// {KBS1=9, MBC=11, SBS=5, KBS2=7}

channel.remove("SBS") // key를 사용하여 삭제
println(channel)
// {KBS1=9, MBC=11, KBS2=7}

 

Map에서 key를 포함하고 있는지 확인하는 function containsKey()를 제공합니다. 

val channel = mutableMapOf("KBS1" to 9, "MBC" to 11, "SBS" to 5)
println(channel.containsKey("MBC")) //key가 존재하면 true, 없으면 false
// true

 

Map에서 key와 value를 각각 확인할 수도 있습니다. keys와 values를 사용하여 얻을 수 있습니다.

val channel = mutableMapOf("KBS1" to 9, "MBC" to 11, "SBS" to 5)
println(channel.keys)
// [KBS1, MBC, SBS]
println(channel.values)
// [9,11,5]

 

SW사업의 투입인력 기준 대가산정을 위해 한국소프트웨어산업협회 KOSA에서 매해 공표하는 평균임금입니다.

 

[자료실]_유의사항_및_일러두기_20231219.hwp
0.01MB
[자료실]_2024년_적용_SW기술자_평균임금_공표_20231219.hwp
0.11MB
[자료실]_2024년_적용_SW기술자_평균임금_공표_20231219.xlsx
0.08MB

 

Kotlin 기본 Data Types에 대한 정리가 끝나가네요.

 

Array(배열)은 지금까지의 기본 Daya Types과는 조금 다릅니다.

Array는 Data Type보다는 Data Structure라고 표현하는 것이 맞습니다.

Array는 index와 value 쌍으로 조합된 값을 할당할 수 있는 데이터 구조를 가지고 있기 때문입니다.

 

Array in Kotlin

 

Array 생성

Array는 위에 있는 constructor를 통해서 생성할 수 있지만, 실제로는 Kotlin에서 제공하는 function들을 사용하는 것이 유용합니다.

  • arrayOf() - Array에 할당할 values를 입력하여 생성합니다.
  • arrayNulls() - Arrya size를 입력하여, 각 value가 null 인 Array를 생성합니다.
  • emptyArray() - 빈 Array를 생성합니다.
  • Array constructors - Array size와 초기화 값으로 Array를 생성합니다.

무슨 말인지 좀 어려워 보입니다. 아래 코드를 보시면 이해가 되겠지요.

//arrayOf
//1,2,3,4,5가 담긴 Array 생성
val intArray = arrayOf(1,2,3,4,5)
println(intArray.joinToString()) //joinToString()는 Array values들을 String로 반환
//출력결과: 1,2,3,4,5

//arrayOfNulls()
//size가 3이고 values가 null인 Array 생성
val nullArray: Array<Int?> = arrayOfNulls(3)
println(nullArray.joinToString())
//출력결과 null, null, null

//emptyArray
//비어있는(size 0인) Array생성
var array1 = emptyArray<String>()
var array2: Array<String> = emptyArray()

//Array constructor
//size가 5이고 초기값이 1인 Array 생성
val initArray = Array<Int>(3, {0})

 

Nested Array (중첩 배열)

Array안에 value에는 Array가 사용될 수 있습니다. 그런 경우를 Nested Array(중첩 배열)이라고 합니다.

 

// two-dimensional array
val twoDArray = Array(2) { Array<Int>(2) { 0 } }
println(twoDArray.contentDeepToString())
// [[0, 0], [0, 0]]

// three-dimensional array
val threeDArray = Array(3) { Array(3) { Array<Int>(3) { 0 } } }
println(threeDArray.contentDeepToString())
// [[[0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0]]]

 


Array value 접근

val oneDimArray = arrayOf(1, 2, 3) // 1차원 배열
val towDimArray = Array(2) { Array<Int>(2) { 0 } } //2차원 중첩배열

// Array value 변경
oneDimArray[0] = 10
towDimArray[0][0] = 2

println(oneDimArray[0].toString()) // 10
println(towDimArray[0][0].toString()) // 2

 

Array 비교

Array의 값들을 비교 위해서 == 혹은 !=을 사용하면 안됩니다. 두 operator는 생성된 Array(Array object(객체))가 동일한 객체를 가리키는지 여부를 확인하기때문입니다.

값을 비교하기 위해서는 Kotlin에서 제공하는 함수를 사용해야 합니다.

val array1 = arrayOf(1,2,3)
val array2 = arrayOf(1,2,3)

println(array1.contentEquals(array2))
//결과: true

array2[0] = 5
println(array1.contentEquals(array2))
//결과: false

 


Kotlin 공식문서에 따르면, 

Use arrays in Kotlin when you have specialized low-level requirements that you need to meet. For example, if you have performance requirements beyond what is needed for regular applications, or you need to build custom data structures. If you don't have these sorts of restrictions, use collections instead.

이라고 되어 있습니다.

대충 이해해보면, "특별한 성능 요구 사항이 있거나 저의하고 싶은 데이터 구조를 만들어야 하는 경우를 제외하고는 가능한 Collection을 사용하록 합니다." 정도입니다. 이유는 아래 내용으로 설명합니다.

  • Collections는 읽기 전용일 수 있으므로 더 많은 제어 기능을 제공하고 명확한 의도가 있는 강력한 코드를 작성할 수 있습니다.
  • Collections에 요소를 추가하거나 제거하는 것은 쉽습니다. 이에 비해 Array는 크기가 고정되어 있습니다. Array에서 요소를 추가하거나 제거하는 유일한 방법은 매번 새 배열을 만드는 것인데 이는 매우 비효율적입니다.
  • Collections이 구조적으로 동일한지 확인하려면 연산자(==)를 사용할 수 있습니다. 배열에는 이 연산자를 사용할 수 없습니다. 대신 배열 비교에서 자세한 내용을 읽을 수 있는 특수 함수를 사용해야 합니다.
val array1 = arrayOf(1,2,3)
val array2 = arrayOf(1,2,3)

println(array1.contentEquals(array2))
//결과: true
println(array1 == array2)
//결과: false
    
array2[0] = 5
println(array1.contentEquals(array2))
//결과: false
println(array1 == array2)
//결과: false
   
val list1 = listOf(1,2,3)
val list2 = mutableListOf(1,2,3)

println(list1 == list2)
//결과: true

list2[0] = 10
println(list1 == list2)
//결과: false

 

위 예제에서 List는 Collection의 한 구현체입니다.


Array to Collections

Collection은 interface입니다. 즉, 누군가가 상속하여 사용해야합니다. 지금 이해가 안가도 상관없습니다.

Collection에는 대표적으로 List와 Set이 있습니다. List와 Set 역시 기회가 되면 자세히 다루겠습니다.

변환은 간단합니다. Kotlin에서 toList()와 toSet() function을 제공합니다.

val simpleArray = arrayOf(1,2,3)

val simpleList:List = array.toList()

val simpleSet:Set = array.toSet()

 

Primitive-type arrays

Kotlin에서는 Boolean, Byte, Char, Double, Float, Int, Long, Short의 경우에는 primitive-type Array를 제공합니다.

혁식은 Array앞에 primitive-type을 붙이면됩니다.

BooleanArray, ByteArray, CharArray, DoubleArray, FloatArray, IntArray, LongArray, ShortArray

Array안에 Data Type이 정해진 Array로 생각하면 됩니다.

예를 들어 CharArray는 Array<Char>과 동일한 뜻으로 보면됩니다.

CharArray in Kotlin

 

위의 Array 코드와 비교해보면 다른 점은 Char로 value의 Type이 정해져 있다는 것 뿐입니다.

 

String

String으로 표현하며 문자열을 의미합니다. "" 큰따옴표를 사용하여 표시합니다.

참고로, JVM에서 String의 크기는 하나의 character당 2bytes정도를 사용한다고 합니다.

정도라고 하면 정확히 2bytes는 아니라는 이야기가 되네요. JVM String은 UTF-16 encoding을 사용합니다. 일반적으로 한 문자당 2bytes로 표현가능하지만, 점점 인코딩 해야하는 글자들이 많아지니 2bytes만으로는 부족했나봅니다. 그래서 2bytes로 표현할 수 있는 문자에 포함되지 못한 문자들을 위해 4bytes로 확장을해서 표현하는 문자가 나타나게 됩니다. 

 

String에 있는 문자열 중 특정 문자에 접근하려면 어떻게 해야할까요?

문자열 출력
문자열 출력 결과

위의 예제처럼 String은 배열처럼 사용할 수 있습니다. 주의할 점은 대부분의 프로그램밍 언어가 그렇듯이 Kotlin에서 배열(Array)이나 리스트(List)의 첫번째 인덱스(Index)는 0부터 시작합니다.

 

두개의 문자열을 합치는 방법은 여러가지가 있습니다.

그중에서 가장 쉬운 방법은 연산자 +를 사용하는 방법입니다. \n은 줄바꿈을 나타내는 escape character입니다.

String 다루기
출력 결과

 

코드의 가독성을 높이기 위해 삼중따옴표안의 문자열을 정렬했습니다. 그렇게 되면 문자열 앞의 공백까지 출력되게 됩니다. 원하지 않는 결과입니다. String에는 trimMargin() function이 있습니다. margin prefix 앞에 공백을 없애는 기능을 합니다. 사용예는 아래와 같습니다.

tirmMargin()

 

String templates $

문자열을 출력할때 편하게 사용할 수 있는 방법입니다.

 

 

 

Characters

Char로 표현하며 문자하나를 의미합니다. 미리 살펴보았듯이 문자하나는 'C' 외따옴표를 사용하여 표시합니다.

 

마찬가지로 먼저 살펴보았던 \ (back slash)를 사용하여 완성되는 escape sequence를 확인해보겠습니다.

  • \t – tab
  • \b – backspace
  • \n – new line (LF)
  • \r – carriage return (CR) 
  • \' – single quotation mark
  • \" – double quotation mark
  • \\ – backslash
  • \$ – dollar sign

back slash를 사용한 escape sequence를 확인해보니 "키보드에 없는 문자는 어떻게 사용하지?"하고 궁금해졌습니다. 

그런 경우에는 unicode를 활용합니다.

unicode는 \u를 사용하여 unicode 임을 선언합니다.

val character = 'C'

val a:Char = 'A'

val unicodeWon = '\uFFE6' //₩

val unicodeBeta = '\u03B2' //β

 

unicode의 규격을 확인해서 내가 사용하고 싶은 코드를 확인하는 것은 방대한 내용때문에 거의 불가능합니다.

 

저는 이런 사이트를 이용합니다. 검색을 통해서 원하는 unicode를 확인할 수 있습니다.

'DEVELOPMENT > Kotlin' 카테고리의 다른 글

8. Kotlin, Basic Data Types - Arrays  (0) 2024.02.26
7. Kotlin, Basic Data Types - String  (1) 2024.02.26
5. Kotlin, Basic Data Types - Boolean  (0) 2024.02.25
4. Kotlin, Basic Data Types - Numbers  (0) 2024.02.23
3. Kotlin, Data Types - Basics  (0) 2024.02.22

Boolean

Boolean은 true, false 값을 가질 수 있으며, ?를 사용해서 null 값도 할당 가능합니다.

연산자는 아래와 같습니다. 

  • || – disjunction (logical OR) 하나라도 true 면 true  
  • && – conjunction (logical AND) 하나라도 false 면 false
  • ! – negation (logical NOT) true면 false, false면 true
val trueValue: Boolean = true
val falseValue: Boolean = false
val nullBoolean: Boolean? = null

println(trueValue || falseValue) // true
println(trueValue && falseValue) // false
println(!trueValue) // false
println(!falseValue) // true
println(nullBoolean) // null

 

|| 과 &&는 lazy(게으른) 연산자입니다.

  • a || b : a가 true이면 b의 값을 확인하지 않음. operand(피연산자)중 하나라도 true이면 true
  • a && b : a가 false이면 b의 값을 확인하지 않음. operand(피연산자)중 하나라도 false 이면 false

 

'DEVELOPMENT > Kotlin' 카테고리의 다른 글

7. Kotlin, Basic Data Types - String  (1) 2024.02.26
6. Kotlin, Basic Data Types - Characters  (0) 2024.02.25
4. Kotlin, Basic Data Types - Numbers  (0) 2024.02.23
3. Kotlin, Data Types - Basics  (0) 2024.02.22
2. Kotlin, Variables  (0) 2024.02.22

Kotlin에는 기본 Data types이 있습니다.

Category Basic Types more...
Integers Byte, Short, Int, Long 기본 정수 숫자(음의 정수,0, 양의 정수 모두 포함)
Types이 나뉘는 기준은 숫자의 크기
Unsigned integers UByte, UShort, UInt, ULong 0을 포함한 양의 정수
Types이 나뉘는 기준은 숫자의 크기
Floating-point numbers Float, Double 음의 실수, 0, 양의 실수
Types이 나뉘는 기준은 숫자의 크기
Booleans Boolean true, false, null 
Characters Char 문자 하나
Strings String 문자열

Integers types

Byte가 기본 값입니다. 1Byte는 8bits입니다.

컴퓨터는 우리가 쓰는 말과 다른 언어로만 이야기합니다. 

그 언어는 0과 1만 알아듣습니다. 8개의 칸에 들어갈 수 있는 것은 0 또는 1입니다.

0과 1로 이루어진 숫자 체계를 이진수라고 합니다.

Bit가 0일때는 숫자 할당이 안되며 1인경우에는 아래와 같이 2에 지수로 표현됩니다.

1BYTE

00000000은 10진수의 0입니다.

00000001은 10진수의 1(20)입니다.

00000010은 10진수의 2(21)입니다.

00000011은 10진수의 3(20 + 21)입니다.

00000100은 10진수의 4(22)입니다.

 

이런 식으로 숫자를 표현하며 최대한으로 표현할 수 있는 숫자는 0 ~ 255까지 256개의 숫자입니다.

그렇다면 실제로 그럴까요?

답은 아닙니다. Integer types은 음의 정수와 양의 정수 모두를 포함합니다.

따라서 -128 ~ 127까지의 256개의 숫자를 표현합니다.

부호를 인식하는 방식이나 표시하는 방식이 시스템이나 언어에 따라 다릅니다.

그렇지만 이론적으로 아래와 같운 설명이 가능합니다.

맨 왼쪽의 bit는 부호를 결정하는 bit입니다.

0이면 양의 정수

1이면 음의 정수

그렇다면 10000000은 어떤수일까요? -0일까요?

아닙니다. 10000000은 -128입니다.

11111111은 계산상으로는 더 커보이지만 맨 왼쪽 bit가 -를 나타내고 그 다음 자리부터 계산하게 되면 127 즉 - 127이 됩니다.

00000001은 양의 정수 1이고, 10000001은 음의 정수 -1입니다.

 

Integer에는 Byte, Short, Int, Long이 있습니다. 차이는 bits 사이즈입니다.

Byte 마찬가지로 맨 오른쪽 bit가 부호비트이고 나머지는 이진수 숫자를 나타내는 bit들입니다.

Type Bits Min value Max value
Byte 8 -128(-27) 127(27-1)
Short 16 -32,768(-215) 32,767(215-1)
Int 32 -2,146,483,648(-231) 2,147,483,647(231-1)
Long 64 -9,223,374,036,854,775,808(-263) 9,223,374,036,854,775,807(263-1)

 

명시적으로 Data type을 선언하지 않고 값을 할당하는 경우 Kotlin에서는 기본적으로 Int로 처리합니다.

즉, 4Bytes(32bits)의 메모리 공간을 사용합니다.

큰수를 다루지 않는 경우는 그 크기에 따라 Byte 또는 Short로 명시적 선언을 해주는 것도 좋습니다.

 

여기서 만약 명시적으로 Data type을 설정하지 않고 Int에 할당할 수 있는 값보다 더 큰 수를 할당한다면 어떻게 될까요?

자동적으로 Long으로 Data type이 선언됩니다.

Integer Types

 

Unsigned integer types

Kotlin에서는 부호가 없는 즉, 0과 양수의 정수 Data types도 제공합니다.

Type Bits Min value Max value
UByte 8 0 255(28-1)
UShort 16 0 65,535(216-1)
UInt 32 0 4,294,967,295(232-1)
ULong 64 0 18,446,744,073,709,551,615(264-1)

 

unsigned integer types

 

Floating-point types

실수의 경우, 부동 소수점 Float과 Double을 제공합니다.

앞에 알아보았던 정수의 경우는 소수점 이하자리가 없기 때문에 간단합니다. 2진수에 대해서만 이해하면 바로 이해가 됩니다.

소수점을 포함한 실수는 좀 복잡합니다. 대략 적으로 이해하기 위해 먼저 간단히 살펴보겠습니다.

Floating point types은 Sign(부호), Exponent(지수부), Mantissa(가수, Fraction이라고도 함)로 구성되어있습니다.

 

10진수 → 2진수로 변환

① 10진수를 나머지가 1혹은 0이될때까지 2로 나눈다

② 각 나눔 단계에서 나머지를 취한다.

무슨 말인지 직접 보면 쉽게 이해가 갑니다.

10진수를 2진수로 변환

 

10진수 소수점 이하 → 2진수로 변환

① 10진수의 c점 이하 숫자에 2을 곱하여 소수점 윗 자리를 취한다.

② 계산의 결과가 0이나 1이 나올때까지 반복합니다.

10진수 소수점 이하를 2진수로 변환

여기서 잠깐 이진수 계산에 따른 컴퓨팅의 한계를 살펴보겠습니다.

10진수 0.3은 2진수로 어떻게 표현될까요.

0.3 X 2 = 0.6  - 0

0.6 X 2 = 1.2  - 1

0.2 X 2 = 0.4 - 0

0.4 X 2 = 0.8 - 0

0.8 X 2 = 1.6 -  1

0.6 X 2 = 1.2 - 1

0.2 X 2 = 0.4 - 0

0.4 X 2 = 0.8 - 0

0.8 X 2 = 1.6 - 1

0.6 X 2 = 1.2 - 1

무엇인가 심상치 않지요? 계산이 끝나지 않고 계속 반복됩니다.

10진수 0.3을 2진수로 변환하게 되면 0.01001100110011001.... 이렇게 1001을 무한반복하게 됩니다.

즉 정확한 10진수 0.3이 아니라 그 근사치까지만 표현이 가능합니다.

이런 이유로 소수점을 포함한 계산은 컴퓨터에서 기본적으로 오류를 범할 수 있는 가능성을 내포하고 있습니다.

 

그렇다면 Floating-points types인 Float와 Double은 어떨까요? 

답은 "마찬가지다." 입니다. 여전히 소수점 이하자리에 대한 오류를 포함하고 있습니다.

따라서 Float와 Double을 통해 계산을 할 경우에는 조심해야겠지요. 지금은 계산 오류가 있을 수 있다 정도만 알겠습니다.

 

Float은 4Bytes 즉, 32bits의 크기를 갖습니다.

  • Sign(부호) : 맨 왼쪽 1bit. 0이면 양수, 1이면 음수
  • Exponent(지수) : Sign bit 이후 8bits, 지수를 나타냅니다.
  • Mantissa(가수) : 23bits, 가수 또는 유효 숫자를 나타냅니다.

도대체 무슨 말일까요? Kotlin만이 아니라 대부분의 언어는 숫자에 따른 규격을 따릅니다.

실수에 대해서는 IEEE Standard for Floating Point Arithmetic(IEEE 754) 규경을 따릅니다.

 

25.625를 Float으로 표현해보겠습니다.

우선 Sign은 양수 이므로 0입니다.

25.625를 이진수로 표현하면 11001.101(2)입니다.

소수점을 맨앞에 1이후로 옮겨보겠습니다. 

1.1001101(2)

소수점이 앞으로 4bit 이동 했습니다.

11001.101(2) = 1.1001101 x 24

로 표현할 수 있으며, 이러한 표현을 정규화 표현 방식이라고 부릅니다.

정규화 표현식의 1.1001101에서 소수점 아래자리 즉 1001101으로 Mantissa로 사용합니다.

 

이제 Exponent(지수)를 채워보겠습니다.
지수가 4입니다. Exponent 8bits의 bias(bias = 2n-1, n은 Exponent bit수)를 더해줍니다. 

4+127 = 131을 이진수로 변환하면 10000011(2)입니다. 가수부에 채워주게 되면 아래와 같이 완성됩니다.

01000001110011010000000000000000이 Float로 표현된 25.625입니다.

검산을 해보려면 IEEE-754 Floating Point Converter에서 확인할 수 있습니다.

 

Double은 8Bytes 즉, 64bits의 크기를 갖습니다.

  • Sign(부호) : 맨 왼쪽 1bit. 0이면 양수, 1이면 음수
  • Exponent(지수) : Sign bit 이후 11bits, 지수를 나타냅니다.
  • Mantissa(가수) : 53bits, 가수 또는 유효 숫자를 나타냅니다.

변환 방식은 Float와 유사합니다.

 

Double과 Float에 대해서 정리해보았습니다.

Type Bits Sign bit Exponent bits Mantissa bits
Float 32 1 8 24
Double 64 1 11 53

 

Kotlin에서 Double과 Float는 정수부와 소수점 그리고 소수점 이하 숫자로 이루어 집니다.

val doubleNmuber = 1.0 //Double
// val one: Double = 1 // Double로 선언한 변수에 Int를 할당하여 Error가 발생

val floatNumber = 1.0f // 숫자뒤에 f 혹은 F를 붙여 float으로 할당
// val floatNumber2: Float = 1.0  // Float으로 선언한 변수에 Double을 할당하여 Error 발생

 

 

Kotlin에서는 몇가지 수의 표현 방식이 있습니다.

  • Decimal : 123
  • Long : 123L
  • Binaries(2진수) : 0b00001011
  • Hexadecimals(16진수): 0x0F
  • Octaldeciamls(8진수): Kotlin에서 지원하지 않음
  • Double : 125.123, 1.25123e10
  • Float : 124.123f, 124,123F
  • Unsigned : 10u, 10U
  • Unsigned Long : 1000uL, 1000UL

Kotlin에서는 코드의 가독성을 위해 숫자 자리수를 표현할 수 있도록 제공해줍니다.

우리가 현실세계에서 사용할때는 보통 , 를 사용합니다. 10,000이런 식으로요. 

Kotlin에서는 _ 를 사용합니다.

val thousand = 1_000
val tenThousand = 10_000
val million = 1000_000
val longNumber = 123_456_789_000_000L
val hexBytes = 0xFF_EF_CA
val bytes = 0b10101001_01110100_11110000

 

Kotlin에서 한가지 재미있는 현상을 볼까요? 

 

Kotlin에는 java에는 없는 === 비교 연산자가 있스니다. 객체(레퍼런스)를 비교하는 연산자 입니다. 나중에 자세히 다뤄보겠습니다.

우선 JVM의 메모리 관리 특성상 Byte의 범위안의 숫자(-128~127)로 할당된 숫자는 nullable 변수에 할당할경우 레퍼런스로 참조하게됩니다.즉, b와 c 모두가 나는 a의 값을 참조해라고 하는거요. 그래서 값도 같고, 객체를 비교했을때도 같습니다.

그러나 Byte 범위 안의 숫자가 아닌 경우, 각자 메모리의 값을 할당하고 값을 저장합니다. 그렇게 되면 서로 다른 객체로 메모리에 저장되겠지요. 지금은 좀 헷갈리지만 때가되면 자세히 다루어보겠습니다. 

 

Kotlin에서는 모든 숫자 types들 간의 명시적인 변환을 지원합니다.

  • toByte() : Byte
  • toShort() : Short
  • toInt() : Int
  • toLong() : Long
  • toFloat() : Float
  • toDouble() : Double

참고로 명시적으로 변환하지 않아도 자동으로 변환되는 경우도 있스빈다. 

// Long으로 명시된 1에 정수 3을 더한 longNumber는 Long type 
val longNumber = 1L + 3

 

우리가 잘 알고 있는 10진수, 앞에서 살펴본 2진수, 그외에도 16진수가 우리에게 친근합니다.

컴퓨터는 2진수를 기반으로 사람과 소통합니다. 컴파일러는 우리가 만든 코드를 2진수 기계어로 변환하여 컴퓨터가 이해하도록 합니다.

물론 코드를 구현하다보면 16진수를 사용하여 수를 표현하기도 합니다.

예를 들어 색을 표현하기 위해 RGB code를 사용합니다.

RGB는 Red, Green, Blue를 의미하며, 빛의 3원색(trichromat of ligth)라고 합니다.

우리가 흔히 말하는 3원색과는 조금 다르죠? 흔히들 3원색하면 빨노파라고 이야기들 합니다. 

이야기가 다른 곳으로 빠지는 듯 하지만 잠시 정확한 지식을 위해 확인해보면,

Magneta(자홍), Yellow(노랑), Cyan(청록)라는 색의 3원색(trichromat of color)가 있습니다.

빛의 3원색은 태양, 전등, 모니터, 촛불 등 광원에서 나오는 색

색의 3원색은 우리가 사물을 바라보았을때 빛을 반사해서 받아들이는(보이는) 색을 말합니다.

초록색 나뭇잎은 초록색 파장의 색만 반사하고 나머지 파장의 색은 흡수하기 때문에 파랗게 보입니다. 이런 것을 우리는 색이라고 하지요.

색의 3원색이 빨노파가 아닌가요? 라고 의문이 듭니다.

3원색은 3가지 색을 조합하면 어떤 색이든 만들 수 있기 때문에 3원색이라고 합니다.

물감을 섞어서 여러가지 색을 만들때 3원색은 정확히 자홍, 노랑, 청록이 맞습니다. 하지만, 빨노파의 기억이 좀 더 정겹기는 합니다.

R을 0~255, G 0~255, B 0~255로 표현하여 색의 조합을 표현합니다.

RGB를 모두 섞으면 흰색 빛이 됩니다. 모니터에서 흰색 화면이 보인다면 그건 R 255, G 255, B 255 입니다.

컴퓨터는 2진수를 이해하기에 흰색을 이진수로 표현하면 R 11111111, G 11111111, B 11111111입니다. 

즉 111111111111111111111111(2)입니다. 이런 경우 코드에서는 좀더 명시적인 16진수를 사용합니다. 

Kotlin에서는 16진수를 0x를 붙여 표현합니다. 그럼 희색은 0xFFFFFF가 되겠네요.

 

'DEVELOPMENT > Kotlin' 카테고리의 다른 글

6. Kotlin, Basic Data Types - Characters  (0) 2024.02.25
5. Kotlin, Basic Data Types - Boolean  (0) 2024.02.25
3. Kotlin, Data Types - Basics  (0) 2024.02.22
2. Kotlin, Variables  (0) 2024.02.22
1. Kotlin "Hello, world!!!"  (0) 2024.02.22

+ Recent posts