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보다 큰수가 아니기 때문에 반복문을 종료하였습니다.
무엇인가를 판단하여 사실인지 아닌지(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()를 사용합니다.
첨언을 하자면 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는 중첩될수 있습니다.
읽기만 가능한 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]
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이 구조적으로 동일한지 확인하려면 연산자(==)를 사용할 수 있습니다. 배열에는 이 연산자를 사용할 수 없습니다. 대신 배열 비교에서 자세한 내용을 읽을 수 있는 특수 함수를 사용해야 합니다.
참고로, 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 앞에 공백을 없애는 기능을 합니다. 사용예는 아래와 같습니다.
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가 되겠네요.