From 7745a3bfa1ee906d20b2e029d1a81e7f226b9db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=82=98=EC=98=81?= Date: Sun, 20 Apr 2025 19:47:04 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=EC=A0=9C=EB=AA=A9=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Chapter01/ria/{readme.md => README.md} | 0 Chapter02/ria/{readme.md => README.md} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename Chapter01/ria/{readme.md => README.md} (100%) rename Chapter02/ria/{readme.md => README.md} (100%) diff --git a/Chapter01/ria/readme.md b/Chapter01/ria/README.md similarity index 100% rename from Chapter01/ria/readme.md rename to Chapter01/ria/README.md diff --git a/Chapter02/ria/readme.md b/Chapter02/ria/README.md similarity index 100% rename from Chapter02/ria/readme.md rename to Chapter02/ria/README.md From e513937e8f66a470f252cf2cd9bef3d195416601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=82=98=EC=98=81?= Date: Sun, 20 Apr 2025 22:15:50 +0900 Subject: [PATCH 2/4] =?UTF-8?q?5=EC=B1=95=ED=84=B0/ria/README.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Chapter05/ria/README.md | 528 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 528 insertions(+) create mode 100644 Chapter05/ria/README.md diff --git a/Chapter05/ria/README.md b/Chapter05/ria/README.md new file mode 100644 index 0000000..16207f4 --- /dev/null +++ b/Chapter05/ria/README.md @@ -0,0 +1,528 @@ +# 람다 +> **람다** : 다른 함수에 넘길 수 있는 작은 코드 조각 +> **함수형 프로그래밍** +> - 일급 시민인 함수 : 함수를 값으로 다룰 수 있음. 함수를 변수에 저장, 파라미터로 전달, 함수에서 다른 함수 리턴 +> - 불변성 : 객체를 만들 때, 일단 만들어진 다음에는 내부 상태가 변하지 않음을 보장하는 방법으로 설계 가능 +> - 부수 효과 없음 : 함수가 같은 입력에 대해 항상 같은 출력을 내놓음 (파라미터에 의해서만 영향) +## 람다식과 멤버 참조 +### 코드 블록을 값으로 다루기 +- 예시1: 이벤트 발생 시 이 핸들러 실행 +- 예시2: 데이터 구조의 모든 원소에 이 연산 적용 + +Java에서는 익명 내부 클래스를 통해 해결했지만, 번거로움 +```java + import java.util.*; + + public class AnonymousInnerClassExample { + public static void main(String[] args) { + List numbers = Arrays.asList(1, 2, 3, 4, 5); + numbers.forEach(new java.util.function.Consumer() { + @Override + public void accept(Integer number) { + System.out.println(number * 2); + } + }); + } + } +``` +함수를 값처럼 다루면 ? 클래스를 선언, 클래스의 인스턴스를 함수에 넘기는 대신 **함수를 직접 다른 함수에 전달** +```kotlin +// 클릭을 처리하는 OnClickListener 인터페이스에 상응하는 인스턴스를 전달받고 싶은 상황 +// OnClickListener에는 onClick이라는 하나의 메소드만 있음 +button.setOnClickListener(object: OnClickListener { + override fun onClick(v: View) { + println("Clicked") + } +}) +``` +이 코드를 람다식으로 변경하면 +```kotlin +button.setOnClickListener { + println("Clicked") +} +``` +메소드가 하나뿐인 익명 객체를 대신 사용할 수 있음! + +### 람다와 컬렉션 +사람의 이름과 나이를 저장하는 데이터 클래스 +```kotlin +data class Person(val name: String, val age: Int) +``` +나이가 가장 많은 사람을 찾으려면 ? +1. for 루프로 직접 찾기 +```kotlin +fun findTheOldest(people: List) { + var maxAge = 0; + var theOldest = Person? = null + for (person in people) { + if (person.age > maxAge) { + maxAge = person.age + theOldest = person + } + } + println(theOldest) +} + +fun main() { + val people = listOf(Person("Alice", 29), Person("Bob", 31)) + findTheOldest(people) //Person(name=Bob, age=31) +} +``` +2. `maxByOrNull` 함수로 컬렉션 검색 +```kotlin +fun main() { + val people = listOf(Person("Alice", 29), Person("Bob", 31)) + println(people.maxByOrNull {it.age}) //Person(name=Bob, age=31) +} +``` +3. 람다 쓰기 (함수나 프로퍼티에 위임할 땐 멤버 참조 사용 가능) +```kotlin +people.maxByOrNull(Person::age) +``` +### 람다식의 문법 +람다는 항상 `{중괄호}`로 싸여 있으며, 파라미터를 지정하고 실제 로직이 담긴 본문 제공 +```kotlin +{ x : Int, y : Int -> x + y } // -> 좌항 : 파라미터, -> 우항 : 본문 +``` +```kotlin +fun main() { + val sum = {x: Int, y: Int -> x + y} + println(sum(1, 2)) // 3 +} +``` +또는 +```kotlin +fun main() { + {println(42)}() // 42 +} +``` +`run` 을 사용해서 인자로 받은 람다를 실행할 수 있음 +```kotlin +fun main() { + run {println(42)} // 42 +} +``` +**최상위 수준의 변수**를 정의할 때 몇 가지 설정과 다른 추가 작업이 필요할 때 +```kotlin +val myFavoriteNum = run { + println("mmmm") + println("hmmmmm") + 42 +} +``` +리스트에서 제일 나이 많은 사람 찾는 예제에서 원래 **정식 람다**로 쓰면 +```kotlin +people.maxByOrNull({p: Person -> p.age}) +``` +함수 호출 시, 맨 뒤의 인자가 람다식이면 **람다를 괄호 밖으로** 빼낼 수 있음, 그리고 빈 괄호도 없애도 됨 +```kotlin +people.maxByOrNull(){p: Person -> p.age} // 람다를 괄호 밖으로 +people.maxByOrNull{p: Person -> p.age} // 빈괄호 생략 +``` +이름을 붙인 인자를 이용해 람다 넘기기 +```kotlin +fun main() { + val people = listOf(Person("Alice", 29), Person("Bob", 31)) + val names = people.joinToString( + separator = " ", + transform = {p: Person -> p.name} + ) + println(names) // Alice Bob +} +``` +람다를 괄호 밖으로 빼면 +```kotlin +people.joinToString(" ") {p: Person -> p.name} +// people.joinToString(separator) {transform} +``` +다시 아까 `maxByOrNull` 예제를 다듬으면 +Person 타입의 객체가 들어있는 컬렉션에 대해서 호출하기 때문에 파라미터도 Person이라는 걸 알 수 있기 때문에 생략 가능 +```kotlin +people.maxByOrNull{p -> p.age} // 타입추론 +``` +디폴트 파라미터 이름 it 사용 +```kotlin +people.maxByOrNull{ it.age } // 유일한 파라미터니까 it +``` +람다를 변수에 저장할 땐 파라미터 타입을 추론할 수 있는 문맥이 없으니까 타입 추론이 불가능함 +```kotlin +val getAge = {p: Person -> p.age} // 타입 명시 +people.maxByOrNull(getAge) +``` +본문이 여러줄인 람다는 마지막 식이 람다의 결과 +```kotlin +fun main() { + val sum = {x: Int, y: Int -> + println("sum of $x and $y") + x+y + } + println(sum(1, 2)) + // sum of 1 and 2 + // 3 +} +``` +### 변수 캡쳐 (현재 영역에 있는 변수 접근) +함수 안에 익명 내부 클래스 선언하면 그 클래스 안에서 함수의 파라미터와 로컬변수에 참조할 수 있는 것처럼 +람다에서도 동일하게 할 수 있음 +```kotlin +fun printMessageWithPrefix(messages: Collection, prefix: String) { + messages.forEach { + println("$prefix $it") + } +} + +fun main() { + val errors = listOf("403 Forbidden", "404 Not Found") + printMessageWithPrefix(errors, "Error: ") +} + +// Error: 403 Forbidden +// Error: 404 Not Found +``` +> 코틀린 람다 vs 자바 람다 : 코틀린 람다 안에서 final 변수가 아닌 변수에 접근할 수 있음. 람다 안에서 바깥 변수를 변경해도 됨 +```kotlin +fun printProblemCounts(responses: Collection) { + var clientErrors = 0 // 람다가 캡쳐한 변수 + var serverErrors = 0 // 람다가 캡쳐한 변수 + responses.forEach { + if (it.startsWith("4")) { + clientErrors++ + } else if (it.startsWith("5")) { + serverErrors++ + } + } + println("$clientErrors client errors, $serverErrors server errors") +} + +fun main() { + val responses = listOf("200 OK", "418 I'm a teapot", "500 Internal Server Error") + printProblemCounts(responses) + // 1 client errors, 1 server errors +} +``` +final이 아닌 변수를 캡쳐한 경우, **변수를 특별한 래퍼로 감싸** 나중에 변경하거나 읽을 수 있게한 다음, 래퍼에 대한 참조를 람다 코드와 같이 저장 +```kotlin +// 변경 가능한 값을 저장할 원소가 단 하나 뿐인 배열을 선언하거나, 변경 가능한 변수에 대한 참조를 저장하는 클래스 선언 +class Ref(var value: T) // 변경 가능한 변수를 캡쳐하는 방법 보여주는 클래스 + +// 이렇게 동작하는데 +fun main() { + val counter = Ref(0) + val inc = { counter.value++ } // 공식적으론 변경 불가능한 변수를 캡쳐, 변수가 가리키는 객체의 필드 값을 바꿀 수 있음 +} + +// 실제 코드에서는 저런 래퍼를 안만들고, 변수를 직접 바꿈 +fun main() { + var counter = 0 + val inc = { counter++ } +} +``` +람다를 **이벤트 핸들러나 다른 비동기적으로 실행되는 코드로 활용**하는 경우, 로컬 변수 변경은 **람다가 실행될 때만** 일어남 +```kotlin +fun tryToCountButtonClicks(button: Button): Int { + var clicks = 0 + button.onClick { click++ } + return clicks +} +// 0 +``` +`onClick` 핸들러는 호출될 때마다 값을 증가시키긴 하지만 값 변경을 관찰할 순 없음 +핸들러는 **clicks가 반환된 이후에 호출**되기 때문 +클릭 횟수를 세는 카운터 변수를 밖으로 옮겨야 함 +```kotlin +fun tryToCountButtonClicks(button: Button) { + var clicks = 0 + button.onClick { + clicks++ + println("Clicked $clicks times") + } +} +``` + +### 멤버 참조 +메소드를 호출하거나 한 프로퍼티에 접근하는 함수 값을 만들어줌 +클래스 이름과 참조하려는 멤버의 사이에 `::` 위치 +```kotlin +val getAge = Person::age +people.maxByOrNull(Person::age) // 멤버참조 +``` +최상위에 선언된 함수나 프로퍼티 참조도 가능 +클래스 이름 생략하고 ::으로 바로 참조 시작 +run에 넘기고, run이 람다를 호출 +```kotlin +fun salute() = println("salute") +fun main { + run(::salute) +} +``` +람다가 인자가 여럿인 다른 함수에 작업 위임 시, 멤버 참조 제공하면 편리 +```kotlin +val action = {person: Person, message: String -> + sendEmail(person, message) +} +val nextAction = ::sendEmail //람다 대신 멤버 참조 쓸 수 있음 +``` +**생성자 참조**를 사용하면 클래스 생성 작업을 연기하거나 저장해둘 수 있음 +```kotlin +data class Person(val name: String, val age: Int) +fun main() { + val createPerson = ::Person + val p = createPerson("Alice", 29) + println(p) + // Person(name=Alice, age=29) +} +``` +**확장 함수**도 같은 방식으로 참조 가능 +```kotlin +fun Person.isAdult() = age >= 21 +val predicate = Person::isAdult +``` +### 값과 엮인 호출 가능 참조 +같은 멤버 참조 구문을 사용해 특정 객체 인스턴스에 대한 메소드 호출에 대한 참조를 만들 수 있음 +```kotlin +fun main() { + val seb = Person("Sebastian", 25) + val personsAgeFunc = Person::age // 사람이 주어지면 나이 반환 + println(personsAgeFunc(seb)) + // 25 + val sebsAgeFunc = seb::age // 값과 엮인 호출 가능 참조 + println(sebsAgeFunc()) // 아무 파라미터를 지정하지 않아도 됨 + // 25 +} +``` +## 자바의 함수형 인터페이스 사용 : 단일 추상 메소드 +```kotlin +button.setOnClickListener { + println("Clicked") +} +``` +```java +public class Button { + public void setOnClickListener(OnClickListener l) {} +} +// OnClickListener 인터페이스에는 메소드가 하나만 있음 +public interface OnClickListener { + void onClick(View v); +} +//8 이전 +button.setOnClickListener(new OnClickListener(){ + @Override + public void onClick(View v) { + + } +}); +// 8 부터 +button.setOnClickListener(view -> {}); +``` +```kotlin +button.setOnClickListener{view -> println()} +``` +OnClickListener가 단일 추상 메소드 인터페이스여서 이런 코드가 가능함 +Runnable을 인자로 받을 때 +```java +// java 코드 +void postponeComputation(int delay, Runnable computation); +``` +코틀린에서는 람다를 Runnable 인스턴스로 변환해줌 (Runnable을 구현하는 익명 클래스의 인스턴스) +```kotlin +// kotlin 코드 +postponeComputation(1000){println(42)} +``` +Runnable을 명시적으로 구현하는 익명 객체로도 같은 효과 +```kotlin +postponeComputation(1000, object : Runnable { + override fun run() { + println(42) + } +}) +``` +근데 명시적으로 객체 선언하면 매번 호출할 때마다 새 인스턴스가 생김 +근데 람다는 자신이 정의한 함수의 변수에 접근하지 않으면 함수가 호출될 때마다 람다에 해당하는 익명 객체가 재사용됨 +```kotlin +postponeComputation(1000){println(42)} // 전체 프로그램에 Runnable 인스턴스 하나만 생성 +``` +람다가 자신을 둘러싼 환경의 변수를 캡처하면 더 이상 각 함수 호출에 같은 인스턴스 재사용 불가능 +```kotlin +fun handleComputation(id: String) { // 이 함수 호출될 때마다 Runnable 인스턴스 생성 + postponeComputation(1000) { + println(id) // id 캡쳐 + } +} +``` +### SAM 변환 (함수형 인터페이스로 명시적 변환) +SAM 생성자의 이름은 사용하려는 함수형 인터페이스의 이름과 같음 +하나의 인자만 받아서 함수형 인터페이스를 구현하는 클래스의 인스턴스를 반환함 +```kotlin +fun createAllDoneRunnable(): Runnable { + return Runnable {println("All done")} +} +fun main() { + createAllDoneRunnable().run() + // All done +} +``` +람다로 생성한 함수형 인터페이스 인스턴스를 변수에 저장해야 하는 경우에도 SAM 생성자를 사용할 수 있음 +```kotlin +val listener = OnClickListener {view -> + val text = when (view.id) { + button1.id -> "First" + button2.id -> "Second" + else -> "Unknown" + } + toast(text) +} +button1.setOnClickListener(listener) +button2.setOnClickListener(listener) +``` +## 코틀린에서 SAM 인터페이스 정의 : fun interface +정확히 하나의 추상 메소드만 포함하지만 다른 비추상 메소드를 여러개 가질 수 있음 +함수 타입의 시그니처에 맞지 않는 여러 복잡한 구조를 표현할 수 있음 +```kotlin +fun interface IntCondition { + fun check(i: Int): Boolean // 추상 메소드는 하나 + fun checkString(s: String) = check(s.toInt()) + fun checkChar(c: Char) = check(c.digitToInt()) +} + +fun main() { + val isOdd = IntCondition { it % 2 != 0 } + println(isOdd.check(1)) + // true + println(isOdd.checkString("2")) + // false + println(isOdd.checkChar('3')) + // true +} +``` +함수형 인터페이스는 동적으로 인스턴스화 됨 +```kotlin +fun checkCondition(i: Int, condition: IntCondition): Boolean { + return condition.check(i) +} +fun main() { + checkCondition(1) {it % 2 != 0} + // true + val isOdd: (Int) -> Boolean = { it % 2 != 0} + checkCondition(1, isOdd) // 시그니처에 일치하는 람다에 대한 참조 이용 +} +``` +## 수신 객체 지정 람다 : with, apply, also +### with +```kotlin +fun alphabet(): String { + val result = StringBuilder() + for (letter in 'A' ..'Z') { + result.append(letter) + } + result.append("\nNow I know the alphabet") + return result.toString() +} +fun main() { + println(alphabet()) + //ABCDEFGHIJKLMNOPQRSTUVWXYZ + //Now I know the alphabet +} +``` +with를 쓰면 +첫번째 인자로 받은 객체를 두번째 인자로 받은 람다의 수신 객체로 만듦 +명시적인 this 참조를 사용해 그 수신 객체에 접근할 수 있음 +this.를 없애고 메소드나 프로퍼티 이름만 사용해 접근도 가능 +```kotlin +fun alphabet(): String { + val stringBuilder = StringBuilder() + return with(stringBuilder) { + for (letter in 'A'..'Z') { + this.append(letter) + } + this.append("\nNow I know the alphabet") + this.toString() + } +} + +// this. 제거 +fun alphabet(): String { + val stringBuilder = StringBuilder() + run with(stringBuilder) { + for (letter in 'A'..'Z') { + append(letter) + } + append("\nNow I know the alphabet") + toString() + } +} + +// with와 식을 본문으로 하는 함수 +fun alphabet(): with(StringBuilder()) { + for (letter in 'A'..'Z') { + append(letter) + } + append("\nNow I know the alphabet") + toString() +} +``` +with에 인자로 넘긴 객체의 클래스와 with를 사용하는 코드가 들어있는 클래스 안에 이름이 같은 메소드가 있으면? +this 참조 앞에 레이블을 붙이면 호출하고 싶은 메소드를 명확하게 정할 수 있음 +```kotlin +this@OuterClass.toString() +``` + +### apply 함수 +항상 자신에 전달된 수신 객체를 반환함 +```kotlin +fun alphabet(): StringBuilder().apply { + for (letter in 'A'..'Z') { + append(letter) + } + append("\nNow I know the alphabet") +}.toString() +``` +buildString +```kotlin +fun alphabet() = buildString { + for (letter in 'A'..'Z') { + append(letter) + } + append("\nNow I know the alphabet") +} +``` +컬렉션 빌더 함수 : buildList, buildSet, buildMap +```kotlin +val fibonacci = buildList { + addAll(listOf(1, 1, 2)) + add(3) + add(index = 0, element = 3) +} +val shouldAdd = true +val fruits = buildSet { + add("Apple") + if (shouldAdd) { + addAll(listOf("Apple", "Banana", "Cherry")) + } +} +val medals = buildMap { + put("Gold", 1) + putAll(listOf("Silver" to 2, "Bronze" to 3)) +} +``` +### also +수신 객체에 대해 어떤 동작을 수행한 후 수신 객체를 돌려줌 +also의 람다 내에서는 수신 객체를 인자로 참조해야 한다는 점이 다름 +파라미터 이름을 부여하거나 디폴트 이름인 it을 사용해야 함 +원래 수신 객체를 인자로 받는 동작을 할 때 유용 +```kotlin +fun main() { + val fruits = listOf("Apple", "Banana", "Cherry") + val uppercaseFruits = mutableListOf() + val reversedLongFruits = fruits + .map { it.uppercase() } + .also { uppercaseFruits.addAll(it) } + .filter { it.length > 5 } + .also { println(it) } + .reversed() + // [BANANA, CHERRY] + println(uppercaseFruits) + // [APPLE, BANANA, CHERRY] + println(reversedLongFruits) + // [CHERRY, BANANA] +} +``` From 16cbfee507a2c5bcf0d8b63e1e9b451bd1f46abc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=82=98=EC=98=81?= Date: Mon, 21 Apr 2025 23:40:22 +0900 Subject: [PATCH 3/4] =?UTF-8?q?6=EC=B1=95=ED=84=B0/ria/README.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Chapter06/ria/README.md | 348 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 348 insertions(+) create mode 100644 Chapter06/ria/README.md diff --git a/Chapter06/ria/README.md b/Chapter06/ria/README.md new file mode 100644 index 0000000..eb91da9 --- /dev/null +++ b/Chapter06/ria/README.md @@ -0,0 +1,348 @@ +# 컬렉션과 시퀀스 +## 컬렉션에 대한 함수형 API +### 원소 제거와 변환: filter, map +* filter : 원소 거르기 +* map : 변환 +```kotlin +data class Person(val name: String, val age: Int) + +// 나이가 30살 이상인 사람들 거르기 +fun main() { + val people = listOf(Person("Alice", 29), Person("Bob", 31)) + println(people.filter {it.age >= 30}) + // [Person(name=Bob, age=31)] +} + +fun main() { + val people = listOf(Person("Alice", 29), Person("Bob", 31)) + //사람들의 이름의 리스트를 출력 + println(people.map{it.name}) + println(people.map(Person::name)) //멤버참조 + // [Alice, Bob] + + //30살 이상인 사람의 이름 출력 + println(people.filter {it.age >= 30}.map(Person::name)) + // [Bob] + + //가장 나이가 많은 사람의 이름 + people.filter { //사람수대로 비교하게 됨 + val oldest = people.maxByOrNull(Person::age) + it.age == oldest?.age + } + + val maxAge = people.maxByOrNull(Person::age)?.age + people.filter {it.age == maxAge} //한 번만 비교 +} +``` +원소의 값 뿐 아니라 인덱스에 따라서도 달라진다면, **filterIndexed**, **mapIndexed를** 사용하면 됨 +```kotlin +fun main() { + val numbers = listOf(1, 2, 3, 4, 5, 6, 7) + val filtered = numbers.filterIndexed{index, element -> + index % 2 == 0 && element > 3 + } + println(filtered) + // [5, 7] + val mapped = numbers.mapIndexed{index, element -> + index + element + } + println(mapped) + // [1, 3, 5, 7, 9, 11, 13] +} +``` +filter와 map을 **맵**에 적용할 수도 있음 +```kotlin +fun main() { + val numbers = mapOf(0 to "zero", 1 to "one") + println(numbers.mapValues { it.value.uppercase() }) + // {0=ZERO, 1=ONE} +} +``` +맵의 경우 **filterKeys**, **mapKeys와** **filterValues**, **mapValues** 존재함 + +### 컬렉션 값 누적 : reduce, fold +* reduce : 컬렉션의 첫 번째 값을 누적기에 넣고, 람다가 호출되며 누적 값과 2번째 원소가 인자로 전달됨 +* fold : 개념적으로 reduce와 비슷하지만, 컬렉션 첫 번째 값을 누적 값으로 시작하는 대신, 임의의 시작 값을 선택할 수 있음 +```kotlin +//reduce +fun main() { + val list = listOf(1, 2, 3, 4) + println(list.reduce {acc, elem -> acc + elem}) // 1+2 3+3 6+4 + // 10 + println(list.reduce {acc, elem -> acc * elem}) // 2, 6, 24 + // 24 +} + +//fold +fun main() { + val people = listOf( + Person("Alex", 29), + Person("Natalia", 28) + ) + val folded = people.fold("") {acc, elem -> acc + person.name} // ""가 누적기 시작값 + println(folded) + // AlexNatalia +} +``` +중간 단계의 모든 누적값을 뽑고 싶으면 **runningReduce**, **runningFold** 사용 가능 +얘네는 **리스트** 반환, reduce, fold는 하나의 결과만 반환 +```kotlin +fun main() { + val list = listOf(1, 2, 3, 4) + val summed = list.runningReduce {acc, elem -> acc + elem} + println(summed) + // [1, 3, 6, 10] + val multiplied = list.runningReduce {acc, elem -> acc * elem} + println(summed) + // [1, 2, 6, 24] + val people = listOf( + Person("Alex", 29), + Person("Natalia", 28) + ) + println(people.runningFold(""){acc, person -> acc + person.name}) + // [, Alex, AlexNatalia] +} +``` +### 컬렉션에 술어 적용 : all, any, none, count, find +* all, any, none : 어떤 조건을 만족하는지 판단하는 연산 +* count : 조건을 만족하는 원소의 개수 반환 +* find : 조건을 만족하는 첫 번째 원소 반환 +```kotlin +val canBeInClub27 = {p: Person -> p.age <= 27} // 27살 이하인지 판단하는 +fun main() { + val people = listOf(Person("Alice", 27), Person("Bob", 31)) + println(people.all(canBeInClub27)) //모든 원소에 대해 + // false + println(people.any(canBeInClub27)) //하나라도 + // true + pritnln(people.count(canBeInClub27)) //개수 + // 1 + println(people.find(canBeInClub27)) //만족하는 원소 하나 찾고 싶으면 (제일 처음 맞는 것 반환, 없으면 null - firstOrNull과 같음) + // Person(name=Alice, age=27) +} +``` +> !all == any, !any == none +> * 빈 리스트에 any : false +> * 빈 리스트에 none : true +> * 빈 리스트에 all : true + +> size 보다 count가 나은 이유는 size는 중간 컬렉션이 생기지만 count는 개수만 추적하기 때문 + +### 리스트를 분할해 리스트의 쌍으로 만들기 : partition +어떤 술어를 만족하는 그룹, 만족하지 않는 그룹 두개로 나누려고 할 때 filter와 filterNot을 사용할 수 있을텐데 +partition이 더 간단하게 할 수 있음 +```kotlin +fun main() { + val people = listOf( + Person("Alice", 26), + Person("Bob", 29), + Person("Carol", 31) + ) + val (comeIn, stayOut) = people.partition(canBeInClub27) + println(comeIn) + // [Person(name=Alice, age=26)] + println(stayOut) + // [Person(name=Bob, age=29), Person(name=Carol, age=31)] +} +``` + +### 리스트를 여러 그룹으로 이루어진 맵으로 바꾸기 : groupBy +```kotlin +fun main() { + val people = listOf( + Person("Alice", 31), + Person("Bob", 29), + Person("Carol", 31) + ) + println(people.groupBy{it.age}) + // {31=[Person(name=Alice, age=31), Person(name=Carol, age=31)], + // 29=[Person(name=Bob, age=29)]} + // 각 그룹의 결과 타입은 Map> +} +``` + +멤버 참조를 활용해 문자열을 첫번째 글자에 따라 분류하는 코드 +```kotlin +fun main() { + val list = listOf("apple", "apricot", "banana", "carrot") + println(list.groupBy(String::first)) + // {a=[apple, apricot], b=[banana], c=[carrot]} +} +``` + +### 컬렉션을 맵으로 변환 : associate, associateWith, associateBy +그룹화 하지 않으면서 맵으로 변환하고 싶을 때 +```kotlin +fun main() { + val people = listOf(Person("Joe", 22), Person("Mary", 31)) + val nameToAge = people.associate {it.name to it.age} + println(nameToAge) + // {Joe=22, Mary=31} + println(nameToAge["Joe"]) + // 22 +} +``` +* associateWith : 컬렉션의 원래 원소를 키로 사용 +* associateBy : 컬렉션의 원래 원소를 값으로 사용, 람다가 만드는 값을 키로 +```kotlin +fun main() { + val people = listOf(Person("Joe", 22), Person("Mary", 31), Person("Jamie", 22)) + val personToAge = people.associateWith {it.age} + println(personToAge) + // {Person(name=Joe, age=22)=22, Person(name=Mary, age=31)=31, Person(name=Jamie, age=22)=22} + val ageToPerson = people.associateBy {it.age} + println(ageToPerson) + // {22=Person(name=Jamie, age=22), 31=Person(name=Mary, age=31)} +} +``` + +### 가변 컬렉션의 원소 변경 : replaceAll, fill +* replaceAll을 mutableList에 적용하면 지정한 람다로 얻은 결과로 모든 원소를 변경 +```kotlin +fun main() { + val names = mutableListOf("Martin", "Samuel") + println(names) + // [Martin, Samuel] + names.replaceAll{it.uppercase()} + println(names) + // [MARTIN, SAMUEL] + names.fill("(redacted)") + println(names) + // [(redacted), (redacted)] +} +``` + +### 컬렉션의 특별한 경우 처리 : ifEmpty +* ifEmpty : 아무 원소도 없을 때 기본 값 생성하는 람다 제공 +* ifBlank : 빈 문자열 대체 +```kotlin +fun main() { + val empty = emptyList() + val full = listOf("apple", "banana", "orage") + println(empty.ifEmpty { listOf("no", "values", "here") }) + // [no, values, here] + println(full.ifEmpty { listOf("no", "values", "here") }) + // [apple, banana, orange] +} +``` + +### 컬렉션 나누기 : chunked, windowed +* windowed : 슬라이딩 윈도우 +* chunked : 지정한 크기만큼 겹치지 않게 나눔 +```kotlin +fun main() { + val temperatures = listOf(27.7, 29.8, 22.0, 35.5, 19.1) + println(temperatures.windowed(3)) + // [27.7, 29.8, 22.0], [29.8, 22.0, 35.5], [22.0, 35.5, 19.1] + println(temperatures.windowed(3) {it.sum() / it.size}) + // [26.5, 29.09999, 25.53333] + println(temperatures.chunked(2)) + // [27.7, 29.8], [22.0, 35.5], [19.1] + println(temperatures.chunked(2) {it.sum()}) + // [57.5, 57.5, 19.1] +} +``` + +### 컬렉션 합치기 : zip +각 리스트의 값들이 서로의 인덱스에 따라 대응되고 있는 걸 안다면, 두 컬렉션에서 같은 인덱스에 있는 원소들의 쌍으로 이뤄진 리스트 만들 수 있음 +```kotlin +val names = listOf("Joe", "Mary", "Jamie") +val ages = listOf(22, 31, 31, 44, 0) + +fun main() { + pritnln(names.zip(ages)) + // [(Joe, 22), (Mary, 31), (Jamie, 31)] // 반대편 컬렉션에서 남은 건 무시 + println(names.zip(ages){name, age -> Person(name, age)}) + // [Person(name=Joe, age=22), Person(name=Mary, 31), Person(name=Jamie, 31)] + println(names zip ages) // 중위 표기 가능하지만 람다 전달 불가능 + // [(Joe, 22), (Mary, 31), (Jamie, 31)] +} +``` +zip을 연쇄 호출하면 `a zip b zip c` 했을 때 ((a, b), c) 이렇게 됨 + +### 내포된 컬렉션의 원소 처리 : flatMap, flatten +```kotlin +val library = listOf( + Book("A", listOf("aaa", "bbb")), + Book("B", listOf("ccc, ddd")), + Book("C", listOf("aaa")) +) + +fun main() { + val authors = library.flatMap {it.authors} + println(authors) + // [aaa, bbb, ccc, ddd, aaa] //중복이 있음 +} +``` +변환할 게 없으면 flatten 사용하면 된다 + +## 지연 계산 컬렉션 연산 : 시퀀스 +시퀀스는 스트림과 비슷하게 중간 임시 컬렉션을 사용하지 않고 컬렉션 연산을 연쇄하는 방법 제공 +큰 컬렉션에 대한 연산을 연쇄 적용해야 할 때는 시퀀스를 꼭 쓰도록 (원소가 많으면 중간 원소 재배열 비용이 더 큼) +```kotlin +people.map(Person::name) //리스트 반환1 + .filter{it.startsWith("A")} //리스트 반환2 + +people.asSequence() //원본 컬렉션을 시퀀스로 변환 + .map(Person::name) + .filter{it.startsWith("A")} + .toList() //시퀀스를 리스트로 변환 +``` + +### 시퀀스 연산 실행 : 중간 연산, 최종 연산 +* 중간 연산 : 다른 시퀀스 반환 +* 최종 연산 : 결과 반환 +```kotlin +fun main() { + println( + listOf(1, 2, 3, 4) + .asSequence() + .map { + print("map($it) ") + it * it + }.filter { + print("filter($it) ") + it % 2 == 0 + } + ) + + //시퀀스 자체에 대한 출력만 나올 뿐 +} + +fun main() { + println( + listOf(1, 2, 3, 4) + .asSequence() + .map { + print("map($it) ") + it * it + }.filter { + print("filter($it) ") + it % 2 == 0 + }.toList() // 모든 계산 수행됨 + ) +} + +fun main() { + println( + listOf(1, 2, 3, 4) + .asSequence() + .map { it * it } + .find { it > 3 } + ) + // 컬렉션이었으면 1, 4, 9, 16 다음에 1, 4 이렇게 찾지만 + // 시퀀스라서 1, 4 하고 끝남 +} +``` + +### 시퀀스 만들기 +* asSequence() +* generateSequence() +```kotlin +fun main() { + val naturalNums = generateSequence(0) {it + 1} + val numbersTo100 = naturalNums.takeWhile {it <= 100} + println(numbersTo100.sum()) // 이때 지연 계산 수행 + // 5050 +} +``` From e796c0f0e1b7c4e36d315f8e5f6ff8d0a775f8c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=82=98=EC=98=81?= Date: Mon, 28 Apr 2025 23:07:25 +0900 Subject: [PATCH 4/4] =?UTF-8?q?7=EC=B1=95=ED=84=B0/ria/README.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Chapter07/ria/README.md | 101 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 Chapter07/ria/README.md diff --git a/Chapter07/ria/README.md b/Chapter07/ria/README.md new file mode 100644 index 0000000..f9f6a9b --- /dev/null +++ b/Chapter07/ria/README.md @@ -0,0 +1,101 @@ +# 널이 될 수 있는 값 +> 널이 될 수 있는 타입 +> 널이 될 가능성이 있는 값을 다루는 구문의 문법 +> 널이 될 수 있는 타입과 널이 될 수 없는 타입의 변환 +> 코틀린의 널 가능성 개념과 자바 코드 사이의 상호운용성 + +### NullPointerException을 피하고 값이 없는 경우 처리 : 널 가능성 +컴파일러가 여러 가지 오류를 컴파일 시 미리 감지해서 실행 시점에 발생할 수 있는 예외의 가능성을 줄임 + +### 널이 될 수 있는 타입으로 널이 될 수 있는 변수 명시 +널이 될 수 있는 타입은 프로그램 안의 프로퍼티나 변수에 null을 허용하게 만드는 방법 +어떤 변수가 널이 될 수 있으면 그 변수에 대해 메소드 호출을 했을 때 npe가 발생할 수 있음 +코틀린은 이런 메소드 호출을 금지함으로써 예외를 방지함 + +null이 인자로 들어올 수 없다면 : `fun strLen(s: String) = s.length` +`strLen(null)` 이렇게 하면 에러 발생함 +null을 포함하는 모든 문자열을 인자로 받을 수 있게 하려면 타입 이름 뒤에 물음표를 붙여야 함 `fun strLenSafe(s: Sting?) = ...` + +어떤 타입이든 타입 이름 뒤에 물음표를 붙이면 그 타입의 변수나 프로퍼티에 null 참조를 저장할 수 있음 +물음표가 없는 타입은 null 참조를 저장할 수 없다는 의미, 기본적으로 모든 타입은 null이 아닌 타입 + +널이 될 수 있는 값을 널이 될 수 없는 타입의 변수에 대입할 수 없음 + +```kotlin +fun main() { + val x: String? = null + val y: String = x + // Error +} +``` + +널이 될 수 있는 타입의 값으로 대체할 수 있는 것? null과 비교하는 것 +일단 null과 비교하고, null이 아님이 확실한 영역에서는 해당 값을 null이 아닌 타입의 값처럼 사용 가능 + +### 안전한 호출 연산자로 null 검사와 메소드 호출 합치기: ?. +`str?.uppercase()` +`if(str != null) str.uppercase()` +위의 둘은 같음 + +호출하려는 값이 null이면 이 호출은 무시되고, null이 아니면 일반 메소드 호출처럼 작동 +따라서 저 결과물이 null이 될 수 있음 + +```kotlin +class Employee(val name: String, val manager: Employee?) + +fun managerName(employee: Employee): String? = employee.manager?.name + +fun main() { + val ceo = Employee("Boss", null) + val developer = Employee("Bob", ceo) + println(managerName(developer)) // Boss + println(managerName(ceo)) // null +} +``` + +### 엘비스 연산자로 null에 대한 기본값 제공: ?: +null 대신 사용할 기본값 지정할 때 사용하는 연산자 +```kotlin +fun greet(name: String?) { + val recipient: String = name ?: "unnamed" + println("Hello, $recipient") +} +``` + +throw와 엘비스 연산자 함께 사용 가능 +```kotlin +val address = person.company?.address + ?: throw IllegalArgumentException("No address") // 예외 발생시킬 수 있음 +``` + +### 예외를 발생시키지 않고 안전하게 타입을 캐스트하기: as? +as를 사용할 때마다 is로 미리 as 변환 가능한 타입인지 검사해볼 수도 있지만 +```kotlin +override fun equals(o: Any?): Boolean { + val otherPerson = o as? Person ?: return false + return otherPerson.firstName == firstName && otherPerson.lastName == lastName +} +``` +파라미터로 받은 값이 원하는 타입인지 쉽게 검사하고 캐스트할 수 있고, 맞지 않으면 쉽게 false 반환할 수 있음 + +### 널이 아님 단언: !! +어떤 값이든 널이 아닌 타입으로 강제로 바꿀 수 있음 +null에 대해 !! 하면 NPE 발생 + +호출된 함수가 언제나 다른 함수에서 null이 아닌 값을 전달 받는다는 사실이 분명하다면 굳이 null 검사를 안해도 되게 단언문 사용하면 됨 + +### let 함수 +안전한 호출 연산자와 함께 사용하면 원하는 식을 평가해서 결과가 null인지 검사한 다음 그 결과를 변수에 넣는 작업을 간단한 식을 사용해 한꺼번에 처리 가능 +```kotlin +fun sendEmail(email: String) {} //null이 아닌 파라미터 넘겨받음 +fun main() { + val email: String? = "abc@bcd.com" + if(email != null) + sendEmail(email) +} + +// email이 null이 아닐 때만 호출됨 +email?.let {email -> sendEmail(email)} +email?.let { sendEmail(it)} +``` +