클래스 내에서 함수를 override 할 때는 operator 키워드를 붙여줄 필요는 없다.
Comparable 을 상속해서 , compareTo 함수를 오버라이딩 해주면 끝.
이 compareTo 는 operator 에서 < == > 등을 비교하는데 쓰인다.
2) In Range
이 문제는 보자마자 뭔 개소리지?? 하는 느낌을 주었다.
애초에 자바에 in 같은 키워드가 없기 때문이다. (적어도 난 안쓴다.)
쿼리로 따지면 Between 같은 느낌의 연산자이다.
"연산자" 이다. < > == 같은 연사자.
따라서 연산자 오버라이딩을 하면 된다.
Range
A range defines a closed interval in the mathematical sense: it is defined by its two endpoint values which are both included in the range. Ranges are defined for comparable types: having an order, you can define whether an arbitrary instance is in the range between two given instances. The main operation on ranges iscontains,which is usually used in the form ofinand!inoperators.
To create a range for your class, call therangeTo()function on the range start value and provide the end value as an argument.rangeTo()is often called in its operator form...
class DateRange(val start: MyDate, val endInclusive: MyDate){
operator fun contains(item: MyDate): Boolean =
item >= start && item <= endInclusive
}
fun checkInRange(date: MyDate, first: MyDate, last: MyDate): Boolean {
return date in DateRange(first, last)
}
이 문제는 이런 식으로 풀면 된다.
DateRange 라는 클래스를 만들고, 이 클래스의 main constructor 는 start 와 endinclusive 를 받는다.
그리고 자기가 갖고 있는 start와 endEnclusive 로 넘어오는 MyDate instance 가 범위 안에 속하는지 아닌지를 반환하는 contains 함수를 정의 해준다.
여기서 알 수 있는 것은, "operator" 키워드를 사용해서, 타 클래스의 연산에 관여할 수 있는 클래스를 만들 수 있다는 것이다.
또한 MyDate는 이 전 문제 (1번) 에서 재 정의한 compareTo 함수를 갖고 있다는 것을 명심하자.
1) compareTo 로 대소를 배교할 수 있는 클래스(MyDate) 를 만들게 됨.
2) 이제 대소를 비교할 수 있으니 contains 함수도 만들 수 있음. in 연산자를 오버라이딩 한 클래스가 대신 이것을 처리 해 줌.
3) RangeTo
확장 함수로 연산자 오버로딩을 할 수 있다.
이 전 2번 문제에서는
date in DateRange(first,last) ... 방식으로 범위 안에 속하는지를 검사 했지만.
date in first..last 형식으로 이를 바꾸는 것이다.
date 도 first 도 last 도 모두 MyDate 클래스의 인스턴스이다.
.. 을 사용하기 위해서는 rangeTo operator 를 만들어 줘야 한다.
operator fun MyDate.rangeTo(other: MyDate) = DateRange(this, other)
class DateRange(override val start: MyDate, override val endInclusive: MyDate): ClosedRange<MyDate>
위와 같이, operator fun MyDate.rangeTo(other: MyDate) = DateRange(this, other)
로 만들면 된다.
2단원 부터 갑자기 난이도가 올라가는 면이 있는데, 대부분이 오버라이딩에 관련된 문제인데 함수의 원형을 알기 힘들기 때문이다.
in 은 단순히 대소 비교를 통해, 범위 안에 값이 들어가는지 확인만 하는 것은 아니다. (아닌가?_
for 문 안에서도 사용될 수 있다.
예를 들면
for(date in firstDate..secondDate) {
}
와 같이 사용된다.
하지만 여기서는 같은 in 키워드여도 다른 것이 필요하다. 단순한 ClosedRange 가 아닌, Iterable 을 상속해야 하며.
Iterabe 인터페이스를 구현하면, Iterator를 반환해야 하고.
이 Iterator를 반환하기 위해 새로운 클래스를 만들어야 한다.
그냥 Iterator를 anonymous object로 전달하자 무한 루프에 빠져 버렸다. (다른 요인일 수도 있다.)
anonymous object로 만들지 말고, 클래스를 하나 만들고, 인스턴스로 Iterator를 하나 만들어서 뱉어주니 잘 작동했다.
class DateRange(val start: MyDate, val end: MyDate): Iterable<MyDate>{
override fun iterator(): Iterator<MyDate> = DateIterator(this)
}
class DateIterator(val dateRange:DateRange) : Iterator<MyDate> {
var current: MyDate = dateRange.start
override fun next(): MyDate {
val result = current
current = current.nextDay()
return result
}
override fun hasNext(): Boolean = current <= dateRange.end
}
fun iterateOverDateRange(firstDate: MyDate, secondDate: MyDate, handler: (MyDate) -> Unit) {
for (date in firstDate..secondDate) {
handler(date)
}
}
이런 식으로 작서 ㅇ되는데...
firstDate..secondDate 를 보면 알 수 있듯이, rangeTo 연산자를 오버라이딩 했다.
rangeTo 함수는 이전 3번처럼 DateRange 인스턴스를 반환하다. DateRange 는 Iterable 을 구현한 클래스이다.
Iterator 는 next 와 hasNext 함수를 상속해줘야 한다. 살짝 IntelliJ의 자동 완성 기능의 도움을 받으면 쉽게 오버라이딩 할 함수의 틀을 만들 수 있다.
5) Operator overloading
오버로딩과 오버라이딩의 차이는 잘 알 것이다.
보통 부모의 함수를 상속받아, 자기 껄로 대체 하는것을 오버라이딩이라고 하고.
함수 이름은 같은데 매개변수가 다른 여러 함수를 만드는 것을 오버로딩이라고 한다.
오버라이딩은 런타임 시에, 오버로딩은 컴파일 시에 정의 된다...
간략히 여기 까지만 설명 하고.
연산자 오버라이딩에서 왜 갑자기 연산자 오버로딩이 제목이 된 걸까? 이를 활용하는 것이 이번 문제의 핵심이자. 정말 재밌는 문제다.
난 못 풀었다.
6) Destructing declarations.
자바스크립트에 "구조 분해 할당" 이란 것이 있다.
그거랑 비슷 한건데,
fun isLeapDay(date: MyDate): Boolean {
val (year, month, dayOfMonth) = date
// 29 February of a leap year return year % 4 == 0 && month == 2 && dayOfMonth == 29 }
이렇게 있을 때 MyDate 의 필드 값들이 year, month, dayOfMonth 에 분해돼 들어가는 느낌을 주는...
이것 또한 연산자로 지정해 줘야 한단다...
다음과 같다.
class MyDate(val year: Int, val month: Int, val dayOfMonth: Int) {
operator fun component1(): Int {
return year
}
operator fun component2(): Int {
return month
}
operator fun component3(): Int {
return dayOfMonth
}
}
componentN() 을 정의해주면 된다.
N 에 1~N 까지으 숫자가 들어간다.
6) Invokable
별 거 없는 문제인데 한 번 기억해두면 좋을 요소가 하나는 있다.
class Invokable {
var numberOfInvocations: Int = 0
private set
operator fun invoke(): Invokable {
numberOfInvocations++
return this
}
}
fun invokeTwice(invokable: Invokable) = invokable()()
짜잔.
invoke 라는 연산자를 오버라이딩하면..
인스턴스에다가 () 를 붙여서 함수처럼 쓸 수 있다... 위에 invokeTwice 는 말 그대로 invoke 두번 한거다.
여기서 private set 부분을 한번 보고 넘어가자.
Kotlin 에서는 클래스 안에 var a = 3 이런 식으로 "필드" 를 선언해 놓으면 자동으로
get 과 set 함수가 생긴다.
자바에서는 아마 @Getter @Setter 등을 붙이거나. (lombok 사용 시에) 직접 만들어야 했을 것이다.
만약 상수 선언을 의미하는 val 을 쓴다면? setter 가 생기지 않는다.
위에서 필드 선언 부분 아래에서 private set 부분을 확인할 수 있다.
set 함수를 private으로 만든 것이다. var 로 선언했기 때문에 값이 변할 수는 있지만, 이 값을 외부에서 직접 특정 값으로 수정하지는 못하게 한 것이다.
"코틀린은 함수 선언시에 기본 값을 지정할 수 있다. 또한 함수 호출 시에 파라미터 이름과 함께 넘길 수 있다."
1.4 Default Arguemnts
위에 설명한, 기본 값 지정에 대한 문제이다. 나처럼 풀면 이렇게 된다.
fun foo(name: String, number: Int, toUpperCase: Boolean) = (if (toUpperCase) name.toUpperCase() else name) + number
fun foo(name: String, number: Int) = foo(name, number, false)
fun foo(name: String) = foo(name, 42)
fun foo(name: String, toUpperCase: Boolean) = foo(name, 42, toUpperCase)
fun useFoo() = listOf( foo("a"), foo("b", number = 1), foo("c", toUpperCase = true), foo(name = "d", number = 2, toUpperCase = true) )
하지만 코틀린은 "기본 값을 지정" 할 수 있다.
다음과 같이 간단히 작성 할 수 있다.
fun foo(name: String, number: Int = 42, toUpperCase: Boolean = false) = (if (toUpperCase) name.toUpperCase() else name) + number
fun useFoo() = listOf( foo("a"), foo("b", number = 1), foo("c", toUpperCase = true), foo(name = "d", number = 2, toUpperCase = true) )
1.5 Lambdas
람다는 자바에서도 8 버전에서부터 지원하는 것으로 알고 있다.
코틀린 또한 당연히 람다를 지원한다.
함수를 파라미터로 받는 함수를, Higher order Function이라고 한다.
이런 함수에, 파라미터를 넘겨줄 때 람다를 쓰면 아주 깨끗한 코드를 만들 수 있다. (사람마다 코드의 미적 기준은 다를 수 있다.)
아래가 그 예시이다.
// Lambdas are code blocks enclosed in curly braces. items.fold(0, { // When a lambda has parameters, they go first, followed by '->' acc: Int, i: Int -> print("acc = $acc, i = $i, ") val result = acc + i println("result = $result") // The last expression in a lambda is considered the return value: result })
함수 타입은 다음과 같이 나타낸다.
(R, T) -> R
코틀린은 변수를 쓸 때 var a: Int = 1 이런 식으로 쓸 수 있다.
함수 타입이 (R, T) -> R 과 같은 방식이란 말은
var sampleFunction: (Int, Int) -> String = { a: Int, b: Int -> "the result is $a + $b" } print(sampleFunction(1, 2))
결과: the result is 3
요런 식으로 쓸 수 있다는 말이다.
코틀린은 특이한 문법이 있다.
가장 마지막에 들어가는 "함수 인자" 는 parenthesis 밖으로 뺄 수 있다. (paraenthesis = ( or ) )
이게 무슨 개소린가 싶기도 한데...
fun higherOrderFunctionDefault(a: Int, b: Int, f: (Int, Int) -> Int): Int { return f(a, b) }
var result = higherOrderFunctionDefault(1,2, {a: Int, b: Int -> a + b})
따라서 위와 같은 경우 init 이 먼저 시작된다. secondary constructor 보다는 init 이 우선시 된다.
main constructor 가 없다? 없을 수는 없다. 사실 암시적으로 갖고 있는 것이다. 마치 자바의 기본 생성자가 있듯이... 만약 main constructor 가 없다면, 암시적으로 호출하고 있다고 생각해야 한다.
그렇다면 순서대로라는 것은 무슨 의미인가?
class InitOrderDemo(name: String) {
val firstProperty = "First property: $name".also(::println)
init {
println("First initializer block that prints ${name}")
}
val secondProperty = "Second property: ${name.length}".also(::println)
init {
println("Second initializer block that prints ${name.length}")
}
}
위와 같은 경우 반드시 순서대로 시작된다. (위의 also 는 kotlin 의 확장 함수 중 하나이다. 자기 자신을 매개변수로 넘기고, 자기 자신을 return 한다.
val person: Person = getPerson().also {
print(it.name)
print(it.age)
}
마지막엔 자동으로 자기 자신을 return 한다.
5. main constructor 에는 연산을 넣을 수 없다. 따라서 연산을 위해서는 클래스 안에 init 을 사용하도록 해라.
6. class 는 클래스 : 부모클래스() 와 같이 상속. interface 는 클래스: 언터페이스 와 같이 상속한다.
7. 상속 하려면 open 키워드를 붙여야 한다.
다른게 무지 많다. 자세히 알아보지도 않고 대충 정리만 해놨다. 무엇보다 1번이 가장 애매한데... 초기화 안하면 컴파일러에서 빽빽돼서 저리 적어 놓았다.
1.8 Nullable Type
꽤나 중요한 문제이다.
fun sendMessageToClient(
client: Client?, message: String?, mailer: Mailer
){
val email = client?.personalInfo?.email
if (email != null && message != null) {
mailer.sendMessage(email, message)
}
}
위가 정답이다. 깔끔하고 멋있다. kotlin 의 ? 의 힘이다.
자료형에 ? 가 붙어있는 것을 볼 수 있는데, null 이 들어갈 수도 있는 자료형을 나타낸다.
인스턴스에 ? 를 붙인뒤, 인스턴스의 자료 혹은 함수에 접근하려고 하면, null 을 자동으로 체크해준다.
만약 접근하려는 놈이 null 일 경우 그냥 null 을 반환한다. 그래서 마지막에 email != null 을 체크하는 것이다.
굳이 이 흉한 코드를 올려놓은 이유는, !! 또한 키워드 중 하나이기 때문이다. ? 자료형의 변수에, !! 를 뒤에 붙여주면.
"이 값은 null 일리가 없다!" 라는 것을 의미한다. 따라서 String? 자료형의 변수는 String 으로 변해서 파라미터로 전달되는 효과가 있다.
1.9 smart casts
제목 그대로 "똑똑한 형변환" 을 의미한다. 일단 kotlin 에서는 if 문과 when 문을 사용해서 변수에 값을 할당할 수 있다는 것을 알고 넘어가자.
var a = 3;
var b = if(a == 3) 4 else 5
이런 식으로 쓸 수 있다는 말이다. (많이 신기함..)
위에서는 If문과 When 문이라고 했지만.. Kotlin 에서의 If 는 "Expression" 이다.
이게 뭔 의미냐면, 반환형이 있다는 말이다.
If 표현의 마지막 문장은 자동으로 "반환 값" 이 된다.
따라서 Kotlin 에는 ? a : b 표현이 없다. If 로 충분하기 때문이다.
when 은 Java에서의 Switch 랑 비슷한데 여러모로 다른 점이 있다.
fun eval(expr: Expr): Int =
when (expr) {
is Num -> expr.value
is Sum -> eval(expr.left) + eval(expr.right)
else -> throw IllegalArgumentException("Unknown expression")
}
위를 보면 알 수 있듯이, when 은 is 로 타입을 확인 할 수 있다. 자바로 따지면 instance of 같은 느낌이다.
왜 "똑똑한 형변환" 일까?
위에서 Expr 이 인터페이스라고 하자. 인터페이스를 구현한 클래스 A : Num , B : Sum 이 있다.
그럼 eval 함수에는 A 도, B 도 들어갈 수 있겠지?
이 때 when 의 is 로 타입을 구분해서, 사용할 수 있다는 말이다.
잘 보면 expr.value, expr.left expr.right 라고 저녛 다른 것들을 사용 하는 것을 볼 수 있다.
interface 가 when 절의 is 를 통과했을 때, "자동으로" is 에서 비교한 타입으로 형변환 해서 넘어간다.
따라서 is Num 을 통과한 녀석은 Num 자료형으로 자동으로 변경돼서 들어간다.
똑똑하다는 말이다.
when의 is 는 타입을 비교할 때 쓴다, 보통은 왼쪽에 "조건식" 이 들어간다.
when (num) {
1 -> "하나"
2 -> "둘"
else -> "둘 초과"
}
이런 식으로 사용할 수 있다.
만약 when 에 아무런 인자도 넘어가지 않는다면, 그냥 "순수한 조건식" 만을 이용해서 사용할 수도 있다.
var a = 3
when {
a == 3 -> "a는 3이 맞어"
else -> "a 는 3이 아니여"
}
이런 식으로 말이다.
물론 위와 같이 쓸 필요는 없다. 위와 같으 2개의 분기로 나눠 질때는 if를 쓰면 된다.
(Kotlin 에서의 Any 는 Java에서의 Object와 비슷한 맥락이지만. 절대 같지는 않다.)
class C {
// Private function, so the return type is the anonymous object type
private fun foo() = object {
val x: String = "x"
}
// Public function, so the return type is Any
fun publicFoo() = object {
val x: String = "x"
}
fun bar() {
val x1 = foo().x // Works
val x2 = publicFoo().x // ERROR: Unresolved reference 'x'
}
}
1.11 SAM Conversion
SAM Conversions
Just like Java 8, Kotlin supports SAM conversions. This means that Kotlin function literals can be automatically converted into implementations of Java interfaces with a single non-default method
1.12 Extension On Collection
문제 자체는 아주 간단하다.
제목 그대로 Collection을 어떻게 확장해서 사용하고 있는지를 알려준다.
차트를 보면 3단원에 제대로 Collection을 다룰 예정인듯 하니 기다리도록 하자.
이렇게 1단원을 끝냈다.
굳이 정리할 필요는 없겠지만, 기본 문법이니 만큼 적어두는 것이 좋다고 생각했다.
이 포스트의 내용 만으로는 완벽히 Kotlin을 이해하기 힘들다.
글을 쓰고 있는 사람부터가 이해하고 쓰는게 아니고, 이해하기 위해, 혹은 이해하고 나서 훗날 기억하기 위해 적고 있기 때문이다.