我可以指示 Kotlin 关联匹配键提取器的第一个元素而不是最后一个元素吗?

Can I instruct Kotlin to associateBy the first element instead of the last element matching the key extractor?

考虑这个很好的例子:

listOf(Pair(1, "a"), Pair(2, "b"), Pair(1, "b")).associateBy { it.first }

输出将最后一个值与每个键相关联:

> (1 to "b", 2 to "b")

有没有办法得到相反的行为?即,将第一个值与键相关联?

> (1 to "a", 2 to "b")

我知道我可以对列表进行排序,使用 groupingBy + reduce 等,但假设列表很大并且性能成为问题。所以我想知道是否有任何方法可以告诉 Kotlin 我打算如何合并我的记录

类似于Java的收集器方法:

Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                Function<? super T, ? extends U> valueMapper,
                                BinaryOperator<U> mergeFunction)

我可以在哪里指定合并函数:

Collectors.toMap(Pair::value1(), Pair::value2(), (first, second) -> first)

那不是 associateBy 所做的,这是您的代码 returns:

{1=(1, b), 2=(2, b)}

它获取列表中的每个项目,并使用您提供的函数生成的键将整个项目存储为映射中的

(如果你想要你提供的输出,(1 to "b", 2 to "b"),你只需在列表上调用 toMap - Pair 是提供 key/value 条目的一种方式一张地图)


你得到两个值的原因 "b" 作为他们的第二个元素是因为你有两个项目具有相同的第一个元素,所以他们都生成相同的键,最后一个(与"b") 最终覆盖地图中的第一个。

不清楚您的示例到底想要什么,但是如果您说您想要收集使用每个键的第一个条目(所以(1, "b")不覆盖 (1, "a") 然后你可以使用 distinctBy:

val wow = listOf(Pair(1, "a"), Pair(2, "b"), Pair(1, "b")).distinctBy { it.first }.toMap()
println(wow)
>>> {1=a, 2=b}

或者如果您只想让第二个元素的每个值第一次出现(这恰好给您相同的结果——这就是示例不清楚的原因!)

如果您想对映射进行更具体的控制,您可能需要 associate:

  • associateBy(您使用的)迭代每个项目,您提供的函数生成 key - value 是项目
  • associateWith 恰恰相反 - 项是键,您的函数生成值。
  • associate 获取一个项目,您的函数生成键 值,因此您可以更好地控制生成的地图的外观

如果您有一个更具体的示例来说明您需要做什么,有人可能会提供更合适的,但这些可能是您想要查看的功能。

如果您担心 memory/performance 有大数据集,您可能想在列表中调用 toSequence() 这样您就不会生成中间集合(但这取决于那些中间集合操作完全是 - distinctBy 可能需要构建一个中间集合,以便它知道要删除什么)。或者查看关联内容的 to 版本(例如 associateByTo),您可以在其中提供可变映射以将内容添加到其中。取决于你在做什么以及收获是否值得!

虽然不是很清楚你想在这里完成什么,但是你指定的java代码在kotlin中没有直接映射解决方案。然而,围绕 groupingaggregate 编写包装器以获得所需的结果非常容易

/**
 * Generates a map where keys are given by keyMapper and values are given by valueMapper.
 * If any two elements would have the same key returned by [keyMapper]
 * then merge function is used to merge the values of such keys.
 * T: type of Iterable elements
 * K: type of keys
 * R: type of values
 *
 * @return Map<K,R>
 */
private inline fun <T, K, R> Iterable<T>.toMapWithMerge(
        crossinline keyMapper: (T) -> K,
        valueMapper: (T) -> R,
        merge: (R, R) -> R
): Map<K, R>{
    return groupingBy(keyMapper).aggregate { key, accumulator:R?, element, first ->
        if(accumulator == null) valueMapper(element)
        else merge(accumulator, valueMapper(element))
    }
}

您可以将其用作

listOf(Pair(1, "a"), Pair(2, "b"), Pair(1, "b"))
                .toMapWithMerge({ it.first }, { it.second }, { first, second -> first })

至于性能部分,它的性能不会比 java 对应部分差,因为它只执行一次迭代,成本不会超过 O(n)

只需事先反转(第一个项目将覆盖最新的,因为它们现在是最新的):

listOf(Pair(1, "a"), Pair(2, "b"), Pair(1, "b")).asReversed().toMap() // {1=a, 2=b}

开销微不足道,asReversed 方法不会创建原始列表的反向副本,它只是创建一个简单的包装器,覆盖了 get 方法,将所有调用委托给原始列表.