[Kotlin] Sequence

in Kotlin

Kotlin의 Sequence에 대해서 알아보려고 한다. 사실, [Kotlin in action] 책에서 설명이 나오지만, 당시에는 대략적으로만 이해하고 넘어갔던 것 같다. 하지만, 최근에 Collection과 Sequence의 차이를 물어보는 질문에 대해서 제대로 답하지 못했던 것 같아서 그 차이를 알기 위해 정리하려 한다.

Continue reading

코틀린에서 사용하는 더블콜론 참조에 대해서 알아보려고 한다.

리플렉션(Reflection)이란

코틀린에서 더블콜론(::)은 리플렉션을 위해 사용한다. 리플렉션이란 코드를 작성하는 시점에는 런타임상 컴파일된 바이트 코드에서 내가 작성한 코드가 어디에 위치하는지 알 수 없기 때문에 바이트 코드를 이용해 내가 참조하려는 값을 찾기위해 사용한다.

자바와 코틀린에서의 리플렉션

1
2
SomeClass.class -> 클래스 그 자체를 리플렉션
someInstance.getClass() -> 인스턴스에서 클래스를 리플렉션
  • 자바에서는 클래스를 참조할 때 위와 같이 사용한다.
1
2
SomeClass::class
someInstance::class
  • 코틀린에서는 더블콜론을 리플렉션을 위해 사용하며 위와 같은 형태로 사용한다.

하지만 우리가 액티비티를 넘길 때는 SomeClass::class.java와 같이 끝에 .java가 붙는 것을 볼 수 있다. 그 이유는 자바에서 쓰는 클래스와 코틀린에서 쓰는 클래스가 다르기 때문이다.

자바에서의 SomeClass.class는 Class를 리턴한다. 반면, 코틀린에서는 SomeClass::class를 하면 KClass를 리턴한다. 그렇기 때문에 KClass를 Class로 바꾸어 주어야하는데 이때 .Java를 이용하여 자바 클래스 값을 받는다.

KClass 안의 java의 getter는 위 그림과 같이 확장함수 형태로 되어있으며 KClass에서 자바의 클래스 타입을 반환한다.

코틀린에서의 함수참조

코틀린에서 SomeClass::class와 같이 클래스를 참조할 수도 있지만 SomeClass:Method와 같이 클래스 내의 메소드도 참조할 수 있고 ::Function 형식의 함수도 참조할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main(args: Array<String>) {
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8)

numbers.filter(::isOdd)
.forEach {
println(it)
}

}

fun isOdd(x: Int): Boolean {
return x % 2 != 0
}

// 단축형
//fun isOdd(x: Int) = x % 2 !=0
  • isOdd() 함수를 filter를 사용한 코드이다. filter를 사용하면서 함수참조를 이용해 ::isOdd 형태로 함수를 호출하였다.
  • 필터의 원형은 IntArray에서 Int 값을 받아 Boolean을 반환하는 inline 함수이다. 마찬가지로 Int를 받아 Boolean을 반환하기 때문에 함수를 참조하여 동일한 형식의 isOdd를 사용할 수 있다.

Reference

Comment and share

코틀린에서 제공되는 isEmpty, isNotEmpty, isBlank, isNotBlank, isNullOrBlank, isNullOrEmpty 함수의 차이점을 알아보려고 한다.

비교를 위해 사용할 문자열은 다음과 같다.

1
2
3
4
5
6
val emptyString = ""
val blankString = " "
val blankNewLineString = " \n"
val withBlankString = " victory"
val nullString: String? = null
val standardString = "standard"

isEmpty

  • CharSequence가 아무 값도 포함하고 있지 않을 때, true를 반환한다.
  • 결과는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
    println(emptyString.isEmpty())
println(blankString.isEmpty())
println(blankNewLineString.isEmpty())
println(withBlankString.isEmpty())
println(nullString?.isEmpty())
println(standardString.isEmpty())

// 결과
true
false
false
false
null
false

isNotEmpty

  • CharSequence가 어떠한 값을 포함하고 있을 때(공백 포함), true를 반환한다.
  • 결과는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
    println(emptyString.isNotEmpty())
println(blankString.isNotEmpty())
println(blankNewLineString.isNotEmpty())
println(withBlankString.isNotEmpty())
println(nullString?.isNotEmpty())
println(standardString.isNotEmpty())

// 결과
false
true
true
true
null
true

isBlank

  • CharSequence가 공백만을 가지고 있을 경우나 empty일 경우, true를 반환한다.
  • 결과는 다음과 같다.
  • 공백+개행문자 -> 공백으로 판단한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
    println(emptyString.isBlank())
println(blankString.isBlank())
println(blankNewLineString.isBlank())
println(withBlankString.isBlank())
println(nullString?.isBlank())
println(standardString.isBlank())

// 결과
true
true
true
false
null
false

isNotBlank

  • CharSequence가 empty 상황이 아니고, 값에 공백만 있지 않을 때 true를 반환한다.
  • 즉, 아무 값도 없거나 공백만 있을 때는 false를 반환한다.
  • 결과는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
    println(emptyString.isNotBlank())
println(blankString.isNotBlank())
println(blankNewLineString.isNotBlank())
println(withBlankString.isNotBlank())
println(nullString?.isNotBlank())
println(standardString.isNotBlank())

// 결과
false
false
false
true
null
true

isNullOrEmpty

  • CharSequence가 비어있거나 null일 경우, true를 반환한다.
  • 공백은 비어있는게 아니다. 그래서 false를 반환한다.
  • 결과는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
    println(emptyString.isNullOrEmpty())
println(blankString.isNullOrEmpty())
println(blankNewLineString.isNullOrEmpty())
println(withBlankString.isNullOrEmpty())
println(nullString.isNullOrEmpty())
println(standardString.isNullOrEmpty())

// 결과
true
false
false
false
true
false

isNullOrBlank

  • CharSequence가 null이거나 blank인 상황일 때, true를 반환한다.
  • 공백+개행 -> 비어있음(blank 상태)
  • 결과는 다음과 같다.
1
2
3
4
5
6
7
8

// 결과
true
true
true
false
true
false

Summary

어려운 내용은 아니지만, 헷갈리는 함수들이다. 그래서 사용할 때 자주 헷갈려서 정리를 했다. 주의해야 할 점은 다음과 같다.

  • empty : 정말 아무 값도 가지지 않는 것을 말한다.
    • blank보다 empty가 작은 개념.
  • blank : 공백만 있거나 정말 아무 값도 가지지 않는 것을 의미한다.
    • 공백+개행문자도 공백으로 판단
    • blank가 더 큰 개념.

Reference

Comment and share

커니의 코틀린이라는 책을 참고하여 코틀린을 공부했다. inline이라는 개념이 나왔지만 간단하게만 설명이 되어있었고, 필자도 간단하게만 정리하고 넘어갔다. 그런데 다시 보니 무슨 개념인지 모르겠어서 정리하려고 한다.

OverView

문서는 다음과 같이 설명되어있다.

고차 함수를 사용하면 런타임 패널티가 있기 때문에 함수 구현 자체를 코드 내부에 넣음으로써 오버헤드를 없앨 수 있다.

무슨 내용인지 잘 와닿지 않는다. 다음의 글을 보고 다시 생각해보자.

일급 함수

  • 일급 함수는 스스로 객체로써 취급되는 함수로 다른 함수를 파라미터로 전달받고 반환할 수 있는 함수를 뜻한다.
  • 코드를 통해서 확인해보자.
1
2
3
4
5
fun print(body: (Int, Int) -> Int){
println(body(5,5))
}

print({a,b -> a})
  • 위 함수는 두 개의 정수를 받아 하나의 정수값만을 출력하는 함수이다.
  • 위와 같은 선언을 하게 된다면 Java에서 아래와 같은 코드로 변화을 하여 사용할 수 있다.
1
2
3
4
5
public final void print(@NotNull Fuction2 body){
Intrinsics.checkParameterIsNotNull(body, "body");
Object result = body.invoke(5,5);
System.out.println(result);
}
  • 위와 같이 자동으로 컨버팅이 될 수 있기 때문에 아래와 같은 유동적인 방법도 사용이 가능하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private fun printResult(body: (Int, Int) -> Int){
println(body(10,5))
}

fun sum(a: Int, b: Int) = a + b
fun subtract(a: Int, b: Int) = a - b

fun main(args:Array<String>){
printResult(::sum)
printResult(::subtract)
}
// 결과
15
5
  • 위의 코드처럼 sum이나 subtract를 유동적으로 붙일 수 있다.

inline

1
2
3
4
5
6
7
8
9
fun doSomething(body: () -> Unit){
body()
}

fun callFunction(){
doSomething{
print("문자열 출력!")
}
}
  • 위의 코드를 자바로 표현하면 다음과 같다.
1
2
3
4
5
6
7
public void doSomething(Function body){
body.invoke();
}

public void callFunction(){
doSomething(System.out.println("문자열 출력!"));
}
  • 위의 코드와 같이 자바로 표현된다. 그리고 이 자바코드는 아래와 같이 변환된다.
1
2
3
4
5
6
7
8
public void callFunction(){
doSomething(new Function(){
@Override
public void invoke(){
System.out.println("문자열 출력!");
}
});
}
  • 문제는 위의 sum이나 subtract처럼 조합하는 함수가 많아질수록 계속 N개만큼의 function 오브젝트가 생성된다. 이럴때 사용하게 되는 것이 inline 키워드이다.
1
2
3
4
5
6
7
8
9
inline fun doSomething(body: () -> Unit){
body()
}

fun callFunction(){
doSomething{
println("문자열 출력")
}
}
  • 위의 코드는 아래와 같이 변환된다.
1
2
3
public void callFunction(){
System.out.println("문자열 출력!");
}
  • 그리고 실제로 컴파일 시 doSomething()의 body()를 호출하는 부분에 저렇게 선언된 함수가 그대로 들어가게 된다.
1
2
// body() 부분에 그대로 들어온다.
System.out.println("문자열 출력!");
  • 위와 같이 Fuction 인스턴스를 만들지 않고 callFunction 내부에 삽입되어 바로 선언되어지게 된다. 때문에 람다함수와 1급 함수가 호출된 곳에서 해당 함수를 가지게 된다.
  • 하지만 inline 함수는 주의할 점이 있는데, private 키워드를 사용하여 함수를 정의할 수 없다. 대신 다른 접근 한정자인 internal을 사용해야 한다.

noinline

  • 모든 람다함수에 inline을 쓰고싶지 않을 수 있다. 이 경우 아래와 같이 해당 람다 함수에 noinline 키워드를 추가해준다.
1
2
3
4
inline fun callLambda(aLambda: () -> Unit, noinline dontInlineLambda: () -> Unit,
aLambda2: () -> Unit){
// 실행.
}
  • 모든 함수를 inline을 사용하여 내부로 컨버팅 되어지길 원치않을 수 있다. 이 경우 위처럼 inline을 먼저 선언한 뒤 람다함수 중 사용하지 않을 함수에 noinline 키워드를 붙여준다.

참고

Comment and share

[Kotlin] Object

in Kotlin

코틀린에는 static 개념이 없다. 사실 개념이 없다기 보다는 static keyword가 없기 때문에 Java의 static 개념을 코틀린에서 표현할 수 없다. 그래서 이를 어떻게 표현하는지 중점적으로 살펴보겠다.

  • 싱글톤을 정의하는 방법
  • 동반 객체 companion object를 이용한 팩토리 메소드 구현
  • 익명 클래스 선언

위의 3가지를 object keyword를 이용해 표현한다.

싱글톤

코틀린에서는 object를 이용하여 클래스를 정의함과 동시에 객체를 생성할 수 있다. 말 그대로 싱글톤을 쉽게 구현할 수 있다. 이해하기 쉽게 간단한 예제를 만들었다.

1
2
3
4
5
6
7
8
9
class SharedPreference{
public static SharedPreference INSTACNE=null;

public SharedPreference(){
if(INSTACNE == null){
INSTACNE = SharedPreference();
}
}
}

위 코드는 자바에서 사용할 수 있는 간단한 싱글톤 패턴 구현 코드이다. 그럼 이제 코틀린에서 object를 사용해 바꿔보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
object SharedPreference{

private const val NAME = "Test"
private const val MODE = Context.MODE_PRIVATE
private latedinit var preferences: SharedPreferences

fun init(context: Context) {
preferences = context.getSharedPreferences(NAME, MODE)
}

// 생략.
}

fun main(args: Array<String>){
SharedPreference.init(applicationContext)
}

위의 코드처럼 object로 선언하면 클래스 선언과 동시에 객체가 생성된다. 따라서 객체 이름을 통해 property나 메소드에 접근할 수 있다.

object 클래스는 가장 바깥 클래스로 선언될 수도 있고, 내부에 중첩된 클래스 형태로 선언될 수도 있다. 하지만, 어떤 방식으로 선언되었던 간에 존재하는 object는 단일 객체만 존재한다.

companion object

코틀린에서는 static을 지원하지 않는 대신 top-level function을 통해 같은 효과를 낼 수 있다. 단, top-level function은 class 내부에 선언된 private property에는 접근할 수 없는 제한을 받는다.

이를 해결하기 위해서 companion object라는 개념이 존재한다. 클래스의 인스턴스 생성과 상관없이 호출해야 하지만 class의 내부 정보에 접근할 수 있는 함수가 필요할 때 companion obejct를 class 내부에 선언한다. Java로 따지면 class 내부에 static 함수를 넣는다고 생각하면 된다.

1
2
3
4
5
6
7
8
9
10
11
class A{
companion object{
fun print(){
println("Companion obejct call!!")
}
}
}

fun main(args: Array<String>){
A.print()
}

이처럼 A 클래스 내부에서 선언된 companion object는 호출할 때 클래스 이름으로 바로 호출할 수 있다. (Java의 static 함수와 동일한 형태이다.)

또한, companion object는 외부 클래스의 private property에도 접근이 가능하기 때문에, factory method를 만들 때 적합하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class User private constructor(val nickname: String){
companion object{
fun newSubscribingUser(email: String) = User(email.substringBefore('@'))

fun newFacebookUser(accountId: Int) = User(getFacebookName(accountId))
}
}

fun main(args: Array<String>){
val subscribingUser = User.newSubscribingUser("jhsw0375@gmail.com")
val facebookUser = User.newFacebookUser(4)

println(subscribingUser.nickname)
}

위에서 User 클래스는 private constructor를 가지기 때문에 외부에서 생성할 수 없다. 따라서 외부에서는 companion으로 제공되는 factory method를 이용해서만 객체를 생성할 수 있도록 제한할 수 있다.

companion object의 사용

companion object는 클래스 내부에 정의된 일반 객체이다. 따라서 아래와 같은 작업이 가능하다.

  • companion object에 이름 명명
  • companion object 내부에 확장 함수나 property 정의
  • 인터페이스 상속
1
2
3
4
5
6
7
8
9
10
class Person(val name: String){
companion object Loader{
fun fromJSON(jsonText: String): Person = ... 생략.
}
}

fun main(args: Array<String>){
person = Person.Loader.fromJSON("{name: 'kim'}")
person = Person.fromJSON("{name: 'lee'}")
}

companion object에 이름을 붙일 수 있으며, 이름을 통해서 호출할 수도 있고 그냥 호출할 수도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface JSONFactory<T> {
fun fromJSON(jsonText: String): T
}

class Person(val name: String){
companion object: JSONFactory{
override fromJSON(jsonText: String): Person = ... 생략
}
}

fun loadFromText<T>(factory: JSONFactory<T>): T{
... 생략.
}

fun main(args: Array<String>){
loadFromText(Person)
}

위의 코드처럼 companion object가 특정 interface를 구현할 수도 있고, 이 interface를 넘겨줄 때는 외부 class 이름을 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface ClickListener{
fun onClick()
}

fun main(args: Array<String>){
setClickAction(object: ClickListener{
override fun onClick(){
println("clicked!")
}
})
}

fun setClickAction(clickListener: ClickListener){
clickListener.onClick()
}

위의 코드에서 익명 클래스는 singleton이 아니다. 따라서 호출시 매번 객체가 생성된다는 점과 익명 클래스 내에서는 외부 클래스의 변수에 접근하여 값을 수정할 수도 있다.

SharedPreferences 예제

Android에서는 간단한 값을 저장하기 위해서 SharedPreferences를 사용한다. 프로그램 어디서나 이 객체를 사용할 수 있어야 하기 때문에 일반적으로 Singleton을 이용해 구현하곤 한다. 자바에서는 싱글톤을 손쉽게 구현할 수 있다. 마찬가지로 코틀린에서도 object 개념을 사용해서 간단하게 구현할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
object SharedPreferenceManager{
private const val PREF_TOKEN = "token"
private const val NAME = "Test"
private const val MODE = Context.MODE_PRIVATE
private latedinit var preferences: SharedPreferences

fun init(context: Context){
preferences = context.getSharedPreferences(NAME, MODE)
}


// 확장 함수를 사용한다. 따라서 edit(), apply() 함수를 호출할 필요가 없다. 모든 작업을 이 함수 하나로 대체할 수 있다.
private inline fun SharedPreferences.edit(operation: (SharedPreferences.Editor) -> Unit){
val editor = edit()
operation(edit)
editor.apply()
}

var token: String
get() = preferences.getString(PREF_TOKEN,"")
set(value) = preferences.edit{
it.putString(PREF_TOKEN, value)
}
}

간단하게 토큰을 저장하는 예제이다. 저장해서 확인해보는 과정까지 거친 코드이므로 잘 동작한다. 확장 함수를 만들어서 이를 통해 edit(), apply() 함수를 직접 호출할 필요가 없다. 작성한 함수만 사용하면 되기 때문이다.

또한, 여러 개의 함수를 만들 필요 없이 하나의 함수만 사용하면 되고 저장할 값이 필요하다면 token 처럼 만들어서 사용자 지정 get,set을 사용하여 값을 가져오고 저장하는 과정을 거치면 된다.

이 클래스를 만들기 위해서 처음에 어떻게 잘 짤 수 있을까를 먼저 고민해보았다. 그런데 바보 같은 생각이라는 걸 깨달았다. 처음부터 잘 짤 수는 없는 것이다. 완벽한 코드는 없으면 코드를 짜면서 공부를 하면서 수정하면 되는 것이다.

그러니 처음부터 완벽한 코드를 짜려고 애쓰지 않도록 마음 먹었다. 리팩토링을 하면 나의 코드를 더 발전시켜 나갈 수 있으니 말이다. 오늘은 여기까지!

참고

Comment and share

  • page 1 of 1
Author's picture

VictoryWoo

기록을 통해 사람들과 공유하는 것을 좋아합니다.


Android Developer