Kotlin和不可变集合?

51

我正在学习Kotlin,并且很可能在未来一年内将其用作主要语言。然而,我一直在得到相互矛盾的研究结果,即Kotlin是否具有不可变集合,我正在努力弄清楚是否需要使用Google Guava。

请问是否有人能给我一些指导?它是否默认使用不可变集合?哪些操作符返回可变或不可变集合?如果没有,是否有计划实现它们?


1
有一种轻量级的方法可以将Kotlin集合保护为不可变的,而不仅仅是只读的,请参见:https://dev59.com/GVsX5IYBdhLWcg3wf_v3#38002121 - Jayson Minard
6个回答

38

Kotlin标准库中的List是只读的:

interface List<out E> : Collection<E> (source)

这是一个通用的有序元素集合。此接口中的方法仅支持对列表的只读访问;通过MutableList接口支持读写访问。

参数
E - 列表中包含的元素类型。

如上所述,还有 MutableList 接口。

interface MutableList<E> : List<E>, MutableCollection<E> (source)

支持添加和删除元素的一般有序元素集合。

参数
E - 列表中包含的元素类型。

因此,Kotlin通过其接口强制执行只读行为,而不像默认Java实现在运行时抛出异常。

同样,还有MutableCollectionMutableIterableMutableIteratorMutableListIteratorMutableMapMutableSet等,详见stdlib文档


1
有趣的方法,尽管确保互操作性是有意义的。我可能会坚持使用Guava集合,因为(上次我检查时)性能要好得多。 - tmn
4
Kotlin的集合是只读的,而非不可变的@nhaarman。我建议修改您的回答。 - Jayson Minard
1
请注意,如果您使用arrayListOf()创建列表,则返回的列表是可变的ArrayList。如果您想要一个不可变的列表,您必须使用listOf()。 - Bitcoin Cash - ADA enthusiast

32

虽然有点混淆,但实际上有三种不可变性而非两种:

  1. 可变的-您应该更改集合(Kotlin的MutableList)
  2. 只读的-您不应更改它(Kotlin的List),但某些东西可能会(转换为Mutable或从Java更改)
  3. 不可变的-没有人可以更改它(Guavas的不可变集合)

因此,在情况(2)中,List只是一个接口,它没有突变的方法,但您可以通过将其转换为MutableList来更改实例。

使用Guava(case (3)),您可以避免其他人甚至通过线程更改集合。

Kotlin选择只读是为了直接使用Java集合,因此在使用Java集合时没有额外的开销或转换。


7
Kotlin的List只是只读的,而不是不可变的。其他调用者(例如Java)可以更改列表。 Kotlin调用者可能会转换列表并更改它。 没有不可变的保护。 - Jayson Minard

18
如其他答案所述,Kotlin具有只读接口以可变集合,使您可以通过只读镜头查看集合。 但是,集合可以通过强制转换进行绕过或从Java进行操作。 但是在合作的Kotlin代码中,这很好,大多数用途不需要真正不可变的集合,如果您的团队避免将集合的可变形式强制转换,则可能不需要完全不可变的集合。
Kotlin集合允许基于更改的复制突变,以及惰性突变。 因此,为了回答您问题的一部分,像filtermapflatmap,运算符+ -这样的东西当针对非懒惰收集使用时会创建副本。 当在Sequence上使用时,它们会在访问集合时修改值并继续保持懒惰(导致另一个Sequence)。 尽管对于Sequence,调用任何像toListtoSettoMap这样的东西都会导致最终副本被制作。 根据命名约定,几乎以to开头的任何内容都在制作副本。
换句话说,大多数操作符返回与您开始的相同类型,如果该类型为“只读”,则将收到副本。如果该类型是惰性的,则会延迟应用更改,直到您完全需要集合。
有些人出于其他原因想要它们,例如并行处理。在这些情况下,最好查看专门设计用于这些目的的高性能集合。只在这些情况下使用它们,而不是所有一般情况下都使用。
在JVM世界中,很难避免与希望使用标准Java集合的库进行交互,将其转换为/从这些集合添加了很多痛苦和开销,对于不支持常见接口的库来说尤其如此。 Kotlin提供了良好的互操作性和无需转换的混合,具有只读保护。
因此,如果您不能避免想要不可变集合,Kotlin可以轻松地与JVM空间中的任何内容配合使用: 此外,Kotlin团队正在为Kotlin本地开发不可变集合,这一努力可以在此处看到:https://github.com/Kotlin/kotlinx.collections.immutable 对于不同的需求和限制,有许多其他的集合框架可供选择,谷歌是您寻找它们的好朋友。 Kotlin团队没有必要为其标准库重新发明它们。 您有很多选择,并且它们专门针对不同的东西,例如性能,内存使用,非装箱,不可变性等。 “选择是好的”...因此还有其他一些选择:HPCCHPCC-RTFastUtilKolobokeTrove等等...

甚至有一些类似Pure4J的努力,由于Kotlin现在支持注解处理,可能可以将其移植到Kotlin以实现类似的理念。


8

Kotlin 1.0标准库中没有不可变集合,但是有只读和可变的接口。同时,您可以使用第三方不可变集合库。

Kotlin的List接口中的方法“仅支持对列表进行只读访问”,而其MutableList接口中的方法则支持“添加和删除元素”。但是,这两者都只是接口

Kotlin的List接口在编译时强制执行只读访问,而不是像java.util.Collections.unmodifiableList(java.util.List)那样将此类检查推迟到运行时(它“返回指定列表的不可修改视图……[其中]试图修改返回的列表……会导致一个UnsupportedOperationException。” 它并不强制实施不可变性。

请考虑以下Kotlin代码:

import com.google.common.collect.ImmutableList
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

fun main(args: Array<String>) {
    val readOnlyList: List<Int> = arrayListOf(1, 2, 3)
    val mutableList: MutableList<Int> = readOnlyList as MutableList<Int>
    val immutableList: ImmutableList<Int> = ImmutableList.copyOf(readOnlyList)

    assertEquals(readOnlyList, mutableList)
    assertEquals(mutableList, immutableList)

    // readOnlyList.add(4) // Kotlin: Unresolved reference: add
    mutableList.add(4)
    assertFailsWith(UnsupportedOperationException::class) { immutableList.add(4) }

    assertEquals(readOnlyList, mutableList)
    assertEquals(mutableList, immutableList)
}

注意,readOnlyList 是一个 List,而诸如 add 的方法无法解析(也无法编译),mutableList 可以自然地被改变,而 immutableList(来自 Google Guava)上的 add 也可以在编译时解析,但会在运行时引发异常。
除最后一个外,所有以上断言都通过了,结果为 Exception in thread "main" java.lang.AssertionError: Expected <[1, 2, 3, 4]>, actual <[1, 2, 3]>。即我们成功地修改了只读 List
请注意,使用 listOf(...) 而不是 arrayListOf(...) 将返回一个有效的不可变列表,因为您无法将其转换为任何可变列表类型。但是,对于变量使用 List 接口并不会防止将 MutableList<E> 分配给它(MutableList<E> 扩展了 List<E>)。
最后,请注意,Kotlin 中的接口(以及 Java 中的接口)不能强制执行不可变性,因为它“无法存储状态”(请参见Interfaces)。因此,如果您想要一个不可变的集合,您需要使用像 Google Guava 提供的那样的东西。
另请参见 ImmutableCollectionsExplained · google/guava Wiki · GitHub

1
你的回答对于 Kotlin 1.0 是正确的,不可变集合在 1.0 版本中不可用。 - Jayson Minard

3
注意:这个答案在这里是因为代码简单且开源,您可以使用这个想法来使您创建的集合不可变。它并不仅仅意味着推广库。
在 Klutter 库中,有新的 Kotlin 不可变包装器,使用 Kotlin 代理将现有的 Kotlin 集合接口包装成一个保护层,而没有任何性能损失。然后就没有办法将集合、其迭代器或其他可能返回的集合转换为可以修改的内容。它们实际上变成了不可变的。
Klutter发布了版本号为1.20.0的更新,新增了针对现有集合的不可变保护器。该保护器基于SO答案@miensol提供的轻量级代理来防止任何修改途径,包括将其强制转换为可变类型然后进行修改。而且,Klutter还进一步保护子集合(如迭代器、listIterator、entrySet等)。所有这些操作都被禁止,使用Kotlin委托实现大多数方法,性能无损耗。只需调用myCollection.asReadonly()保护)或myCollection.toImmutable()复制再保护),结果是相同的受保护接口。

下面是代码示例,展示了这种技术简单易行的方式,基本上通过将接口委托给实际类,同时重写突变方法和任何返回的子集合都被即时包装。
/**
 * Wraps a List with a lightweight delegating class that prevents casting back to mutable type
 */
open class ReadOnlyList <T>(protected val delegate: List<T>) : List<T> by delegate, ReadOnly, Serializable {
    companion object {
        @JvmField val serialVersionUID = 1L
    }

    override fun iterator(): Iterator<T> {
        return delegate.iterator().asReadOnly()
    }

    override fun listIterator(): ListIterator<T> {
        return delegate.listIterator().asReadOnly()
    }

    override fun listIterator(index: Int): ListIterator<T> {
        return delegate.listIterator(index).asReadOnly()
    }

    override fun subList(fromIndex: Int, toIndex: Int): List<T> {
        return delegate.subList(fromIndex, toIndex).asReadOnly()
    }

    override fun toString(): String {
        return "ReadOnly: ${super.toString()}"
    }

    override fun equals(other: Any?): Boolean {
        return delegate.equals(other)
    }

    override fun hashCode(): Int {
        return delegate.hashCode()
    }
}

随着助手扩展功能的增加,使得访问变得更加容易:
/**
 * Wraps the List with a lightweight delegating class that prevents casting back to mutable type,
 * specializing for the case of the RandomAccess marker interface being retained if it was there originally
 */
fun <T> List<T>.asReadOnly(): List<T> {
    return this.whenNotAlreadyReadOnly {
        when (it) {
            is RandomAccess -> ReadOnlyRandomAccessList(it)
            else -> ReadOnlyList(it)
        }
    }
}

/**
 * Copies the List and then wraps with a lightweight delegating class that prevents casting back to mutable type,
 * specializing for the case of the RandomAccess marker interface being retained if it was there originally
 */
@Suppress("UNCHECKED_CAST")
fun <T> List<T>.toImmutable(): List<T> {
    val copy = when (this) {
        is RandomAccess -> ArrayList<T>(this)
        else -> this.toList()
    }
    return when (copy) {
        is RandomAccess ->  ReadOnlyRandomAccessList(copy)
        else -> ReadOnlyList(copy)
    }
}

你可以看到这个想法并进行推广,从这段代码中创建缺失的类,该代码重复了其他引用类型的模式。你也可以在这里查看完整的代码:

https://github.com/kohesive/klutter/blob/master/core-jdk6/src/main/kotlin/uy/klutter/core/common/Immutable.kt

而且,测试显示了一些以前允许修改的技巧,但现在不再允许,并阻止使用这些包装器进行强制转换和调用。

https://github.com/kohesive/klutter/blob/master/core-jdk6/src/test/kotlin/uy/klutter/core/collections/TestImmutable.kt


0
现在我们有https://github.com/Kotlin/kotlinx.collections.immutable
fun Iterable<T>.toImmutableList(): ImmutableList<T>
fun Iterable<T>.toImmutableSet(): ImmutableSet<T>

fun Iterable<T>.toPersistentList(): PersistentList<T>
fun Iterable<T>.toPersistentSet(): PersistentSet<T>

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