Skip to content

Commit

Permalink
Allow size hinting for ImmutableMultimap and subtypes.
Browse files Browse the repository at this point in the history
RELNOTES=Add ImmutableMultimap.builderWithExpectedKeys and ImmutableMultimap.Builder.expectedValuesPerKey.
PiperOrigin-RevId: 660016290
  • Loading branch information
lowasser authored and Google Java Core Libraries committed Aug 6, 2024
1 parent 91b6ebe commit c3d5b17
Show file tree
Hide file tree
Showing 19 changed files with 664 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,51 @@ public static Test suite() {
return suite;
}

public void testBuilderWithExpectedKeysNegative() {
try {
ImmutableListMultimap.builderWithExpectedKeys(-1);
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException expected) {
}
}

public void testBuilderWithExpectedKeysZero() {
ImmutableListMultimap.Builder<String, String> builder =
ImmutableListMultimap.builderWithExpectedKeys(0);
builder.put("key", "value");
assertThat(builder.build().entries()).containsExactly(Maps.immutableEntry("key", "value"));
}

public void testBuilderWithExpectedKeysPositive() {
ImmutableListMultimap.Builder<String, String> builder =
ImmutableListMultimap.builderWithExpectedKeys(1);
builder.put("key", "value");
assertThat(builder.build().entries()).containsExactly(Maps.immutableEntry("key", "value"));
}

public void testBuilderWithExpectedValuesPerKeyNegative() {
ImmutableListMultimap.Builder<String, String> builder = ImmutableListMultimap.builder();
try {
builder.expectedValuesPerKey(-1);
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException expected) {
}
}

public void testBuilderWithExpectedValuesPerKeyZero() {
ImmutableListMultimap.Builder<String, String> builder =
ImmutableListMultimap.<String, String>builder().expectedValuesPerKey(0);
builder.put("key", "value");
assertThat(builder.build().entries()).containsExactly(Maps.immutableEntry("key", "value"));
}

public void testBuilderWithExpectedValuesPerKeyPositive() {
ImmutableListMultimap.Builder<String, String> builder =
ImmutableListMultimap.<String, String>builder().expectedValuesPerKey(1);
builder.put("key", "value");
assertThat(builder.build().entries()).containsExactly(Maps.immutableEntry("key", "value"));
}

public void testBuilder_withImmutableEntry() {
ImmutableListMultimap<String, Integer> multimap =
new Builder<String, Integer>().put(Maps.immutableEntry("one", 1)).build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package com.google.common.collect;

import static com.google.common.truth.Truth.assertThat;

import com.google.common.annotations.GwtCompatible;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.annotations.J2ktIncompatible;
Expand Down Expand Up @@ -60,6 +62,50 @@ public void testBuilder_withImmutableEntryAndNullContents() {
}
}

public void testBuilderWithExpectedKeysNegative() {
try {
ImmutableMultimap.builderWithExpectedKeys(-1);
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException expected) {
}
}

public void testBuilderWithExpectedKeysZero() {
ImmutableMultimap.Builder<String, String> builder =
ImmutableMultimap.builderWithExpectedKeys(0);
builder.put("key", "value");
assertThat(builder.build().entries()).containsExactly(Maps.immutableEntry("key", "value"));
}

public void testBuilderWithExpectedKeysPositive() {
ImmutableMultimap.Builder<String, String> builder =
ImmutableMultimap.builderWithExpectedKeys(1);
builder.put("key", "value");
assertThat(builder.build().entries()).containsExactly(Maps.immutableEntry("key", "value"));
}

public void testBuilderWithExpectedValuesPerKeyNegative() {
try {
ImmutableMultimap.builder().expectedValuesPerKey(-1);
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException expected) {
}
}

public void testBuilderWithExpectedValuesPerKeyZero() {
ImmutableMultimap.Builder<String, String> builder =
ImmutableMultimap.<String, String>builder().expectedValuesPerKey(0);
builder.put("key", "value");
assertThat(builder.build().entries()).containsExactly(Maps.immutableEntry("key", "value"));
}

public void testBuilderWithExpectedValuesPerKeyPositive() {
ImmutableMultimap.Builder<String, String> builder =
ImmutableMultimap.<String, String>builder().expectedValuesPerKey(1);
builder.put("key", "value");
assertThat(builder.build().entries()).containsExactly(Maps.immutableEntry("key", "value"));
}

private static class StringHolder {
@Nullable String string;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,79 @@ public static Test suite() {
return suite;
}

public void testBuilderWithExpectedKeysNegative() {
try {
ImmutableSetMultimap.builderWithExpectedKeys(-1);
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException expected) {
}
}

public void testBuilderWithExpectedKeysZero() {
ImmutableSetMultimap.Builder<String, String> builder =
ImmutableSetMultimap.builderWithExpectedKeys(0);
builder.put("key", "value");
assertThat(builder.build().entries()).containsExactly(Maps.immutableEntry("key", "value"));
}

public void testBuilderWithExpectedKeysPositive() {
ImmutableSetMultimap.Builder<String, String> builder =
ImmutableSetMultimap.builderWithExpectedKeys(1);
builder.put("key", "value");
assertThat(builder.build().entries()).containsExactly(Maps.immutableEntry("key", "value"));
}

public void testBuilderWithExpectedValuesPerKeyNegative() {
ImmutableSetMultimap.Builder<String, String> builder = ImmutableSetMultimap.builder();
try {
builder.expectedValuesPerKey(-1);
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException expected) {
}
}

public void testBuilderWithExpectedValuesPerKeyZero() {
ImmutableSetMultimap.Builder<String, String> builder =
ImmutableSetMultimap.<String, String>builder().expectedValuesPerKey(0);
builder.put("key", "value");
assertThat(builder.build().entries()).containsExactly(Maps.immutableEntry("key", "value"));
}

public void testBuilderWithExpectedValuesPerKeyPositive() {
ImmutableSetMultimap.Builder<String, String> builder =
ImmutableSetMultimap.<String, String>builder().expectedValuesPerKey(1);
builder.put("key", "value");
assertThat(builder.build().entries()).containsExactly(Maps.immutableEntry("key", "value"));
}

public void testBuilderWithExpectedValuesPerKeyNegativeOrderValuesBy() {
ImmutableSetMultimap.Builder<String, String> builder =
ImmutableSetMultimap.<String, String>builder().orderValuesBy(Ordering.natural());
try {
builder.expectedValuesPerKey(-1);
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException expected) {
}
}

public void testBuilderWithExpectedValuesPerKeyZeroOrderValuesBy() {
ImmutableSetMultimap.Builder<String, String> builder =
ImmutableSetMultimap.<String, String>builder()
.orderValuesBy(Ordering.natural())
.expectedValuesPerKey(0);
builder.put("key", "value");
assertThat(builder.build().entries()).containsExactly(Maps.immutableEntry("key", "value"));
}

public void testBuilderWithExpectedValuesPerKeyPositiveOrderValuesBy() {
ImmutableSetMultimap.Builder<String, String> builder =
ImmutableSetMultimap.<String, String>builder()
.orderValuesBy(Ordering.natural())
.expectedValuesPerKey(1);
builder.put("key", "value");
assertThat(builder.build().entries()).containsExactly(Maps.immutableEntry("key", "value"));
}

public void testBuilder_withImmutableEntry() {
ImmutableSetMultimap<String, Integer> multimap =
new Builder<String, Integer>().put(Maps.immutableEntry("one", 1)).build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.google.common.collect;

import static com.google.common.collect.CollectPreconditions.checkNonnegative;
import static java.util.Objects.requireNonNull;

import com.google.common.annotations.GwtCompatible;
Expand Down Expand Up @@ -198,6 +199,19 @@ public static <K, V> Builder<K, V> builder() {
return new Builder<>();
}

/**
* Returns a new builder with a hint for how many distinct keys are expected to be added. The
* generated builder is equivalent to that returned by {@link #builder}, but may perform better if
* {@code expectedKeys} is a good estimate.
*
* @throws IllegalArgumentException if {@code expectedKeys} is negative
* @since NEXT
*/
public static <K, V> Builder<K, V> builderWithExpectedKeys(int expectedKeys) {
checkNonnegative(expectedKeys, "expectedKeys");
return new Builder<>(expectedKeys);
}

/**
* A builder for creating immutable {@code ListMultimap} instances, especially {@code public
* static final} multimaps ("constant multimaps"). Example:
Expand All @@ -224,6 +238,23 @@ public static final class Builder<K, V> extends ImmutableMultimap.Builder<K, V>
*/
public Builder() {}

/** Creates a new builder with a hint for the number of distinct keys. */
Builder(int expectedKeys) {
super(expectedKeys);
}

/**
* {@inheritDoc}
*
* @since NEXT
*/
@CanIgnoreReturnValue
@Override
public Builder<K, V> expectedValuesPerKey(int expectedValuesPerKey) {
super.expectedValuesPerKey(expectedValuesPerKey);
return this;
}

@CanIgnoreReturnValue
@Override
public Builder<K, V> put(K key, V value) {
Expand Down
71 changes: 67 additions & 4 deletions android/guava/src/com/google/common/collect/ImmutableMultimap.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.CollectPreconditions.checkEntryNotNull;
import static com.google.common.collect.CollectPreconditions.checkNonnegative;
import static com.google.common.collect.Maps.immutableEntry;
import static java.util.Objects.requireNonNull;

Expand Down Expand Up @@ -127,6 +128,19 @@ public static <K, V> Builder<K, V> builder() {
return new Builder<>();
}

/**
* Returns a new builder with a hint for how many distinct keys are expected to be added. The
* generated builder is equivalent to that returned by {@link #builder}, but may perform better if
* {@code expectedKeys} is a good estimate.
*
* @throws IllegalArgumentException if {@code expectedKeys} is negative
* @since NEXT
*/
public static <K, V> Builder<K, V> builderWithExpectedKeys(int expectedKeys) {
checkNonnegative(expectedKeys, "expectedKeys");
return new Builder<>(expectedKeys);
}

/**
* A builder for creating immutable multimap instances, especially {@code public static final}
* multimaps ("constant multimaps"). Example:
Expand All @@ -151,13 +165,22 @@ public static class Builder<K, V> {
@CheckForNull Map<K, ImmutableCollection.Builder<V>> builderMap;
@CheckForNull Comparator<? super K> keyComparator;
@CheckForNull Comparator<? super V> valueComparator;
int expectedValuesPerKey = ImmutableCollection.Builder.DEFAULT_INITIAL_CAPACITY;

/**
* Creates a new builder. The returned builder is equivalent to the builder generated by {@link
* ImmutableMultimap#builder}.
*/
public Builder() {}

/** Creates a new builder with a hint for the number of distinct keys. */
Builder(int expectedKeys) {
if (expectedKeys > 0) {
builderMap = Platform.preservesInsertionOrderOnPutsMapWithExpectedSize(expectedKeys);
}
// otherwise, leave it null to be constructed lazily
}

Map<K, ImmutableCollection.Builder<V>> ensureBuilderMapNonNull() {
Map<K, ImmutableCollection.Builder<V>> result = builderMap;
if (result == null) {
Expand All @@ -167,8 +190,46 @@ Map<K, ImmutableCollection.Builder<V>> ensureBuilderMapNonNull() {
return result;
}

ImmutableCollection.Builder<V> newValueCollectionBuilder() {
return ImmutableList.builder();
ImmutableCollection.Builder<V> newValueCollectionBuilderWithExpectedSize(int expectedSize) {
return ImmutableList.builderWithExpectedSize(expectedSize);
}

/**
* Provides a hint for how many values will be associated with each key newly added to the
* builder after this call. This does not change semantics, but may improve performance if
* {@code expectedValuesPerKey} is a good estimate.
*
* <p>This may be called more than once; each newly added key will use the most recent call to
* {@link #expectedValuesPerKey} as its hint.
*
* @throws IllegalArgumentException if {@code expectedValuesPerKey} is negative
* @since NEXT
*/
@CanIgnoreReturnValue
public Builder<K, V> expectedValuesPerKey(int expectedValuesPerKey) {
checkNonnegative(expectedValuesPerKey, "expectedValuesPerKey");

// Always presize to at least 1, since we only bother creating a value collection if there's
// at least one element.
this.expectedValuesPerKey = Math.max(expectedValuesPerKey, 1);

return this;
}

/**
* By default, if we are handed a value collection bigger than expectedValuesPerKey, presize to
* accept that many elements.
*
* <p>This gets overridden in ImmutableSetMultimap.Builder to only trust the size of {@code
* values} if it is a Set and therefore probably already deduplicated.
*/
int expectedValueCollectionSize(int defaultExpectedValues, Iterable<?> values) {
if (values instanceof Collection<?>) {
Collection<?> collection = (Collection<?>) values;
return Math.max(defaultExpectedValues, collection.size());
} else {
return defaultExpectedValues;
}
}

/** Adds a key-value mapping to the built multimap. */
Expand All @@ -177,7 +238,7 @@ public Builder<K, V> put(K key, V value) {
checkEntryNotNull(key, value);
ImmutableCollection.Builder<V> valuesBuilder = ensureBuilderMapNonNull().get(key);
if (valuesBuilder == null) {
valuesBuilder = newValueCollectionBuilder();
valuesBuilder = newValueCollectionBuilderWithExpectedSize(expectedValuesPerKey);
ensureBuilderMapNonNull().put(key, valuesBuilder);
}
valuesBuilder.add(value);
Expand Down Expand Up @@ -224,7 +285,9 @@ public Builder<K, V> putAll(K key, Iterable<? extends V> values) {
}
ImmutableCollection.Builder<V> valuesBuilder = ensureBuilderMapNonNull().get(key);
if (valuesBuilder == null) {
valuesBuilder = newValueCollectionBuilder();
valuesBuilder =
newValueCollectionBuilderWithExpectedSize(
expectedValueCollectionSize(expectedValuesPerKey, values));
ensureBuilderMapNonNull().put(key, valuesBuilder);
}
while (valuesItr.hasNext()) {
Expand Down
Loading

0 comments on commit c3d5b17

Please sign in to comment.