Optimize SmallSortedMap to improve getAllFields() efficiency.

PiperOrigin-RevId: 944593820
This commit is contained in:
Protobuf Team Bot 2026-07-08 11:03:46 -07:00 committed by Copybara-Service
parent ba087538d2
commit 706fc1e3cb
4 changed files with 470 additions and 503 deletions

View file

@ -53,23 +53,23 @@ final class FieldSet<T extends FieldSet.FieldDescriptorLite<T>> {
void internalMergeFrom(Object to, Object from);
}
private final SmallSortedMap<T, Object> fields;
private final SmallSortedMap<T> fields;
private boolean isImmutable;
private boolean hasLazyField;
/** Construct a new FieldSet. */
private FieldSet() {
this.fields = SmallSortedMap.newFieldMap();
this.fields = new SmallSortedMap<>();
}
/** Construct an empty FieldSet. This is only used to initialize DEFAULT_INSTANCE. */
@SuppressWarnings("unused")
private FieldSet(final boolean dummy) {
this(SmallSortedMap.<T>newFieldMap());
this(new SmallSortedMap<T>());
makeImmutable();
}
private FieldSet(SmallSortedMap<T, Object> fields) {
private FieldSet(SmallSortedMap<T> fields) {
this.fields = fields;
makeImmutable();
}
@ -102,7 +102,7 @@ final class FieldSet<T extends FieldSet.FieldDescriptorLite<T>> {
if (isImmutable) {
return;
}
int n = fields.getNumArrayEntries(); // Optimisation: hoist out of hot loop.
int n = fields.size(); // Optimisation: hoist out of hot loop.
for (int i = 0; i < n; ++i) {
Entry<T, Object> entry = fields.getArrayEntryAt(i);
Object value = entry.getValue();
@ -110,12 +110,6 @@ final class FieldSet<T extends FieldSet.FieldDescriptorLite<T>> {
((GeneratedMessageLite<?, ?>) value).makeImmutable();
}
}
for (Map.Entry<T, Object> entry : fields.getOverflowEntries()) {
Object value = entry.getValue();
if (value instanceof GeneratedMessageLite) {
((GeneratedMessageLite<?, ?>) value).makeImmutable();
}
}
fields.makeImmutable();
isImmutable = true;
}
@ -145,7 +139,7 @@ final class FieldSet<T extends FieldSet.FieldDescriptorLite<T>> {
return equals(this.fields, other.fields);
}
private static boolean equals(SmallSortedMap<?, ?> m1, SmallSortedMap<?, ?> m2) {
private static boolean equals(SmallSortedMap<?> m1, SmallSortedMap<?> m2) {
if (m1.size() != m2.size()) {
return false;
}
@ -194,15 +188,13 @@ final class FieldSet<T extends FieldSet.FieldDescriptorLite<T>> {
public FieldSet<T> clone() {
// We can't just call fields.clone because List objects in the map
// should not be shared.
// TODO: b/513203684 - Consider passing the capacity to the new FieldSet and SmallSortedMap.
FieldSet<T> clone = FieldSet.newFieldSet();
int n = fields.getNumArrayEntries(); // Optimisation: hoist out of hot loop.
int n = fields.size(); // Optimisation: hoist out of hot loop.
for (int i = 0; i < n; i++) {
Map.Entry<T, Object> entry = fields.getArrayEntryAt(i);
clone.setField(entry.getKey(), entry.getValue());
}
for (Map.Entry<T, Object> entry : fields.getOverflowEntries()) {
clone.setField(entry.getKey(), entry.getValue());
}
clone.hasLazyField = hasLazyField;
return clone;
}
@ -218,7 +210,7 @@ final class FieldSet<T extends FieldSet.FieldDescriptorLite<T>> {
/** Get a simple map containing all the fields. */
public Map<T, Object> getAllFields() {
if (hasLazyField) {
SmallSortedMap<T, Object> result =
SmallSortedMap<T> result =
cloneAllFieldsMap(fields, /* copyList= */ false, /* resolveLazyFields= */ true);
if (fields.isImmutable()) {
result.makeImmutable();
@ -228,16 +220,13 @@ final class FieldSet<T extends FieldSet.FieldDescriptorLite<T>> {
return fields.isImmutable() ? fields : Collections.unmodifiableMap(fields);
}
private static <T extends FieldDescriptorLite<T>> SmallSortedMap<T, Object> cloneAllFieldsMap(
SmallSortedMap<T, Object> fields, boolean copyList, boolean resolveLazyFields) {
SmallSortedMap<T, Object> result = SmallSortedMap.newFieldMap();
int n = fields.getNumArrayEntries(); // Optimisation: hoist out of hot loop.
private static <T extends FieldDescriptorLite<T>> SmallSortedMap<T> cloneAllFieldsMap(
SmallSortedMap<T> fields, boolean copyList, boolean resolveLazyFields) {
int n = fields.size(); // Optimisation: hoist out of hot loop.
SmallSortedMap<T> result = new SmallSortedMap<>(n);
for (int i = 0; i < n; i++) {
cloneFieldEntry(result, fields.getArrayEntryAt(i), copyList, resolveLazyFields);
}
for (Map.Entry<T, Object> entry : fields.getOverflowEntries()) {
cloneFieldEntry(result, entry, copyList, resolveLazyFields);
}
return result;
}
@ -463,17 +452,12 @@ final class FieldSet<T extends FieldSet.FieldDescriptorLite<T>> {
* caller to check that all required fields are present.
*/
public boolean isInitialized() {
int n = fields.getNumArrayEntries(); // Optimisation: hoist out of hot loop.
int n = fields.size(); // Optimisation: hoist out of hot loop.
for (int i = 0; i < n; i++) {
if (!isInitialized(fields.getArrayEntryAt(i))) {
return false;
}
}
for (final Map.Entry<T, Object> entry : fields.getOverflowEntries()) {
if (!isInitialized(entry)) {
return false;
}
}
return true;
}
@ -527,13 +511,10 @@ final class FieldSet<T extends FieldSet.FieldDescriptorLite<T>> {
/** Like {@link Message.Builder#mergeFrom(Message)}, but merges from another {@link FieldSet}. */
public void mergeFrom(final FieldSet<T> other) {
int n = other.fields.getNumArrayEntries(); // Optimisation: hoist out of hot loop.
int n = other.fields.size(); // Optimisation: hoist out of hot loop.
for (int i = 0; i < n; i++) {
mergeFromField(other.fields.getArrayEntryAt(i));
}
for (final Map.Entry<T, Object> entry : other.fields.getOverflowEntries()) {
mergeFromField(entry);
}
}
private static Object cloneIfMutable(Object value) {
@ -623,25 +604,19 @@ final class FieldSet<T extends FieldSet.FieldDescriptorLite<T>> {
/** See {@link Message#writeTo(CodedOutputStream)}. */
public void writeTo(final CodedOutputStream output) throws IOException {
int n = fields.getNumArrayEntries(); // Optimisation: hoist out of hot loop.
int n = fields.size(); // Optimisation: hoist out of hot loop.
for (int i = 0; i < n; i++) {
final Map.Entry<T, Object> entry = fields.getArrayEntryAt(i);
writeField(entry.getKey(), entry.getValue(), output);
}
for (final Map.Entry<T, Object> entry : fields.getOverflowEntries()) {
writeField(entry.getKey(), entry.getValue(), output);
}
}
/** Like {@link #writeTo} but uses MessageSet wire format. */
public void writeMessageSetTo(final CodedOutputStream output) throws IOException {
int n = fields.getNumArrayEntries(); // Optimisation: hoist out of hot loop.
int n = fields.size(); // Optimisation: hoist out of hot loop.
for (int i = 0; i < n; i++) {
writeMessageSetTo(fields.getArrayEntryAt(i), output);
}
for (final Map.Entry<T, Object> entry : fields.getOverflowEntries()) {
writeMessageSetTo(entry, output);
}
}
private void writeMessageSetTo(final Map.Entry<T, Object> entry, final CodedOutputStream output)
@ -819,27 +794,21 @@ final class FieldSet<T extends FieldSet.FieldDescriptorLite<T>> {
*/
public int getSerializedSize() {
int size = 0;
int n = fields.getNumArrayEntries(); // Optimisation: hoist out of hot loop.
int n = fields.size(); // Optimisation: hoist out of hot loop.
for (int i = 0; i < n; i++) {
final Map.Entry<T, Object> entry = fields.getArrayEntryAt(i);
size += computeFieldSize(entry.getKey(), entry.getValue());
}
for (final Map.Entry<T, Object> entry : fields.getOverflowEntries()) {
size += computeFieldSize(entry.getKey(), entry.getValue());
}
return size;
}
/** Like {@link #getSerializedSize} but uses MessageSet wire format. */
public int getMessageSetSerializedSize() {
int size = 0;
int n = fields.getNumArrayEntries(); // Optimisation: hoist out of hot loop.
int n = fields.size(); // Optimisation: hoist out of hot loop.
for (int i = 0; i < n; i++) {
size += getMessageSetSerializedSize(fields.getArrayEntryAt(i));
}
for (final Map.Entry<T, Object> entry : fields.getOverflowEntries()) {
size += getMessageSetSerializedSize(entry);
}
return size;
}
@ -990,16 +959,16 @@ final class FieldSet<T extends FieldSet.FieldDescriptorLite<T>> {
*/
static final class Builder<T extends FieldDescriptorLite<T>> {
private SmallSortedMap<T, Object> fields;
private SmallSortedMap<T> fields;
private boolean hasLazyField;
private boolean isMutable;
private boolean hasNestedBuilders;
private Builder() {
this(SmallSortedMap.<T>newFieldMap());
this(new SmallSortedMap<T>());
}
private Builder(SmallSortedMap<T, Object> fields) {
private Builder(SmallSortedMap<T> fields) {
this.fields = fields;
this.isMutable = true;
}
@ -1029,7 +998,7 @@ final class FieldSet<T extends FieldSet.FieldDescriptorLite<T>> {
return FieldSet.emptySet();
}
isMutable = false;
SmallSortedMap<T, Object> fieldsForBuild = fields;
SmallSortedMap<T> fieldsForBuild = fields;
if (hasNestedBuilders) {
// Make a copy of the fields map with all Builders replaced by Message.
fieldsForBuild =
@ -1042,14 +1011,11 @@ final class FieldSet<T extends FieldSet.FieldDescriptorLite<T>> {
}
private static <T extends FieldDescriptorLite<T>> void replaceBuilders(
SmallSortedMap<T, Object> fieldMap, boolean partial) {
int n = fieldMap.getNumArrayEntries(); // Optimisation: hoist out of hot loop.
SmallSortedMap<T> fieldMap, boolean partial) {
int n = fieldMap.size(); // Optimisation: hoist out of hot loop.
for (int i = 0; i < n; i++) {
replaceBuilders(fieldMap.getArrayEntryAt(i), partial);
}
for (Map.Entry<T, Object> entry : fieldMap.getOverflowEntries()) {
replaceBuilders(entry, partial);
}
}
private static <T extends FieldDescriptorLite<T>> void replaceBuilders(
@ -1119,7 +1085,7 @@ final class FieldSet<T extends FieldSet.FieldDescriptorLite<T>> {
/** Get a simple map containing all the fields. */
public Map<T, Object> getAllFields() {
if (hasLazyField) {
SmallSortedMap<T, Object> result =
SmallSortedMap<T> result =
cloneAllFieldsMap(fields, /* copyList= */ false, /* resolveLazyFields= */ true);
if (fields.isImmutable()) {
result.makeImmutable();
@ -1353,17 +1319,12 @@ final class FieldSet<T extends FieldSet.FieldDescriptorLite<T>> {
* caller to check that all required fields are present.
*/
public boolean isInitialized() {
int n = fields.getNumArrayEntries(); // Optimisation: hoist out of hot loop.
int n = fields.size(); // Optimisation: hoist out of hot loop.
for (int i = 0; i < n; i++) {
if (!FieldSet.isInitialized(fields.getArrayEntryAt(i))) {
return false;
}
}
for (final Map.Entry<T, Object> entry : fields.getOverflowEntries()) {
if (!FieldSet.isInitialized(entry)) {
return false;
}
}
return true;
}
@ -1372,13 +1333,10 @@ final class FieldSet<T extends FieldSet.FieldDescriptorLite<T>> {
*/
public void mergeFrom(final FieldSet<T> other) {
ensureIsMutable();
int n = other.fields.getNumArrayEntries(); // Optimisation: hoist out of hot loop.
int n = other.fields.size(); // Optimisation: hoist out of hot loop.
for (int i = 0; i < n; i++) {
mergeFromField(other.fields.getArrayEntryAt(i));
}
for (final Map.Entry<T, Object> entry : other.fields.getOverflowEntries()) {
mergeFromField(entry);
}
}
// Avoid iterator allocation.

View file

@ -8,6 +8,7 @@
package com.google.protobuf;
import static com.google.protobuf.Internal.checkNotNull;
import static java.lang.Math.min;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Descriptors.EnumDescriptor;
@ -128,8 +129,12 @@ public abstract class GeneratedMessage extends AbstractMessage implements Serial
* @param getBytesForString whether to generate ByteString for string fields
*/
private Map<FieldDescriptor, Object> getAllFieldsMutable(boolean getBytesForString) {
final TreeMap<FieldDescriptor, Object> result = new TreeMap<>();
final FieldAccessorTable fieldAccessorTable = internalGetFieldAccessorTable();
// Cap the initial map capacity at 256. Messages with an enormous number of fields (such as
// giant oneofs) are typically very sparse in practice, so allocating a massive array upfront
// would be pointlessly wasteful.
final SmallSortedMap<FieldDescriptor> result =
new SmallSortedMap<>(min(fieldAccessorTable.fields.length, 256));
final Descriptor descriptor = fieldAccessorTable.descriptor;
final List<FieldDescriptor> fields = descriptor.getFields();
@ -599,8 +604,12 @@ public abstract class GeneratedMessage extends AbstractMessage implements Serial
/** Internal helper which returns a mutable map. */
private Map<FieldDescriptor, Object> getAllFieldsMutable() {
final TreeMap<FieldDescriptor, Object> result = new TreeMap<>();
final FieldAccessorTable fieldAccessorTable = internalGetFieldAccessorTable();
// Cap the initial map capacity at 256. Messages with an enormous number of fields (such as
// giant oneofs) are typically very sparse in practice, so allocating a massive array upfront
// would be pointlessly wasteful.
final SmallSortedMap<FieldDescriptor> result =
new SmallSortedMap<>(min(fieldAccessorTable.fields.length, 256));
final Descriptor descriptor = fieldAccessorTable.descriptor;
final List<FieldDescriptor> fields = descriptor.getFields();

View file

@ -7,124 +7,109 @@
package com.google.protobuf;
import static java.util.Objects.requireNonNull;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* A custom map implementation from FieldDescriptor to Object optimized to minimize the number of
* memory allocations for instances with a small number of mappings. The implementation stores the
* first {@code k} mappings in an array for a configurable value of {@code k}, allowing direct
* access to the corresponding {@code Entry}s without the need to create an Iterator. The remaining
* entries are stored in an overflow map. Iteration over the entries in the map should be done as
* follows:
* A custom map implementation from {@link FieldSet.FieldDescriptorLite} to Object.
*
* <p>This implementation is heavily optimized for insertion and iteration when the map is built
* (via {@code put()}) with keys (FieldDescriptors) in ascending sorted order. When entries are
* added in increasing order of the FieldDescriptor, no sorting overhead is incurred. The entries
* are simply appended to a backing array, making memory allocation and insertion highly efficient.
*
* <p>Iteration over the entries is straightforward and avoids the creation of an {@code Iterator}
* object. It should be done as follows:
*
* <pre>{@code
* for (int i = 0; i < fieldMap.getNumArrayEntries(); i++) {
* for (int i = 0; i < fieldMap.size(); i++) {
* process(fieldMap.getArrayEntryAt(i));
* }
* for (Map.Entry<K, V> entry : fieldMap.getOverflowEntries()) {
* process(entry);
* }
* }</pre>
*
* The resulting iteration is in order of ascending field tag number. The object returned by {@link
* #entrySet()} adheres to the same contract but is less efficient as it necessarily involves
* creating an object for iteration.
*
* <p>The tradeoff for this memory efficiency is that the worst case running time of the {@code
* put()} operation is {@code O(k + lg n)}, which happens when entries are added in descending
* order. {@code k} should be chosen such that it covers enough common cases without adversely
* affecting larger maps. In practice, the worst case scenario does not happen for extensions
* because extension fields are serialized and deserialized in order of ascending tag number, but
* the worst case scenario can happen for DynamicMessages.
* <p>If entries are added out of order, the map defers sorting until necessary (e.g., when
* iterating, querying size, or retrieving elements). This lazily sorts and deduplicates the
* underlying array.
*
* <p>The running time for all other operations is similar to that of {@code TreeMap}.
*
* <p>Instances are not thread-safe until {@link #makeImmutable()} is called, after which any
* modifying operation will result in an {@link UnsupportedOperationException}.
* <p>Modifying operations (such as {@code put()}, {@code remove()}, or {@code clear()}) are not
* thread-safe until {@link #makeImmutable()} is called, after which any modifying operation will
* result in an {@link UnsupportedOperationException}. However, instances are thread-safe for
* concurrent read-only operations (such as {@code get()} or {@code size()}) even before {@code
* makeImmutable()} is called, as long as no modifying operations are performed concurrently.
*
* @author darick@google.com Darick Tong
*/
// This class is final for all intents and purposes because the constructor is
// private. However, the FieldDescriptor-specific logic is encapsulated in
// a subclass to aid testability of the core logic.
class SmallSortedMap<K extends FieldSet.FieldDescriptorLite<K>, V> extends AbstractMap<K, V> {
@SuppressWarnings("unchecked") // entries is known to contain Entry objects.)
class SmallSortedMap<K extends FieldSet.FieldDescriptorLite<K>> extends AbstractMap<K, Object> {
static final int DEFAULT_FIELD_MAP_ARRAY_SIZE = 16;
/**
* Creates a new instance for mapping FieldDescriptors to their values. The {@link
* #makeImmutable()} implementation will convert the List values of any repeated fields to
* unmodifiable lists.
*/
static <FieldDescriptorT extends FieldSet.FieldDescriptorLite<FieldDescriptorT>>
SmallSortedMap<FieldDescriptorT, Object> newFieldMap() {
return new SmallSortedMap<FieldDescriptorT, Object>() {
@Override
public void makeImmutable() {
if (!isImmutable()) {
for (int i = 0; i < getNumArrayEntries(); i++) {
final Map.Entry<FieldDescriptorT, Object> entry = getArrayEntryAt(i);
if (entry.getKey().isRepeated()) {
final List<?> value = (List) entry.getValue();
entry.setValue(Collections.unmodifiableList(value));
}
}
for (Map.Entry<FieldDescriptorT, Object> entry : getOverflowEntries()) {
if (entry.getKey().isRepeated()) {
final List<?> value = (List) entry.getValue();
entry.setValue(Collections.unmodifiableList(value));
}
}
}
super.makeImmutable();
}
};
}
/** Creates a new instance for testing. */
static <K extends FieldSet.FieldDescriptorLite<K>, V> SmallSortedMap<K, V> newInstanceForTest() {
return new SmallSortedMap<>();
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// Only has Entry elements inside.
// Can't declare this as Entry[] because Entry is generic, so you get "generic array creation"
// error. Instead, use an Object[], and cast to Entry on read.
// null Object[] means 'empty'.
// TODO: b/513203684 - Consider maintaining two arrays for keys and values.
private Object[] entries;
// Number of elements in entries that are valid, like ArrayList.size.
private int entriesSize;
private Map<K, V> overflowEntries;
// Number of elements in entries that are valid, like ArrayList.size.
private int size;
// Whether {@link #makeImmutable()} has been called. If true, the map is immutable, and
// guaranteed to be sorted and deduplicated.
private boolean isImmutable;
// The EntrySet is a stateless view of the Map. It's initialized the first
// time it is requested and reused henceforth.
private volatile EntrySet lazyEntrySet;
private SmallSortedMap() {
this.overflowEntries = Collections.emptyMap();
private volatile boolean isSortedAndDedupped;
SmallSortedMap() {
isImmutable = false;
entries = null;
size = 0;
isSortedAndDedupped = true;
}
/** Make this map immutable from this point forward. */
SmallSortedMap(int initialCapacity) {
isImmutable = false;
entries = new Object[initialCapacity];
size = 0;
isSortedAndDedupped = true;
}
/**
* Make this map immutable from this point forward. Immutable maps are guaranteed to be sorted and
* deduplicated.
*/
public void makeImmutable() {
if (!isImmutable) {
// Note: There's no need to wrap the entries in an unmodifiableList
// because none of the array's accessors are exposed. The iterator() of
// overflowEntries, on the other hand, is exposed so it must be made
// unmodifiable.
overflowEntries =
overflowEntries.isEmpty()
? Collections.<K, V>emptyMap()
: Collections.unmodifiableMap(overflowEntries);
isImmutable = true;
if (isImmutable) {
return;
}
ensureSortedAndDeduplicated();
if (entries != null) {
for (int i = 0; i < size; i++) {
Entry entry = (Entry) entries[i];
if (entry.getKey().isRepeated()) {
entry.setValue(Collections.unmodifiableList((List<?>) entry.getValue()));
}
}
}
isImmutable = true;
}
/**
@ -134,42 +119,26 @@ class SmallSortedMap<K extends FieldSet.FieldDescriptorLite<K>, V> extends Abstr
return isImmutable;
}
/**
* @return The number of entries in the entry array.
*/
public int getNumArrayEntries() {
return entriesSize;
}
/**
* @return The array entry at the given {@code index}.
*/
public Map.Entry<K, V> getArrayEntryAt(int index) {
if (index >= entriesSize) {
public Map.Entry<K, Object> getArrayEntryAt(int index) {
ensureSortedAndDeduplicated();
if (index >= size) {
throw new ArrayIndexOutOfBoundsException(index);
}
@SuppressWarnings("unchecked")
Entry e = (Entry) entries[index];
return e;
}
/**
* @return There number of overflow entries.
*/
public int getNumOverflowEntries() {
return overflowEntries.size();
}
/**
* @return An iterable over the overflow entries.
*/
public Iterable<Map.Entry<K, V>> getOverflowEntries() {
return overflowEntries.isEmpty() ? Collections.emptySet() : overflowEntries.entrySet();
return (Entry) entries[index];
}
@Override
public int size() {
return entriesSize + overflowEntries.size();
ensureSortedAndDeduplicated();
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
/**
@ -179,9 +148,9 @@ class SmallSortedMap<K extends FieldSet.FieldDescriptorLite<K>, V> extends Abstr
*/
@Override
public boolean containsKey(Object o) {
@SuppressWarnings("unchecked")
final K key = (K) o;
return binarySearchInArray(key) >= 0 || overflowEntries.containsKey(key);
ensureSortedAndDeduplicated();
return binarySearch(key) >= 0;
}
/**
@ -190,59 +159,76 @@ class SmallSortedMap<K extends FieldSet.FieldDescriptorLite<K>, V> extends Abstr
* <p>{@inheritDoc}
*/
@Override
public V get(Object o) {
@SuppressWarnings("unchecked")
public Object get(Object o) {
final K key = (K) o;
final int index = binarySearchInArray(key);
ensureSortedAndDeduplicated();
final int index = binarySearch(key);
if (index >= 0) {
@SuppressWarnings("unchecked")
Entry e = (Entry) entries[index];
return e.getValue();
}
return overflowEntries.get(key);
return null;
}
@Override
public void putAll(Map<? extends K, ?> map) {
checkMutable();
if (map instanceof SmallSortedMap) {
// Avoid iterator allocation if map is also a SmallSortedMap.
SmallSortedMap<? extends K> smallSortedMap = (SmallSortedMap<? extends K>) map;
// Avoid sorting and deduplicating the input map by accessing its internal array directly.
ensureCapacity(size + smallSortedMap.size);
final int thisMapSize = size;
final int limit = size + smallSortedMap.size;
for (int i = thisMapSize; i < limit; i++) {
final Entry entry = (Entry) smallSortedMap.entries[i - thisMapSize];
put(entry.getKey(), entry.getValue());
}
} else {
ensureCapacity(size + map.size());
for (Map.Entry<? extends K, ?> entry : map.entrySet()) {
put((K) entry.getKey(), (Object) entry.getValue());
}
}
}
@Override
@CanIgnoreReturnValue
public V put(K key, V value) {
@SuppressWarnings("PreferPreconditions") // unsupported
public Object put(K key, Object value) {
checkMutable();
final int index = binarySearchInArray(key);
if (index >= 0) {
// Replace existing array entry.
@SuppressWarnings("unchecked")
Entry e = (Entry) entries[index];
return e.setValue(value);
requireNonNull(key);
// ensureCapacity relies on the original putSorted to work correctly, so defer updating it until
// after the call to ensureCapacity().
boolean putSorted = isSortedAndDedupped;
if (size > 0) {
int comp = key.compareTo(((Entry) entries[size - 1]).getKey());
if (comp < 0) {
putSorted = false;
} else if (comp == 0) {
// Overwrite existing value.
((Entry) entries[size - 1]).setValue(value);
// We could choose to return the previous value here, but we don't for consistency with
// the code below.
return null;
}
}
ensureEntryArrayMutable();
final int insertionPoint = -(index + 1);
if (insertionPoint >= DEFAULT_FIELD_MAP_ARRAY_SIZE) {
// Put directly in overflow.
return getOverflowEntriesMutable().put(key, value);
}
// Insert new Entry in array.
if (entriesSize == DEFAULT_FIELD_MAP_ARRAY_SIZE) {
// Shift the last array entry into overflow.
@SuppressWarnings("unchecked")
final Entry lastEntryInArray = (Entry) entries[DEFAULT_FIELD_MAP_ARRAY_SIZE - 1];
entriesSize--;
getOverflowEntriesMutable().put(lastEntryInArray.getKey(), lastEntryInArray.getValue());
}
System.arraycopy(
entries, insertionPoint, entries, insertionPoint + 1, entries.length - insertionPoint - 1);
entries[insertionPoint] = new Entry(key, value);
entriesSize++;
return null;
ensureCapacity(size + 1);
entries[size++] = new Entry(key, value);
isSortedAndDedupped = putSorted;
return null; // Note: doesn't return previous value to optimize for insertion speed
}
@Override
public void clear() {
checkMutable();
if (entriesSize != 0) {
if (size != 0) {
entries = null;
entriesSize = 0;
}
if (!overflowEntries.isEmpty()) {
overflowEntries.clear();
size = 0;
isSortedAndDedupped = true;
}
}
@ -253,40 +239,92 @@ class SmallSortedMap<K extends FieldSet.FieldDescriptorLite<K>, V> extends Abstr
*/
@Override
@CanIgnoreReturnValue
public V remove(Object o) {
public Object remove(Object o) {
checkMutable();
@SuppressWarnings("unchecked")
ensureSortedAndDeduplicated();
final K key = (K) o;
final int index = binarySearchInArray(key);
if (index >= 0) {
return removeArrayEntryAt(index);
}
// overflowEntries might be Collections.unmodifiableMap(), so only
// call remove() if it is non-empty.
if (overflowEntries.isEmpty()) {
int index = binarySearch(key);
if (index < 0) {
return null;
} else {
return overflowEntries.remove(key);
}
Object oldValue = ((Entry) entries[index]).getValue();
System.arraycopy(entries, index + 1, entries, index, size - index - 1);
// Clear out the unused entry at the end to allow garbage collection.
entries[--size] = null;
return oldValue;
}
private void ensureCapacity(int minCapacity) {
if (entries == null || entries.length == 0) {
entries = new Object[Math.max(DEFAULT_FIELD_MAP_ARRAY_SIZE, minCapacity)];
return;
}
if (minCapacity > entries.length) {
// Catch before growing: if we are out of order, we might be full due to duplicates.
// Sorting and deduplicating in-place will collapse duplicates and free up capacity.
if (!isSortedAndDedupped) {
ensureSortedAndDeduplicated();
// If deduplication collapsed the size enough to satisfy minCapacity, return without
// growing.
if (minCapacity <= entries.length) {
return;
}
}
int oldCapacity = entries.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
if (newCapacity - MAX_ARRAY_SIZE > 0) {
newCapacity = MAX_ARRAY_SIZE;
}
entries = Arrays.copyOf(entries, newCapacity);
}
}
@CanIgnoreReturnValue
private V removeArrayEntryAt(int index) {
checkMutable();
@SuppressWarnings("unchecked")
final V removed = ((Entry) entries[index]).getValue();
// shift items across
System.arraycopy(entries, index + 1, entries, index, entriesSize - index - 1);
entriesSize--;
if (!overflowEntries.isEmpty()) {
// Shift the first entry in the overflow to be the last entry in the
// array.
final Iterator<Map.Entry<K, V>> iterator = getOverflowEntriesMutable().entrySet().iterator();
entries[entriesSize] = new Entry(iterator.next());
entriesSize++;
iterator.remove();
/**
* Ensures that the entries in the map are sorted and deduplicated.
*
* <p>Immutable maps are guaranteed to be already sorted and deduplicated.
*/
private void ensureSortedAndDeduplicated() {
if (isImmutable) {
return;
}
if (!isSortedAndDedupped) {
// Synchronize on the map to support concurrent read-only access. When multiple threads
// concurrently invoke read operations on a mutable map that has unsorted entries, they must
// safely coordinate the lazy sorting and deduplication without data corruption.
synchronized (this) {
if (isSortedAndDedupped) {
return;
}
if (size <= 1) {
return;
}
Arrays.sort(entries, 0, size);
// Resolve duplicates in-place (stable, preserving last write)
int newSize = 0;
for (int i = 0; i < size; i++) {
Entry entry = (Entry) entries[i];
if (newSize > 0 && ((Entry) entries[newSize - 1]).getKey().equals(entry.getKey())) {
entries[newSize - 1] = entry;
} else {
entries[newSize] = entry;
newSize++;
}
}
if (newSize < this.size) {
this.size = newSize;
// Clear out the unused entries to allow garbage collection.
Arrays.fill(entries, newSize, entries.length, null);
}
this.isSortedAndDedupped = true;
}
}
return removed;
}
/**
@ -294,36 +332,22 @@ class SmallSortedMap<K extends FieldSet.FieldDescriptorLite<K>, V> extends Abstr
* @return The returned integer position follows the same semantics as the value returned by
* {@link java.util.Arrays#binarySearch()}.
*/
private int binarySearchInArray(K key) {
int left = 0;
int right = entriesSize - 1;
// Optimization: For the common case in which entries are added in
// ascending tag order, check the largest element in the array before
// doing a full binary search.
if (right >= 0) {
@SuppressWarnings("unchecked")
int cmp = key.compareTo(((Entry) entries[right]).getKey());
if (cmp > 0) {
return -(right + 2); // Insert point is after "right".
} else if (cmp == 0) {
return right;
}
}
while (left <= right) {
int mid = (left + right) / 2;
@SuppressWarnings("unchecked")
int cmp = key.compareTo(((Entry) entries[mid]).getKey());
private int binarySearch(K key) {
int low = 0;
int high = size - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
K midKey = ((Entry) entries[mid]).getKey();
int cmp = midKey.compareTo(key);
if (cmp < 0) {
right = mid - 1;
low = mid + 1;
} else if (cmp > 0) {
left = mid + 1;
high = mid - 1;
} else {
return mid;
return mid; // key found
}
}
return -(left + 1);
return -(low + 1); // key not found.
}
/**
@ -334,14 +358,14 @@ class SmallSortedMap<K extends FieldSet.FieldDescriptorLite<K>, V> extends Abstr
* <p>{@inheritDoc}
*/
@Override
public Set<Map.Entry<K, V>> entrySet() {
public Set<Map.Entry<K, Object>> entrySet() {
ensureSortedAndDeduplicated();
if (lazyEntrySet == null) {
lazyEntrySet = new EntrySet();
}
return lazyEntrySet;
}
/**
* @throws UnsupportedOperationException if {@link #makeImmutable()} has has been called.
*/
@ -351,42 +375,16 @@ class SmallSortedMap<K extends FieldSet.FieldDescriptorLite<K>, V> extends Abstr
}
}
/**
* @return a {@link SortedMap} to which overflow entries mappings can be added or removed.
* @throws UnsupportedOperationException if {@link #makeImmutable()} has been called.
*/
private SortedMap<K, V> getOverflowEntriesMutable() {
checkMutable();
if (overflowEntries.isEmpty() && !(overflowEntries instanceof TreeMap)) {
overflowEntries = new TreeMap<K, V>();
}
return (SortedMap<K, V>) overflowEntries;
}
/**
* Lazily creates the entry array. Any code that adds to the array must first call this method.
*/
private void ensureEntryArrayMutable() {
checkMutable();
if (entries == null) {
entries = new Object[DEFAULT_FIELD_MAP_ARRAY_SIZE];
}
}
/**
* Entry implementation that implements Comparable in order to support binary search within the
* entry array. Also checks mutability in {@link #setValue()}.
*/
private class Entry implements Map.Entry<K, V>, Comparable<Entry> {
private class Entry implements Map.Entry<K, Object>, Comparable<Entry> {
private final K key;
private V value;
private Object value;
Entry(Map.Entry<K, V> copy) {
this(copy.getKey(), copy.getValue());
}
Entry(K key, V value) {
Entry(K key, Object value) {
this.key = key;
this.value = value;
}
@ -397,7 +395,7 @@ class SmallSortedMap<K extends FieldSet.FieldDescriptorLite<K>, V> extends Abstr
}
@Override
public V getValue() {
public Object getValue() {
return value;
}
@ -408,9 +406,9 @@ class SmallSortedMap<K extends FieldSet.FieldDescriptorLite<K>, V> extends Abstr
@Override
@CanIgnoreReturnValue
public V setValue(V newValue) {
public Object setValue(Object newValue) {
checkMutable();
final V oldValue = this.value;
final Object oldValue = this.value;
this.value = newValue;
return oldValue;
}
@ -447,10 +445,11 @@ class SmallSortedMap<K extends FieldSet.FieldDescriptorLite<K>, V> extends Abstr
}
/** Stateless view of the entries in the field map. */
private class EntrySet extends AbstractSet<Map.Entry<K, V>> {
private class EntrySet extends AbstractSet<Map.Entry<K, Object>> {
@Override
public Iterator<Map.Entry<K, V>> iterator() {
public Iterator<Map.Entry<K, Object>> iterator() {
ensureSortedAndDeduplicated();
return new EntryIterator();
}
@ -466,94 +465,36 @@ class SmallSortedMap<K extends FieldSet.FieldDescriptorLite<K>, V> extends Abstr
*/
@Override
public boolean contains(Object o) {
@SuppressWarnings("unchecked")
final Map.Entry<K, V> entry = (Map.Entry<K, V>) o;
final V existing = get(entry.getKey());
final V value = entry.getValue();
final Map.Entry<K, Object> entry = (Map.Entry<K, Object>) o;
final Object existing = get(entry.getKey());
final Object value = entry.getValue();
return existing == value || (existing != null && existing.equals(value));
}
@Override
@CanIgnoreReturnValue
public boolean add(Map.Entry<K, V> entry) {
if (!contains(entry)) {
put(entry.getKey(), entry.getValue());
return true;
}
return false;
}
/**
* Throws a {@link ClassCastException} if o is not of the expected type.
*
* <p>{@inheritDoc}
*/
@Override
@CanIgnoreReturnValue
public boolean remove(Object o) {
@SuppressWarnings("unchecked")
final Map.Entry<K, V> entry = (Map.Entry<K, V>) o;
if (contains(entry)) {
SmallSortedMap.this.remove(entry.getKey());
return true;
}
return false;
}
@Override
public void clear() {
SmallSortedMap.this.clear();
}
}
/**
* Iterator implementation that switches from the entry array to the overflow entries
* appropriately.
*/
private class EntryIterator implements Iterator<Map.Entry<K, V>> {
private class EntryIterator implements Iterator<Map.Entry<K, Object>> {
private int pos = -1;
private Iterator<Map.Entry<K, V>> lazyOverflowIterator;
@Override
public boolean hasNext() {
return (pos + 1) < entriesSize
|| (!overflowEntries.isEmpty() && getOverflowIterator().hasNext());
// Sorting is needed in case the map was mutated during iteration (e.g. by a put operation).
ensureSortedAndDeduplicated();
return (pos + 1) < size;
}
@Override
public Map.Entry<K, V> next() {
// Always increment pos so that we know whether the last returned value
// was from the array or from overflow.
if (++pos < entriesSize) {
@SuppressWarnings("unchecked")
Entry e = (Entry) entries[pos];
return e;
@SuppressWarnings("unchecked") // entries is known to contain Entry objects.
public Map.Entry<K, Object> next() {
ensureSortedAndDeduplicated();
if (!hasNext()) {
throw new NoSuchElementException();
}
return getOverflowIterator().next();
}
@Override
public void remove() {
checkMutable();
if (pos < entriesSize) {
removeArrayEntryAt(pos--);
} else {
getOverflowIterator().remove();
}
}
/**
* It is important to create the overflow iterator only after the array entries have been
* iterated over because the overflow entry set changes when the client calls remove() on the
* array entries, which invalidates any existing iterators.
*/
private Iterator<Map.Entry<K, V>> getOverflowIterator() {
if (lazyOverflowIterator == null) {
lazyOverflowIterator = overflowEntries.entrySet().iterator();
}
return lazyOverflowIterator;
return (Entry) entries[++pos];
}
}
@ -568,42 +509,29 @@ class SmallSortedMap<K extends FieldSet.FieldDescriptorLite<K>, V> extends Abstr
return super.equals(o);
}
SmallSortedMap<?, ?> other = (SmallSortedMap<?, ?>) o;
SmallSortedMap<?> other = (SmallSortedMap<?>) o;
final int size = size();
if (size != other.size()) {
return false;
}
// Best effort try to avoid allocating an entry set.
final int numArrayEntries = getNumArrayEntries();
if (numArrayEntries != other.getNumArrayEntries()) {
return entrySet().equals(other.entrySet());
}
for (int i = 0; i < numArrayEntries; i++) {
if (!getArrayEntryAt(i).equals(other.getArrayEntryAt(i))) {
final Object[] thisEntries = entries;
final Object[] otherEntries = other.entries;
for (int i = 0; i < size; i++) {
if (!thisEntries[i].equals(otherEntries[i])) {
return false;
}
}
if (numArrayEntries != size) {
return overflowEntries.equals(other.overflowEntries);
}
return true;
}
@Override
public int hashCode() {
int h = 0;
final int listSize = getNumArrayEntries();
final int listSize = size();
for (int i = 0; i < listSize; i++) {
h += entries[i].hashCode();
}
// Avoid the iterator allocation if possible.
if (getNumOverflowEntries() > 0) {
h += overflowEntries.hashCode();
}
return h;
}
}

View file

@ -10,7 +10,6 @@ package com.google.protobuf;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.protobuf.SmallSortedMap.DEFAULT_FIELD_MAP_ARRAY_SIZE;
import static java.lang.Math.min;
import java.util.AbstractMap;
import java.util.ArrayList;
@ -20,6 +19,8 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@ -105,14 +106,9 @@ public class SmallSortedMapTest {
runPutAndGetTest(DEFAULT_FIELD_MAP_ARRAY_SIZE);
}
@Test
public void testPutAndGetOverflowEntries() {
runPutAndGetTest(DEFAULT_FIELD_MAP_ARRAY_SIZE * 2);
}
private void runPutAndGetTest(int numElements) {
SmallSortedMap<TestFieldDescriptor, Integer> map1 = SmallSortedMap.newInstanceForTest();
SmallSortedMap<TestFieldDescriptor, Integer> map3 = SmallSortedMap.newInstanceForTest();
SmallSortedMap<TestFieldDescriptor> map1 = new SmallSortedMap<>();
SmallSortedMap<TestFieldDescriptor> map3 = new SmallSortedMap<>();
// Test with puts in ascending order.
for (int i = 0; i < numElements; i++) {
@ -123,14 +119,14 @@ public class SmallSortedMapTest {
assertThat(map3.put(new TestFieldDescriptor(i), i + 1)).isNull();
}
assertThat(map1.getNumArrayEntries()).isEqualTo(min(16, numElements));
assertThat(map3.getNumArrayEntries()).isEqualTo(min(16, numElements));
assertThat(map1.size()).isEqualTo(numElements);
assertThat(map3.size()).isEqualTo(numElements);
List<SmallSortedMap<TestFieldDescriptor, Integer>> allMaps = new ArrayList<>();
List<SmallSortedMap<TestFieldDescriptor>> allMaps = new ArrayList<>();
allMaps.add(map1);
allMaps.add(map3);
for (SmallSortedMap<TestFieldDescriptor, Integer> map : allMaps) {
for (SmallSortedMap<TestFieldDescriptor> map : allMaps) {
assertThat(map).hasSize(numElements);
for (int i = 0; i < numElements; i++) {
assertThat(map).containsEntry(new TestFieldDescriptor(i), Integer.valueOf(i + 1));
@ -142,105 +138,94 @@ public class SmallSortedMapTest {
@Test
public void testReplacingPut() {
SmallSortedMap<TestFieldDescriptor, Integer> map = SmallSortedMap.newInstanceForTest();
SmallSortedMap<TestFieldDescriptor> map = new SmallSortedMap<>();
for (int i = 0; i < DEFAULT_FIELD_MAP_ARRAY_SIZE * 2; i++) {
assertThat(map.put(new TestFieldDescriptor(i), i + 1)).isNull();
assertThat(map.remove(new TestFieldDescriptor(i + 1))).isNull();
}
for (int i = 0; i < DEFAULT_FIELD_MAP_ARRAY_SIZE * 2; i++) {
assertThat(map.put(new TestFieldDescriptor(i), i + 2)).isEqualTo(Integer.valueOf(i + 1));
assertThat(map.put(new TestFieldDescriptor(i), i + 2)).isNull();
}
}
@Test
public void testRemove() {
SmallSortedMap<TestFieldDescriptor, Integer> map = SmallSortedMap.newInstanceForTest();
SmallSortedMap<TestFieldDescriptor> map = new SmallSortedMap<>();
for (int i = 0; i < DEFAULT_FIELD_MAP_ARRAY_SIZE + 3; i++) {
assertThat(map.put(new TestFieldDescriptor(i), i + 1)).isNull();
assertThat(map.remove(new TestFieldDescriptor(i + 1))).isNull();
}
assertThat(map.getNumArrayEntries()).isEqualTo(16);
assertThat(map.getNumOverflowEntries()).isEqualTo(3);
assertThat(map).hasSize(19);
assertThat(map.keySet())
.isEqualTo(
makeSortedKeySet(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18));
assertThat(map.remove(new TestFieldDescriptor(1))).isEqualTo(Integer.valueOf(2));
assertThat(map.getNumArrayEntries()).isEqualTo(16);
assertThat(map.getNumOverflowEntries()).isEqualTo(2);
assertThat(map).hasSize(18);
assertThat(map.keySet())
.isEqualTo(makeSortedKeySet(0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18));
assertThat(map.remove(new TestFieldDescriptor(4))).isEqualTo(Integer.valueOf(5));
assertThat(map.getNumArrayEntries()).isEqualTo(16);
assertThat(map.getNumOverflowEntries()).isEqualTo(1);
assertThat(map).hasSize(17);
assertThat(map.keySet())
.isEqualTo(makeSortedKeySet(0, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18));
assertThat(map.remove(new TestFieldDescriptor(3))).isEqualTo(Integer.valueOf(4));
assertThat(map.getNumArrayEntries()).isEqualTo(16);
assertThat(map.getNumOverflowEntries()).isEqualTo(0);
assertThat(map).hasSize(16);
assertThat(map.keySet())
.isEqualTo(makeSortedKeySet(0, 2, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18));
assertThat(map.remove(new TestFieldDescriptor(3))).isNull();
assertThat(map.getNumArrayEntries()).isEqualTo(16);
assertThat(map.getNumOverflowEntries()).isEqualTo(0);
assertThat(map).hasSize(16);
assertThat(map.remove(new TestFieldDescriptor(0))).isEqualTo(Integer.valueOf(1));
assertThat(map.getNumArrayEntries()).isEqualTo(15);
assertThat(map.getNumOverflowEntries()).isEqualTo(0);
assertThat(map).hasSize(15);
}
@Test
public void testRemoveAtArrayEnd() {
SmallSortedMap<TestFieldDescriptor> map = new SmallSortedMap<>();
for (int i = 0; i < DEFAULT_FIELD_MAP_ARRAY_SIZE; i++) {
assertThat(map.put(new TestFieldDescriptor(i), i + 1)).isNull();
}
assertThat(map.remove(new TestFieldDescriptor(DEFAULT_FIELD_MAP_ARRAY_SIZE - 1)))
.isEqualTo(Integer.valueOf(DEFAULT_FIELD_MAP_ARRAY_SIZE));
assertThat(map).hasSize(DEFAULT_FIELD_MAP_ARRAY_SIZE - 1);
}
@Test
public void testClear() {
SmallSortedMap<TestFieldDescriptor, Integer> map = SmallSortedMap.newInstanceForTest();
SmallSortedMap<TestFieldDescriptor> map = new SmallSortedMap<>();
for (int i = 0; i < DEFAULT_FIELD_MAP_ARRAY_SIZE * 2; i++) {
assertThat(map.put(new TestFieldDescriptor(i), i + 1)).isNull();
}
map.clear();
assertThat(map.getNumArrayEntries()).isEqualTo(0);
assertThat(map.getNumOverflowEntries()).isEqualTo(0);
assertThat(map.size()).isEqualTo(0);
assertThat(map).isEmpty();
}
@Test
public void testGetArrayEntryAndOverflowEntries() {
SmallSortedMap<TestFieldDescriptor, Integer> map = SmallSortedMap.newInstanceForTest();
public void testGetArrayEntry() {
SmallSortedMap<TestFieldDescriptor> map = new SmallSortedMap<>();
for (int i = 0; i < DEFAULT_FIELD_MAP_ARRAY_SIZE * 2; i++) {
assertThat(map.put(new TestFieldDescriptor(i), i + 1)).isNull();
}
assertThat(map.getNumArrayEntries()).isEqualTo(DEFAULT_FIELD_MAP_ARRAY_SIZE);
for (int i = 0; i < map.getNumArrayEntries(); i++) {
Map.Entry<TestFieldDescriptor, Integer> entry = map.getArrayEntryAt(i);
assertThat(map.size()).isEqualTo(DEFAULT_FIELD_MAP_ARRAY_SIZE * 2);
for (int i = 0; i < map.size(); i++) {
Map.Entry<TestFieldDescriptor, Object> entry = map.getArrayEntryAt(i);
assertThat(entry.getKey()).isEqualTo(new TestFieldDescriptor(i));
assertThat(entry.getValue()).isEqualTo(Integer.valueOf(i + 1));
}
Iterator<Map.Entry<TestFieldDescriptor, Integer>> it = map.getOverflowEntries().iterator();
assertThat(map.getNumOverflowEntries()).isEqualTo(DEFAULT_FIELD_MAP_ARRAY_SIZE);
for (int i = DEFAULT_FIELD_MAP_ARRAY_SIZE; i < DEFAULT_FIELD_MAP_ARRAY_SIZE * 2; i++) {
assertThat(it.hasNext()).isTrue();
Map.Entry<TestFieldDescriptor, Integer> entry = it.next();
assertThat(entry.getKey()).isEqualTo(new TestFieldDescriptor(i));
assertThat(entry.getValue()).isEqualTo(Integer.valueOf(i + 1));
}
assertThat(it.hasNext()).isFalse();
}
@Test
public void testEntrySetContains() {
SmallSortedMap<TestFieldDescriptor, Integer> map = SmallSortedMap.newInstanceForTest();
SmallSortedMap<TestFieldDescriptor> map = new SmallSortedMap<>();
for (int i = 0; i < DEFAULT_FIELD_MAP_ARRAY_SIZE * 2; i++) {
assertThat(map.put(new TestFieldDescriptor(i), i + 1)).isNull();
}
Set<Map.Entry<TestFieldDescriptor, Integer>> entrySet = map.entrySet();
Set<Map.Entry<TestFieldDescriptor, Object>> entrySet = map.entrySet();
for (int i = 0; i < DEFAULT_FIELD_MAP_ARRAY_SIZE * 2; i++) {
assertThat(entrySet)
.contains(
@ -253,97 +238,44 @@ public class SmallSortedMapTest {
}
}
@Test
public void testEntrySetAdd() {
SmallSortedMap<TestFieldDescriptor, Integer> map = SmallSortedMap.newInstanceForTest();
Set<Map.Entry<TestFieldDescriptor, Integer>> entrySet = map.entrySet();
for (int i = 0; i < DEFAULT_FIELD_MAP_ARRAY_SIZE * 2; i++) {
Map.Entry<TestFieldDescriptor, Integer> entry =
new AbstractMap.SimpleEntry<>(new TestFieldDescriptor(i), i + 1);
assertThat(entrySet.add(entry)).isTrue();
assertThat(entrySet.add(entry)).isFalse();
}
for (int i = 0; i < DEFAULT_FIELD_MAP_ARRAY_SIZE * 2; i++) {
assertThat(map).containsEntry(new TestFieldDescriptor(i), Integer.valueOf(i + 1));
}
assertThat(map.getNumArrayEntries()).isEqualTo(16);
assertThat(map.getNumOverflowEntries()).isEqualTo(16);
assertThat(map).hasSize(32);
}
@Test
public void testEntrySetRemove() {
SmallSortedMap<TestFieldDescriptor, Integer> map = SmallSortedMap.newInstanceForTest();
Set<Map.Entry<TestFieldDescriptor, Integer>> entrySet = map.entrySet();
for (int i = 0; i < DEFAULT_FIELD_MAP_ARRAY_SIZE * 2; i++) {
assertThat(map.put(new TestFieldDescriptor(i), i + 1)).isNull();
}
for (int i = 0; i < DEFAULT_FIELD_MAP_ARRAY_SIZE * 2; i++) {
Map.Entry<TestFieldDescriptor, Integer> entry =
new AbstractMap.SimpleEntry<>(new TestFieldDescriptor(i), i + 1);
assertThat(entrySet.remove(entry)).isTrue();
assertThat(entrySet.remove(entry)).isFalse();
}
assertThat(map).isEmpty();
assertThat(map.getNumArrayEntries()).isEqualTo(0);
assertThat(map.getNumOverflowEntries()).isEqualTo(0);
assertThat(map).isEmpty();
}
@Test
public void testEntrySetClear() {
SmallSortedMap<TestFieldDescriptor, Integer> map = SmallSortedMap.newInstanceForTest();
SmallSortedMap<TestFieldDescriptor> map = new SmallSortedMap<>();
for (int i = 0; i < DEFAULT_FIELD_MAP_ARRAY_SIZE * 2; i++) {
assertThat(map.put(new TestFieldDescriptor(i), i + 1)).isNull();
}
map.clear();
assertThat(map).isEmpty();
assertThat(map.getNumArrayEntries()).isEqualTo(0);
assertThat(map.getNumOverflowEntries()).isEqualTo(0);
assertThat(map.size()).isEqualTo(0);
assertThat(map).isEmpty();
}
@Test
public void testEntrySetIteratorNext() {
SmallSortedMap<TestFieldDescriptor, Integer> map = SmallSortedMap.newInstanceForTest();
SmallSortedMap<TestFieldDescriptor> map = new SmallSortedMap<>();
for (int i = 0; i < DEFAULT_FIELD_MAP_ARRAY_SIZE * 2; i++) {
assertThat(map.put(new TestFieldDescriptor(i), i + 1)).isNull();
}
Iterator<Map.Entry<TestFieldDescriptor, Integer>> it = map.entrySet().iterator();
Iterator<Map.Entry<TestFieldDescriptor, Object>> it = map.entrySet().iterator();
for (int i = 0; i < DEFAULT_FIELD_MAP_ARRAY_SIZE * 2; i++) {
assertThat(it.hasNext()).isTrue();
Map.Entry<TestFieldDescriptor, Integer> entry = it.next();
Map.Entry<TestFieldDescriptor, Object> entry = it.next();
assertThat(entry.getKey()).isEqualTo(new TestFieldDescriptor(i));
assertThat(entry.getValue()).isEqualTo(Integer.valueOf(i + 1));
}
assertThat(it.hasNext()).isFalse();
}
@Test
public void testEntrySetIteratorRemove() {
SmallSortedMap<TestFieldDescriptor, Integer> map = SmallSortedMap.newInstanceForTest();
for (int i = 0; i < DEFAULT_FIELD_MAP_ARRAY_SIZE * 2; i++) {
assertThat(map.put(new TestFieldDescriptor(i), i + 1)).isNull();
}
Iterator<Map.Entry<TestFieldDescriptor, Integer>> it = map.entrySet().iterator();
for (int i = 0; i < DEFAULT_FIELD_MAP_ARRAY_SIZE * 2; i++) {
assertThat(map).containsKey(new TestFieldDescriptor(i));
it.next();
it.remove();
assertThat(map).doesNotContainKey(new TestFieldDescriptor(i));
assertThat(map).hasSize(32 - i - 1);
}
}
@Test
public void testMapEntryModification() {
SmallSortedMap<TestFieldDescriptor, Integer> map = SmallSortedMap.newInstanceForTest();
SmallSortedMap<TestFieldDescriptor> map = new SmallSortedMap<>();
for (int i = 0; i < DEFAULT_FIELD_MAP_ARRAY_SIZE * 2; i++) {
assertThat(map.put(new TestFieldDescriptor(i), i + 1)).isNull();
}
Iterator<Map.Entry<TestFieldDescriptor, Integer>> it = map.entrySet().iterator();
Iterator<Map.Entry<TestFieldDescriptor, Object>> it = map.entrySet().iterator();
for (int i = 0; i < DEFAULT_FIELD_MAP_ARRAY_SIZE * 2; i++) {
Map.Entry<TestFieldDescriptor, Integer> entry = it.next();
Map.Entry<TestFieldDescriptor, Object> entry = it.next();
entry.setValue(i + 23);
}
for (int i = 0; i < DEFAULT_FIELD_MAP_ARRAY_SIZE * 2; i++) {
@ -353,7 +285,7 @@ public class SmallSortedMapTest {
@Test
public void testMakeImmutable() {
SmallSortedMap<TestFieldDescriptor, Integer> map = SmallSortedMap.newInstanceForTest();
SmallSortedMap<TestFieldDescriptor> map = new SmallSortedMap<>();
for (int i = 0; i < DEFAULT_FIELD_MAP_ARRAY_SIZE * 2; i++) {
assertThat(map.put(new TestFieldDescriptor(i), i + 1)).isNull();
}
@ -387,16 +319,16 @@ public class SmallSortedMapTest {
} catch (UnsupportedOperationException expected) {
}
Set<Map.Entry<TestFieldDescriptor, Integer>> entrySet = map.entrySet();
Set<Map.Entry<TestFieldDescriptor, Object>> entrySet = map.entrySet();
try {
entrySet.clear();
assertWithMessage("Expected UnsupportedOperationException").fail();
} catch (UnsupportedOperationException expected) {
}
Iterator<Map.Entry<TestFieldDescriptor, Integer>> it = entrySet.iterator();
Iterator<Map.Entry<TestFieldDescriptor, Object>> it = entrySet.iterator();
while (it.hasNext()) {
Map.Entry<TestFieldDescriptor, Integer> entry = it.next();
Map.Entry<TestFieldDescriptor, Object> entry = it.next();
try {
entry.setValue(0);
assertWithMessage("Expected UnsupportedOperationException").fail();
@ -432,6 +364,146 @@ public class SmallSortedMapTest {
}
}
@Test
public void testConcurrentPutsEnsureSortedAndDeduplicated() throws Exception {
final SmallSortedMap<TestFieldDescriptor> map = new SmallSortedMap<>();
map.put(new TestFieldDescriptor(10), 100);
map.put(new TestFieldDescriptor(20), 200);
map.put(new TestFieldDescriptor(30), 300);
int numThreads = 10;
Thread[] threads = new Thread[numThreads];
final AtomicInteger errors = new AtomicInteger(0);
final CountDownLatch latch = new CountDownLatch(1);
for (int i = 0; i < numThreads; i++) {
final int index = i;
threads[i] =
new Thread(
new Runnable() {
@Override
public void run() {
try {
latch.await();
synchronized (map) {
map.put(new TestFieldDescriptor(index), index * 10);
}
} catch (Exception e) {
errors.incrementAndGet();
}
}
});
threads[i].start();
}
latch.countDown();
for (int i = 0; i < numThreads; i++) {
threads[i].join();
}
assertThat(errors.get()).isEqualTo(0);
assertThat(map.size()).isEqualTo(13);
int[] expectedKeys = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 30};
int i = 0;
for (Map.Entry<TestFieldDescriptor, Object> entry : map.entrySet()) {
assertThat(entry.getKey().getNumber()).isEqualTo(expectedKeys[i++]);
}
}
@Test
public void testConcurrentReadsWithUnsortedKeys() throws Exception {
final SmallSortedMap<TestFieldDescriptor> map = new SmallSortedMap<>();
map.put(new TestFieldDescriptor(50), 500);
map.put(new TestFieldDescriptor(10), 100);
map.put(new TestFieldDescriptor(30), 300);
map.put(new TestFieldDescriptor(20), 200);
map.put(new TestFieldDescriptor(40), 400);
int numThreads = 10;
Thread[] threads = new Thread[numThreads];
final AtomicInteger errors = new AtomicInteger(0);
final CountDownLatch latch = new CountDownLatch(1);
for (int i = 0; i < numThreads; i++) {
threads[i] =
new Thread(
new Runnable() {
@Override
public void run() {
try {
latch.await();
assertThat(map.get(new TestFieldDescriptor(30)))
.isEqualTo(Integer.valueOf(300));
assertThat(map.containsKey(new TestFieldDescriptor(20))).isTrue();
assertThat(map.size()).isEqualTo(5);
} catch (Exception e) {
errors.incrementAndGet();
}
}
});
threads[i].start();
}
latch.countDown();
for (int i = 0; i < numThreads; i++) {
threads[i].join();
}
assertThat(errors.get()).isEqualTo(0);
int[] expectedKeys = {10, 20, 30, 40, 50};
int index = 0;
for (Map.Entry<TestFieldDescriptor, Object> entry : map.entrySet()) {
assertThat(entry.getKey().getNumber()).isEqualTo(expectedKeys[index++]);
}
}
@Test
@SuppressWarnings("ModifyCollectionInEnhancedForLoop") // For testing
public void testReplacingPutOutOfOrderDuringIteration() {
SmallSortedMap<TestFieldDescriptor> map = new SmallSortedMap<>();
map.put(new TestFieldDescriptor(10), 100);
map.put(new TestFieldDescriptor(20), 200);
map.put(new TestFieldDescriptor(30), 300);
List<Object> values = new ArrayList<>();
int iterations = 0;
for (Map.Entry<TestFieldDescriptor, Object> entry : map.entrySet()) {
iterations++;
if (entry.getKey().getNumber() == 10) {
// Replaces preexisting entry 20, but is out of order relative to the last key (30).
map.put(new TestFieldDescriptor(20), 201);
}
values.add(entry.getValue());
}
assertThat(iterations).isEqualTo(3);
assertThat(values).containsExactly(100, 201, 300).inOrder();
}
@Test
@SuppressWarnings("ModifyCollectionInEnhancedForLoop") // For testing
public void testRemoveAndPutDuringIteration() {
SmallSortedMap<TestFieldDescriptor> map = new SmallSortedMap<>();
map.put(new TestFieldDescriptor(10), 100);
map.put(new TestFieldDescriptor(20), 200);
map.put(new TestFieldDescriptor(30), 300);
List<Integer> visitedKeys = new ArrayList<>();
for (Map.Entry<TestFieldDescriptor, Object> entry : map.entrySet()) {
int keyNumber = entry.getKey().getNumber();
visitedKeys.add(keyNumber);
if (keyNumber == 20) {
map.remove(new TestFieldDescriptor(20));
map.put(new TestFieldDescriptor(20), 201);
}
}
assertThat(visitedKeys).containsExactly(10, 20, 30).inOrder();
assertThat(map.get(new TestFieldDescriptor(20))).isEqualTo(Integer.valueOf(201));
}
private Set<TestFieldDescriptor> makeSortedKeySet(Integer... keys) {
Set<TestFieldDescriptor> set = new TreeSet<>();
for (Integer key : keys) {