Kotlin ยืมกล่องเครื่องมือของ Java มาทั้งใบ แล้วเปลี่ยนสามอย่าง
คอร์สนี้ไม่ได้เริ่มที่ว่าตัวแปรคืออะไร
มันเริ่มที่คำถามที่มีประโยชน์กว่า — Kotlin ตัดสินใจต่างจากภาษาที่คุณใช้อยู่ตรงไหน และมันจ่ายอะไรไปกับการตัดสินใจนั้น
คอร์สนี้สร้าง orderctl เครื่องมือบรรทัดคำสั่งที่ประมวลผลออเดอร์อาหาร
ตัวเดิมกับที่ คอร์ส Go จากศูนย์ และ
คอร์ส Java จากศูนย์ สร้างไว้
การเขียนของชิ้นเดียวกันสามภาษา ทำให้คำถาม “ภาษาไหนดีกว่า” กลายเป็นคำถามที่ตอบได้ ด้วยการชี้ไปที่ code สามชุดที่ทำงานเดียวกัน แทนที่จะเถียงกันด้วยความรู้สึก
บทนี้เป็นบทแนวคิดกับการติดตั้ง code ที่รันได้จริงมีแค่ hello world · ของจริงเริ่มบทที่ 2
Kotlin ไม่ได้แข่งกับ JVM มันนั่งอยู่บนนั้น
หัวข้อที่มีชื่อว่า “Kotlin ไม่ได้แข่งกับ JVM มันนั่งอยู่บนนั้น”ทุกอย่างที่ Java ทำได้ Kotlin ทำได้ · ที่ต่างคือสามข้อที่ภาษาบังคับให้คุณเขียนไม่เหมือนกัน
Go สร้างทุกอย่างใหม่หมด ตั้งแต่ตัวเก็บขยะจนถึงตัวจัดคิว · Kotlin ไม่ทำแบบนั้น มันแปลลง bytecode ของ JVM แล้วเรียก library ของ Java ได้ทุกตัวโดยไม่ต้องห่ออะไร
| เรื่อง | Go เลือก | Java เลือก | Kotlin เลือก | บทที่กลับมา |
|---|---|---|---|---|
| ค่าที่หายไป | struct กับชนิดพื้นฐานแตะได้เสมอ | null ได้ทุกที่ ตรวจตอนรัน | ชนิดบอกเองว่ารับ null ได้ไหม | 2 |
| ความผิดพลาด | เป็นค่าที่ฟังก์ชันคืนกลับมา | exception ที่บางตัว compiler บังคับให้จัดการ | exception ที่ไม่มีตัวไหนถูกบังคับ | 6 |
| การเข้ากับ interface | ดูจากรูปร่าง ไม่ต้องประกาศ | ต้องเขียน implements ออกมา | ต้องเขียน : ออกมา เหมือน Java | 5 |
| การใช้ซ้ำโดยไม่สืบทอด | ฝัง struct แล้วได้ method มาฟรี | เขียน method ส่งต่อเองทุกตัว | by คำเดียว compiler เขียนให้ | 5 |
| การจัดรูปแบบ | gofmt ที่ไม่มีตัวเลือก | ไม่มีตัวไหนในกล่อง | ไม่มีตัวไหนในกล่องเหมือนกัน | บทนี้ |
อ่านตารางนี้เป็นสามกลุ่ม
- สามแถวแรก คือสามอย่างที่ Kotlin เปลี่ยน
- แถวที่สี่ คือข้อที่ Go มีในรูปของตัวเองอยู่แล้ว และ Java ไม่มี
- แถวสุดท้าย คือข้อที่ Kotlin ไม่ได้แก้อะไรเลย และคอร์สนี้จะไม่แกล้งทำเป็นไม่เห็น
เลือกรุ่นก่อน — ตารางของ Kotlin มีแถวเดียว
หัวข้อที่มีชื่อว่า “เลือกรุ่นก่อน — ตารางของ Kotlin มีแถวเดียว”Kotlin มีตารางอายุเหมือนกัน แต่มันสั้นกว่าที่คุณคาด และครอบของแคบกว่าที่คุณคาด
คอร์ส Java บอกว่ารุ่นที่ได้รับการดูแลยาวเรียกว่า LTS และออกทุกสองปี · ฝั่ง Kotlin ไม่มีคำว่า LTS เลย แต่มีตารางอายุอยู่จริงในหน้าเอกสารรุ่น ใต้หัวข้อที่ชื่อ Standard library security support
| Kotlin release line | Release date | End of support | Latest bug fix release |
|---|---|---|---|
| 2.4 | June 3, 2026 | December 3, 2027 | 2.4.10 |
ทั้งตารางมีแถวเดียว และประโยคที่อยู่เหนือมันคือข้อที่ต้องอ่านให้ครบทุกคำ
“Starting with Kotlin 2.4.0, the Kotlin standard library for the JVM has an 18–month support window for each release line.”
มันครอบ kotlin-stdlib บน JVM สามอย่างที่มันไม่ครอบคือ
- ตัว compiler เอง
- plugin ของ compiler
- library อื่นของ JetBrains
และคำที่หัวข้อนั้นใช้คือ security ไม่ใช่ LTS · คอร์สนี้ใช้ Kotlin 2.4.10 ซึ่งเป็นรุ่นเดียวกับที่ระบบตรวจของเว็บนี้ตรึงไว้ และเป็นแถวเดียวที่ตารางนั้นมี
ติดตั้งแล้วดูว่าได้อะไรมา
หัวข้อที่มีชื่อว่า “ติดตั้งแล้วดูว่าได้อะไรมา”เจ็ดคำสั่ง ไม่มีตัวไหนจัดรูปแบบให้ และมีหนึ่งตัวที่ดึง dependency ได้จริง
$ kotlinc -versioninfo: kotlinc-jvm 2.4.10 (JRE …)ตัวเลขในวงเล็บคือ JDK ที่ compiler กำลังรันอยู่ ซึ่งเปลี่ยนตามเครื่อง · คอร์สนี้จึงผูกกับ 2.4 เท่านั้น ไม่ผูกกับข้อความเต็ม
คอร์ส Java มีกล่องเตือนเรื่อง java --version กับ java -version ที่ไปคนละสาย
· ฝั่ง Kotlin เรื่องเดียวกันแรงกว่า เพราะสองคำสั่งในกล่องเดียวกันทำคนละอย่าง
| คำสั่ง | ไปทางไหน |
|---|---|
kotlinc -version | stderr · stdout ว่างเปล่า |
kotlinc --version | ไม่มีรูปนี้ · ตอบ error: invalid argument: --version แล้วต่อด้วย info: use -help for more information ทั้งคู่ออก stderr รหัสออก 1 |
kotlin -version | stdout และข้อความคนละแบบ (Kotlin version 2.4.10-release-377 (JRE …)) |
สคริปต์ที่เขียน V=$(kotlinc -version) จะได้ค่าว่าง แล้วเงื่อนไขที่ตามมาก็ผ่านไปเงียบๆ
· ถ้าต้องดักผลจริง ให้เขียน kotlinc -version 2>&1
kotlinc ไม่ได้มาตัวเดียว มันมาเป็นชุดเจ็ดคำสั่ง · แต่เปิดอ่านข้างในแล้วมีตัวจริงตัวเดียว
kotlincตัวจริง · แปล source เป็น.classหรือเป็น jarkotlinตั้งKOTLIN_RUNNER=1แล้วเรียกkotlinc· รันสิ่งที่แปลไว้แล้ว และเปิด REPL ได้kotlinrตั้งค่าตัวเดียวกันเป๊ะ แล้วเรียกตัวเดียวกันkotlinc-jvmไม่ตั้งอะไรเลย เรียกkotlincต่อตรงๆkotlinc-jsกับkotlinc-wasmเปลี่ยนKOTLIN_COMPILERไปเป้าหมายที่คอร์สนี้ไม่แตะkaptตั้งKOTLIN_TOOLเป็น annotation processor รุ่นเก่า ซึ่งคอร์สนี้ไม่ใช้
หกตัวหลังเป็นสคริปต์สั้นๆ ที่ตั้ง environment variable หนึ่งตัวแล้วเรียก kotlinc ต่อ
· นี่คือคำอธิบายของกล่องเตือนข้างบน — kotlinc -version กับ kotlin -version
คือสคริปต์เดียวกันที่เดินคนละกิ่ง จึงพิมพ์คนละที่และคนละข้อความ
ถ้าคุณมาจาก Go คุณจะมองหา gofmt · ไม่มี
ตัวที่คนใช้กันคือ ktlint และ ktfmt ซึ่งอยู่นอกกล่องทั้งคู่ ต้องเลือกเอง ตั้งค่าเอง
· ข้อนี้ Kotlin ไม่ได้แก้อะไรจาก Java เลย มันรับมรดกมาทั้งดุ้น
ส่วน dependency ต้องแยกสองกรณี · สำหรับโปรเจกต์ .kt ที่แปลจริง ไม่มีตัวไหนดึงให้
คุณต่อ -cp เอง จนกว่าจะมี Gradle ในบทที่ 3
แต่ script .main.kts ดึงเองได้ และตัวที่ทำงานคือ kotlin-main-kts.jar ที่อยู่ในกล่อง
// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์@file:Repository("https://repo1.maven.org/maven2")@file:DependsOn("org.apache.commons:commons-text:1.12.0")$ kotlin dep.main.ktsok Orderctl พร้อมมันติดต่อ Maven Central จริงและเก็บลง ~/.m2 · แต่มันคือ CLI scripting ตัวเดิม
ที่ยังเป็น Alpha คอร์สนี้จึงไม่สร้างอะไรบนมัน
โปรแกรมแรก
หัวข้อที่มีชื่อว่า “โปรแกรมแรก”สั้นที่สุดในสามภาษา และไม่มีอะไรครอบข้อความเลย
สร้าง Hello.kt แล้วใส่แค่นี้
fun main() { println("orderctl พร้อมทำงาน")}เทียบกับสองคอร์สก่อนหน้าที่พิมพ์ข้อความเดียวกัน
| ภาษา | สิ่งที่ต้องเขียนรอบข้อความ |
|---|---|
| Go | package main · import "fmt" · func main() · fmt.Println |
| Java 25 | void main() · IO.println |
| Kotlin | fun main() · println |
println ของ Kotlin อยู่ใน kotlin.io ซึ่งถูกนำเข้าให้โดยปริยายทุก file
· ไม่มีอะไรให้พิมพ์เพิ่ม
แต่รันมันไม่ได้ด้วยคำสั่งเดียว
หัวข้อที่มีชื่อว่า “แต่รันมันไม่ได้ด้วยคำสั่งเดียว”อีกสองภาษามีทางลัดจาก source ถึงผลลัพธ์ในคำสั่งเดียว · Kotlin ไม่มี
ลองท่าที่คุณคุ้นจากคอร์ส Java ดูก่อน
$ kotlin Hello.kterror: could not find or load main class Hello.ktต้องแปลก่อนเสมอ
$ kotlinc Hello.kt -d out$ kotlin -cp out HelloKtorderctl พร้อมทำงานสังเกตว่าชื่อที่พิมพ์ตอนรันคือ HelloKt ไม่ใช่ Hello และไม่มีใครเขียนชื่อนั้นไว้ที่ไหนเลย
HelloKt มาจากไหน
หัวข้อที่มีชื่อว่า “HelloKt มาจากไหน”JVM รันได้แต่ method ที่อยู่ใน class · Kotlin มีฟังก์ชันลอยได้ จึงต้องมีใครสักคนสร้าง class ให้
$ ls outMETA-INFHelloKt.classcompiler เอาชื่อ file มาตัดนามสกุลออก ทำอักษรแรกเป็นตัวใหญ่ แล้วเติม Kt ต่อท้าย
· ทุกอย่างที่คุณเขียนไว้ระดับบนสุดของ file กลายเป็นสมาชิก static ของ class ตัวนั้น
ส่วน META-INF ข้างในมี main.kotlin_module ซึ่งเป็นบันทึกที่ Kotlin เขียนถึงตัวเอง
· ลบทิ้งแล้วโปรแกรมยังรันได้ตามปกติ เพราะ JVM ไม่รู้จักมัน · แต่ kotlinc รอบหน้าอ่านมัน
เพื่อหาของระดับบนสุดที่ file อื่นประกาศไว้ ลบแล้วการแปล file ที่เรียกข้ามกันจะได้
unresolved reference ทั้งที่ .class ยังอยู่ครบ
กฎนี้ไม่สนใจว่าคุณตั้งชื่อ file อย่างไร · hello.kt ก็ได้ HelloKt เหมือนกัน
และ my_app.kt ได้ My_appKt โดยขีดล่างยังอยู่ครบ
นี่คือ file classfile classclass ที่ compiler ตั้งชื่อจากชื่อ file แล้วเติม `Kt` ต่อท้าย · `Hello.kt` ได้ `HelloKt` ซึ่งเป็นชื่อที่ต้องพิมพ์ตอนรันและที่โผล่ในทุก stack trace ทั้งที่ไม่มีใครเขียนมันไว้ · มันจะโผล่ในทุก stack trace และในทุกคำสั่งที่ต้องเอ่ยชื่อ class ทั้งที่ไม่มีบรรทัดไหนใน source พูดถึงมัน
Kotlin มี Kotlin scriptKotlin scriptfile `.kts` ที่ `kotlinc -script` รันได้ทันทีโดยไม่มี build tool · CLI scripting ยังเป็น Alpha และ compiler ไม่พิมพ์คำเตือนอะไรเลยตอนรัน คอร์สนี้จึงไม่ใช้มันเป็นแกนของบทไหน ซึ่งเป็น file .kts ที่รันได้ด้วยคำสั่งเดียวจริงๆ
· hello.kts มีบรรทัดเดียว ไม่ต้องมี fun main ครอบ
println("orderctl พร้อมทำงาน (script)")$ kotlin hello.ktsorderctl พร้อมทำงาน (script)kotlinc -script hello.kts ก็ได้ผลเดียวกัน · สิ่งที่คำสั่ง kotlin รับไม่ได้คือ .kt
ไม่ใช่ source ทุกชนิด
แต่ CLI scripting ยังมีสถานะ Alpha และ compiler ไม่พิมพ์คำเตือนอะไรเลยตอนรัน · คุณจึงไม่มีทางรู้จากหน้าจอว่ากำลังยืนอยู่บนของที่ยังไม่นิ่ง
คอร์สนี้จึงไม่ใช้ .kts เป็นแกนของบทไหน · ยกเว้นที่เดียวคือ build.gradle.kts
ในบทที่ 3 ซึ่งเป็นคนละเรื่องกัน เพราะ Gradle ประมวลผลมันด้วยกลไกของตัวเอง
ชั่งของที่ส่งต่อได้ ออกมาเป็นไบต์
หัวข้อที่มีชื่อว่า “ชั่งของที่ส่งต่อได้ ออกมาเป็นไบต์”สามภาษา สาม hello world เดียวกัน แล้ววัดว่าปลายทางต้องมีอะไรบ้าง
flowchart LR A["Hello.kt"] -->|"kotlinc Hello.kt -d thin.jar"| D["thin.jar<br/>1,004 ไบต์"] A -->|"kotlinc Hello.kt -include-runtime -d hello.jar"| E["hello.jar<br/>5,609,464 ไบต์"] D --> C["รันบน JVM เปล่าได้"] E --> C E -.-> F["kotlin-stdlib<br/>1,841,933 ไบต์"] E -.-> G["kotlin-reflect<br/>3,750,100 ไบต์"] E -.-> H["annotations<br/>17,536 ไบต์"]
คำบรรยายภาพ: hello world ทั้งสองรูปรันบน JVM เปล่าได้เท่ากัน · ที่ต่างคือรูปหนักมีของสามใบติดมาด้วย
kotlinc ห่อผลเป็น jar ได้เลย และมันมีสองโหมดที่ให้ตัวเลขห่างกันมาก
$ kotlinc Hello.kt -d thin.jar$ kotlinc Hello.kt -include-runtime -d hello.jar$ ls -l thin.jar hello.jar| สิ่งที่ได้ | ไบต์ |
|---|---|
thin.jar | 1,004 |
hello.jar (-include-runtime) | 5,609,464 |
-include-runtime ทำให้ของที่ส่งต่อโตขึ้นราวห้าพันหกร้อยเท่า จากหนึ่งพันไบต์เศษ
เป็นห้าเมกะไบต์เศษ
แล้วรูปเบาต้องมี stdlib ที่ปลายทางไหม
หัวข้อที่มีชื่อว่า “แล้วรูปเบาต้องมี stdlib ที่ปลายทางไหม”คำตอบที่คนส่วนใหญ่เดา กับคำตอบที่เครื่องให้ ไม่ตรงกัน
ลองรัน thin.jar บน JVM เปล่า โดยไม่มี kotlin-stdlib.jar อยู่ที่ไหนเลย
$ java -jar thin.jarorderctl พร้อมทำงานมันรันได้ · เหตุผลอยู่ใน bytecode
$ javap -c -p out/HelloKt.class 2: getstatic // Field java/lang/System.out:Ljava/io/PrintStream; 6: invokevirtual // Method java/io/PrintStream.println:(Ljava/lang/Object;)Vprintln ของ Kotlin เป็น inline function · compiler คัดเนื้อของมันมาวางที่จุดเรียก
แล้วสิ่งที่เหลือคือ System.out.println ของ Java ตรงๆ · ชื่อเดียวที่ขึ้นต้นด้วย
kotlin/ ใน class file นี้คือ annotation kotlin/Metadata ซึ่ง JVM ข้ามไปเมื่อหาไม่เจอ
เปลี่ยนโปรแกรมให้แตะของที่ inline ไม่ได้ แค่นี้พอ
fun main() { val skus = listOf(" tea ", "cake") println(skus.map { it.trim().uppercase() })}$ java -jar needs.jarException in thread "main" java.lang.NoClassDefFoundError: kotlin/collections/CollectionsKtใส่ kotlin-stdlib.jar เข้า class path แล้วได้ [TEA, CAKE]
เส้นแบ่งไม่ได้อยู่ที่ภาษา มันอยู่ที่ว่าคุณเรียกอะไร · hello world ไม่ต้องมี stdlib เพราะมันไม่ได้เรียกอะไรของ stdlib เลย · แต่โปรแกรมจริงเรียกตั้งแต่บรรทัดที่สอง
ห้าเมกะไบต์นั้นเป็นของใคร
หัวข้อที่มีชื่อว่า “ห้าเมกะไบต์นั้นเป็นของใคร”ไม่ใช่ stdlib ทั้งก้อน · ตัวที่ใหญ่ที่สุดคือตัวที่คุณไม่ได้ขอ
นับรายการข้างใน hello.jar แยกตาม package ระดับบน
$ unzip -l hello.jar | awk '{print $4}' | grep '^kotlin/' | cut -d/ -f2 | sort | uniq -c | sort -rn2432 reflect 157 jvm 153 collections-include-runtime ไม่ได้มัดแค่ kotlin-stdlib มันมัดสามใบ
| ใบ | ไบต์ |
|---|---|
kotlin-stdlib.jar | 1,841,933 |
kotlin-reflect.jar | 3,750,100 |
annotations-13.0.jar | 17,536 |
| รวม | 5,609,569 |
รวมสามใบได้ 5,609,569 ไบต์ ซึ่งเทียบกับ hello.jar ที่ 5,609,464 แล้วห่างกันหนึ่งร้อยไบต์เศษ
· ส่วนต่างมาจากการบีบอัดและ manifest
kotlin-reflect ใหญ่เป็นสองเท่าของ stdlib และ hello world ไม่ได้เรียกมันสักบรรทัด
· นี่คือราคาจริงของคำว่า “พึ่งตัวเองได้”
kotlin-stdlibkotlin-stdliblibrary ที่ต้องเดินทางไปกับโปรแกรมเสมอ เพราะ JVM ไม่มีให้ · `-include-runtime` มัดมันเข้า jar จนโปรแกรมสามบรรทัดโตจาก 1,004 เป็น 5,609,464 ไบต์ จึงเป็นของที่ต้องคิดถึงตั้งแต่วันแรก แต่ไม่ใช่ด้วยเหตุผลที่คนมักบอกกัน
· Java ได้ IO.println มาฟรีเพราะ java.lang.IO อยู่ใน JDK · Kotlin ได้ println
มาฟรีเหมือนกันเพราะมัน inline ทิ้ง · ที่ไม่ฟรีคือทุกอย่างหลังจากนั้น
kotlin-stdlib.jar มีขนาดคงที่ ไม่ได้โตตามโปรเจกต์ของคุณ
· โปรเจกต์ที่มี code หนึ่งหมื่นบรรทัดก็ยังลาก 1.8 เมกะไบต์เท่าเดิม
แปลว่าอัตราส่วน “ห้าพันหกร้อยเท่า” เป็นเรื่องของ hello world ล้วนๆ · พอโปรเจกต์โตขึ้น สัดส่วนนั้นหดลงเรื่อยๆ · สิ่งที่ไม่หดคือข้อที่ว่าคุณต้องส่ง stdlib ไปด้วยเสมอ ซึ่งเป็นข้อที่ Java ไม่ต้องเจอ
สิ่งที่บทนี้ยังไม่ทำ
หัวข้อที่มีชื่อว่า “สิ่งที่บทนี้ยังไม่ทำ”สี่อย่างที่ยังไม่มี และแต่ละอย่างมาถึงเมื่อมีเหตุผลให้ต้องมี
- ไม่มี
packageจึงยังไม่มีโครงไดเรกทอรีให้จำ - ไม่มี build tool จึงยังไม่มี Gradle ให้อ่าน
- ไม่มีเทสต์ ทั้งที่
kotlin-test.jarอยู่ในกล่องแล้ว — บทที่ 7 จะกลับมาที่ข้อนี้ - ไม่มี dependency ซึ่งจะกลายเป็นเรื่องน่าสนใจในบทที่ 3 · Kotlin ไปได้ถึงบทที่ 2 โดยไม่มี build tool เท่ากับคอร์ส Java พอดี แต่จ่ายด้วยการแปลที่แยกเป็นสองคำสั่ง
สรุปก่อนไปต่อ
หัวข้อที่มีชื่อว่า “สรุปก่อนไปต่อ”บทนี้วางกรอบและติดตั้ง ยังไม่มี feature ของ
orderctlเกิดขึ้นจริง
คุณรู้แล้วว่า Kotlin นั่งอยู่บน JVM ตัวเดิม เปลี่ยนสามอย่างที่ภาษาบังคับ
แปลและรันโปรแกรมแรกได้ และรู้ว่า HelloKt มาจากไหน
บทหน้าเราลงของจริง — data class และ sealed interface · จุดที่คนมาจาก Java
ต้องปรับความคิดมากที่สุดคือ init ของ Kotlin ปฏิเสธค่าที่ผิดได้ แต่ซ่อมมันไม่ได้
ต่างจาก compact constructor ของ Java ที่ปฏิเสธก็ได้ ซ่อมก็ได้
บทนี้อิงต้นทางที่ลงวันที่กำกับ อ่านต่อได้โดยตรง:
- Kotlin releases (เข้าถึง 2026-08-28) — หัวข้อ Standard library security support · “Starting with Kotlin 2.4.0, the Kotlin standard library for the JVM has an 18–month support window for each release line.”
- Kotlin component stability (เข้าถึง 2026-08-28) — สถานะของ CLI scripting
- Kotlin command-line compiler (เข้าถึง 2026-08-28) —
kotlinc-include-runtimeและการรันด้วยkotlin - Kotlin/JVM — calling Kotlin from Java (เข้าถึง 2026-08-28) — กฎการตั้งชื่อ class จากชื่อ file และ
@JvmName - ตัวเลขไบต์ทั้งสามตัววัดเองบน
kotlinc-jvm 2.4.10· macOS arm64 เมื่อ 2026-08-28 · รันซ้ำได้ด้วยคำสั่งที่ยกมาในหัวข้อนั้น
เช็กความเข้าใจ — บทที่ 1
ข้อ 1 / 3ทำไม `kotlin Hello.kt` ถึงตอบ `could not find or load main class`