我可以指示 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中没有直接映射解决方案。然而,围绕 grouping
和 aggregate
编写包装器以获得所需的结果非常容易
/**
* 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
方法,将所有调用委托给原始列表.
考虑这个很好的例子:
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中没有直接映射解决方案。然而,围绕 grouping
和 aggregate
编写包装器以获得所需的结果非常容易
/**
* 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
方法,将所有调用委托给原始列表.