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:
committed by
Ilya Gorbunov
parent
fa811c731f
commit
bf9d3d87a6
@@ -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
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user