KT-15363 Collection Builders

Added container builders for lists, sets, and maps.
The new experimental type inference only works for the simple builders
with a single generic type. The versions with nullability and the maps
still require explicit specification of the types. Obviously explicit
specification is required for all users who are not using the new
experimental inference. Improving the type inference is something that
should be done separately and many things – including these builders –
will benefit from it, however, this is not a blocker for these builders
in my opinion.
This commit is contained in:
Fleshgrinder
2019-12-25 14:22:19 +01:00
committed by Ilya Gorbunov
parent fa811c731f
commit bf9d3d87a6
7 changed files with 234 additions and 13 deletions
@@ -182,3 +182,10 @@ internal actual fun checkCountOverflow(count: Int): Int {
return count
}
/**
* JS map and set implementations do not make use of capacities or load factors.
*/
@PublishedApi
@kotlin.internal.InlineOnly
internal actual inline fun mapCapacity(expectedSize: Int) = expectedSize
@@ -95,3 +95,21 @@ internal actual inline fun <K, V> Map<K, V>.toSingletonMapOrSelf(): Map<K, V> =
internal actual fun <K, V> Map<out K, V>.toSingletonMap(): Map<K, V> =
with(entries.iterator().next()) { java.util.Collections.singletonMap(key, value) }
/**
* Calculate the initial capacity of a map, based on Guava's
* [com.google.common.collect.Maps.capacity](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/Maps.java)
* approach. This is similar to [java.util.HashMap.putMapEntries] (JDK8+) but provides further optimizations for small or large sizes.
*/
@PublishedApi
internal actual fun mapCapacity(expectedSize: Int): Int = when {
// We are not coercing the value to a valid one and not throwing an exception. It is up to the receiving class to
// properly handle negative values. Note that all built-in classes do exactly that and will throw.
expectedSize < 0 -> expectedSize
expectedSize < MIN_CAPACITY -> expectedSize + 1
expectedSize < MAX_CAPACITY -> ((expectedSize / 0.75F) + 1.0F).toInt()
else -> MAX_CAPACITY
}
// java.util.HashMap.MAX_CAPACITY
private const val MAX_CAPACITY = 1 shl 30
private const val MIN_CAPACITY = 3
@@ -0,0 +1,51 @@
package samples.collections
import samples.*
@RunWith(Enclosed::class)
class Builders {
class Lists {
@Sample
fun buildListSample() {
val x = listOf('b', 'c')
val y = buildList(x.size + 2) {
add('a')
addAll(x)
add('d')
}
assertPrints(y, "[a, b, c, d]")
}
}
class Sets {
@Sample
fun buildSetSample() {
val x = setOf('b', 'c')
val y = buildSet(x.size + 2) {
add('a')
addAll(x)
add('d')
}
assertPrints(y, "[a, b, c, d]")
}
}
class Maps {
@Sample
fun buildMapSample() {
val x = mapOf('b' to 2, 'c' to 3)
val y = buildMap<Char, Int>(x.size + 2) {
put('a', 1)
putAll(x)
put('d', 4)
}
assertPrints(y, "{a=1, b=2, c=3, d=4}")
}
}
}
@@ -5,6 +5,7 @@
@file:kotlin.jvm.JvmMultifileClass
@file:kotlin.jvm.JvmName("CollectionsKt")
@file:UseExperimental(kotlin.experimental.ExperimentalTypeInference::class)
package kotlin.collections
@@ -153,6 +154,34 @@ public inline fun <T> MutableList(size: Int, init: (index: Int) -> T): MutableLi
return list
}
/**
* Build a new read-only [List] with the [elements][E] from the [builderAction].
*
* @sample samples.collections.Builders.Lists.buildListSample
*/
@SinceKotlin("1.3")
@ExperimentalStdlibApi
@kotlin.internal.InlineOnly
public inline fun <E> buildList(@BuilderInference builderAction: MutableList<E>.() -> Unit): List<E> {
contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
return ArrayList<E>().apply(builderAction)
}
/**
* Build a new read-only [List] with the given [expectedSize] and [elements][E] from the [builderAction].
*
* @sample samples.collections.Builders.Lists.buildListSample
* @throws IllegalArgumentException if the given [expectedSize] is negative.
*/
@SinceKotlin("1.3")
@ExperimentalStdlibApi
@kotlin.internal.InlineOnly
public inline fun <E> buildList(expectedSize: Int, @BuilderInference builderAction: MutableList<E>.() -> Unit): List<E> {
contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
return ArrayList<E>(expectedSize).apply(builderAction)
}
/**
* Returns an [IntRange] of the valid indices for this collection.
* @sample samples.collections.Collections.Collections.indicesOfCollection
+30 -13
View File
@@ -5,6 +5,7 @@
@file:kotlin.jvm.JvmMultifileClass
@file:kotlin.jvm.JvmName("MapsKt")
@file:UseExperimental(kotlin.experimental.ExperimentalTypeInference::class)
package kotlin.collections
@@ -123,22 +124,38 @@ public inline fun <K, V> linkedMapOf(): LinkedHashMap<K, V> = LinkedHashMap<K, V
public fun <K, V> linkedMapOf(vararg pairs: Pair<K, V>): LinkedHashMap<K, V> = pairs.toMap(LinkedHashMap(mapCapacity(pairs.size)))
/**
* Calculate the initial capacity of a map, based on Guava's com.google.common.collect.Maps approach. This is equivalent
* to the Collection constructor for HashSet, (c.size()/.75f) + 1, but provides further optimisations for very small or
* very large sizes, allows support non-collection classes, and provides consistency for all map based class construction.
* Build a new read-only [Map] with the [key][K]-[value][V] pairs from the [builderAction] while preserving the insertion order.
*
* @sample samples.collections.Builders.Maps.buildMapSample
*/
@PublishedApi
internal fun mapCapacity(expectedSize: Int): Int {
if (expectedSize < 3) {
return expectedSize + 1
}
if (expectedSize < INT_MAX_POWER_OF_TWO) {
return expectedSize + expectedSize / 3
}
return Int.MAX_VALUE // any large value
@SinceKotlin("1.3")
@ExperimentalStdlibApi
@kotlin.internal.InlineOnly
public inline fun <K, V> buildMap(@BuilderInference builderAction: MutableMap<K, V>.() -> Unit): Map<K, V> {
contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
return LinkedHashMap<K, V>().apply(builderAction)
}
private const val INT_MAX_POWER_OF_TWO: Int = Int.MAX_VALUE / 2 + 1
/**
* Build a new read-only [Map] with the given [expectedSize] and [key][K]-[value][V] pairs from the [builderAction] while preserving
* the insertion order.
*
* @sample samples.collections.Builders.Maps.buildMapSample
* @throws IllegalArgumentException if the given [expectedSize] is negative.
*/
@SinceKotlin("1.3")
@ExperimentalStdlibApi
@kotlin.internal.InlineOnly
public inline fun <K, V> buildMap(expectedSize: Int, @BuilderInference builderAction: MutableMap<K, V>.() -> Unit): Map<K, V> {
contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
return LinkedHashMap<K, V>(mapCapacity(expectedSize)).apply(builderAction)
}
/**
* Calculate the initial capacity of a map.
*/
@PublishedApi
internal expect fun mapCapacity(expectedSize: Int): Int
/** Returns `true` if this map is not empty. */
@kotlin.internal.InlineOnly
@@ -5,9 +5,11 @@
@file:kotlin.jvm.JvmMultifileClass
@file:kotlin.jvm.JvmName("SetsKt")
@file:UseExperimental(kotlin.experimental.ExperimentalTypeInference::class)
package kotlin.collections
import kotlin.contracts.*
internal object EmptySet : Set<Nothing>, Serializable {
private const val serialVersionUID: Long = 3406603774387020532
@@ -88,6 +90,35 @@ public inline fun <T> linkedSetOf(): LinkedHashSet<T> = LinkedHashSet()
*/
public fun <T> linkedSetOf(vararg elements: T): LinkedHashSet<T> = elements.toCollection(LinkedHashSet(mapCapacity(elements.size)))
/**
* Build a new read-only [Set] with the [elements][E] from the [builderAction] while preserving the insertion order.
*
* @sample samples.collections.Builders.Sets.buildSetSample
*/
@SinceKotlin("1.3")
@ExperimentalStdlibApi
@kotlin.internal.InlineOnly
public inline fun <E> buildSet(@BuilderInference builderAction: MutableSet<E>.() -> Unit): Set<E> {
contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
return LinkedHashSet<E>().apply(builderAction)
}
/**
* Build a new read-only [Set] with the given [expectedSize] and [elements][E] from the [builderAction] while preserving the insertion
* order.
*
* @sample samples.collections.Builders.Sets.buildSetSample
* @throws IllegalArgumentException if the given [expectedSize] is negative.
*/
@SinceKotlin("1.3")
@ExperimentalStdlibApi
@kotlin.internal.InlineOnly
public inline fun <E> buildSet(expectedSize: Int, @BuilderInference builderAction: MutableSet<E>.() -> Unit): Set<E> {
contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
return LinkedHashSet<E>(mapCapacity(expectedSize)).apply(builderAction)
}
/** Returns this Set if it's not `null` and the empty set otherwise. */
@kotlin.internal.InlineOnly
public inline fun <T> Set<T>?.orEmpty(): Set<T> = this ?: emptySet()
@@ -0,0 +1,68 @@
package test.collections
import kotlin.test.*
class ContainerBuilderTest {
@Test fun buildList() {
val x = buildList {
add('b')
add('c')
}
val y = buildList(4) {
add('a')
addAll(x)
add('d')
}
assertEquals(listOf('a', 'b', 'c', 'd'), y)
}
@Test fun exceptionIsThrownIfExpectedListSizeIsNegative() {
assertFailsWith(IllegalArgumentException::class) {
buildList<Any?>(-1) {}
}
}
@Test fun buildSet() {
val x = buildSet {
add('b')
add('c')
}
val y = buildSet(4) {
add('a')
addAll(x)
add('d')
}
assertEquals(setOf('a', 'b', 'c', 'd'), y)
}
@Test fun exceptionIsThrownIfExpectedSetSizeIsNegative() {
assertFailsWith(IllegalArgumentException::class) {
buildSet<Any?>(-1) {}
}
}
@Test fun buildMap() {
val x = buildMap<Char, Int> {
put('b', 2)
put('c', 3)
}
val y = buildMap<Char, Int>(4) {
put('a', 1)
putAll(x)
put('d', 4)
}
assertEquals(mapOf('a' to 1, 'b' to 2, 'c' to 3, 'd' to 4), y)
}
@Test fun exceptionIsThrownIfExpectedMapSizeIsNegative() {
assertFailsWith(IllegalArgumentException::class) {
buildMap<Any?, Any?>(-1) {}
}
}
}