코틀린 인 액션의 내용을 정리
자바에서는 무명 내부 클래스 를 통해 코드 블록을 넘길 수 있다.
특히 한 가지 메소드만 가진 인터페이스를 SAM(Single Abstract Method, 단일 추상 메소드) 인터페이스라고 한다.
자바의 함수형 인터페이스 는 전부 SAM 인터페이스 로 구현되어 있다.
예시
public interface onClickListener {
void onClick(View v);
}
button.setOnClickListener(v -> v.setText("텍스트가 바뀝니다."))
요약: JVM 위에서 람다식을 제대로 쓰려면, 무명 내부 클래스 혹은 익명 클래스를 익숙하게 쓸 수 있어야 한다.
요약: 컬렉션에서 제공하는 라이브러리 함수들을 달달 외워놓자.
다음과 같이 코드를 간략화 할 수 있다.
people.maxBy({ p: Person -> p.age })
people.maxBy() { p: Person -> p.age }
people.maxBy { p: Person -> p.age }
people.maxBy { p -> p.age }
people.maxBy { it.age }
people.maxBy(Person::age)
람다 내에서 외부 변수에 접근하기 위해 레퍼런스를 사용한다.
즉 변수를 Wrapper Class 로 감싸서, 레퍼런스를 통해 참조한 객체의 내부 멤버를 바꾼다.
예시
var clicks = 0
fun tryToCountButtonClicks(button: Button): Int {
button.onClick { clicks++ }
}
안드로이드에서 이런 코드를 많이 한다.
매우 함수형스럽지 않은, 자바스러운 코드가 되었다.
변수 포획을 활용하면 반드시 var 를 쓰게 된다.
val 로 통일하기 위해서라도, 언어차원에서 변수 포획을 지원하지 말아야 했다고 생각한다.
내 안드로이드 팀원들은, 이런게 있는지도 모르는게 하고 싶다.
사용한다면 객체 생명주기와,
Strong vs Weak vs Soft vs Phantom Reference 에 관한 내용을 숙지하자.
예시: Activity binding 과 Fragment binding
Person::age 에서 :: 를 멤버 참조 라고 부른다.
::Person 을 통해 Person 의 생성자 참조 를 할 수 있다.
val createPerson = ::Person
val p: Person = createPerson(name = "Pyro")
val getName = p::name
println(getName())
위에서 p::name 을 바운드 맴버 참조(Bound Member Reference) 라고 한다.
함수형 API 라는 표현을 쓰지만, 나는 함수형 Operator 라는 표현을 더 좋아한다.
RxMarbles 에서 직관적으로 학습을 해보자.
하나하나 다 보면 졸리니까, 필요할 때마다 찾아서 공부한다.
맵 자료형의 경우 filterKeys, filterValues, mapKeys, mapValues 라는 게 존재한다.
하나하나 다 외우려고 하면 힘드니깐, 대충 넘어가자.
listOf(1,2,3,4).find { it == 6 }
?.let { print(it) }
?: run { print("No Number") }
SQL 의 GROUP BY 랑 비슷하다. 쿼리 짜는 기분으로 사용하면 된다.
리스트 안에 리스트들이 있을 경우, 이를 하나의 리스트로 합치기 위해 사용한다.
보통 groupBy 로 묶어서 이것저것 연산을 한 후에, 다시 하나의 리스트로 복원하기 위해 사용한다.
참고로 FlatMap vs SwitchMap vs ConcatMap 이 어떻게 다른지 물어보는 면접 질문이 많이 나온다.
asSequence 문법을 쓰지 않으면 결과 컬렉션이 바로 생성된다. : Eager Evaluation
asSequence 문법을 쓰면 Lazy Evaluation 을 한다.
asSequence() 랑 자바의 stream() 이랑 똑같다. 다만 asSequence 가 stream 보다 먼저 나왔다.
asSequence 를 하고 나서 만들어진 sequence 는 toList 와 같은
최종(Terminal) 연산 을 하기 전에는 계산이 수행되지 않는다.
최종 연산 전의 연산들을 중간(Intermediate) 연산 이라고 한다.
asSequence 로 리스트를 시퀀스를 만들지 않고,
generateSequence 문법을 통해 시퀀스를 로직으로 만들 수도 있다. (예시: 피보나치 수열)
관련 면접 질문: Hot Observable vs Cold Observable
자바의 함수형 인터페이스(Functional Interface) 란,
곧 SAM(Single Abstract Method, 단일 추상 메서드) 인터페이스이다.
람다식(Labmda Expression) 을 함수 인자로 넘기면,
람다식을 함수형 인터페이스 익명 클래스 객체로 만들어서 넘긴다.
익명 클래스 내부 로직에서 클래스 외부 변수 값을 참조할 때는
변수 포획(Capture Variable) 기법을 사용한다.
코틀린의 람다식을 자바의 함수형 인터페이스로 바꾸고 싶을 때는 SAM 생성자로 감싸야 한다.
아래 코드에서는 Runnable { println() }
을 통해 println 을 Runnable SAM 생성자로 감쌌다.
fun createAllDoneRunnable(): Runnable {
return Runnable { println("Hello") }
}
createAllDoneRonnable().run()
솔직히 그냥 코틀린 람다로 통일하면 볼일이 없는 코드다.
Runnable SAM 생성자로 객체를 만들 수 있다는 사실을 머릿속에서 지워버리자.
Runnable 객체를 직접 넘기기 보다는, 람다식을 직접 넘기는 것을 개인적으로 선호한다.
몇몇 분들은 Runnable 혹은 Listener를 싱글톤으로 만들어서 메모리 절약을 할 수 있다고 우기겠지만,
내 경험상 그런 분들은 객체의 생명주기를 이해못하고 코딩해서 메모리 문제를 발생시켰을 가능성이 크다.
생성자 참조 혹은 리플렉션 참조 람다를 프레임워크에게 넘겨서, 프레임워크가 메모리를 관리할 수 있도록 하자.
apply vs with vs let vs also vs run
그냥 한번이라도 직접 사용해보는게 이해가 더 잘 되는 것 같다. 굳이 책으로 공부하지 말자.
binding 을 통해 view 를 가져오려고 할 때 개인적으로 많이 쓴다.
AS-IS
binding.button0.setOnClickListener {
expression += 0
binding.textView.text = expression.toString()
}
binding.button1.setOnClickListener {
expression += 1
binding.textView.text = expression.toString()
}
binding.button2.setOnClickListener {
expression += 2
binding.textView.text = expression.toString()
}
TO-BE
with(binding) {
button0.setOnClickListener {
expression += 0
textView.text = expression.toString()
}
button1.setOnClickListener {
expression += 1
textView.text = expression.toString()
}
button2.setOnClickListener {
expression += 2
textView.text = expression.toString()
}
}
개인적으로 sequence 혹은 stream 에서 시스템 콜 혹은 프레임워크 함수 콜을 통해
side effect 를 발생시키고 싶을 때 많이 쓴다.
아래에서는 println 을 썼지만, 스트림 과정 중에 디버깅을 위해 로깅을 하는 용도로 매우 유용하다.
스트림 중간에 프록시 서버 로직을 집어 넣는다는 느낌으로 쓰면 편하다.
fun calculate(input: String): Int {
val list = input.split(" ")
return list.subList(1, list.size)
.apply { println(toString()) }
.chunked(2)
.apply { println(toString()) }
.map { Pair(IntArithmetics.from(it[0]), it[1].toInt()) }
.apply { println(toString()) }
.fold(list[0].toInt()) { acc, curr -> curr.first.apply(acc, curr.second) }
}