This is an automated email from the ASF dual-hosted git repository. chaokunyang pushed a commit to branch releases-0.12 in repository https://gitbox.apache.org/repos/asf/fory.git
commit 9ab5c713a86dd5a3cd73370b41e80e7737bb5e6d Author: Shawn Yang <[email protected]> AuthorDate: Wed Sep 17 20:05:11 2025 +0800 feat(java): support concurent map serialization when being updated (#2617) <!-- **Thanks for contributing to Apache Fory™.** **If this is your first time opening a PR on fory, you can refer to [CONTRIBUTING.md](https://github.com/apache/fory/blob/main/CONTRIBUTING.md).** Contribution Checklist - The **Apache Fory™** community has requirements on the naming of pr titles. You can also find instructions in [CONTRIBUTING.md](https://github.com/apache/fory/blob/main/CONTRIBUTING.md). - Apache Fory™ has a strong focus on performance. If the PR you submit will have an impact on performance, please benchmark it first and provide the benchmark result here. --> ## Why? <!-- Describe the purpose of this PR. --> ## What does this PR do? <!-- Describe the details of this PR. --> ## Related issues Fixes #2618 ## Does this PR introduce any user-facing change? <!-- If any user-facing interface changes, please [open an issue](https://github.com/apache/fory/issues/new/choose) describing the need to do so and update the document if necessary. Delete section if not applicable. --> - [ ] Does this PR introduce any public API change? - [ ] Does this PR introduce any binary protocol compatibility change? ## Benchmark <!-- When the PR has an impact on performance (if you don't know whether the PR will have an impact on performance, you can submit the PR first, and if it will have impact on performance, the code reviewer will explain it), be sure to attach a benchmark data here. Delete section if not applicable. --> --- .../fory/builder/BaseObjectCodecBuilder.java | 29 ++-- .../java/org/apache/fory/codegen/Expression.java | 24 +++- .../org/apache/fory/codegen/ExpressionUtils.java | 22 ++- .../fory/collection/IterableOnceMapSnapshot.java | 152 ++++++++++++++++++++ .../collection/ConcurrentMapSerializer.java | 105 ++++++++++++++ .../serializer/collection/MapLikeSerializer.java | 3 + .../fory/serializer/collection/MapSerializers.java | 18 ++- .../fory-core/native-image.properties | 1 + .../org/apache/fory/codegen/ExpressionTest.java | 5 +- .../collection/IterableOnceMapSnapshotTest.java | 159 +++++++++++++++++++++ 10 files changed, 494 insertions(+), 24 deletions(-) diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java index 0b129581b..ebde19e67 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java @@ -97,6 +97,7 @@ import org.apache.fory.codegen.Expression.ListExpression; import org.apache.fory.codegen.Expression.Literal; import org.apache.fory.codegen.Expression.Reference; import org.apache.fory.codegen.Expression.Return; +import org.apache.fory.codegen.Expression.Variable; import org.apache.fory.codegen.Expression.While; import org.apache.fory.codegen.ExpressionUtils; import org.apache.fory.codegen.ExpressionVisitor.ExprHolder; @@ -1237,8 +1238,9 @@ public abstract class BaseObjectCodecBuilder extends CodecBuilder { new If( neqNull(entry), inline ? writeChunk : new Assign(entry, inline(writeChunk)))); }); - - return new If(not(inlineInvoke(map, "isEmpty", PRIMITIVE_BOOLEAN_TYPE)), whileAction); + return new ListExpression( + new If(not(inlineInvoke(map, "isEmpty", PRIMITIVE_BOOLEAN_TYPE)), whileAction), + new Invoke(serializer, "onMapWriteFinish", map)); } private Tuple2<Expression, Expression> getMapKVSerializer(Class<?> keyType, Class<?> valueType) { @@ -1268,12 +1270,16 @@ public abstract class BaseObjectCodecBuilder extends CodecBuilder { TypeRef<?> keyType, TypeRef<?> valueType) { ListExpression expressions = new ListExpression(); - Expression key = invoke(entry, "getKey", "key", keyType); - Expression value = invoke(entry, "getValue", "value", valueType); boolean keyMonomorphic = isMonomorphic(keyType); boolean valueMonomorphic = isMonomorphic(valueType); Class<?> keyTypeRawType = keyType.getRawType(); Class<?> valueTypeRawType = valueType.getRawType(); + Expression key = + keyMonomorphic ? new Variable("key", keyType) : invoke(entry, "getKey", "key", keyType); + Expression value = + valueMonomorphic + ? new Variable("value", valueType) + : invoke(entry, "getValue", "value", valueType); Expression keyTypeExpr = keyMonomorphic ? getClassExpr(keyTypeRawType) @@ -1395,6 +1401,9 @@ public abstract class BaseObjectCodecBuilder extends CodecBuilder { new While( Literal.ofBoolean(true), () -> { + Expression keyAssign = new Assign(key, invokeInline(entry, "getKey", keyType)); + Expression valueAssign = + new Assign(value, invokeInline(entry, "getValue", valueType)); Expression breakCondition; if (keyMonomorphic && valueMonomorphic) { breakCondition = or(eqNull(key), eqNull(value)); @@ -1450,20 +1459,16 @@ public abstract class BaseObjectCodecBuilder extends CodecBuilder { writeValue); } return new ListExpression( + keyAssign, + valueAssign, new If(breakCondition, new Break()), writeKey, writeValue, new Assign(chunkSize, add(chunkSize, ofInt(1))), new If( inlineInvoke(iterator, "hasNext", PRIMITIVE_BOOLEAN_TYPE), - new ListExpression( - new Assign( - entry, - cast(inlineInvoke(iterator, "next", OBJECT_TYPE), MAP_ENTRY_TYPE)), - new Assign( - key, - tryInlineCast(inlineInvoke(entry, "getKey", OBJECT_TYPE), keyType)), - new Assign(value, invokeInline(entry, "getValue", valueType))), + new Assign( + entry, cast(inlineInvoke(iterator, "next", OBJECT_TYPE), MAP_ENTRY_TYPE)), list(new Assign(entry, new Literal(null, MAP_ENTRY_TYPE)), new Break())), new If(eq(chunkSize, ofInt(MAX_CHUNK_SIZE)), new Break())); }); diff --git a/java/fory-core/src/main/java/org/apache/fory/codegen/Expression.java b/java/fory-core/src/main/java/org/apache/fory/codegen/Expression.java index a6a751e8c..ce6bc390a 100644 --- a/java/fory-core/src/main/java/org/apache/fory/codegen/Expression.java +++ b/java/fory-core/src/main/java/org/apache/fory/codegen/Expression.java @@ -290,18 +290,27 @@ public interface Expression { } class Variable extends AbstractExpression { + private static final Expression NULL_STUB = Literal.ofInt(-1); + private final String namePrefix; - private Expression from; + private final Expression from; + private TypeRef<?> type; + + public Variable(String namePrefix, TypeRef<?> type) { + this(namePrefix, NULL_STUB); + this.type = type; + } public Variable(String namePrefix, Expression from) { super(from); this.namePrefix = namePrefix; this.from = from; + this.type = from.type(); } @Override public TypeRef<?> type() { - return from.type(); + return type; } @Override @@ -311,12 +320,17 @@ public interface Expression { @Override public ExprCode doGenCode(CodegenContext ctx) { + String name = ctx.newName(namePrefix); + if (from == NULL_STUB) { + String decl = + StringUtils.format("${type} ${name};", "type", ctx.type(type()), "name", name); + return new ExprCode(decl, FalseLiteral, Code.variable(type().getRawType(), name)); + } StringBuilder codeBuilder = new StringBuilder(); ExprCode targetExprCode = from.genCode(ctx); if (StringUtils.isNotBlank(targetExprCode.code())) { codeBuilder.append(targetExprCode.code()).append('\n'); } - String name = ctx.newName(namePrefix); String decl = StringUtils.format( "${type} ${name} = ${from};", @@ -458,6 +472,10 @@ public interface Expression { } } + public Object getValue() { + return value; + } + @Override public String toString() { if (value == null) { diff --git a/java/fory-core/src/main/java/org/apache/fory/codegen/ExpressionUtils.java b/java/fory-core/src/main/java/org/apache/fory/codegen/ExpressionUtils.java index 87389c70c..1207c3a7c 100644 --- a/java/fory-core/src/main/java/org/apache/fory/codegen/ExpressionUtils.java +++ b/java/fory-core/src/main/java/org/apache/fory/codegen/ExpressionUtils.java @@ -103,7 +103,20 @@ public class ExpressionUtils { return new LogicalAnd(true, left, right); } - public static LogicalOr or(Expression left, Expression right, Expression... expressions) { + public static Expression or(Expression left, Expression right, Expression... expressions) { + if (left instanceof Literal) { + Literal l = (Literal) left; + if (!(Boolean) (l.getValue())) { + if (expressions.length == 0) { + return right; + } + LogicalOr logicalOr = new LogicalOr(right, expressions[0]); + for (int i = 1; i < expressions.length; i++) { + logicalOr = new LogicalOr(logicalOr, expressions[i]); + } + return logicalOr; + } + } LogicalOr logicalOr = new LogicalOr(left, right); for (Expression expression : expressions) { logicalOr = new LogicalOr(logicalOr, expression); @@ -125,7 +138,12 @@ public class ExpressionUtils { return new BitAnd(left, right); } - public static Not not(Expression target) { + public static Expression not(Expression target) { + if (target instanceof Literal) { + Literal literal = (Literal) target; + Boolean b = (Boolean) literal.getValue(); + return Literal.ofBoolean(!b); + } return new Not(target); } diff --git a/java/fory-core/src/main/java/org/apache/fory/collection/IterableOnceMapSnapshot.java b/java/fory-core/src/main/java/org/apache/fory/collection/IterableOnceMapSnapshot.java new file mode 100644 index 000000000..b903f200d --- /dev/null +++ b/java/fory-core/src/main/java/org/apache/fory/collection/IterableOnceMapSnapshot.java @@ -0,0 +1,152 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fory.collection; + +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import org.apache.fory.annotation.Internal; + +/** + * A specialized map implementation that creates a snapshot of entries for single iteration. This + * class is designed to efficiently handle concurrent map serialization by creating a lightweight + * snapshot that can be iterated once without holding references to the original map entries. + * + * <p>The implementation uses an array-based storage for entries and provides optimized iteration + * through a custom iterator. It includes memory management features such as automatic array + * reallocation when clearing large collections to prevent memory leaks. + * + * <p>This class is marked as {@code @Internal} and should not be used directly by application code. + * It's specifically designed for internal serialization purposes. + * + * @param <K> the type of keys maintained by this map + * @param <V> the type of mapped values + * @since 1.0 + */ +@Internal +public class IterableOnceMapSnapshot<K, V> extends AbstractMap<K, V> { + /** Threshold for array reallocation during clear operation to prevent memory leaks. */ + private static final int CLEAR_ARRAY_SIZE_THRESHOLD = 2048; + + /** Array storing the map entries for iteration. */ + ObjectArray<Entry<K, V>> array; + + /** Reference to the original map (used for snapshot creation). */ + Map<K, V> map; + + /** Current number of entries in the snapshot. */ + int size; + + /** Current iteration index for the iterator. */ + int iterIndex; + + /** Cached entry set for this map. */ + private final EntrySet entrySet; + + /** Cached iterator for this map. */ + private final EntryIterator iterator; + + /** + * Constructs a new empty IterableOnceMapSnapshot. Initializes the internal array with a default + * capacity of 16 entries and creates the entry set and iterator instances. + */ + public IterableOnceMapSnapshot() { + array = new ObjectArray<>(16); + entrySet = new EntrySet(); + iterator = new EntryIterator(); + } + + @Override + public int size() { + return size; + } + + @Override + public Set<Entry<K, V>> entrySet() { + return entrySet; + } + + /** + * Entry set implementation for the IterableOnceMapSnapshot. Provides a view of the map entries + * and supports iteration through the custom EntryIterator. + */ + class EntrySet extends AbstractSet<Entry<K, V>> { + + @Override + public Iterator<Entry<K, V>> iterator() { + return iterator; + } + + @Override + public int size() { + return size; + } + } + + /** + * Iterator implementation for the IterableOnceMapSnapshot. This iterator is designed for + * single-pass iteration and maintains its position through the iterIndex field. It provides + * efficient access to map entries stored in the underlying array. + */ + class EntryIterator implements Iterator<Entry<K, V>> { + + @Override + public boolean hasNext() { + return iterIndex < size; + } + + @Override + public Entry<K, V> next() { + return array.get(iterIndex++); + } + } + + /** + * Creates a snapshot of the specified map by copying all its entries into the internal array. + * This method is used to create a stable view of a concurrent map for serialization purposes. + * + * <p>The method iterates through all entries in the provided map and adds them to the internal + * array. The iteration index is reset to 0 to prepare for subsequent iteration. + * + * @param map the map to create a snapshot of + */ + public void setMap(Map<K, V> map) { + ObjectArray<Entry<K, V>> array = this.array; + int size = 0; + for (Entry<K, V> kvEntry : map.entrySet()) { + array.add(kvEntry); + size++; + } + this.size = size; + } + + @Override + public void clear() { + if (size > 2048) { + array = new ObjectArray<>(16); + } else { + array.clear(); + } + iterIndex = 0; + size = 0; + } +} diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ConcurrentMapSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ConcurrentMapSerializer.java new file mode 100644 index 000000000..751b77ab2 --- /dev/null +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ConcurrentMapSerializer.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fory.serializer.collection; + +import java.util.Map; +import java.util.concurrent.ConcurrentMap; +import org.apache.fory.Fory; +import org.apache.fory.collection.IterableOnceMapSnapshot; +import org.apache.fory.collection.ObjectArray; +import org.apache.fory.memory.MemoryBuffer; + +/** + * Serializer for concurrent map implementations that require thread-safe serialization. + * + * <p>This serializer extends {@link MapSerializer} to provide specialized handling for concurrent + * maps such as {@link java.util.concurrent.ConcurrentHashMap}. The key feature is the use of {@link + * IterableOnceMapSnapshot} to create stable snapshots of concurrent maps during serialization, + * avoiding potential {@link java.util.ConcurrentModificationException} and ensuring thread safety. + * + * <p>The serializer maintains a pool of reusable {@link IterableOnceMapSnapshot} instances to + * minimize object allocation overhead during serialization. + * + * <p>This implementation is particularly important for concurrent maps because: + * + * <ul> + * <li>Concurrent maps can be modified during iteration, causing exceptions + * <li>Creating snapshots ensures consistent serialization state + * <li>Object pooling reduces garbage collection pressure + * </ul> + * + * @param <T> the type of concurrent map being serialized + * @since 1.0 + */ +@SuppressWarnings({"rawtypes", "unchecked"}) +public class ConcurrentMapSerializer<T extends ConcurrentMap> extends MapSerializer<T> { + /** Pool of reusable IterableOnceMapSnapshot instances for efficient serialization. */ + protected final ObjectArray<IterableOnceMapSnapshot> snapshots = new ObjectArray<>(1); + + /** + * Constructs a new ConcurrentMapSerializer for the specified concurrent map type. + * + * @param fory the Fory instance for serialization context + * @param type the class type of the concurrent map to serialize + * @param supportCodegen whether code generation is supported for this serializer + */ + public ConcurrentMapSerializer(Fory fory, Class<T> type, boolean supportCodegen) { + super(fory, type, supportCodegen); + } + + /** + * Creates a snapshot of the concurrent map for safe serialization. + * + * <p>This method retrieves a reusable {@link IterableOnceMapSnapshot} from the pool, or creates a + * new one if none are available. It then creates a snapshot of the concurrent map to avoid + * concurrent modification issues during serialization. The map size is written to the buffer + * before returning the snapshot. + * + * @param buffer the memory buffer to write serialization data to + * @param value the concurrent map to serialize + * @return a snapshot of the map for safe iteration during serialization + */ + @Override + public IterableOnceMapSnapshot onMapWrite(MemoryBuffer buffer, T value) { + IterableOnceMapSnapshot snapshot = snapshots.popOrNull(); + if (snapshot == null) { + snapshot = new IterableOnceMapSnapshot(); + } + snapshot.setMap(value); + buffer.writeVarUint32Small7(snapshot.size()); + return snapshot; + } + + /** + * Cleans up the snapshot after serialization and returns it to the pool for reuse. + * + * <p>This method is called after the map serialization is complete. It clears the snapshot to + * remove all references to the serialized data and returns the snapshot instance to the pool for + * future reuse, improving memory efficiency. + * + * @param map the snapshot that was used for serialization + */ + @Override + public void onMapWriteFinish(Map map) { + IterableOnceMapSnapshot snapshot = (IterableOnceMapSnapshot) map; + snapshot.clear(); + snapshots.add(snapshot); + } +} diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapLikeSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapLikeSerializer.java index 3dc89a074..c46e1d7fb 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapLikeSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapLikeSerializer.java @@ -168,6 +168,7 @@ public abstract class MapLikeSerializer<T> extends Serializer<T> { } } } + onMapWriteFinish(map); } @Override @@ -936,6 +937,8 @@ public abstract class MapLikeSerializer<T> extends Serializer<T> { */ public abstract Map onMapWrite(MemoryBuffer buffer, T value); + public void onMapWriteFinish(Map map) {} + /** * Read data except size and elements, return empty map to be filled. * diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapSerializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapSerializers.java index 1b7112813..ce243123f 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapSerializers.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapSerializers.java @@ -32,6 +32,7 @@ import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentSkipListMap; import org.apache.fory.Fory; +import org.apache.fory.collection.IterableOnceMapSnapshot; import org.apache.fory.collection.LazyMap; import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.memory.Platform; @@ -243,8 +244,8 @@ public class MapSerializers { } } - public static final class ConcurrentHashMapSerializer extends MapSerializer<ConcurrentHashMap> { - + public static final class ConcurrentHashMapSerializer + extends ConcurrentMapSerializer<ConcurrentHashMap> { public ConcurrentHashMapSerializer(Fory fory, Class<ConcurrentHashMap> type) { super(fory, type, true); } @@ -265,10 +266,19 @@ public class MapSerializers { } public static final class ConcurrentSkipListMapSerializer - extends SortedMapSerializer<ConcurrentSkipListMap> { + extends ConcurrentMapSerializer<ConcurrentSkipListMap> { public ConcurrentSkipListMapSerializer(Fory fory, Class<ConcurrentSkipListMap> cls) { - super(fory, cls); + super(fory, cls, true); + } + + @Override + public IterableOnceMapSnapshot onMapWrite(MemoryBuffer buffer, ConcurrentSkipListMap value) { + IterableOnceMapSnapshot snapshot = super.onMapWrite(buffer, value); + if (!fory.isCrossLanguage()) { + fory.writeRef(buffer, value.comparator()); + } + return snapshot; } @Override diff --git a/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties b/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties index c9dc1b45d..9be63527c 100644 --- a/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties +++ b/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties @@ -201,6 +201,7 @@ Args=--initialize-at-build-time=org.apache.fory.memory.MemoryBuffer,\ org.apache.fory.codegen.CompileUnit,\ org.apache.fory.codegen.Expression$Literal,\ org.apache.fory.codegen.Expression$Reference,\ + org.apache.fory.codegen.Expression$Variable,\ org.apache.fory.codegen.JaninoUtils,\ org.apache.fory.collection.ClassValueCache,\ org.apache.fory.collection.ForyObjectMap,\ diff --git a/java/fory-core/src/test/java/org/apache/fory/codegen/ExpressionTest.java b/java/fory-core/src/test/java/org/apache/fory/codegen/ExpressionTest.java index a541443c7..5146ebd2d 100644 --- a/java/fory-core/src/test/java/org/apache/fory/codegen/ExpressionTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/codegen/ExpressionTest.java @@ -27,7 +27,6 @@ import static org.testng.Assert.assertNull; import org.apache.fory.codegen.Code.ExprCode; import org.apache.fory.codegen.Expression.ListExpression; import org.apache.fory.codegen.Expression.Literal; -import org.apache.fory.codegen.Expression.LogicalOr; import org.apache.fory.codegen.Expression.Reference; import org.apache.fory.codegen.Expression.Return; import org.testng.Assert; @@ -86,12 +85,12 @@ public class ExpressionTest { @Test public void testMultipleOr() { CodegenContext ctx = new CodegenContext(); - LogicalOr or = + Expression or = or( Literal.ofBoolean(false), neq(Literal.ofInt(3), Literal.ofInt(4)), neq(Literal.ofInt(5), Literal.ofInt(6))); ExprCode exprCode = or.genCode(ctx); - Assert.assertEquals(exprCode.value().code(), "((false || (3 != 4)) || (5 != 6))"); + Assert.assertEquals(exprCode.value().code(), "((3 != 4) || (5 != 6))"); } } diff --git a/java/fory-core/src/test/java/org/apache/fory/collection/IterableOnceMapSnapshotTest.java b/java/fory-core/src/test/java/org/apache/fory/collection/IterableOnceMapSnapshotTest.java new file mode 100644 index 000000000..4ccff48b5 --- /dev/null +++ b/java/fory-core/src/test/java/org/apache/fory/collection/IterableOnceMapSnapshotTest.java @@ -0,0 +1,159 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fory.collection; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import org.testng.annotations.Test; + +public class IterableOnceMapSnapshotTest { + + @Test + public void testSetMap() { + IterableOnceMapSnapshot<String, Integer> snapshot = new IterableOnceMapSnapshot<>(); + Map<String, Integer> originalMap = new HashMap<>(); + originalMap.put("key1", 1); + originalMap.put("key2", 2); + originalMap.put("key3", 3); + + snapshot.setMap(originalMap); + + assertEquals(snapshot.size(), 3); + assertFalse(snapshot.isEmpty()); + + // Verify all entries are present + Set<Map.Entry<String, Integer>> entries = snapshot.entrySet(); + assertEquals(entries.size(), 3); + + Map<String, Integer> resultMap = new HashMap<>(); + for (Map.Entry<String, Integer> entry : entries) { + resultMap.put(entry.getKey(), entry.getValue()); + } + + assertEquals(resultMap, originalMap); + } + + @Test + public void testIterator() { + IterableOnceMapSnapshot<String, Integer> snapshot = new IterableOnceMapSnapshot<>(); + Map<String, Integer> originalMap = new HashMap<>(); + originalMap.put("a", 1); + originalMap.put("b", 2); + originalMap.put("c", 3); + + snapshot.setMap(originalMap); + + Iterator<Map.Entry<String, Integer>> iterator = snapshot.entrySet().iterator(); + assertTrue(iterator.hasNext()); + + int count = 0; + while (iterator.hasNext()) { + Map.Entry<String, Integer> entry = iterator.next(); + assertNotNull(entry); + assertTrue(originalMap.containsKey(entry.getKey())); + assertEquals(originalMap.get(entry.getKey()), entry.getValue()); + count++; + } + + assertEquals(count, 3); + assertFalse(iterator.hasNext()); + } + + @Test + public void testClear() { + IterableOnceMapSnapshot<String, Integer> snapshot = new IterableOnceMapSnapshot<>(); + Map<String, Integer> originalMap = new HashMap<>(); + originalMap.put("key1", 1); + originalMap.put("key2", 2); + + snapshot.setMap(originalMap); + assertEquals(snapshot.size(), 2); + + snapshot.clear(); + + assertEquals(snapshot.size(), 0); + assertTrue(snapshot.isEmpty()); + assertTrue(snapshot.entrySet().isEmpty()); + } + + @Test + public void testReuseAfterClear() { + IterableOnceMapSnapshot<String, Integer> snapshot = new IterableOnceMapSnapshot<>(); + + // First use + Map<String, Integer> map1 = new HashMap<>(); + map1.put("a", 1); + map1.put("b", 2); + snapshot.setMap(map1); + assertEquals(snapshot.size(), 2); + + snapshot.clear(); + assertEquals(snapshot.size(), 0); + + // Second use + Map<String, Integer> map2 = new HashMap<>(); + map2.put("x", 10); + map2.put("y", 20); + map2.put("z", 30); + snapshot.setMap(map2); + assertEquals(snapshot.size(), 3); + + // Verify content + Map<String, Integer> resultMap = new HashMap<>(); + for (Map.Entry<String, Integer> entry : snapshot.entrySet()) { + resultMap.put(entry.getKey(), entry.getValue()); + } + assertEquals(resultMap, map2); + } + + @Test + public void testEmptyMapSet() { + IterableOnceMapSnapshot<String, Integer> snapshot = new IterableOnceMapSnapshot<>(); + Map<String, Integer> emptyMap = new HashMap<>(); + + snapshot.setMap(emptyMap); + + assertEquals(snapshot.size(), 0); + assertTrue(snapshot.isEmpty()); + assertFalse(snapshot.entrySet().iterator().hasNext()); + } + + @Test + public void testEntrySetSize() { + IterableOnceMapSnapshot<String, Integer> snapshot = new IterableOnceMapSnapshot<>(); + Map<String, Integer> originalMap = new HashMap<>(); + originalMap.put("key1", 1); + originalMap.put("key2", 2); + originalMap.put("key3", 3); + originalMap.put("key4", 4); + + snapshot.setMap(originalMap); + + assertEquals(snapshot.entrySet().size(), 4); + assertEquals(snapshot.entrySet().size(), snapshot.size()); + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
