在 Kotlin 中查找并返回嵌套列表中的第一个匹配项?

8
考虑以下两个类:
class ObjectA(val objectBs: List<ObjectB>,
              val otherFields: Any)

class ObjectB(val key: String,
              val otherFields: Any)

任务是在ObjectA列表中查找并返回具有特定键的第一个ObjectB。

仅实现目标相当简单,但是以漂亮而高效的方式实现似乎相当棘手。我找不到任何类似于“firstIn”或“findIn”函数,它允许我在迭代ObjectA列表时返回类型不同于ObjectA的其他类型。

我有一些方法,其中一种看起来非常好,但效率非常低:

listOfA.mapNotNull { 
    it.objectBs.firstOrNull { 
        item -> item.key == wantedKey
   } 
}.firstOrNull()

这段代码明显的低效之处在于,当找到一个匹配项时它不会停止对listOfA的迭代(而且只能有一个匹配项,需要明确说明)。使用filter或find方法的方法也有类似的问题,需要对至少一个ObjectB列表进行冗余迭代。
在kotlin标准库中是否有适用于这种情况的东西?

1
我不确定,但也许序列可以帮助。你能试试 list.asSequence() 吗? - Andrii Turkovskyi
5个回答

16

如果你想要一种优雅的解决方案,你只需要像这样使用flatMap

val result: ObjectB? = listOfA.flatMap { it.objectBs }.firstOrNull { it.key == "myKey" }

如果您想要效率,可以像这样做:

val result: ObjectB? = objectAs.firstOrNull {
    it.objectBs.map(ObjectB::key).contains("myKey")
}?.objectBs?.firstOrNull { it.key == "myKey" }

你也可以将它们包装在Optional中,并将其放入函数中,以便此操作的用户可以拥有简洁的API:

fun List<ObjectA>.findFirstObjectB(key: String): Optional<ObjectB> {
    return Optional.ofNullable(firstOrNull {
        it.objectBs.map(ObjectB::key).contains(key)
    }?.objectBs?.firstOrNull { it.key == key })
}

第二种解决方案不会遍历所有项目,并且会在找到第一个匹配项时停止。 - Adam Arold
不,它不是,就像我所说的那样。 - Adam Arold
谁给我点了踩,能解释一下吗? - Adam Arold
1
第二段代码并没有遍历所有的项,但是它必须在一个潜在的大型ObjectB列表中搜索两次关键字。我猜需要进行一些测试才能确定它是否比flatmap解决方案更有效。那个解决方案确实遍历了所有的ObjectA,但是没有对它们执行任何操作,我直觉上认为这比运行冗余的字符串比较要快。 - UncleBob
1
从渐近意义上讲,第一个具有n * m的复杂度,其中nObjectA的数量,而mObjectA中的ObjectB的数量。第二个具有n * 2m的复杂度。它通过在遍历A时进行短路来交换两次遍历B的成本。 - Adam Arold

9
将所有嵌套元素转换为平面Sequence,它们可以被懒惰地迭代,并消除了不必要的迭代开销。这个技巧是通过组合asSequenceflatMap来实现的:
listOfA.asSequence().flatMap { it.objectBs.asSequence() }.find { it.key == wantedKey }

我编写并运行了以下代码以确保其按预期工作:
class PrintSequenceDelegate<out T>(private val wrappedSequence: Sequence<T>) : Sequence<T> by wrappedSequence {
    override fun iterator(): Iterator<T> {
        val wrappedIterator = wrappedSequence.iterator()
        return object : Iterator<T> by wrappedIterator {
            override fun next(): T =
                wrappedIterator.next().also { println("Retrieving: $it") }
        }
    }
}

fun <T> Sequence<T>.toPrintDelegate() = PrintSequenceDelegate(this)

fun main() {
    val listOfLists = List(3) { i -> List(3) { j -> "$i$j" } }
    println("List of lists: $listOfLists")
    val found = listOfLists.asSequence().toPrintDelegate().flatMap { it.asSequence().toPrintDelegate() }.find { it == "11" }
    println(if (found != null) "Found: $found" else "Not found")
}

输出:

List of lists: [[00, 01, 02], [10, 11, 12], [20, 21, 22]]
Retrieving: [00, 01, 02]
Retrieving: 00
Retrieving: 01
Retrieving: 02
Retrieving: [10, 11, 12]
Retrieving: 10
Retrieving: 11
Found: 11


因此我们可以看到,位于包含嵌套列表中找到的元素 (12) 之后的元素不会被迭代,下面的嵌套列表 ([20, 21, 22]) 也不会被迭代。

3

虽然简单,但它可以高效地完成工作:

fun findBWithKey(listOfA: List<ObjectA>, wantedKey: String): ObjectB? {
  listOfA.forEach { 
        it.objectBs.forEach { item ->        
             if(item.key == wantedKey){
                 return item
             }
        } 
    }

    return null
}

我也喜欢使用mapfirst,但是使用这些扩展函数来高效地完成给定的任务变得不必要的困难。


0
一个简单的flatMap就可以解决问题:
listOfA.flatMap { it.objectBs }.first { it.key == wantedKey }

这将基本上给你一个中间列表,其中包含所有组合在一起的内容,以便您可以轻松查询第一个匹配项。


这将遍历所有项,而OP关心效率问题。 - Adam Arold

-1
如果性能很关键,我会研究一下协程序列
此外,你还可以通过在listOfA上使用firstOrNull来稍微优化你的代码:
listOfA.filterNotNull().firstOrNull { item ->
    item.objectBs.firstOrNull { it.key == wantedKey } != null
}

在将代码变得过于复杂之前,我会进行一些性能测试,以查看此代码是否会引起任何问题。


1
如果我看得正确,那段代码会返回ObjectA而不是ObjectB。 - UncleBob

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接