diff --git a/scala/fory-scala/src/main/scala/org/apache/fory/serializer/scala/CollectionSerializer.scala b/scala/fory-scala/src/main/scala/org/apache/fory/serializer/scala/CollectionSerializer.scala
index 3b18d757ca..65d49f019c 100644
--- a/scala/fory-scala/src/main/scala/org/apache/fory/serializer/scala/CollectionSerializer.scala
+++ b/scala/fory-scala/src/main/scala/org/apache/fory/serializer/scala/CollectionSerializer.scala
@@ -19,6 +19,7 @@
package org.apache.fory.serializer.scala
+import org.apache.fory.context.CopyContext
import org.apache.fory.context.ReadContext
import org.apache.fory.context.WriteContext
import org.apache.fory.resolver.TypeResolver
@@ -82,6 +83,91 @@ abstract class AbstractScalaCollectionSerializer[A, T <: Iterable[A]](
override def onCollectionRead(collection: util.Collection[_]): T = {
collection.asInstanceOf[JavaCollectionBuilder[A, T]].builder.result()
}
+
+ override def copy(copyContext: CopyContext, value: T): T = {
+ if (isImmutable) {
+ value
+ } else if (
+ value.isInstanceOf[mutable.IndexedSeq[_]] &&
+ !value.isInstanceOf[mutable.Growable[_]]) {
+ copyIndexedSeq(copyContext, value, value.asInstanceOf[mutable.IndexedSeq[A]])
+ } else if (value.isInstanceOf[mutable.Iterable[_]]) {
+ newMutableCopy(value, value.size) match {
+ case result: mutable.Iterable[_] with mutable.Growable[_] =>
+ val growable = result.asInstanceOf[mutable.Iterable[A] with mutable.Growable[A]]
+ copyContext.reference(value, growable.asInstanceOf[T])
+ copyElements(copyContext, value, growable)
+ growable.asInstanceOf[T]
+ case _ =>
+ copyWithBuilder(copyContext, value, newCopyBuilder(value))
+ }
+ } else {
+ copyWithBuilder(copyContext, value, newCopyBuilder(value))
+ }
+ }
+
+ protected def newCopyBuilder(value: T): mutable.Builder[A, T] =
+ value.iterableFactory.newBuilder[A].asInstanceOf[mutable.Builder[A, T]]
+
+ private def newMutableCopy(value: T, numElements: Int): scala.collection.Iterable[A] = {
+ val builder = newCopyBuilder(value)
+ builder.sizeHint(numElements)
+ builder.result()
+ }
+
+ private def copyElements(
+ copyContext: CopyContext,
+ value: T,
+ result: mutable.Growable[A]): Unit = {
+ val iterator = value.iterator
+ while (iterator.hasNext) {
+ result.addOne(copyContext.copyObject(iterator.next()).asInstanceOf[A])
+ }
+ }
+
+ private def copyWithBuilder(
+ copyContext: CopyContext,
+ value: T,
+ builder: mutable.Builder[A, T]): T = {
+ val iterator = value.iterator
+ while (iterator.hasNext) {
+ builder.addOne(copyContext.copyObject(iterator.next()).asInstanceOf[A])
+ }
+ // A recursive copy through a mutable child may already have constructed and registered the
+ // canonical immutable copy of this collection. Reuse it instead of returning a second builder
+ // result that would split the cycle's identity.
+ val recursiveCopy = copyContext.getCopyObject(value)
+ if (recursiveCopy != null) {
+ recursiveCopy
+ } else {
+ val result = builder.result()
+ copyContext.reference(value, result)
+ result
+ }
+ }
+
+ private def copyIndexedSeq(
+ copyContext: CopyContext,
+ value: T,
+ indexed: mutable.IndexedSeq[A]): T = {
+ val result = indexed match {
+ case arraySeq: mutable.ArraySeq[_] =>
+ val sourceArray = arraySeq.array.asInstanceOf[AnyRef]
+ val array =
+ java.lang.reflect.Array.newInstance(sourceArray.getClass.getComponentType, indexed.size)
+ mutable.ArraySeq.make(array.asInstanceOf[Array[_]]).asInstanceOf[mutable.IndexedSeq[A]]
+ case _ =>
+ mutable.ArraySeq.make(new Array[Any](indexed.size)).asInstanceOf[mutable.IndexedSeq[A]]
+ }
+ val copied = result.asInstanceOf[T]
+ copyContext.reference(value, copied)
+ var i = 0
+ while (i < indexed.size) {
+ result.update(i, copyContext.copyObject(indexed(i)).asInstanceOf[A])
+ i += 1
+ }
+ copied
+ }
}
/**
@@ -171,6 +257,12 @@ class ScalaSortedSetSerializer[A, T <: scala.collection.SortedSet[A]](
typeResolver: TypeResolver,
cls: Class[T])
extends AbstractScalaCollectionSerializer[A, T](typeResolver, cls) {
+ override protected def newCopyBuilder(value: T): mutable.Builder[A, T] = {
+ val factory = value.sortedIterableFactory.evidenceIterableFactory[Any](
+ value.ordering.asInstanceOf[Ordering[Any]])
+ factory.newBuilder.asInstanceOf[mutable.Builder[A, T]]
+ }
+
override def onCollectionWrite(
writeContext: WriteContext,
value: T): util.Collection[_] = {
diff --git a/scala/fory-scala/src/main/scala/org/apache/fory/serializer/scala/MapSerializer.scala b/scala/fory-scala/src/main/scala/org/apache/fory/serializer/scala/MapSerializer.scala
index 7b95bde2e5..0f6a2d040c 100644
--- a/scala/fory-scala/src/main/scala/org/apache/fory/serializer/scala/MapSerializer.scala
+++ b/scala/fory-scala/src/main/scala/org/apache/fory/serializer/scala/MapSerializer.scala
@@ -20,6 +20,7 @@
package org.apache.fory.serializer.scala
import org.apache.fory.collection.MapEntry
+import org.apache.fory.context.CopyContext
import org.apache.fory.context.ReadContext
import org.apache.fory.context.WriteContext
import org.apache.fory.resolver.TypeResolver
@@ -45,7 +46,9 @@ import scala.collection.{Factory, mutable}
* invoke [[java.util.Map#put]] to add it into builder.
*
`onMapRead`: create scala map from builder.
*/
-abstract class AbstractScalaMapSerializer[K, V, T](typeResolver: TypeResolver, cls: Class[T])
+abstract class AbstractScalaMapSerializer[K, V, T <: scala.collection.Map[K, V]](
+ typeResolver: TypeResolver,
+ cls: Class[T])
extends MapLikeSerializer[T](typeResolver, cls) {
private val ReferenceBytes = 4L
// Lower-bound shallow owner cost for the retained Scala map wrapper itself. Key/value slots are
@@ -84,6 +87,72 @@ abstract class AbstractScalaMapSerializer[K, V, T](typeResolver: TypeResolver, c
override def onMapRead(map: util.Map[_, _]): T = {
map.asInstanceOf[MapBuilder[K, V, T]].builder.result()
}
+
+ override def copy(copyContext: CopyContext, value: T): T = {
+ if (isImmutable) {
+ value
+ } else if (value.isInstanceOf[mutable.Map[_, _]]) {
+ newMutableMapCopy(value, value.size) match {
+ case result: mutable.Map[_, _] =>
+ val mutableResult = result.asInstanceOf[mutable.Map[K, V]]
+ copyContext.reference(value, mutableResult.asInstanceOf[T])
+ copyEntries(copyContext, value, mutableResult)
+ mutableResult.asInstanceOf[T]
+ case _ =>
+ copyWithBuilder(copyContext, value, newCopyBuilder(value))
+ }
+ } else {
+ copyWithBuilder(copyContext, value, newCopyBuilder(value))
+ }
+ }
+
+ protected def newCopyBuilder(value: T): mutable.Builder[(K, V), T] =
+ value.mapFactory.newBuilder[K, V].asInstanceOf[mutable.Builder[(K, V), T]]
+
+ private def newMutableMapCopy(value: T, numElements: Int): scala.collection.Map[K, V] = {
+ val builder = newCopyBuilder(value)
+ builder.sizeHint(numElements)
+ builder.result()
+ }
+
+ private def copyEntries(
+ copyContext: CopyContext,
+ value: T,
+ result: mutable.Map[K, V]): Unit = {
+ val iterator = value.iterator
+ while (iterator.hasNext) {
+ val entry = iterator.next()
+ result.addOne(
+ (
+ copyContext.copyObject(entry._1).asInstanceOf[K],
+ copyContext.copyObject(entry._2).asInstanceOf[V]))
+ }
+ }
+
+ private def copyWithBuilder(
+ copyContext: CopyContext,
+ value: T,
+ builder: mutable.Builder[(K, V), T]): T = {
+ val iterator = value.iterator
+ while (iterator.hasNext) {
+ val entry = iterator.next()
+ builder.addOne(
+ (
+ copyContext.copyObject(entry._1).asInstanceOf[K],
+ copyContext.copyObject(entry._2).asInstanceOf[V]))
+ }
+ // A recursive copy through a mutable child may already have constructed and registered the
+ // canonical immutable copy of this map. Reuse it instead of returning a second builder result
+ // that would split the cycle's identity.
+ val recursiveCopy = copyContext.getCopyObject(value)
+ if (recursiveCopy != null) {
+ recursiveCopy
+ } else {
+ val result = builder.result()
+ copyContext.reference(value, result)
+ result
+ }
+ }
}
/**
@@ -154,6 +223,12 @@ class ScalaSortedMapSerializer[K, V, T <: scala.collection.SortedMap[K, V]](
typeResolver: TypeResolver,
cls: Class[T])
extends AbstractScalaMapSerializer[K, V, T](typeResolver, cls) {
+ override protected def newCopyBuilder(value: T): mutable.Builder[(K, V), T] = {
+ val factory = value.sortedMapFactory.sortedMapFactory[Any, Any](
+ value.ordering.asInstanceOf[Ordering[Any]])
+ factory.newBuilder.asInstanceOf[mutable.Builder[(K, V), T]]
+ }
+
override def onMapWrite(
writeContext: WriteContext,
value: T): util.Map[_, _] = {
diff --git a/scala/fory-scala/src/main/scala/org/apache/fory/serializer/scala/RangeSerializer.scala b/scala/fory-scala/src/main/scala/org/apache/fory/serializer/scala/RangeSerializer.scala
index 9c163236b5..1937162ca0 100644
--- a/scala/fory-scala/src/main/scala/org/apache/fory/serializer/scala/RangeSerializer.scala
+++ b/scala/fory-scala/src/main/scala/org/apache/fory/serializer/scala/RangeSerializer.scala
@@ -19,6 +19,7 @@
package org.apache.fory.serializer.scala
+import org.apache.fory.context.CopyContext
import org.apache.fory.context.ReadContext
import org.apache.fory.context.WriteContext
import org.apache.fory.reflect.FieldAccessor
@@ -38,6 +39,8 @@ class RangeSerializer[T <: Range](typeResolver: TypeResolver, cls: Class[T])
private val rangeClass = cls
private val graphMemoryBytes = GraphMemoryEstimates.shallowObjectBytes(cls)
+ override def copy(copyContext: CopyContext, value: T): T = value
+
override def write(writeContext: WriteContext, value: T): Unit = {
val buffer = writeContext.getBuffer
buffer.writeVarInt32(value.start)
@@ -79,6 +82,9 @@ class NumericRangeSerializer[A, T <: NumericRange[A]](typeResolver: TypeResolver
with Shareable {
private val ctr = RangeUtils.lookupCache.get(cls)
private val graphMemoryBytes = GraphMemoryEstimates.shallowObjectBytes(cls)
+
+ override def copy(copyContext: CopyContext, value: T): T = value
+
private val getter =
FieldAccessor.createAccessor(
cls.getDeclaredFields.find(f => f.getType == classOf[Integral[?]]).get)
diff --git a/scala/fory-scala/src/test/scala/org/apache/fory/serializer/scala/NativeCopyTest.scala b/scala/fory-scala/src/test/scala/org/apache/fory/serializer/scala/NativeCopyTest.scala
new file mode 100644
index 0000000000..e18bf27cc6
--- /dev/null
+++ b/scala/fory-scala/src/test/scala/org/apache/fory/serializer/scala/NativeCopyTest.scala
@@ -0,0 +1,139 @@
+/*
+ * 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.scala
+
+import org.apache.fory.Fory
+import org.apache.fory.scala.ForyScala
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+
+import scala.collection.mutable
+
+private case class CollectionHolder(values: List[Int])
+
+class NativeCopyTest extends AnyWordSpec with Matchers {
+ private def fory: Fory =
+ ForyScala.builder()
+ .withXlang(false)
+ .withRefTracking(true)
+ .withRefCopy(true)
+ .requireClassRegistration(false)
+ .suppressClassRegistrationWarnings(false)
+ .build()
+
+ "native Scala collection copy" should {
+ "copy immutable collections and ranges" in {
+ val runtime = fory
+ val list = List("a", "b")
+ val copiedList = runtime.copy(list).asInstanceOf[List[String]]
+ copiedList shouldEqual list
+ copiedList should not be theSameInstanceAs(list)
+
+ val range = Range.inclusive(1, 4)
+ runtime.copy(range) shouldBe theSameInstanceAs(range)
+
+ val numericRange = scala.collection.immutable.NumericRange.inclusive(1, 4, 1)
+ runtime.copy(numericRange) shouldBe theSameInstanceAs(numericRange)
+
+ val sortedSet = scala.collection.immutable.TreeSet(3, 1, 2)
+ val copiedSet = runtime.copy(sortedSet).asInstanceOf[scala.collection.immutable.TreeSet[Int]]
+ copiedSet.toList shouldEqual List(1, 2, 3)
+ copiedSet should not be theSameInstanceAs(sortedSet)
+
+ val sortedMap = scala.collection.immutable.TreeMap(3 -> "c", 1 -> "a", 2 -> "b")
+ val copiedMap =
+ runtime.copy(sortedMap).asInstanceOf[scala.collection.immutable.TreeMap[Int, String]]
+ copiedMap.toList shouldEqual List(1 -> "a", 2 -> "b", 3 -> "c")
+ copiedMap should not be theSameInstanceAs(sortedMap)
+
+ val holder = CollectionHolder(list.map(_.length))
+ val copiedHolder = runtime.copy(holder).asInstanceOf[CollectionHolder]
+ copiedHolder shouldEqual holder
+ copiedHolder should not be theSameInstanceAs(holder)
+ copiedHolder.values should not be theSameInstanceAs(holder.values)
+ }
+
+ "copy mutable collections and maps" in {
+ val runtime = fory
+ val list = mutable.ListBuffer(1, 2)
+ val copiedList = runtime.copy(list).asInstanceOf[mutable.ListBuffer[Int]]
+ copiedList shouldEqual list
+ copiedList should not be theSameInstanceAs(list)
+
+ val indexed = mutable.ArraySeq[AnyRef](null)
+ indexed.update(0, indexed)
+ val copiedIndexed = runtime.copy(indexed).asInstanceOf[mutable.ArraySeq[AnyRef]]
+ copiedIndexed(0) shouldBe theSameInstanceAs(copiedIndexed)
+ }
+
+ "preserve cycles in mutable collections and maps" in {
+ val runtime = fory
+ val list = mutable.ArrayBuffer.empty[AnyRef]
+ list += list
+ val copiedList = runtime.copy(list).asInstanceOf[mutable.ArrayBuffer[AnyRef]]
+ copiedList should not be theSameInstanceAs(list)
+ copiedList.head shouldBe theSameInstanceAs(copiedList)
+
+ val map = mutable.LinkedHashMap.empty[String, AnyRef]
+ map.put("self", map)
+ val copiedMap = runtime.copy(map).asInstanceOf[mutable.LinkedHashMap[String, AnyRef]]
+ copiedMap should not be theSameInstanceAs(map)
+ copiedMap("self") shouldBe theSameInstanceAs(copiedMap)
+ }
+
+ "preserve identity across immutable collection and mutable map cycles" in {
+ val runtime = fory
+ val map = mutable.LinkedHashMap.empty[String, AnyRef]
+ val list = List[AnyRef](map)
+ map.put("list", list)
+
+ val copiedList = runtime.copy(list).asInstanceOf[List[AnyRef]]
+ val copiedMap = copiedList.head.asInstanceOf[mutable.LinkedHashMap[String, AnyRef]]
+ if (copiedList.asInstanceOf[AnyRef] eq list.asInstanceOf[AnyRef]) {
+ throw new AssertionError("immutable list was not copied")
+ }
+ if (copiedMap.asInstanceOf[AnyRef] eq map.asInstanceOf[AnyRef]) {
+ throw new AssertionError("mutable map was not copied")
+ }
+ if (!(copiedMap("list").asInstanceOf[AnyRef] eq copiedList.asInstanceOf[AnyRef])) {
+ throw new AssertionError("copied mutable map must point to the outer copied immutable list")
+ }
+ }
+
+ "preserve identity across immutable map and mutable collection cycles" in {
+ val runtime = fory
+ val list = mutable.ListBuffer.empty[AnyRef]
+ val map = scala.collection.immutable.Map[String, AnyRef]("list" -> list)
+ list += map
+
+ val copiedMap = runtime.copy(map).asInstanceOf[scala.collection.immutable.Map[String, AnyRef]]
+ val copiedList = copiedMap("list").asInstanceOf[mutable.ListBuffer[AnyRef]]
+ if (copiedMap.asInstanceOf[AnyRef] eq map.asInstanceOf[AnyRef]) {
+ throw new AssertionError("immutable map was not copied")
+ }
+ if (copiedList.asInstanceOf[AnyRef] eq list.asInstanceOf[AnyRef]) {
+ throw new AssertionError("mutable list was not copied")
+ }
+ if (!(copiedList.head.asInstanceOf[AnyRef] eq copiedMap.asInstanceOf[AnyRef])) {
+ throw new AssertionError("copied mutable list must point to the outer copied immutable map")
+ }
+ }
+ }
+}