[Kotlin] map reduce filter – รวมคำสั่งดีๆ ที่ใช้กับ List

หนึ่งในความสามารถน่ารักๆ ของภาษา Kotlin คือการใช้ lambda (หรือก็คือ Closure / Anonymous Function นั่นเอง ) จัดการกับข้อมูล Collection ประเภท List

ซึ่งคำสั่งที่ตัวภาษา build-in มาให้ที่ใช้ได้นั้นมีมากมายสุดๆ บล๊อกนี้เลยจะรวบรวมคำสั่งที่ใช้กับ List พร้อมตัวอย่างเอาไว้

คำสั่งหมวด Element

forEach / forEachIndexed

เป็นการวนลูปข้อมูลทุกตัวใน list ซึ่ง iterator ของข้อมูลแต่ละตัวจะแทนด้วยคีย์เวิร์ด it (ตามมาตราฐานของภาษา Kotlin)

สำหรับ forEach จะเป็นการวน element ทุกตัว แต่ถ้าต้องการ counter หรือตัวนับ index ด้วยสามารถเปลี่ยนไปใช้ forEachIndexed แทนได้ โดยการเขียนจะต้องรับ parameter 2 ตัวคือ key กับ value มา

val data = listOf(10, 20, 30, 40, 50)
data.forEach {
    println(it)
}
//print:
10
20
30
40
50

val data = listOf(10, 20, 30, 40, 50)
data.forEachIndexed { key, value ->
    println("$key = $value")
}
//print:
0 - 10
1 - 20
2 - 30
3 - 40
4 - 50

ในภาษา Kotlin การใช้ forEach ไม่จำกัดว่าจะต้องเขียนแบบ pure function (ฟังก์ชันที่ห้ามยุ่งกับตัวแปรหรือค่าภายนอก) ทำให้เราสามารถดึงค่านอก lambda มาใช้ด้วยได้ (แต่ก็ไม่ควรทำนะ ถึงมันจะทำได้ก็เถอะ)

val data = listOf(10, 20, 30, 40, 50)
var x = 0
data.forEach {
    x += it
}
println(x) //150

elementAt / elementArOrElse / elementArOrNull

เป็นการขอข้อมูลในตัวแหน่งที่ระบุ จริงๆ มันก็เหมือนกับการ access List ด้วยการใช้ [ ] แบบธรรมดา แต่เรามักจะใช้กับกรณีที่ไม่แน่ใจว่าข้อมูลตำแหน่งนั้นมีอยู่จริงหรือเปล่า

val data = listOf(10, 20, 30, 40, 50)

//elementAt จะได้คำตอบเหมือนการใช้ [ ] แบบปกติกับ array หรือ list เลย
data.elementAt(0) //10
data[0] //10

//elementAtOrElse เป็นการใส่เงื่อนไขเพิ่มว่าถ้า index ที่ขอไปไม่มีอยู่ มันจะตอบ else กลับมาแทน
//โดยการเขียน else จะอยู่ในรูป lambda สำหรับกรณีที่ต้องการใช้ index ด้วยสามารถอ้างได้ผ่านตัวแปร it 
data.elementAtOrElse(0) {-10} //10
data.elementAtOrElse(20) {-10} //-10
data.elementAtOrElse(25) { it + 5 } //25

//elementAtOrNull จะคล้ายๆ กับ OrElse แต่ถ้าไม่มี index นั้นอยู่ จะตอบเป็น null กลับมาแทน
data.elementAtOrNull(0) //10
data.elementAtOrNull(20) //null

first / firstOrNull / last / lastOrNull

ใช้ในการขอข้อมูลตัวแรกสุด (หรือตัวหลังสุด) ของ List โดยข้อแตกต่างระหว่าง first กับ firstOrNull (และ last กับ lastOrNull) คือถ้าใช้แบบไม่มี OrNull แล้วไม่มีข้อมูลตัวนั้นขึ้นมาจะเจอ NoSuchElementException ตอนรันจริง

แต่นอกจากการข้อ first กับ last แบบธรรมดา เราสามารถใส่ lambda ลงไปได้ด้วย เช่น "ขอตัวแรกที่มีค่ามากกว่า 25"

val data = listOf(10, 20, 30, 40, 50)
data.first() //10
data.last() //50

data.first { it > 25 } //30
data.last { it > 25 } //50

//แต่ถ้า list ไม่มีข้อมูลอยู่เลย เช่น
val data = listOf<Int>()

data.firstOrNull() //null
data.firstOrNull() //null

data.first() //Runtime Error: NoSuchElementException
data.last() //Runtime Error: NoSuchElementException

indexOf / indexOfFirst / indexOfLast

คล้ายๆ กับ indexOf ในภาษา Javaคือค้นหาตำแหน่งของข้อมูลใน list (ตอบกลับเป็น index ตำแหน่งที่หาเจอ) ... ในกรณีที่หาข้อมูลตัวนั้นไม่เจอเลยจะ return ค่าเป็น -1

ส่วนข้อแตกต่างระหว่าง indexOf กับ indexOfFirst กับ indexOfLast คือ indexOf จะหาข้อมูลตัวนั้นตรงๆ เลย ... แต่ indexOfFirst กับ indexOfLast จะหาด้วยเงื่อนไข lambda

val data = listOf(10, 20, 30, 40, 50)

data.indexOf(30) //2
data.indexOf(60) //-1

//หา index ของข้อมูลตัวแรกที่มากกว่า 35: ในที่นี้คือเจอ 40 (index: 3)
data.indexOfFirst { it > 35 } //3

//หา index ของข้อมูลตัวแรกที่มากกว่า 35 จากด้านหลังของ list: ในที่นี้คือเจอ 50 (index: 4)
data.indexOfLast { it > 35 } //4

single / singleOrNull

ใช้สำหรับการขอข้อมูลที่ต้องมีเพียงตัวเดียวใน list ถ้าข้อมูลที่ค้นหามีมากกว่า 1 ตัวจะ throw IllegalArgumentException ออกมา

listOf(100).single() //100
listOf(100, 200).single() //Runtime Error: IllegalArgumentException 

listOf(100, 200).single{ it > 150 } //200
listOf(100, 200, 300).single{ it > 150 } //Runtime Error: IllegalArgumentException

คำสั่งหมวด Aggregate

any

ใช้สำหรับเช็กว่าข้อมูลที่อยู่ในลิสต์มีอย่างน้อย 1 ตัวที่ตรงกับเงื่อนไขของเรา เราจะต้องเขียน lambda ในรูปของ condition true/false

val data = listOf(10, 20, 30, 40, 50)

data.any { it > 25 } //true
data.any { it > 100 } //false

all

เหมือนกับคำสั่ง any แต่เป็นการเช็กว่าข้อมูลในลิสต์ทุกตัวต้องตรงกับเงื่อนไข

val data = listOf(10, 20, 30, 40, 50)

data.all { it > 5 } //true
data.all { it > 25 } //false

none

เป็นส่วนกลับของ all คือแทนที่จะเช็กว่าข้อมูลทุกตัวตรงกับเงื่อนไข อันนี้จะเป็นการเช็กว่าไม่มีข้อมูลตัวไหนตรงกับเงื่อนไขแทน

val data = listOf(10, 20, 30, 40, 50)

data.none{ it > 100 } //true
data.none{ it > 10 } //false

count

นับจำนวนข้อมูลที่ตรงกับเงื่อนไข

val data = listOf(10, 20, 30, 40, 50)

//นับข้อมูลทุกตัวที่ตรงกับเงื่อนไข
data.count { it > 25 } //3

//แค่นับทุกตัวทั้งหมดเลย มีค่าเท่ากับการใช้ size
data.count() //5
data.size //5

contains / containsAll

เช็กว่าใน list มีข้อมูลตัวนั้นๆ หรือเปล่า ถ้าเป็น contains จะหาแค่ 1 ตัว ส่วน containsAll จะหาแบบหลายตัวและต้องเจอทุกตัวถึงจะตรงเงื่อนไข

val data = listOf(10, 20, 30, 40, 50)

data.contains(10) //true
data.contains(100) //false

data.containsAll( listOf(10, 20, 30) ) //true
data.containsAll( listOf(40, 50, 60) ) //false

max / maxBy / min / minBy

เป็นการหาค่ามากที่สุด (หรือน้อยที่สุด) ใน List โดยเราสามารถใส่เงื่อนไขการหาแบบ lambda ได้ด้วย maxBy (และminBy)

val data = listOf(10, 20, 30, 40, 50)
data.min() //10
data.max() //50

data.minBy{ -it } //50
data.maxBy{ -it } //10

หรือถ้า List ของเราเป็นตัวแปรชนิดอื่น min กับ max จะทำงานเมื่อ class นั้น implements มาจาก Comparable (แบบเดียวกับ Java เลย)

data class Foo(var x:Int = 0): Comparable<Foo>{
    override fun compareTo(other: Foo): Int {
        return x - other.x
    }
}

val data = listOf(Foo(1), Foo(2). Foo(3))
data.min() //Foo(x=1)
data.max() //Foo(x=3

sum / sumBy

หาผลรวมค่าทั้งหมดใน List หรือใช้แบบระบุตัวที่จะให้ sum ผ่าน lambda ด้วย sumBy

val data = listOf(10, 20, 30, 40, 50)
data.sum() //150

//หรือถ้า list เป็น object
data class Foo(var x:Int, var y:Int)
val data = listOf( Foo(1, 10), Foo(2, 20), Foo(3, 30) )
//sum ด้วยค่า x
data.sumBy{ it.x } //6
//sum ด้วยค่า y
data.sumBy{ it.y } //60

หมวด Map

map / mapIndexed

เป็นการแปลงข้อมูลทุกตัวด้วยเงื่อนไขแบบเดียวกัน ถ้าต้องการ index ด้วยให้ใช้ mapIndexed

val data = listOf(10, 20, 30, 40, 50)

data.map{ it / 10 } //[1, 2, 3, 4, 5]
data.map{ it + 2 } //[12, 22, 32, 42, 52]

data.mapIndexed{ key, value ->
    key
}
//[0, 1, 2, 3, 4]

mapNotNull

ใช้ในกรณีที่ List นั้นมีสมาชิกบางตัวที่เป็น null แล้วไม่ต้องการยุ่งกับค่าพวกนั้น

val data = listOf(10, 20, 30, null, 50, null, 70)
data.mapNotNull{ it } //[10, 20, 30, 50, 70]

flatMap

ปกติการ map จะมีจำนวนข้อมูล input เท่ากับ output เช่นลิสต์เดิมมี 10 ตัว ลิสต์ใหม่ที่ได้หลัง map ก็จะได้ 10 ตัว

แต่ถ้าเป็น flatMap จะเป็นการขยายข้อมูล output เช่นถ้าข้อมูลเข้า 10 ตัวอาจจะทำให้มีข้อมูลออก 20 ตัว (ถ้าต้องการให้ output มีจำนวนน้อยลงจะใช้ filter)

val data = listOf(1, 2, 3)

data.flatMap{ listOf(it, it * 10) } //[1, 10, 2, 20, 3, 30]
//map ข้อมูลแต่ละตัวเป็น [it, it *10] คือถ้าข้อมูลเป็น 1, 2, 3
//ผลก็จะออกมาเป็น [1, 10] และ [2, 20] และ [3, 30] แล้วค่อยเอามาต่อกันเป็นลิสต์ใหม่

หมวด Filter และการ Sub-list

filter / filterNot

ใช้ในการกรองข้อมูลใน List ให้เหลือแค่ตัวที่ตรงตามเงื่อนไข ส่วน filterNot จะเป็นตัวที่ไม่ตรงเงื่อนไขแทน

val data = listOf(10, 20, 30, 40, 50)

//เลือกเฉพาะข้อมูลที่มากกว่า 25
data.filter{ it > 25 } //[30, 40, 50]

//เลือกเฉพาะข้อมูลที่'ไม่'มากกว่า 25
data.filterNot{ it > 25 } //[10, 20]
data.filter{ it <= 25 } //[10, 20]

filterNotNull

เหมือนกับ filter แต่จะเลือกเฉพาะตัวที่ไม่ใช่ null มาคิด

val data = listOf(10, 20, 30, null, 50)

//เลือกเฉพาะข้อมูลที่มากกว่า 25
data.filter{ it > 25 } //[30, 50]

subList

ใช้ตัดส่วนของ List ออกมา parameter แรกคือจุดเริ่ม ส่วน parameter ที่สองคือจุดสิ้นสุด (ไม่รวมตัวนั้น)

val data = listOf(10, 20, 30, 40, 50)

data.subList(0, 1) //[10]
data.subList(0, 2) //[10, 20]
data.subList(3, 5) //[40, 50]
data.subList(0, 6) //Runtime Error: IndexOutOfBoundsException

take / takeLast / takeWhile

เป็นรูปย่อของ subList ในกรณีที่ต้องการแค่ x ตัว take จะเริ่มนับจากหัวแถว, takeLast จะเริ่มนับจากท้ายแถว, ส่วน takeWhile จะเอาไปเรื่อยๆ ถ้ายังตรงเงื่อนไขใน lambda

val data = listOf(10, 20, 30, 40, 50)

data.take(3) //[10, 20, 30]
data.takeLast(3) //[30, 40, 50]
data.takeWhile{ it < 25} //[10, 20]

drop / dropLast / dropWhile

คล้ายๆ กับ take แต่อันนี้จะเป็นการลบข้อมูลออกแทน (พูดง่ายๆ คือส่วนกลับของ take)

val data = listOf(10, 20, 30, 40, 50)

data.drop(3) //[40, 50]
data.dropLast(3) //[10, 20]
data.dropWhile{ it < 25} //[30, 40, 50]

slice

ใช้สำหรับเลือกข้อมูลออกมา จัดตามลำดับ index ที่ระบุให้ไป

val data = listOf(10, 20, 30, 40, 50)

//ขอข้อมูลตัวที่ 2, 3, 0 ตามลำดับ
data.slice( listOf(2, 3, 0) ) //[30, 40, 10]

หมวด Reduce

fold / foldRight

ใช้เพื่อลดข้อมูลทั้ง List ให้เหลือแค่ค่าเดียวด้วยวิธีการที่ระบุใน lambda โดย fold จะเริ่มคำนวณค่าทีละคู่จากด้านหัวแถวไปจนถึงท้ายแถว (parameter แรกคือค่าเริ่มต้น) ส่วน foldRight จะคิดเหมือนกันแค่คิดจากด้านท้ายแถวมาด้านหน้า

val data = listOf(1, 2, 3)

data.fold(0) { total, x -> total + x }
//6
//เริ่มที่ 0 จากนั้นไล่บวกค่าทีละตัว

data.fold(100) { total, x -> total - x }
//94
//เริ่มที่ 100 จากนั้นไล่ลบตัวเลขทีละตัวใน list

reduce / reduceRight

เหมือนกับ fold แต่จะไม่มีการกำหนดค่าเริ่มต้น ... reduce จะใช้ค่าตัวแรกใน List เป็นค่าเริ่มต้นแทน

val data = listOf(10, 5, 1)

data.reduce{ total, x -> total + x }
//16
//ไล่บวกค่าทีละตัว 10+5 = 15 แล้ว 15+1 = 16

data.reduce{ total, x -> total - x }
//4
//ไล่ลบเลขทีละตัวจากด้านหัวแถว เริ่มจาก 10-5 และเอาไป -1 ต่อ

data.reduceRight{ total, x -> total - x }
//-14
//ไล่ลบตัวเลขทีละตัวใน list เริ่มจากด้านขวา 1-5 = -4 แล้ว -4-10 = -14

หมวดการจัดกลุ่ม List

partition

สำหรับใช้จัดกลุ่มข้อมูล แยกตามเงื่อนไขใน lambda (ต้องเป็นเงื่อนไข boolean เท่านั้น) ... โดยจะแยกข้อมูลออกเป็นข้อมูลชนิด Pair ข้อมูลที่ตรงกับ true จะอยู่ในกลุ่มแรก นอกนั้นจะอยู่กลุ่มที่สอง

val data = listOf(1, 2, 3, 4, 5)
val separate = data.partition{ it % 2 == 0 }

//([2, 4], [1, 3, 5])
//separate จะเป็นข้อมูลประเภท Pair เข้าถึงได้ด้วย .first และ .second

separate.first //[2,4]
separate.second //[1,3,5]

groupBy

สำหรับใช้จัดกลุ่มข้อมูล เหมือนกับ partition แต่สามารถจัดกลุ่มได้มากกว่า 2 กลุ่ม

val data = listOf(1, 2, 3, 4, 5)

//จัดกลุ่มที่มากกว่า 2 (ได้2กลุ่มคือ true, false)
data.groupBy{ it > 2 } //(true=[1, 2], false=[3, 4, 5])

//จัดกลุ่มตามผลหาร (ได้3กลุ่มคือ 0, 1, 2)
data.groupBy{ it / 2 } //(0=[1], 1=[2, 3], 2=[4, 5])

concat

การต่อ List ใน Kotlin นั้นทำได้ง่ายๆ ด้วยเครื่องหมาย +

val data = listOf(1, 2, 3)
data + listOf(4, 5, 6) //[1, 2, 3, 4, 5, 6]

zip

ใช้เพื่อผสาน List 2 ตัวเข้าด้วยกันแบบ "ตัว-ต่อ-ตัว" ถ้าลิสต์ 2 ตัวมีขนาดไม่เท่ากัน จะยึดตัวสั้นกว่าเป็นเกณฑ์ ... ข้อมูลในลิสต์ทั้ง 2 ตัวไม่จำเป็นต้องเป็นชนิดเดียวกันก็ได้ ผลที่ได้จะออกมาเป็น Pair เสมอ

val data = listOf(1, 2, 3, 4)

data.zip( listOf(5, 6, 7, 8) )
//[(1, 5), (2, 6), (3, 7), (4, 8)]

data.zip( listOf("A", "B") )
//[(1, A), (2, B)]

ส่วนใหญ่ zip ไม่หากรณีใช้งานค่อยข้างยาก มักใช้กับ foreach ที่ต้องการวนลูป List 2 ตัวพร้อมๆ กันทีละ indexๆ

val dataA = listOf("A", "B", "C", "D")
val dataB = listOf(1, 2, 3, 4)

for( (a, b) in dataA.zip(dataB) ){
    println("$a $b")
}

//print:
A 1
B 2
C 3
D 4

reverse

ใช้เพื่อกลับ List จากหัวไปท้าย

val data = listOf(10, 20, 30, 40, 50)
data.reverse() //[50, 40, 30, 20, 10]

หมวดการเรียงข้อมูล

sorted / sortedDecending

ใช้เพื่อเรียงข้อมูล จากน้อยไปมาก (และใช้ sortedDecending เมื่ออยากเรียงจากมากไปน้อย) ถ้าข้อมูลเป็น object จะเรียงได้เมื่อ class นั้น implements มาจาก Comparable (แบบเดียวกับ Java เลย)

val data = listOf(5,3,4,1,2)
data.sorted() //[1, 2, 3, 4, 5]
data.sortedDecending() //[5, 4, 3, 2, 1]

//หรือถ้าเป็น object
data class Foo(var x:Int): Comparable<Foo>{
    override fun compareTo(other: Foo): Int {
        return x - other.x
    }
}
val data = listOf( Foo(5), Foo(3), Foo(8) )
data.sorted() //[Foo(x=3), Foo(x=3), Foo(x=8)]

sortedBy / sortedDecendingBy

เป็นการเรียงแบบใส่เงื่อนไขว่าจะใช้ attribute อะไรเป็นหลักในการเรียง

val data = listOf(5, 3, 4, 1, 2)
data.sortedBy{ -it } //[5, 4, 3, 2, 1]

//หรือถ้าเป็น object
data class Foo(var x:Int, var y:Int, var z:Int)
val data = listOf( Foo(1,2,3), Foo(2,1,3), Foo(3,2,1) )

data.sortedBy{ it.x } 
//[Foo(x=1, y=2, z=3), Foo(x=2, y=1, z=3), Foo(x=3, y=2, z=1)]

data.sortedBy{ it.y } 
//[Foo(x=2, y=1, z=3), Foo(x=3, y=2, z=1), Foo(x=1, y=2, z=3)]

sortWith

เรียงข้อมูลโดยกำหนดว่าให้เรียงตามฟิลด์ไหนบ้างตามลำดับ ส่วนใหญ่จะใช้กับฟังก์ชันช่วยเหลือของ Kotlin คือ compareBy จากนั้นเราจะกำหนดว่าจะให้เทียบข้อมูลด้วยฟิลด์ไหน (ถ้าค่าเท่ากับ จะใช้ฟิลด์ต่อไปเช็กแทน) ถ้าเทียบกับ Java คือการใช้คลาส Compactor ในการเปรียบเทียบค่อ

data class Foo(var x:Int, var y:Int, var z:Int)
val data = listOf( Foo(1,2,3), Foo(1,2,4), Foo(2,3,5) )

data2.sortedWith(compareBy( {it.x}, {it.y}, {it.z} ))
//[Foo(x=1, y=2, z=3), Foo(x=1, y=2, z=4), Foo(x=2, y=3, z=5)]
589 Total Views 3 Views Today
Ta

Ta

สิ่งมีชีวิตตัวอ้วนๆ กลมๆ เคลื่อนที่ไปไหนโดยการกลิ้ง .. ถนัดการดำรงชีวิตโดยไม่โดนแสงแดด
ปัจจุบันเป็น Senior Software Engineer อยู่ที่ Centrillion Technology
งานอดิเรกคือ เขียนโปรแกรม อ่านหนังสือ เขียนบทความ วาดรูป และ เล่นแบดมินตัน

You may also like...

ใส่ความเห็น

อีเมลของคุณจะไม่แสดงให้คนอื่นเห็น ช่องที่ต้องการถูกทำเครื่องหมาย *