在 Kotlin 中比较两个列表

57

我接触了Kotlin中的equals函数,可以用于比较两个相同类型的列表。这适用于纯Kotlin与数据类。

在我的Kotlin项目中,我正在使用一个Java库,其中一个回调方法返回一段时间内X秒钟的对象列表。尝试比较旧列表和新列表的每个调用,但equals会返回false,即使项是相同且相等的。

val mOldList: MutableList<MyObject>()? = null

override fun updatedList(list: MutableList<MyObject>){
    // other code
    if (mOldList.equals(list)) // false everytime
}

这是因为Java库中的equals方法吗?

如果有比较列表的其他建议,将不胜感激。


这两个列表都是Java List吗? - Khemraj Sharma
当你说“items are same and equal”时,你是指什么?是引用相等还是结构相等?也就是说,MyObject是否重写了equals()方法? - Robby Cornelissen
我是指结构相等。不确定库中是否覆盖了equals()方法。 - Aswin
如果没有重写equals()方法,就无法实现结构相等。 - Robby Cornelissen
2
在Java中,“换句话说,如果两个列表包含相同顺序的相同元素,则定义它们相等。这个定义确保了equals方法在List接口的不同实现之间正常工作。”因此,如果返回false,则必须存在不同的元素。(而不同的List类型不应该有影响。) - Alexey Romanov
显示剩余2条评论
14个回答

37

只是提供信息,如果你的自定义对象基于 data class(它会自动为你重写 equals 方法),你可以无需任何额外工作即可调用 list1 == list2


谢谢。对于那些想知道 listOfdataClass1.map { it.string } == listOfdataClass2.map { it.string } 的人,它也能按预期工作。 - Sai
11
如果元素的顺序不相同,这种方法将行不通,一个选择是在比较之前对两个列表进行排序。 - Salim Mazari Boufares
1
@SalimMazariBoufares 排序和检查是完全错误的方法。假设我们按id排序,A1A2具有相同的id。在排序后,list1 = [A1, A2],这个列表仍然是[A1, A2]。在排序后,list1 = [A2,A1],列表仍然是[A2,A1],如果equals方法包含除id以外的任何其他参数,则会出现问题。 - Farid
什么是id?Id指的是标识符,它是唯一的,因此两个对象是相同的。但我明白你的意思,这取决于排序策略,如果两个不同的对象具有相同的值,排序时它们可以在具有相同元素的两个列表中交换位置。对于字面量,这不会引起问题。 - Salim Mazari Boufares
@SalimMazariBoufares 为我的示例编写一个简单的测试用例,你就会看到问题。 - Farid
显示剩余2条评论

21

如果您不关心两个列表中元素的顺序,并且您的目标只是检查这两个列表是否具有完全相同的元素,没有其他元素,则可以将两个相互 containsAll 调用视为:

var list1 = mutableListOf<String>()
var list2 = mutableListOf<String>()

if(list1.containsAll(list2) && list2.containsAll(list1)) {
    //both lists are of the same elements
}

18
我会将此投票否定 - 这绝不是最佳方式。 - shabunc
2
@shabunc,您能否说明这种方式有什么问题? - DruidKuma
5
@DruidKuma 因为你实际上是调用了两次 containsAll 方法,而不是使用 O(n) 次比较。这样做的性能不能比每个索引进行线性比较更优。可以参考这个链接:https://dev59.com/uGLVa4cB1Zd3GeqPz89c。 - shabunc
3
应该使用Set而不是ListHashSet(而不是TreeSet - 这很重要)集合具有O(1)的查找复杂度(列表具有O(N))。因此,只需将两个mutableListOf替换为mutableSetOf即可。 - Manushin Igor
5
如果你的列表中没有重复项,你可以先比较它们的大小,然后使用containsAll()方法就足够了。 - handhand
显示剩余4条评论

19

Java的列表实现了equals方法,如果两个列表以相同顺序包含相同元素,则定义这两个列表相等。我猜,你的MyObject类缺少equals方法。


1
MyObject是一个来自库的类,因此我无法重写equals()方法。 - Aswin
1
如果可能的话,您可以始终扩展MyObject,然后覆盖equals() - Henry
是的,非常简单。如果MyObject是final的会发生什么? - Farid
使用装饰器? - Dakshinamurthy Karra

13

使用zip

zip 返回一个由此数组和与之相同索引的另一个数组元素组成的一对对列表。返回列表的长度以最短集合为准。

fun listsEqual(list1: List<Any>, list2: List<Any>): Boolean {

    if (list1.size != list2.size)
        return false

    val pairList = list1.zip(list2)

    return pairList.all { (elt1, elt2) ->
        elt1 == elt2       
    }
}

2
该方法分配了多个不必要的对象:对于最长列表中的每个元素,至少需要一个新列表和新对。此外,还分配了几个枚举器,但 JIT 可以将它们放在堆栈上。因此,该方法需要 O(N) 的额外堆内存。 - Manushin Igor
1
好的观点。在这方面,@XIII-th下面的回答更好 https://dev59.com/rVQK5IYBdhLWcg3wUOV5#58310635 - amynbe

10
这是一个使用扩展函数的简短版本:
fun <T> List<T>.deepEquals(other: List<T>) =
    size == other.size && asSequence()
        .mapIndexed { index, element -> element == other[index] }
        .all { it }

你可以这样使用它:
listOf("Hola", "Mundo").deepEquals(listOf("Hello", "World"))

7
为了尽早终止并避免遍历整个列表,你应该使用 this.asSequence().mapIndexed {...} - Jakob Ulbrich

9

您可以使用以下实现来比较两个Collection

infix fun <T> Collection<T>.deepEqualTo(other: Collection<T>): Boolean {
    // check collections aren't same
    if (this !== other) {
        // fast check of sizes
        if (this.size != other.size) return false
        val areNotEqual = this.asSequence()
            .zip(other.asSequence())
            // check this and other contains same elements at position
            .map { (fromThis, fromOther) -> fromThis == fromOther }
            // searching for first negative answer
            .contains(false)
        if (areNotEqual) return false
    }
    // collections are same or they are contains same elements with same order
    return true
}

或者忽略变体:
infix fun <T> Collection<T>.deepEqualToIgnoreOrder(other: Collection<T>): Boolean {
    // check collections aren't same
    if (this !== other) {
        // fast check of sizes
        if (this.size != other.size) return false
        val areNotEqual = this.asSequence()
            // check other contains next element from this
            .map { it in other }
            // searching for first negative answer
            .contains(false)
        if (areNotEqual) return false
    }
    // collections are same or they are contains same elements
    return true
}

注意:两个函数仅比较深度的第一层。


2
第二个答案的时间复杂度是O(N^2)。对于列表而言,“it in other” 语句的时间复杂度为O(N),并且它被调用了N次。第二种情况的正确解决办法类似于“return this.toSet() == other.toSet()”。 - Manushin Igor
1
@ManushinIgor,是的,你的解决方案比我的好。谢谢。 - Sergei Bubenshchikov

2

比较两个列表的最佳方法

创建以下扩展函数。如果顺序不重要,这将非常有用。

fun <T> List<T>.isEqualsIgnoreOrder(other: List<T>) = this.size == other.size && this.toSet() == other.toSet()

在你的类中使用扩展函数来比较列表,如下所示。它返回布尔值。

list1.isEqualsIgnoreOrder(list2)

1

我知道这不是一个好的解决方案,但它能够工作。

val list1 = listOf<String>()
val list2 = listOf<String>()

fun <T> isSame(list1: List<T>, list2: List<T>): Boolean {
    if (list1.size != list2.size) return false

    val hash1 = list1.map { it.hashCode() }.toSet()
    val hash2 = list2.map { it.hashCode() }.toSet()

    return (hash1.intersect(hash2)).size == hash1.size
}

如果你使用除了 String 以外的对象,就需要实现 hasCode() 方法。


1

1
请注意,此函数要求相同的元素顺序才能得到“true”的答案。 - Manushin Igor

0

您可以遍历一个列表,并检查第二个列表中相应位置的值。以下是一个示例。

var list1 = mutableListOf<String>()
var list2 = mutableListOf<String>()

list1.forEachIndexed { i, value ->
    if (list2[i] == value)
    {
        // your implementaion
    }  
}

此外,您可以过滤更改后的值列表。

var list1 = mutableListOf<String>()
var list2 = mutableListOf<String>()

val changedList = list1.filterIndexed { i, value -> 
    list2[i] != value)
}

2
如果列表的大小不同,此方法可能会失败。此外,此方法会在内存中分配新列表。 - Manushin Igor

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