Deep cloning of any objects.
- Intuitive.
- Fast.
- Thread safe.
- Supports parallel execution.
- Java 8+ compatible.
Maven:
<dependency>
<groupId>io.github.sugar-cubes</groupId>
<artifactId>sugar-cubes-cloner</artifactId>
<version>1.2.3</version>
</dependency>
Gradle:
implementation "io.github.sugar-cubes:sugar-cubes-cloner:1.2.3"
It is recommended also to include Objenesis library into your application.
- To get simple, convenient and configurable way of deep-cloning of objects of any types.
- Make clean and extensible classes structure.
Many projects use serialization for object cloning. The raw solution is:
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
new ObjectOutputStream(buffer).writeObject(original);
return new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray())).readObject();
This code can be simplified with Apache commons-lang SerializationUtils.clone(object) or Spring Framework SerializationUtils.deserialize(SerializationUtils.serialize(object)).
Pros:
- portable, works on any JVM
- no external dependencies
Cons:
- slow
- requires all objects to be serializable
- may be customized only by changing serialization process
Still serialization, but with non-standard libraries, such as:
Faster than java.io serialization.
Class | Description |
---|---|
Cloner | The cloner interface. |
ClonerException | Wrapper for all (checked and unchecked) exceptions, happened during cloning. Unchecked. |
Cloners | Factory for standard cloners. |
CopyAction | Copy action (skip/null/original/clone). |
CopyPolicy | Set of class/field rules for cloning. |
ObjectCopier | Object copier interface. |
Predicates | Predicates factory to configure policies. |
ReflectionClonerBuilder | Builder for creating custom cloners. |
TraversalAlgorithm | Depth-first (default) or breadth-first. |
SomeObject clone = Cloners.reflection().clone(original);
Cloner cloner = Cloners.builder()
.setTypeAction(NonCloneableType.class, CopyAction.NULL)
.build();
SomeObject clone = cloner.clone(original);
Here the instances of NonCloneableType in the original's object tree will be replaced with nulls in the clone.
Cloner cloner = Cloners.builder()
.setTypeAction(NonCloneableType.class, CopyAction.ORIGINAL)
.build();
SomeObject clone = cloner.clone(original);
Here the instances of NonCloneableType in the original's object tree will be copied by reference into the clone.
The same thing can also be done with annotations:
@TypePolicy(CopyAction.NULL)
public class NonCloneableType {
or
@TypePolicy(CopyAction.ORIGINAL)
public class NonCloneableType {
Skip transient fields:
Cloner cloner = Cloners.builder()
.fieldAction(field -> Modifier.isTransient(field.getModifiers()), CopyAction.SKIP)
.build();
SomeObject clone = cloner.clone(original);
Or (Hibernate case):
Cloner cloner = Cloners.builder()
.fieldAction(Predicates.annotatedWith(Transient.class), CopyAction.SKIP)
.build();
SomeObject clone = cloner.clone(original);
If you wish to create a shallow copy of an object, i.e. another object which references same children as the original, you may do:
Cloner cloner = Cloners.builder()
.fieldPolicy(CopyPolicy.original())
.build();
SomeObject clone = cloner.clone(original);
If you want to have deep cloning by default and shallow cloning of all instances of SomeObject
, you may do:
Cloner cloner = Cloners.builder()
.shallow(SomeObject.class)
.build();
Cloner cloner =
// new builder instance
Cloners.builder()
// custom allocator
.objectFactoryProvider(new ObjenesisObjectFactoryProvider())
// copy thread locals by reference
.typeAction(Predicates.subclass(ThreadLocal.class), CopyAction.ORIGINAL)
// copy closeables by reference
.typeAction(Predicates.subclass(AutoCloseable.class), CopyAction.ORIGINAL)
// skip SomeObject.cachedValue field when cloning
.fieldAction(SomeObject.class, "cachedValue", CopyAction.SKIP)
// set fields with @Transient annotation to null
.fieldAction(Predicates.annotatedWith(Transient.class), CopyAction.NULL)
// custom copier for SomeOtherObject type
.copier(CustomObject.class, new CustomObjectCopier())
// parallel mode
.mode(CloningMode.PARALLEL)
// create cloner
.build();
// perform cloning
SomeObject clone = cloner.clone(original);
It's possible to use annotations to configure field/type actions and custom type copiers.
Annotation | Description |
---|---|
FieldPolicy | Field copy policy. |
TypeCopier | Type copier. |
TypePolicy | Type copy policy. |
There is three modes of execution: recursive, sequential (default) and parallel.
In sequential mode does not use recursion. Uses Depth-first (by default) or Breadth-first algorithm for the object graph traversal.
In parallel mode the order of copying is unpredictable.
If the Objenesis library is available in the classpath, uses it to instantiate objects. Otherwise, uses reflection.
The priority of copy configurations is:
- (high) builder configuration;
- annotations;
- (low) default configuration (JDK immutable classes).
The library requires Java 8 or higher.
Default configuration of reflection cloner does not clone lambdas and method references. These can be cloned using UnsafeObjectFactoryProvider.
Java 9+ restricts access to objects members via Reflection API. To solve this one may
- use
--illegal-access=permit
JVM argument (works on Java below 17); - if you add the cloner into classpath, use
--add-opens
JVM arguments, e.g.--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED
; - if you have an application with the modules which are properly configured, then use cloner's module name, i.e.
--add-opens java.base/java.lang=io.github.sugarcubes.cloner --add-opens java.base/java.lang.invoke=io.github.sugarcubes.cloner --add-opens java.base/java.util=io.github.sugarcubes.cloner --add-opens java.base/java.util.concurrent=io.github.sugarcubes.cloner
; - the library also contains a Java agent which opens modules for the cloner, just run java with argument
-javaagent:/path/to/sugar-cubes-cloner-1.2.3.jar
.