mirror of
https://github.com/protocolbuffers/protobuf
synced 2026-08-26 02:23:14 -04:00
Optimize SmallSortedMap to improve getAllFields() efficiency.
PiperOrigin-RevId: 944593820
This commit is contained in:
parent
ba087538d2
commit
706fc1e3cb
4 changed files with 470 additions and 503 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue