collection ของ Kotlin ทำครบทีละขั้น จนกว่าคุณจะขอเป็นอย่างอื่น
บทที่ 2 ให้ชนิดของ orderctl มาครบ และบทที่ 3 ให้โปรเจกต์ Gradle ที่อ่าน JSON ได้
สิ่งที่ยังไม่มีคือคำตอบ — ยอดรวมเท่าไร แต่ละหมวดเท่าไร และใบเสร็จเรียงอย่างไรให้ผลเหมือนเดิมทุกรอบ
คอร์ส Java แก้โจทย์เดียวกันนี้ด้วย
Stream และ คอร์ส Go แก้ด้วย for กับ map
ผลของ Totals ในบทนี้ตรงกับของคอร์ส Java ทุกบรรทัด เพราะเป็นออเดอร์ชุดเดียวกัน
· ที่ต่างคือทางที่ใช้ไปถึงมัน
ห้องทดลองอื่นของสองบทนี้วัดคนละเรื่องกัน ตัวเลขของมันจึงเทียบกันตรงๆ ไม่ได้
ห้องทดลองทุกอันในบทนี้รันแบบเดียวกัน — เปลี่ยนบรรทัด mainClass ใน build.gradle.kts
ที่บทที่ 3 เขียนไว้ ให้ชี้ไปที่ file class ของห้องนั้น แล้วสั่ง gradle -q run
แก้ Line ครั้งเดียว เพื่อให้มันรู้ว่าตัวเองอยู่หมวดไหน
หัวข้อที่มีชื่อว่า “แก้ Line ครั้งเดียว เพื่อให้มันรู้ว่าตัวเองอยู่หมวดไหน”คำถาม “หมวดไหนกี่สตางค์” ตอบไม่ได้เลยถ้า
Lineยังไม่มีช่องสำหรับหมวด
Domain.kt ของบทที่ 2 ยังอยู่นอกโปรเจกต์ Gradle · ย้ายมันเข้ามาที่
src/main/kotlin/orderctl/ เติม package orderctl ที่หัว file แล้วแก้สามที่
- เพิ่มช่อง
categoryให้Line - เพิ่ม argument
categoryให้lineOfด้วย ไม่งั้นมันเรียกLineด้วยค่าไม่ครบ - ลบ
fun mainของบทที่ 2 ทิ้ง เพราะmainของโปรเจกต์อยู่ที่Main.ktตั้งแต่บทที่แล้ว
package orderctl
@JvmInlinevalue class Sku(val code: String) { init { require(code.isNotBlank()) { "sku ว่างไม่ได้" } }}
@JvmInlinevalue class Satang(val v: Long) { init { require(v >= 0) { "ราคาติดลบไม่ได้" } }}
data class Line(val sku: Sku, val category: String, val qty: Int, val unit: Satang) { init { require(qty > 0) { "qty ต้องมากกว่า 0 แต่ได้ $qty" } }
fun subtotal(): Satang = Satang(unit.v * qty)}
fun lineOf(sku: String, category: String, qty: Int, unitSatang: Long) = Line(Sku(sku.trim().uppercase()), category.trim().lowercase(), qty, Satang(unitSatang))block ข้างบนตัด Discount Status และ apply ของบทที่ 2 ออกไปเพื่อให้สั้น
· ทั้งสามยังอยู่ใน Domain.kt ต่อไปตามเดิม และบทนี้ไม่แตะมัน
Totals สี่ตัว ที่ไม่มีตัวแปรสะสมสักตัว
หัวข้อที่มีชื่อว่า “Totals สี่ตัว ที่ไม่มีตัวแปรสะสมสักตัว”หัวข้อนี้สร้าง
Totalsซึ่งเป็นของชิ้นเดียวที่บทนี้เพิ่มเข้าโปรเจกต์จริง
// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์ · ต้องมีชนิดของ Domain อยู่ข้างกันpackage orderctl
object Totals {
fun grossSatang(lines: List<Line>): Long = lines.sumOf { it.subtotal().v }
fun byCategory(lines: List<Line>): Map<String, Long> = lines.groupingBy { it.category } .fold(0L) { acc, line -> acc + line.subtotal().v } .toSortedMap()
fun report(lines: List<Line>): List<Line> = lines.sortedWith(compareBy<Line> { it.category }.thenByDescending { it.sku.code })
fun receiptLine(lines: List<Line>): String = "${lines.size} รายการ รวม ${grossSatang(lines)} สตางค์"}ยอดทุกตัวคืนเป็น Long ไม่ใช่ Satang · เหตุผลอยู่ในบทที่ 2 ซึ่งวัดไว้แล้วว่า
value class เข้ากล่องทันทีที่ไปอยู่ใน generic · Map<String, Satang> จึงห่อทุกค่าที่มันเก็บ
คอร์ส Java เขียนของชิ้นเดียวกันเป็น public final class Totals ที่มี constructor ส่วนตัว
ไว้กันไม่ให้ใครสร้าง instance · Kotlin มีคำสำหรับเรื่องนี้อยู่แล้วคือ object
ห้องทดลองที่เรียกมันทั้งสี่ตัว
// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์ · ต้องมีชนิดของ Domain กับ Totals อยู่ข้างกันpackage orderctl
fun main() { val lines = listOf( lineOf("tea", "drink", 2, 3500), lineOf("coffee", "drink", 1, 6000), lineOf("cake", "dessert", 1, 12000), )
println("รวม ${Totals.grossSatang(lines)} สตางค์") for ((cat, sum) in Totals.byCategory(lines)) { println(" $cat $sum") } println(Totals.report(lines).map { it.sku.code }) println(Totals.receiptLine(lines))}$ gradle -q runรวม 25000 สตางค์ dessert 12000 drink 13000[CAKE, TEA, COFFEE]3 รายการ รวม 25000 สตางค์สี่ตัวนี้ใช้เครื่องมือคนละชั้นกัน และความต่างของมันคือเนื้อหาที่เหลือของบท
sumOfรับ lambda ที่คืนLongแล้วบวกให้ ไม่มีตัวแปรให้ประกาศไว้ก่อนgroupingByแล้วfoldสะสมยอดต่อคีย์โดยไม่ต้องตั้งค่าเริ่มต้นให้คีย์ใหม่เองsortedWithรับตัวเทียบที่ต่อกันด้วยthenByDescendingได้ทีละกุญแจtoSortedMapคือบรรทัดที่ทำให้ผลไหลออกไปในลำดับที่ทำซ้ำได้
List ของ Kotlin คือคำสัญญาตอนแปล ไม่ใช่ตอนรัน
หัวข้อที่มีชื่อว่า “List ของ Kotlin คือคำสัญญาตอนแปล ไม่ใช่ตอนรัน”หัวข้อนี้แสดงว่า
Listปิดทางเขียนได้จริงตอนแปล แต่ตอนรันมันไม่มีใครยืนเฝ้า
Kotlin แยกชนิดของ collection เป็นคู่ — ตัวอ่านอย่างเดียวกับตัวที่เขียนได้ · เอกสารทางการเรียกตัวแรกว่า “A read-only interface that provides operations for accessing collection elements”
เรียก add บน List แล้ว compiler ปฏิเสธตั้งแต่ตอนแปล
// ❌ แปลไม่ผ่าน — ตั้งใจ · List ไม่มี method สำหรับเขียนpackage orderctl
fun addLine(lines: List<Line>, extra: Line) { lines.add(extra)}AddLine.kt:5:11: error: unresolved reference 'add'. lines.add(extra) ^^^นี่คือ read-only collectionread-only collection`List` ที่ไม่มี method สำหรับเขียน แต่ไม่ใช่ของที่แก้ไม่ได้ · การมองผ่าน `List` เป็นการตั้งชื่อใหม่ให้ของเดิม ไม่ใช่การถ่ายสำเนา คนที่ถือ `MutableList` ตัวเดียวกันยังเติมของแล้วเราเห็น · ต้อง `toList()` ถึงจะได้สำเนาจริง · ข้อที่คนมาจาก Java เดาผิดคือคำว่า read-only ไม่ได้แปลว่าของข้างในแก้ไม่ได้ มันแปลว่าหน้าต่างบานนี้ไม่มีปุ่มเขียน
package orderctl
fun main() { val backing = mutableListOf("tea", "cake") val view: List<String> = backing val copy: List<String> = backing.toList()
backing.add("coffee")
println("view = $view") println("copy = $copy")
val sneak = listOf("tea", "cake") as MutableList<String> println("sneak class = ${sneak.javaClass.name}") sneak[0] = "TEA" println("sneak set = $sneak") try { sneak.add("coffee") } catch (e: UnsupportedOperationException) { println("sneak add = ${e.javaClass.simpleName}") }}$ gradle -q runview = [tea, cake, coffee]copy = [tea, cake]sneak class = java.util.Arrays$ArrayListsneak set = [TEA, cake]sneak add = UnsupportedOperationExceptionอ่านผลนี้เป็นสองข้อ
viewเห็นcoffeeที่เพิ่มทีหลัง เพราะการมองผ่านListเป็นการตั้งชื่อใหม่ให้ของเดิม ไม่ใช่การถ่ายสำเนา ·copyที่มาจากtoList()ต่างหากที่เป็นสำเนาจริง- บรรทัด cast ไม่มีคำเตือนสักตัว แม้แปลด้วย
-Werror -Wextraและsetก็สำเร็จจริง · ที่ล้มคือaddเพราะของที่listOfคืนมาเป็นหน้าต่างบน array ที่ขยายไม่ได้
List.of("tea", "cake") ของ Java คืน java.util.ImmutableCollections$List12
ซึ่งโยน UnsupportedOperationException ตั้งแต่ set
listOf("tea", "cake") ของ Kotlin คืน java.util.Arrays$ArrayList ซึ่ง set ผ่าน
Kotlin เก็บความปลอดภัยไว้ที่ชั้นชนิด ส่วน Java เก็บไว้ที่ชั้นตอนรัน · ผลคือ Kotlin จับความผิดพลาดได้เร็วกว่าเมื่อคุณเขียนชนิดออกมาตรงๆ แต่จับไม่ได้เลยเมื่อมีคน cast ข้ามหน้าไป
รูปที่ปลอดภัยทั้งสองชั้นคือคืน toList() ออกไป ซึ่งเป็นสำเนาที่ผู้เรียกแก้แล้วไม่ทะลุถึงต้นทาง
mapOf สัญญาลำดับไว้เป็นลายลักษณ์อักษร
หัวข้อที่มีชื่อว่า “mapOf สัญญาลำดับไว้เป็นลายลักษณ์อักษร”หัวข้อนี้เทียบลำดับการวน map ของสามภาษา และ Kotlin เป็นภาษาเดียวที่เอกสารรับปากไว้
package orderctl
fun main() { val ordered = mapOf("drink" to 1, "dessert" to 2, "food" to 3) val hashed = hashMapOf("drink" to 1, "dessert" to 2, "food" to 3)
println("mapOf = ${ordered.keys}") println("hashMapOf = ${hashed.keys}") println("mapOf class = ${ordered.javaClass.name}") println("hashMapOf class = ${hashed.javaClass.name}")}$ gradle -q runmapOf = [drink, dessert, food]hashMapOf = [dessert, drink, food]mapOf class = java.util.LinkedHashMaphashMapOf class = java.util.HashMapmapOf คืนลำดับที่ใส่เข้าไป และเอกสารของมันเขียนไว้ว่า
“Entries of the map are iterated in the order they were specified.”
| สัญญาเรื่องลำดับ | |
|---|---|
mapOf ของ Kotlin | เอกสารรับปากว่าวนตามลำดับที่ใส่ |
Map.of ของ Java | ไม่รับปาก และคอร์ส Java วัดแล้วว่าลำดับที่ได้ไม่ตรงกันข้ามรอบรัน |
map ของ Go | runtime สุ่มจุดเริ่มโดยตั้งใจ |
hashMapOf ไม่ได้รับปากอะไรเลย · ลำดับที่เห็นข้างบนคงที่บนเครื่องนี้เพราะกุญแจเป็น String
ที่คำนวณ hash แบบเดิมทุกรอบ ไม่ใช่เพราะมีใครสัญญาไว้
byCategory ใช้ groupingBy ซึ่งคืน LinkedHashMap · ลำดับที่ได้จึงเป็นลำดับที่หมวดนั้น
โผล่มาครั้งแรกในรายการ ซึ่งวัดแล้วได้ {drink=13000, dessert=12000}
นั่นทำซ้ำได้ แต่มันไม่ใช่ลำดับที่เรียง · toSortedMap() ต่างหากที่ให้
{dessert=12000, drink=13000} ซึ่งตรงกับที่คอร์ส Java ขอด้วย TreeMap::new
กฎเดียวกันทั้งสามภาษา — อะไรที่ไหลออกไปเป็น output ต้องถูกเรียงก่อนเสมอ · Kotlin ทำให้คุณลืมกฎนี้ได้ง่ายกว่า เพราะลำดับที่มันให้มาดูเหมือนถูกอยู่แล้ว
ค่าปริยายของ Kotlin คือทำแต่ละขั้นให้ครบก่อน
หัวข้อที่มีชื่อว่า “ค่าปริยายของ Kotlin คือทำแต่ละขั้นให้ครบก่อน”หัวข้อนี้พิมพ์ลำดับการทำงานจริงของ
mapต่อfilterออกมาทั้งสองรูป
Stream ของ Java lazy มาตั้งแต่บรรทัดแรก · List ของ Kotlin ไม่ใช่
package orderctl
fun main() { val xs = listOf(1, 2, 3)
print("list : ") val a = xs.map { print("m$it "); it * 2 } .filter { print("f$it "); it > 2 } println("=> $a")
print("seq : ") val b = xs.asSequence() .map { print("m$it "); it * 2 } .filter { print("f$it "); it > 2 } .toList() println("=> $b")
print("first: ") val c = xs.asSequence() .map { print("m$it "); it * 2 } .first { print("f$it "); it > 2 } println("=> $c")}$ gradle -q runlist : m1 m2 m3 f2 f4 f6 => [4, 6]seq : m1 f2 m2 f4 m3 f6 => [4, 6]first: m1 f2 m2 f4 => 4แถวแรกทำ map ให้ครบทั้งสามตัวก่อน แล้วค่อยเริ่ม filter · แถวที่สองสลับกันไปทีละตัว
เอกสารของ Kotlin อธิบายสองรูปนี้ไว้ตรงๆ ว่า
“Sequence performs all the processing steps one-by-one for every single element”
ส่วนฝั่ง Iterable นั้น “each processing step completes and returns its result – an intermediate collection”
flowchart LR A["listOf(1, 2, 3)"] --> B["map"] B --> C["List กลาง<br/>2 4 6"] C --> D["filter"] D --> E["List ผลลัพธ์<br/>4 6"] A -.->|"asSequence()"| F["map แล้ว filter<br/>ทีละสมาชิก"] F -.-> G["toList()<br/>สร้าง List ใบเดียวตอนจบ"]
คำบรรยายภาพ: เส้นทึบสร้าง List ขึ้นมาหนึ่งใบต่อหนึ่งขั้น · เส้นประไม่สร้างใบกลางเลย และปลายทางเป็นตัวที่ตัดสินว่างานจะเริ่มเมื่อไร
SequenceSequenceท่อที่คำนวณทีละสมาชิกจนถึงตัวปิดท้าย ไม่ทำให้ครบทีละขั้นแบบ `List` · เดินซ้ำได้ ต่างจาก `Stream` ของ Java ที่ใช้ครั้งเดียว และต้องขอ `constrainOnce()` เองถ้าอยากห้าม ซื้ออะไร วัดด้วยจำนวนครั้งที่ lambda ถูกเรียก
หัวข้อที่มีชื่อว่า “Sequence ซื้ออะไร วัดด้วยจำนวนครั้งที่ lambda ถูกเรียก”หัวข้อนี้นับจำนวนครั้งที่
mapทำงานจริงเมื่อปลายทางต้องการคำตอบเดียว
ห้องทดลองนี้มีหนึ่งพันรายการ และถามคำถามที่ตอบได้ตั้งแต่รายการที่สาม
// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์ · ต้องมีชนิดของ Domain อยู่ข้างกันpackage orderctl
fun main() { val lines = (1..1000).map { lineOf("SKU$it", "drink", 1, it.toLong()) }
var eagerCalls = 0 val a = lines.map { eagerCalls++; it.subtotal().v }.first { it >= 3 }
var lazyCalls = 0 val b = lines.asSequence().map { lazyCalls++; it.subtotal().v }.first { it >= 3 }
println("list ได้ $a เรียก map $eagerCalls ครั้ง") println("seq ได้ $b เรียก map $lazyCalls ครั้ง")}$ gradle -q runlist ได้ 3 เรียก map 1000 ครั้งseq ได้ 3 เรียก map 3 ครั้งคำตอบเท่ากัน งานที่ทำต่างกันหนึ่งพันครั้งเทียบกับสามครั้ง
หน้าเอกสาร Sequences เขียนว่า “the lazy nature of sequences adds some overhead which may be significant when processing smaller collections or doing simpler computations”
Totals ทั้งสี่ตัวในบทนี้จึงไม่มีตัวไหนใช้ asSequence() เลย · ออเดอร์หนึ่งใบมีไม่กี่รายการ
และทุกสายจบด้วยการกินสมาชิกทุกตัวอยู่แล้ว
เส้นแบ่งที่ใช้ตัดสินได้จริงมีสองข้อ
- ปลายทางกินสมาชิกทุกตัวหรือไม่ · ถ้ากินครบ
Sequenceไม่ได้ลดงานลงเลย - สายยาวกี่ขั้น · ทุกขั้นบน
Listสร้างListใบใหม่หนึ่งใบ ส่วนSequenceไม่สร้างสักใบ
Sequence เดินซ้ำได้ แต่ไม่ใช่ทุกตัว
หัวข้อที่มีชื่อว่า “Sequence เดินซ้ำได้ แต่ไม่ใช่ทุกตัว”หัวข้อนี้แสดงข้อที่กลับด้านกับ
Streamของ Java และข้อยกเว้นที่ตามมาด้วย
package orderctl
fun main() { val seq = listOf(1, 2, 3).asSequence().map { it * 2 } println("รอบแรก = ${seq.toList()}") println("รอบสอง = ${seq.toList()}")
val once = listOf(1, 2, 3).asSequence().map { it * 2 }.constrainOnce() println("constrainOnce รอบแรก = ${once.toList()}") try { once.toList() } catch (e: IllegalStateException) { println("constrainOnce รอบสอง = ${e.javaClass.simpleName}: ${e.message}") }}$ gradle -q runรอบแรก = [2, 4, 6]รอบสอง = [2, 4, 6]constrainOnce รอบแรก = [2, 4, 6]constrainOnce รอบสอง = IllegalStateException: This sequence can be consumed only once.Stream ของ Java ตัวเดียวกันนี้ตอบ IllegalStateException: stream has already been operated upon or closed
ตั้งแต่รอบที่สอง โดยไม่ต้องขออะไร
เอกสารเขียนไว้ครบทั้งประโยคว่า “Sequences can be iterated multiple times; however, some sequence implementations might constrain themselves to be iterated only once.”
ตัวที่วัดแล้วว่าเดินซ้ำไม่ได้คือ Sequence ที่สร้างจาก Iterator — listOf(1, 2, 3).iterator().asSequence()
คืน [1, 2, 3] ในรอบแรก แล้วรอบสองโยน IllegalStateException พร้อมข้อความเดียวกับ constrainOnce
กฎที่ใช้ได้จริงคือ ถ้าต้นทางเป็น collection ที่ยังอยู่ครบ สายนั้นเดินซ้ำได้
ถ้าต้นทางเป็นสิ่งที่ไหลผ่านไปแล้ว มันเดินซ้ำไม่ได้ · ชนิด Sequence เองไม่บอกคุณว่าเป็นแบบไหน
forEach เป็น inline functioninline functionfunction ที่ compiler คัดเนื้อ lambda ไปวางที่จุดเรียก · ผลคือ `return` ข้างใน lambda กระโดดออกจาก function ที่ห่ออยู่ได้ ซึ่ง lambda ของ Java ทำไม่ได้เลย จึง return ออกได้
หัวข้อที่มีชื่อว่า “forEach เป็น inline function จึง return ออกได้”หัวข้อนี้แสดงข้อที่ lambda ของ Java ทำไม่ได้เลย และเป็นเหตุผลที่ Kotlin ไม่ต้องมี
breakใน lambda
package orderctl
fun findFirstBig(xs: List<Int>): Int? { xs.forEach { if (it > 10) return it } return null}
fun findFirstBigSeq(xs: Sequence<Int>): Int? { xs.forEach { if (it > 10) return it } return null}
fun main() { println(findFirstBig(listOf(1, 20, 3))) println(findFirstBigSeq(sequenceOf(1, 20, 3)))}$ gradle -q run2020return it อยู่ข้างใน lambda แต่มันคืนค่าออกจาก findFirstBig ทั้งฟังก์ชัน
· ทำได้เพราะ forEach ประกาศไว้เป็น inline compiler จึงคัดเนื้อ lambda ไปวางที่จุดเรียก
แล้วสิ่งที่เหลือคือลูปธรรมดาที่มี return อยู่ข้างใน
และมันทำงานเหมือนกันทั้งบน List และบน Sequence เพราะ forEach ของทั้งคู่เป็น inline
ฝั่ง Java ทำแบบนี้ไม่ได้ · forEach ที่นั่นรับ Consumer<T> ซึ่ง accept คืน void
· lambda ที่คืนค่าออกมาจึงถูก javac ปฏิเสธด้วยข้อความ incompatible types: unexpected return value
รูปที่แปลผ่านคือรูปที่ไม่ทำอะไรเลย
// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์ · แปลผ่าน แต่คืน null เสมอpublic static Integer findFirstBig(List<Integer> xs) { xs.forEach(x -> { if (x > 10) return; }); return null;}return; เปล่าๆ จบแค่การเรียก lambda รอบนั้นรอบเดียว · ลูปเดินต่อจนครบ แล้ว method คืน null
ทั้งที่ 20 อยู่ในรายการ · วัดแล้วบน JDK 25
lambda ของ Kotlin แก้ตัวแปรข้างนอกได้ด้วย
หัวข้อที่มีชื่อว่า “lambda ของ Kotlin แก้ตัวแปรข้างนอกได้ด้วย”หัวข้อนี้แสดงผลข้างเคียงข้อที่สองของ
inlineซึ่งเป็นข้อที่ Java ปฏิเสธตั้งแต่ตอนแปล
package orderctl
fun sumWithForEach(xs: List<Long>): Long { var acc = 0L xs.forEach { acc += it } return acc}เขียนสิ่งเดียวกันใน Java แล้ว javac ปฏิเสธ
// ❌ แปลไม่ผ่าน — ตั้งใจ · lambda ของ Java แก้ตัวแปรข้างนอกไม่ได้package orderctl;
import java.util.List;
public final class CaptureBad {
private CaptureBad() {}
public static long sumWithForEach(List<Long> xs) { long acc = 0L; xs.forEach(x -> acc += x); return acc; }}CaptureBad.java:12: error: local variables referenced from a lambda expression must be final or effectively final xs.forEach(x -> acc += x); ^1 errorทางออกที่คนเขียน Java ใช้กันคือห่อค่าไว้ใน array หนึ่งช่อง แล้วเขียนลง acc[0]
ซึ่งเป็นการหลบกฎ ไม่ใช่การทำตามกฎ
แปล sumOf { it } กับลูป for ที่เขียนเองไว้ใน file เดียวกัน แล้วเปิด javap -c ดู
· ทั้ง file ได้ class ออกมาใบเดียว และไม่มีคำสั่ง invokedynamic สักตัว
javac แปล xs.forEach(x -> ...) ของ Java แล้วได้ invokedynamic หนึ่งตัว
ซึ่งเป็นจุดที่ JVM สร้างอ็อบเจ็กต์ของ lambda ขึ้นมาตอนรัน
นี่คือราคาที่ Kotlin ไม่ต้องจ่ายและ Java ต้องจ่าย · แต่มันมาพร้อมข้อแลก คือ inline
คัดเนื้อ lambda ไปวางทุกจุดเรียก ของที่แปลออกมาจึงยาวขึ้นตามจำนวนจุดที่เรียกมัน
เหตุผลสองในสามข้อที่ปกป้องลูปธรรมดา หายไปที่นี่
หัวข้อที่มีชื่อว่า “เหตุผลสองในสามข้อที่ปกป้องลูปธรรมดา หายไปที่นี่”หัวข้อนี้กลับไปอ่านสามข้อที่คอร์ส Java ยกมาปฏิเสธ
Streamแล้วดูว่าข้อไหนยังยืนอยู่
บทที่ 4 ของคอร์ส Java ปิดท้ายด้วยสามกรณีที่ลูปธรรมดาชนะ Stream
ข้อแรกหายไปทั้งข้อ — Kotlin ไม่มี checked exception เลย
· รูปที่ javac ตอบ unreported exception IOException; must be caught or declared to be thrown
แปลผ่านเงียบสนิทที่นี่
package orderctl
import java.nio.file.Filesimport java.nio.file.Path
fun readAll(paths: List<Path>): List<String> = paths.map { Files.readString(it) }IOException ยังโยนออกมาตอนรันเหมือนเดิมทุกประการ · ที่หายไปคือคำสั่งของ compiler
ให้พิมพ์คำว่า catch ก่อน · บทที่ 6 กลับมาที่ราคาของข้อนี้
ข้อที่สองหายไปเช่นกัน — หัวข้อที่แล้วแสดงแล้วว่า return ออกจากกลาง forEach ได้จริง
· สายที่ต้องหยุดกลางคันจึงไม่ต้องถอยกลับไปเขียน for
ข้อที่สามยังอยู่ แต่เปลี่ยนรูป — คอร์ส Java เตือนเรื่องการเขียนลงของที่ใช้ร่วมกัน
ผ่าน parallelStream() · Sequence ไม่มีรูปขนานให้เลย กับดักนั้นจึงไม่มีที่ให้เกิด
ราคาคือคุณไปหาการทำงานพร้อมกันที่อื่น · List ของ Kotlin เป็น java.util.List
จึงเรียก .stream() ได้ตรงๆ และอีกทางคือ coroutine ในบทที่ 8
สรุปก่อนไปต่อ
หัวข้อที่มีชื่อว่า “สรุปก่อนไปต่อ”บทนี้ทำให้
orderctlตอบยอดรวม ยอดต่อหมวด ใบรายงาน และบรรทัดใบเสร็จได้ ด้วยTotalsตัวเดียว
สิ่งที่เพิ่มเข้าโปรเจกต์คือช่อง category ใน Line และ object Totals ที่มีสี่ฟังก์ชัน
· ไม่มีตัวแปรสะสมเหลืออยู่เลยสักตัว
สิ่งที่ควรติดตัวไปมีสี่ข้อ
Listปิดทางเขียนตอนแปล ไม่ใช่ตอนรัน · ต้องการสำเนาจริงให้เรียกtoList()mapOfรับปากลำดับที่ใส่ แต่ลำดับที่ใส่ยังไม่ใช่ลำดับที่เรียงasSequence()คุ้มเมื่อปลายทางไม่กินสมาชิกทุกตัว หรือเมื่อสายยาวหลายขั้นforEachเป็น inline จึงreturnออกจากฟังก์ชันที่ห่ออยู่ได้ ซึ่ง Java ทำไม่ได้
บทหน้าเป็นเรื่อง interface กับ delegation · Kotlin ได้การส่งต่อฟรีแบบ embedding ของ Go
มาด้วยคำเดียวคือ by แล้วได้กับดักของ Go ติดมาด้วยในที่ที่มองไม่เห็น
บทนี้อิงต้นทางที่ลงวันที่กำกับ อ่านต่อได้โดยตรง:
- Sequences (เข้าถึง 2026-08-28) — ประโยคเรื่องลำดับการทำงานทีละสมาชิก ประโยคเรื่องการเดินซ้ำ และประโยคเตือนเรื่องต้นทุนของความ lazy
- Collections overview (เข้าถึง 2026-08-28) — คำที่เอกสารใช้เรียก interface อ่านอย่างเดียว
mapOf— kotlin.collections (เข้าถึง 2026-08-28) — “Entries of the map are iterated in the order they were specified.”- บทที่ 4 ของคอร์ส Java — สามกรณีที่ลูปธรรมดาชนะ
Streamและตัวเลขชุดเดียวกับบทนี้ - บทที่ 4 ของคอร์ส Go — ยอดต่อหมวดที่เขียนด้วย
forและการเรียงคีย์ก่อนพิมพ์ - ผลทุกบรรทัดในบทนี้วัดเองบน
kotlinc-jvm 2.4.10กับ JDK 25 และ Gradle Kotlin DSL บน macOS arm64 เมื่อ 2026-08-28 · รันซ้ำได้ด้วยคำสั่งที่ยกมาในแต่ละหัวข้อ
เช็กความเข้าใจ — บทที่ 4
ข้อ 1 / 3`val view: List<String> = backing` เมื่อ `backing` เป็น `MutableList` แล้วมีคนเรียก `backing.add("coffee")` ทีหลัง · `view` เห็นอะไร