
io.timeandspace.smoothie.SmoothieMapBuilder Maven / Gradle / Ivy
/*
* Copyright (C) The SmoothieMap Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.timeandspace.smoothie;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import io.timeandspace.collect.Equivalence;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.jetbrains.annotations.Contract;
import java.util.function.Supplier;
import java.util.function.ToLongFunction;
import static io.timeandspace.smoothie.Utils.checkNonNegative;
import static io.timeandspace.smoothie.Utils.checkNonNull;
/**
* SmoothieMapBuilder is used to configure and create {@link SmoothieMap}s. A new builder could be
* created via {@link SmoothieMap#newBuilder()} method. A SmoothieMap is created using {@link
* #build()} method.
*
* SmoothieMapBuilder is mutable: all its configuration methods change its state, and return the
* receiver builder object to enable the "fluent builder" pattern:
*
{@code
* SmoothieMap.newBuilder().expectedSize(100_000).doShrink(false).build();
* }
*
* SmoothieMapBuilder could be used to create any number of SmoothieMap objects. Created
* SmoothieMaps don't retain a link to the builder and therefore don't depend on any possible
* subsequent modifications of the builder's state.
*
* @param the type of keys in created SmoothieMaps
* @param the type of values in created SmoothieMaps
*/
public final class SmoothieMapBuilder {
static final long UNKNOWN_SIZE = Long.MAX_VALUE;
private static final double MAX_EXPECTED_SIZE_ERROR_FRACTION = 0.05;
@Contract(value = " -> new", pure = true)
public static SmoothieMapBuilder create() {
return new SmoothieMapBuilder<>();
}
/** Mirror field: {@link SmoothieMap#allocateIntermediateSegments}. */
private boolean allocateIntermediateCapacitySegments;
/** Mirror field: {@link SmoothieMap#splitBetweenTwoNewSegments}. */
private boolean splitBetweenTwoNewSegments;
/** Mirror field: {@link SmoothieMap#doShrink}. */
private boolean doShrink;
private Equivalence keyEquivalence = Equivalence.defaultEquality();
private @Nullable Supplier> keyHashFunctionFactory = null;
private Equivalence valueEquivalence = Equivalence.defaultEquality();
private long expectedSize = UNKNOWN_SIZE;
private long minPeakSize = UNKNOWN_SIZE;
private SmoothieMapBuilder() {
defaultOptimizationConfiguration();
}
/**
* Specifies whether SmoothieMaps created using this builder should operate in the "low-garbage"
* mode (if {@link OptimizationObjective#LOW_GARBAGE} is passed into this method) or the
* "footprint" mode (if {@link OptimizationObjective#FOOTPRINT} is passed into this method). See
* the documentation for these enum constants for more details about the modes.
*
* By default, SmoothieMaps operate in the "mixed" mode which is a compromise between the
* "low-garbage" and the "footprint" modes. Calling {@link #defaultOptimizationConfiguration()}
* configures this mode explicitly.
*
* @param optimizationObjective the primary optimization objective for created SmoothieMaps
* @return this builder back
* @throws NullPointerException if the provided optimization objective is null
* @see #defaultOptimizationConfiguration
*/
@CanIgnoreReturnValue
@Contract("_ -> this")
public SmoothieMapBuilder optimizeFor(OptimizationObjective optimizationObjective) {
Utils.checkNonNull(optimizationObjective);
switch (optimizationObjective) {
case LOW_GARBAGE:
this.allocateIntermediateCapacitySegments = false;
this.splitBetweenTwoNewSegments = false;
this.doShrink = false;
break;
case FOOTPRINT:
this.allocateIntermediateCapacitySegments = true;
this.splitBetweenTwoNewSegments = true;
this.doShrink = true;
break;
default:
throw new AssertionError("Unknown OptimizationObjective: " + optimizationObjective);
}
return this;
}
/**
* Specifies that SmoothieMaps created using this builder should operate in "mixed" mode which
* is a compromise between {@link OptimizationObjective#FOOTPRINT} and {@link
* OptimizationObjective#LOW_GARBAGE}.
*
* @implSpec the "mixed" mode includes {@linkplain
* #allocateIntermediateCapacitySegments(boolean) allocating intermediate-capacity segments} and
* {@linkplain #doShrink(boolean) turning SmoothieMap shrinking on}, but doesn't include
* {@linkplain #splitBetweenTwoNewSegments(boolean) splitting segments between two new ones}.
*
* @return this builder back
* @see #optimizeFor
*/
@CanIgnoreReturnValue
@Contract(" -> this")
public SmoothieMapBuilder defaultOptimizationConfiguration() {
this.allocateIntermediateCapacitySegments = true;
this.splitBetweenTwoNewSegments = false;
this.doShrink = true;
return this;
}
/**
* Specifies whether during the growth of SmoothieMaps created with this builder they should
* first allocate intermediate-capacity segments and then reallocate them as full-capacity
* segments when needed, or allocate full-capacity segments right away.
*
* SmoothieMap stores the entries in small segments which are mini hash tables on
* their own. This hash table becomes full when the number of entries stored in it exceeds N.
* At this point, a segment is split into two parts. At least one new segment should be
* allocated upon a split to become the second part (or two new segments, if configured via
* {@link #splitBetweenTwoNewSegments(boolean)}). The population of each of these two parts just
* after the split is approximately N/2, subject to some variability (while their joint
* population is N + 1). The entry storage capacity of segments is decoupled from the hash
* table, so a newly allocated segment may have entry storage capacity smaller than N. A segment
* of storage capacity of approximately (2/3) * N is called an intermediate-capacity
* segment, and a segment of storage capacity N is called a full-capacity segment.
*
*
Thus, upon a segment split, a newly allocated intermediate-capacity segment is on average
* 75% full in terms of the entry storage capacity, while a newly allocated full-capacity
* segment is on average 50% full. When the number of entries stored in an intermediate-capacity
* segment grows beyond its storage capacity (approximately, (2/3) * N), it's reallocated
* in place as a full-capacity segment.
*
*
Thus, intermediate-capacity segments reduce the total SmoothieMap's footprint per stored
* entry as well as the variability of the footprint at different SmoothieMap's sizes. The
* drawbacks of intermediate-capacity segments are:
*
* - They are transient: being allocated and reclaimed during the growth of a SmoothieMap.
* The total amount of garbage produced by a SmoothieMap instance (assuming it grows from
* size 0 and neither {@link #expectedSize(long)} nor {@link #minPeakSize(long)} was
* configured) becomes comparable with the SmoothieMap's footprint, though still lower than
* the total amount of garbage produced by a typical open-addressing hash table
* implementation such as {@link java.util.IdentityHashMap}, and comparable to the the total
* amount of garbage produced by entry-based hash maps ({@link java.util.HashMap} and {@link
* java.util.concurrent.ConcurrentHashMap}) without pre-configured capacity and assuming
* that entries are not removed from them.
* - Reallocation of intermediate-capacity segments into full-capacity segments
* contributes the amortized cost of inserting entries into a SmoothieMap.
* - The mix of full-capacity and intermediate-capacity segments precludes certain
* variables to be hard-coded and generally adds some unpredictability (from the CPU
* perspective) to SmoothieMap's key lookup and insertion operations which might slower them
* relative to the setup where only full-capacity segments are used.
*
*
* By default, intermediate-capacity segments are allocated as a part of {@linkplain
* #defaultOptimizationConfiguration() the "mixed" mode (the default mode)} of SmoothieMap's
* operation. Intermediate-capacity segments are allocated in the "mixed" and the {@linkplain
* OptimizationObjective#FOOTPRINT "footprint"} modes, but are not allocated in the {@linkplain
* OptimizationObjective#LOW_GARBAGE "low-garbage"} mode.
*
*
Disabling allocating intermediate-capacity segments (that is, passing {@code false} into
* this method) also implicitly disables {@linkplain #splitBetweenTwoNewSegments(boolean)
* splitting between two new segments} because it doesn't make sense to allocate two new
* full-capacity segments and move entries there while we already have one full-capacity segment
* at hand (the old one).
*
* @param allocateIntermediateCapacitySegments whether the created SmoothieMaps should allocate
* intermediate-capacity segments during the growth
* @return this builder back
* @see #splitBetweenTwoNewSegments(boolean)
*/
@CanIgnoreReturnValue
@Contract("_ -> this")
public SmoothieMapBuilder allocateIntermediateCapacitySegments(
boolean allocateIntermediateCapacitySegments) {
if (!allocateIntermediateCapacitySegments) {
splitBetweenTwoNewSegments(false);
}
this.allocateIntermediateCapacitySegments = allocateIntermediateCapacitySegments;
return this;
}
/**
* Specifies whether when segments are split in SmoothieMaps created with this builder, the
* entries from the segment being split should be moved into two new intermediate-capacity
* segments or the entries should be distributed between the old segment one newly allocated
* segment (full-capacity or intermediate-capacity).
*
* See the description of the model of segments and the definitions of
* intermediate-capacity and full-capacity segments in the documentation for
* {@link #allocateIntermediateCapacitySegments(boolean)}. Moving entries from the old segment
* into two newly allocated intermediate-capacity segments means that just after the split they
* are jointly 75% full in terms of the entry storage capacity. Both newly allocated segments
* may then be reallocated as full-capacity segments when needed. One full-capacity segment and
* one newly allocated intermediate-capacity segment are jointly 60% full. Two full-capacity
* segments, the old one and a newly allocated one (if the allocation of intermediate-capacity
* segments is turned off) are jointly 50% full.
*
*
Thus, splitting between two newly allocated segments lowers the footprint per entry and
* the footprint variability at different SmoothieMap's sizes as much as possible. The downside
* of this strategy is that during SmoothieMap's growth, one full-capacity and two
* intermediate-capacity segments are allocated and then dropped per every 2 * N entries (given
* that a segment's hash table capacity is N) every time the map doubles in size. This is a
* significant rate of heap memory churn, exceeding that of typical open-addressing hash table
* implementations such as {@link java.util.IdentityHashMap} and entry-based hash maps: {@link
* java.util.HashMap} or {@link java.util.concurrent.ConcurrentHashMap}.
*
*
By default, during a split, entries are distributed between the old (full-capacity)
* segment and a newly allocated intermediate-capacity segment, as per {@linkplain
* #defaultOptimizationConfiguration() the "mixed" mode (the default mode)} of SmoothieMap's
* operation, that is, by default, splitting is not done between two newly allocated
* segments. Splitting between two new segments is not done in the {@linkplain
* OptimizationObjective#LOW_GARBAGE "low-garbage"} and the "mixed" modes. It's enabled only in
* the {@linkplain OptimizationObjective#FOOTPRINT "footprint"} mode.
*
*
Enabling splitting between two new segments also implicitly enables {@linkplain
* #allocateIntermediateCapacitySegments(boolean) allocation of intermediate-capacity segments}
* because the former doesn't make sense without the latter.
*
* @param splitBetweenTwoNewSegments whether created SmoothieMaps should move entries from
* full-capacity segments into two newly allocated intermediate-capacity segments during
* growth
* @return this builder back
* @see #allocateIntermediateCapacitySegments(boolean)
* @see OptimizationObjective#FOOTPRINT
*/
@CanIgnoreReturnValue
@Contract("_ -> this")
public SmoothieMapBuilder splitBetweenTwoNewSegments(boolean splitBetweenTwoNewSegments) {
if (splitBetweenTwoNewSegments) {
allocateIntermediateCapacitySegments(true);
}
this.splitBetweenTwoNewSegments = splitBetweenTwoNewSegments;
return this;
}
/**
* Specifies whether SmoothieMaps created with this builder should automatically shrink, i. e.
* reclaim the allocated heap space when they reduce in size.
*
* By default, shrinking is turned on as a part of {@linkplain
* #defaultOptimizationConfiguration() the "mixed" mode (the default mode)} of SmoothieMap's
* operation. The "mixed" and the {@linkplain OptimizationObjective#FOOTPRINT "footprint"} modes
* turn shrinking on, but the {@linkplain OptimizationObjective#LOW_GARBAGE "low-garbage"} mode
* turns shrinking off.
*
*
Automatic shrinking creates some low-volume stream of allocations and reclamations of
* heap memory objects when entries are dynamically put into and removed from the SmoothieMap
* even if the number of entries in the map remains relatively stable during this process,
* though at a much lower rate than garbage is produced by entry-based Map implementations such
* as {@link java.util.HashMap}, {@link java.util.TreeMap}, or {@link
* java.util.concurrent.ConcurrentHashMap} during a similar process.
*
* @param doShrink whether the created SmoothieMaps should automatically reclaim memory when
* they reduce in size
* @return this builder back
*/
@CanIgnoreReturnValue
@Contract("_ -> this")
public SmoothieMapBuilder doShrink(boolean doShrink) {
this.doShrink = doShrink;
return this;
}
/**
* Sets the {@link Equivalence} {@linkplain io.timeandspace.collect.map.ObjObjMap#keyEquivalence
* used for comparing keys} in SmoothieMaps created with this builder to the given equivalence.
*
* @param keyEquivalence the key equivalence to use in created SmoothieMaps
* @return this builder back
* @throws NullPointerException if the given equivalence object is null
* @see #defaultKeyEquivalence()
* @see io.timeandspace.collect.map.ObjObjMap#keyEquivalence
* @see #valueEquivalence(Equivalence)
*/
@CanIgnoreReturnValue
@Contract("_ -> this")
public SmoothieMapBuilder keyEquivalence(Equivalence keyEquivalence) {
checkNonNull(keyEquivalence);
this.keyEquivalence = keyEquivalence;
return this;
}
/**
* Sets the {@link Equivalence} {@linkplain io.timeandspace.collect.map.ObjObjMap#keyEquivalence
* used for comparing keys} in SmoothieMaps created with this builder to {@link
* Equivalence#defaultEquality()}.
*
* @return this builder back
* @see #keyEquivalence(Equivalence)
* @see io.timeandspace.collect.map.ObjObjMap#keyEquivalence
*/
@CanIgnoreReturnValue
@Contract(" -> this")
public SmoothieMapBuilder defaultKeyEquivalence() {
this.keyEquivalence = Equivalence.defaultEquality();
return this;
}
/**
* Sets a key hash function to be used in {@linkplain SmoothieMap SmoothieMaps} created with
* this builder.
*
* The default key hash function derives 64-bit hash codes from the 32-bit result of calling
* {@link Object#hashCode()} on the key objects (or {@link Equivalence#hash}, if {@link
* #keyEquivalence(Equivalence)} is configured for the builder). This means that if the number
* of entries in the SmoothieMap approaches or exceeds 2^32 (4 billion), a large number of
* hash code collisions is inevitable. Therefore, it's recommended to always configure a custom
* key hash function (using this method) for ultra-large SmoothieMaps.
*
*
The specified hash function must be consistent with {@link Object#equals} on the key
* objects, or a custom key equivalence if specified via {@link #keyEquivalence(Equivalence)} in
* the same way as {@link Object#hashCode()} must be consistent with {@code equals()}. This is
* the user's responsibility to ensure this. When {@link #keyEquivalence(Equivalence)} is called
* on a builder object, the key hash function, if already configured to a non-default, is
* not reset. On the other hand, if {@code keyHashFunction()} (or {@link
* #keyHashFunctionFactory(Supplier)}) is never called on a builder, or {@link
* #defaultKeyHashFunction()} is called, it's not necessary to configure the key hash function
* along with any custom {@link #keyEquivalence(Equivalence)} because by default {@code
* SmoothieMapBuilder} does respect {@link Equivalence#hash}.
*
* @param hashFunction a key hash function for each {@code SmoothieMap} created using this
* builder
* @return this builder back
* @throws NullPointerException if the given hash function object is null
* @see #keyHashFunctionFactory(Supplier)
* @see #defaultKeyHashFunction()
* @see #keyEquivalence(Equivalence)
*/
@CanIgnoreReturnValue
@Contract("_ -> this")
public SmoothieMapBuilder keyHashFunction(ToLongFunction hashFunction) {
checkNonNull(hashFunction);
this.keyHashFunctionFactory = () -> hashFunction;
return this;
}
/**
* Sets a factory to obtain a key hash function for each {@link SmoothieMap} created using this
* builder. Compared to {@link #keyHashFunction(ToLongFunction)}, this method allows inserting
* variability or randomness into the hash function:
* {@code
* builder.keyHashFunctionFactory(() -> {
* LongHashFunction hashF = LongHashFunction.xx(ThreadLocalRandom.current().nextLong());
* return hashF::hashChars;
* });}
*
* Hash functions returned by the specified factory must be consistent with {@link
* Object#equals} on the key objects, or a custom key equivalence if specified via {@link
* #keyEquivalence(Equivalence)} in the same way as {@link Object#hashCode()} must be consistent
* with {@code equals()}. This is the user's responsibility to ensure the consistency. When
* {@link #keyEquivalence(Equivalence)} is called on a builder object, the key hash function
* factory, if already configured to a non-default, is not reset. See the Javadoc comment
* for {@link #keyHashFunction(ToLongFunction)} for more information.
*
* @param hashFunctionFactory the factory to create a key hash function for each {@code
* SmoothieMap} created using this builder
* @return this builder back
* @throws NullPointerException if the given hash function factory object is null
* @see #keyHashFunction(ToLongFunction)
* @see #defaultKeyHashFunction()
*/
@CanIgnoreReturnValue
@Contract("_ -> this")
public SmoothieMapBuilder keyHashFunctionFactory(
Supplier> hashFunctionFactory) {
checkNonNull(hashFunctionFactory);
this.keyHashFunctionFactory = hashFunctionFactory;
return this;
}
/**
* Specifies that SmoothieMaps created with this builder should use the default key hash
* function which derives a 64-bit hash code from the 32-bit result of calling {@link
* Object#hashCode()} on the key object (or {@link Equivalence#hash}, if {@link
* #keyEquivalence(Equivalence)} is configured for the builder).
*
* @return this builder back
* @see #keyHashFunction(ToLongFunction)
* @see #keyHashFunctionFactory(Supplier)
*/
@CanIgnoreReturnValue
@Contract(" -> this")
public SmoothieMapBuilder defaultKeyHashFunction() {
this.keyHashFunctionFactory = null;
return this;
}
/**
* Sets the {@link Equivalence} {@linkplain
* io.timeandspace.collect.map.ObjObjMap#valueEquivalence used for comparing values} in
* SmoothieMaps created with this builder to the given equivalence.
*
* @param valueEquivalence the value equivalence to use in created SmoothieMaps
* @return this builder back
* @throws NullPointerException if the given equivalence object is null
* @see #defaultValueEquivalence()
* @see io.timeandspace.collect.map.ObjObjMap#valueEquivalence()
* @see #keyEquivalence(Equivalence)
*/
@CanIgnoreReturnValue
@Contract("_ -> this")
public SmoothieMapBuilder valueEquivalence(Equivalence valueEquivalence) {
checkNonNull(valueEquivalence);
this.valueEquivalence = valueEquivalence;
return this;
}
/**
* Sets the {@link Equivalence} {@linkplain
* io.timeandspace.collect.map.ObjObjMap#valueEquivalence used for comparing values} in
* SmoothieMaps created with this builder to {@link Equivalence#defaultEquality()}.
*
* @return this builder back
* @see #valueEquivalence(Equivalence)
* @see io.timeandspace.collect.map.ObjObjMap#valueEquivalence()
*/
@CanIgnoreReturnValue
@Contract(" -> this")
public SmoothieMapBuilder defaultValueEquivalence() {
this.valueEquivalence = Equivalence.defaultEquality();
return this;
}
/**
* Specifies the expected steady-state size of each {@link SmoothieMap} created using
* this builder. The steady-state size is the map size after it's fully populated (when the map
* is used in a simple populate-then-access pattern), or if entries are dynamically inserted
* into and removed from the map, the steady-state size is the map size at which it should
* eventually balance.
*
* The default expected size is considered unknown. Calling {@link #unknownExpectedSize()}
* after this method has been once called on the builder with a specific value overrides that
* values and sets the expected size to be unknown again.
*
*
Calling this method is a performance hint for created SmoothieMap(s) and doesn't affect
* the semantics of operations. The configured value must not be the exact steady-state size of
* a created SmoothieMap, but it should be within 5% from the actual steady-state size. If the
* steady-state size of created SmoothieMap(s) cannot be known with this level of precision,
* configuring {@code expectedSize()} might make more harm than good and therefore shouldn't be
* done. {@link #minPeakSize(long)} might be known with more confidence though and it's
* recommended to configure it instead.
*
*
In theory, the {@linkplain #minPeakSize(long) minimum peak size} might be less than the
* expected size by at most 5% (higher difference is not possible if the expected size is
* configured according with the configuration above). If both expected size and minimum peak
* size are specified and the latter is less than the former by more than 5% an {@link
* IllegalStateException} is thrown when {@link #build()} is called.
*
*
Most often, the minimum peak size should be equal to the expected size (e. g. in the
* populate-then-access usage pattern, there is no ground for differentiating the peak and the
* expected sizes). Because of this, by default, if the expected size is configured for a
* builder and the minimum peak size is not configured, the expected size is used as a
* substitute for the minimum peak size.
*
*
When entries are dynamically put into and removed from a SmoothieMap, the minimum peak
* size might also be greater than the expected size, if after reaching the peak size
* SmoothieMaps are consistently expected to shrink. This might also be the case
* when X entries are first inserted into a SmoothieMap and then some entries are removed until
* the map reduces to some specific size Y smaller than X.
*
* @param expectedSize the expected steady-state size of created SmoothieMaps
* @return this builder back
* @throws IllegalArgumentException if the provided expected size is not positive
* @see #unknownExpectedSize()
* @see #minPeakSize(long)
*/
@CanIgnoreReturnValue
@Contract("_ -> this")
public SmoothieMapBuilder expectedSize(long expectedSize) {
checkNonNegative(expectedSize, "expected size");
this.expectedSize = expectedSize;
return this;
}
/**
* Specifies that the expected size of each {@link SmoothieMap} created using this builder is
* unknown.
*
* @return this builder back
* @see #expectedSize(long)
*/
@CanIgnoreReturnValue
@Contract(" -> this")
public SmoothieMapBuilder unknownExpectedSize() {
this.expectedSize = UNKNOWN_SIZE;
return this;
}
/**
* Specifies the minimum bound of the peak size of each {@link SmoothieMap} created
* using this builder. For example, it may be unknown what size the created SmoothieMap will
* have after it is fully populated (or reaches "saturation" if entries are dynamically put into
* and removed from the map): 1 million entries, or 2 million, or 3 million. But if it known
* that under no circumstance the peak size of the map will be less than 1 million, then it
* makes sense to configure this bound via this method.
*
* The default minimum bound for the peak size of created SmoothieMaps is considered unknown,
* or 0. However, to configure unknown minimum bound of the peak size (the override a specific
* values which may have been once specified for the builder), {@link #unknownMinPeakSize()}
* should be called and not this method.
*
*
Calling this method is a performance hint for created SmoothieMap(s) and doesn't affect
* the semantics of operations. The configured value may be used to preallocate data structure
* when a SmoothieMap is created, so it's better to err towards a value lower than the actual
* peak size than towards a value higher than the actual peak size, especially when the
* {@linkplain #optimizeFor optimization objective} is set to {@link
* OptimizationObjective#LOW_GARBAGE}.
*
*
If the minimum peak size is not configured for a SmoothieMapBuilder, but {@linkplain
* #expectedSize(long) expected size} is configured, the minimum peak size of created maps is
* assumed to be equal to the expected size, although in some cases it may be about 5% less than
* that even if the expected size is configured properly. If this is the case, configure the
* minimum peak size along with the expected size explicitly. See the documentation for {@link
* #expectedSize(long)} method for more information.
*
* @param minPeakSize the minimum bound for the peak size of created SmoothieMaps
* @return this builder back
* @throws IllegalArgumentException if the provided minimum peak size is not positive
* @see #unknownMinPeakSize()
* @see #expectedSize(long)
*/
@CanIgnoreReturnValue
@Contract("_ -> this")
public SmoothieMapBuilder minPeakSize(long minPeakSize) {
checkNonNegative(minPeakSize, "minimum peak size");
this.minPeakSize = minPeakSize;
return this;
}
/**
* Specifies that the minimum bound of the peak size of each {@link SmoothieMap} created using
* this builder is unknown.
*
* @return this builder back
* @see #minPeakSize(long)
*/
@CanIgnoreReturnValue
@Contract(" -> this")
public SmoothieMapBuilder unknownMinPeakSize() {
this.minPeakSize = UNKNOWN_SIZE;
return this;
}
/**
* Creates a new {@link SmoothieMap} with the configurations from this builder. The created
* {@code SmoothieMap} doesn't hold an internal reference to the builder, and all subsequent
* changes to the builder doesn't affect the configurations of the map.
*
* @return a new {@code SmoothieMap} with the configurations from this builder
*/
@Contract(" -> new")
public SmoothieMap build() {
boolean isDefaultKeyEquivalence = keyEquivalence.equals(Equivalence.defaultEquality());
boolean isDefaultValueEquivalence = valueEquivalence.equals(Equivalence.defaultEquality());
if (keyHashFunctionFactory == null && isDefaultKeyEquivalence &&
isDefaultValueEquivalence) {
return new SmoothieMap<>(this);
} else if (keyHashFunctionFactory != null && isDefaultKeyEquivalence &&
isDefaultValueEquivalence) {
return new SmoothieMapWithCustomKeyHashFunction<>(this);
} else if (!isDefaultKeyEquivalence && isDefaultValueEquivalence) {
return new SmoothieMapWithCustomKeyEquivalence<>(this);
} else if (keyHashFunctionFactory == null && isDefaultKeyEquivalence) {
Utils.verifyThat(!isDefaultValueEquivalence);
return new SmoothieMapWithCustomValueEquivalence<>(this);
} else {
return new SmoothieMapWithCustomKeyAndValueEquivalences<>(this);
}
}
boolean allocateIntermediateCapacitySegments() {
return allocateIntermediateCapacitySegments;
}
boolean splitBetweenTwoNewSegments() {
return splitBetweenTwoNewSegments;
}
boolean doShrink() {
return doShrink;
}
Equivalence keyEquivalence() {
return keyEquivalence;
}
ToLongFunction keyHashFunction() {
if (keyHashFunctionFactory != null) {
return keyHashFunctionFactory.get();
}
if (keyEquivalence.equals(Equivalence.defaultEquality())) {
return DefaultHashFunction.instance();
}
return new EquivalenceBasedHashFunction<>(keyEquivalence);
}
Equivalence valueEquivalence() {
return valueEquivalence;
}
private void checkSizes() {
if (expectedSize == UNKNOWN_SIZE || minPeakSize == UNKNOWN_SIZE) {
// Nothing to check
return;
}
if (minPeakSize <
(long) ((double) expectedSize * (1.0 - MAX_EXPECTED_SIZE_ERROR_FRACTION))) {
throw new IllegalStateException("minPeakSize[" + minPeakSize + "] is less than " +
"expectedSize[" + expectedSize + "] * " +
(1.0 - MAX_EXPECTED_SIZE_ERROR_FRACTION));
}
}
long expectedSize() {
checkSizes();
return expectedSize;
}
long minPeakSize() {
checkSizes();
return minPeakSize != UNKNOWN_SIZE ? minPeakSize : expectedSize;
}
}