Kotlin Koans Workshop - I. introduction
이미지 출처: https://kotlinlang.org/assets/images/open-graph/kotlin_250x250.png
테스트케이스로 코틀린 문법익히기
Chapter1 - 주요기능 소개
Kotlin Github repository의 kotlin-koans 프로젝트를 기반으로 포스팅 되었음을 밝힘니다.
출처: https://github.com/Kotlin/kotlin-koans
목차
- 표현식으로 문자열 리턴하기
- Java코드를 Kotlin코드로 변환하기
- joinToString함수로 prefix와 postfix 붙이기
- default arguments 선언하기
- 람다식 이용하기
- String 템플릿 사용하기
- data class 사용하기
- nullable type 이용하기
- smart cast 이용하기
- 함수 확장하기
- 객체 표현식 이용하기
- SAM(Single Abstract Method) 구현하기
- 콜렉션 확장기능 이용하기
표현식으로 문자열 리턴하기 목차 ⇈
src package
1
2
3
4
5
6
7
// kotlin function에서는 리턴값을 표현식으로 선언할 수 있습니다.
fun task0() = "OK"
// java의 메서드와 유사하게 표현하면 다음과 같습니다.
fun task0(): String {
return "OK"
}
test package
1
2
3
4
5
6
// task0 function의 리턴값으로 문자열 "OK"가 리턴되면 성공하는 케이스입니다.
class N00StartKtTest {
@Test fun testOk() {
assertEquals("OK", task0())
}
}
Java코드를 Kotlin코드로 변환하기 목차 ⇈
src package(Java)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// IntelliJ IDEA 또는 Android Studio를 사용하는 경우 Java코드를 Kotlin코드로 변환 할 수 있습니다.
// .java 파일을 통째로 .kt 파일로 바꿀수도 있고, copy & paste만으로도 변환이 가능합니다.
// 아래 task1 메서드를 복사해서 IntelliJ IDEA 또는 Android Studio의 편집기의 .kt파일에 붙여 넣으면 자동으로 코드가 변환됩니다.
public String task1(Collection<Integer> collection) {
StringBuilder sb = new StringBuilder();
sb.append("{");
Iterator<Integer> iterator = collection.iterator();
while (iterator.hasNext()) {
Integer element = iterator.next();
sb.append(element);
if (iterator.hasNext()) {
sb.append(", ");
}
}
sb.append("}");
return sb.toString();
}
src package(Kotlin)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Kotlin 문법으로 변경된 function은 아래와 같습니다.
fun task1(collection: Collection<Int>): String {
val sb = StringBuilder()
sb.append("{")
val iterator = collection.iterator()
while (iterator.hasNext()) {
val element = iterator.next()
sb.append(element)
if (iterator.hasNext()) {
sb.append(", ")
}
}
sb.append("}")
return sb.toString()
}
test package
1
2
3
4
5
6
7
// 배열의 정수아이템을 문자열로 변경하고 해당값을 비교하는 케이스입니다.
// 이번 예제의 목적은 이미구현된 코드를 IDEA를 이용하여 변환하는 것이 목적입니다.
class N01JavaToKotlinConverterKtTest {
@Test fun collection() {
assertEquals("{1, 2, 3, 42, 555}", task1(listOf(1, 2, 3, 42, 555)))
}
}
joinToString함수로 prefix와 postfix 붙이기 목차 ⇈
src package
1
2
3
4
// 아래와 같이 Kotlin collection에서 제공하는 joinToString 함수를 이용하여 prefix와 postfix를 붙일수 있습니다.
fun task2(collection: Collection<Int>): String {
return collection.joinToString(prefix = "{", postfix = "}")
}
test package
1
2
3
4
5
6
// 문자열의 시작과 끝에 특정 문자를 조합하는 케이스입니다.
class N02NamedArgumentsKtTest {
@org.junit.Test fun testJoinToString() {
assertEquals("{1, 2, 3, 42, 555}", task2(listOf(1, 2, 3, 42, 555)))
}
}
default arguments 선언하기 목차 ⇈
src package(Kotlin)
1
2
3
4
5
6
7
8
9
10
// default argument를 함수의 선언부에 정의해서 함수 호출시 전달되지 않는값에 대해 자동으로 기본값을 할당합니다.
fun foo(name: String, number: Int = 42, toUpperCase: Boolean = false) =
(if (toUpperCase) name.toUpperCase() else name) + number
fun task3(): String {
return (foo("a") +
foo("b", number = 1) +
foo("c", toUpperCase = true) +
foo(name = "d", number = 2, toUpperCase = true))
}
src package(Java)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Kotlin 코드를 Java코드로 표현하면 아래와 같습니다.
private int defaultNumber = 42;
public String foo(String name, int number, boolean toUpperCase) {
return (toUpperCase ? name.toUpperCase() : name) + number;
}
public String foo(String name, int number) {
return foo(name, number, false);
}
public String foo(String name, boolean toUpperCase) {
return foo(name, defaultNumber, toUpperCase);
}
public String foo(String name) {
return foo(name, defaultNumber);
}
test package
1
2
3
@Test fun testDefaultAndNamedParams() {
assertEquals("a42b1C42D2", task3())
}
람다식 이용하기 목차 ⇈
src package
1
2
3
// 람다식을 이용하여 loop문을 직접적으로 사용하지 않고 배열을 순회할 수 있습니다.
// 아래 function에 사용되는 any함수의 경우 람다식 안의 결과가 1건이라도 true이면 true를 리턴합니다.
fun task4(collection: Collection<Int>) = collection.any { element -> element % 2 == 0 }
test package
1
2
3
4
5
6
7
@Test fun contains() {
assertTrue(task4(listOf(1, 2, 3)))
}
@Test fun notContains() {
assertFalse(task4(listOf(1, 3, 5)))
}
String 템플릿 사용하기 목차 ⇈
src package
1
2
3
4
// Kotlin에서는 dollar sign($)을 이용하여 문자열선언과 함게 변수를 정의할 수 있습니다.
// 아래처럼 task5 function을 정의하면 호출시 """\d{2} (JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC) \d{4}""" 문자열이 리턴됩니다.
val month = "(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)"
fun task5(): String = """\d{2} $month \d{4}"""
test package
1
2
3
4
5
6
7
8
9
10
11
@Test fun match() {
assertTrue("11 MAR 1952".matches(task5().toRegex()))
}
@Test fun match1() {
assertTrue("24 AUG 1957".matches(task5().toRegex()))
}
@Test fun doNotMatch() {
assertFalse("24 RRR 1957".matches(task5().toRegex()))
}
data class 사용하기 목차 ⇈
src package
1
2
3
4
5
6
// class 선언시에 'data' modifier를 추가하면 toString() function을 이용하여 객체의 내용을 출력할 수 있습니다.
data class Person(val name: String, val age: Int)
fun task6(): List<Person> {
return listOf(Person("Alice", 29), Person("Bob", 31))
}
test package
1
2
3
@Test fun testListOfPeople() {
assertEquals("[Person(name=Alice, age=29), Person(name=Bob, age=31)]", task6().toString())
}
nullable type 이용하기 목차 ⇈
src package
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fun sendMessageToClient(
client: Client?, message: String?, mailer: Mailer
) {
// 자료형 뒤에 safe call operator(?)를 붙이면 null값을 할당할 수 있고 safe call을 이용하여 NPE을 방지할 수 있습니다.
// email 상수에 값을 할당할때 safe call을 이용하면 다음과 같이 동작합니다.
// client가 null이 아니면 personalInfo속성에 접근
// personalInfo가 null이 아니면 email값을 할당
val email: String? = client?.personalInfo?.email
if (email != null && message != null) {
mailer.sendMessage(email, message)
}
}
class Client (val personalInfo: PersonalInfo?)
class PersonalInfo (val email: String?)
interface Mailer {
fun sendMessage(email: String, message: String)
}
test package
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
fun testSendMessageToClient(
client: Client?,
message: String?,
email: String? = null,
shouldBeInvoked: Boolean = false
) {
var invoked = false
sendMessageToClient(client, message, object : Mailer {
override fun sendMessage(actualEmail: String, actualMessage: String) {
invoked = true
assertEquals("The message is not as expected:",
message, actualMessage)
assertEquals("The email is not as expected:",
email, actualEmail)
}
})
assertEquals("The function 'sendMessage' should${if (shouldBeInvoked) "" else "n't"} be invoked",
shouldBeInvoked, invoked)
}
@Test fun everythingIsOk() {
testSendMessageToClient(Client(PersonalInfo("bob@gmail.com")),
"Hi Bob! We have an awesome proposition for you...",
"bob@gmail.com",
true)
}
@Test fun noMessage() {
testSendMessageToClient(Client(PersonalInfo("bob@gmail.com")), null)
}
@Test fun noEmail() {
testSendMessageToClient(Client(PersonalInfo(null)), "Hi Bob! We have an awesome proposition for you...")
}
@Test fun noPersonalInfo() {
testSendMessageToClient(Client(null), "Hi Bob! We have an awesome proposition for you...")
}
@Test fun noClient() {
testSendMessageToClient(null, "Hi Bob! We have an awesome proposition for you...")
}
smart cast 이용하기 목차 ⇈
src package
1
2
3
4
5
6
7
8
9
10
11
// 조상클래스가 동일한경우 type을 조건으로 when표현식을 사용할 수 있습니다.
// 조건에 따라 분기된 람다식에서 추가적인 캐스팅절차없이 원래의 자료형을 곧바로 사용할 수 있습니다.
sealed class 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.left) + eval(e.right)
}
test package
1
2
3
4
5
6
7
8
9
10
11
@Test fun testNum() {
assertEquals("'eval' on Num should work:", 2, eval(Num(2)))
}
@Test fun testSum() {
assertEquals("'eval' on Sum should work:", 3, eval(Sum(Num(2), Num(1))))
}
@Test fun testRecursion() {
assertEquals("'eval' should work recursively:", 6, eval(Sum(Sum(Num(1), Num(2)), Num(3))))
}
함수 확장하기 목차 ⇈
src package
1
2
3
4
5
// Kotlin에서는 기존 클래스의 function을 확작하여 사용할 수 있습니다.
data class RationalNumber(val numerator: Int, val denominator: Int)
fun Int.r(): RationalNumber = RationalNumber(this, 1)
fun Pair<Int, Int>.r(): RationalNumber = RationalNumber(this.first, this.second)
test package
1
2
3
4
5
6
7
@Test fun testIntExtension() {
assertEquals("Rational number creation error: ", RationalNumber(4, 1), 4.r())
}
@Test fun testPairExtension() {
assertEquals("Rational number creation error: ", RationalNumber(2, 3), Pair(2, 3).r())
}
객체 표현식 이용하기 목차 ⇈
src package
1
2
3
4
5
6
7
8
9
// 객체 표현식을 이용하여 객체를 생성 할 수 있습니다.
// 아래와 같이 객체 표현식을 이용하여 abstract메서드를 구현하면서 객체를 생성 할 수 있습니다.
fun task10(): List<Int> {
val arrayList = arrayListOf(1, 5, 2)
Collections.sort(arrayList, object : Comparator<Int> {
override fun compare(x: Int, y: Int) = y - x
})
return arrayList
}
test package
1
2
3
@Test fun testSort() {
assertEquals(listOf(5, 2, 1), task10())
}
SAM(Single Abstract Method) 구현하기 목차 ⇈
src package
1
2
3
4
5
6
// 하나의 abstract method를 가지고 있는 클래스를 구현해야 하는 경웅 람다식을 이용하여 간단하게 정의 할 수도 있습니다.
fun task11(): List<Int> {
val arrayList = arrayListOf(1, 5, 2)
Collections.sort(arrayList, { x, y -> y - x })
return arrayList
}
test package
1
2
3
@Test fun testSort() {
assertEquals(listOf(5, 2, 1), task11())
}
콜렉션 확장기능 이용하기 목차 ⇈
src package
1
2
3
4
5
// Kotlin 표준 라이브러리에는 컬렉션 작업을 보다 편리하게 해주는 많은 확장 기능이 포함되어 있습니다.
// 'sortedDescending'을 아래와 같이 사용할 경우 별도로 Comparator를 구현하지 않아도 됩니다.
fun task12(): List<Int> {
return arrayListOf(1, 5, 2).sortedDescending()
}
test package
1
2
3
@Test fun testSort() {
assertEquals(listOf(5, 2, 1), task12())
}