A flexible deep and shallow copy framework for Java objects.
- Latest release
- Features
- Maven artifact
- Quick Start
- Default Copy Behaviour
- Copy Modes
- Copy Sessions
- Configuration
- Built-in Identity Types
- Custom Cloners
- Copyable Interface
- @CopyCreator
- @CopyIgnore
- CopyCallback
- Module System
- Building
- Contributing
- License
The latest stable release of object-copier is 1.2.0.
Download the latest object-copier release binaries here. Previous releases are available from the releases section.
- Deep and shallow copy of arbitrary Java objects
- Reflection-based by default – copies object fields via reflection
- Circular reference detection via reusable copy sessions
- Pluggable cloners – register custom
TypeClonerimplementations per type - Superclass cloner inheritance – a cloner registered for
Animalis automatically used forDog Copyableinterface – let objects control their own copy behaviourCopyCallbackinterface –preCopy/postCopylifecycle hooks@CopyCreator– custom factory method for instance creation@CopyIgnore– exclude individual fields from copying- Built-in support for collections, maps, arrays,
Optional, enums, records and all common JDK value types - Thread-safe after construction
object-copier is available as Maven artifact from the
Maven Central Repository. To add object-copier to your
project with Maven, add the following code to your pom.xml. You may need to adapt the object-copier version number.
Maven
<dependency>
<groupId>org.xmlobjects</groupId>
<artifactId>object-copier</artifactId>
<version>1.2.0</version>
</dependency>Gradle
implementation 'org.xmlobjects:object-copier:1.2.0'// Create a default Copier (no configuration needed)
Copier copier = CopierBuilder.newCopier();
// Deep copy
MyObject clone = copier.deepCopy(original);
// Shallow copy
MyObject shallowClone = copier.shallowCopy(original);A Copier is immutable after construction and safe to share across threads.
The examples above use the default configuration described next.
By default, object-copier performs copying via reflection. This includes creating instances and reading/writing fields of a class.
Important defaults:
finalfields are skipped in general and are not copiedstaticand synthetic fields are skipped- A no-arg constructor is expected for default instance creation
If a class does not meet these requirements, use one of the supported alternatives:
- Implement
Copyableto control copy behaviour in the type itself - Use
@CopyCreatorto provide custom instance creation - Register a custom
TypeCloner(orObjectCloner) viaCopierBuilder
| Mode | Behaviour |
|---|---|
deepCopy |
All reachable objects are recursively cloned |
shallowCopy |
Only the top-level object is cloned; field references are shared |
Both modes support an optional dest target object and an optional template class:
// Copy into an existing object
copier.deepCopy(src, dest);
// Copy using a superclass as the field template
copier.deepCopy(src, dest, AbstractBase.class);A CopySession tracks all clones created during a copy operation and is used to detect circular references. By default each top-level copy call creates its own session. Pass an explicit session to share the clone cache across multiple calls:
try (CopySession session = copier.createSession()) {
BuildingA cloneA = copier.deepCopy(buildingA, session);
BuildingB cloneB = copier.deepCopy(buildingB, session);
// Cross-references between A and B are resolved correctly
}CopySession implements AutoCloseable. Closing it releases the internal clone map.
CopySession is not thread-safe and must not be shared across concurrent threads.
Use one CopySession per thread when copying concurrently.
You can also look up a previously created clone:
MyObject clone = session.lookupClone(original, MyObject.class);Use CopierBuilder to configure a Copier:
Copier copier = CopierBuilder.newInstance()
.withCloner(MyType.class, new MyTypeCloner())
.withSelfCopy(ImmutablePoint.class) // returned as-is, no copy
.withNullCopy(Placeholder.class) // always returns null
.build();Types registered with withSelfCopy are returned as-is without cloning. Useful for truly immutable types that are not already detected automatically (e.g. custom value objects).
Types registered with withNullCopy are replaced with null during a copy. Useful for excluding specific types such as caches or external handles.
The following JDK types are automatically treated as immutable and returned as-is (no copy):
- All primitive wrappers (
Integer,Long,Boolean, …) String,BigDecimal,BigIntegerURI,URL,UUID,Pattern,Charset,Locale,Currency- All
java.timetypes (LocalDate,ZonedDateTime,Duration, …) Enumconstants andrecordtypesCollections.emptyList(),emptyMap(),emptySet()OptionalInt,OptionalLong,OptionalDouble
Extend TypeCloner<T> to control instantiation and field copying for a specific type:
public class PersonCloner extends TypeCloner<Person> {
@Override
protected Person newInstance(Person src, CopyMode mode, CopyContext context) {
return new Person(src.getId()); // custom construction
}
@Override
protected void deepCopy(Person src, Person dest, CopyContext context) {
dest.setName(context.deepCopy(src.getName()));
dest.setAddress(context.deepCopy(src.getAddress()));
}
}
Copier copier = CopierBuilder.newInstance()
.withCloner(Person.class, new PersonCloner())
.build();Extend ObjectCloner<T> if you only need custom instantiation but want automatic reflection-based field copying:
public class PersonCloner extends ObjectCloner<Person> {
public PersonCloner() {
super(Person.class);
}
@Override
protected Person newInstance(Person src, CopyMode mode, CopyContext context) {
return new Person(src.getId());
}
}A cloner registered for a superclass is automatically used for all subclasses that have no cloner of their own:
CopierBuilder.newInstance()
.withCloner(AbstractFeature.class, new FeatureCloner())
.build();
// FeatureCloner is also used for Building, Road, etc.Implement Copyable<T> to let a class control its own copy behaviour. This is the recommended approach for complex class hierarchies:
public class Building extends AbstractFeature implements Copyable<Building> {
@Override
public void deepCopyTo(Building dest, CopyContext context) {
dest.setName(context.deepCopy(getName()));
dest.setParts(context.deepCopy(getParts()));
}
}The default implementations of newInstance, shallowCopyTo and deepCopyTo fall back to reflection, so you only need to override what differs.
Use @CopyCreator on a newInstance(CopyMode, CopyContext) method when a class cannot be instantiated via a no-arg constructor.
In contrast to the Copyable interface, the @CopyCreator method can be private:
public class ImmutableId {
private String value;
private ImmutableId(String value) {
this.value = value;
}
@CopyCreator
private ImmutableId newInstance(CopyMode mode, CopyContext context) {
return new ImmutableId(value);
}
}Annotate fields that should be excluded from all copy operations:
public class MyObject {
private String name;
@CopyIgnore
private transient CachedResult cache; // never copied
}@CopyIgnore is evaluated once per class at first copy and then cached permanently – no runtime overhead.
Implement CopyCallback to receive lifecycle notifications during copying. Both src and clone receive the callbacks independently:
public class Building implements Copyable<Building>, CopyCallback {
@Override
public void preCopy(CopyMode mode, CopyContext context) {
// called on src before the clone is created
}
@Override
public void postCopy(CopyMode mode, CopyContext context) {
// called on clone after all fields have been copied
}
}isRoot is true only for the top-level object of a copy operation.
CopyContext provides exclude(Object) and include(Object) to temporarily prevent an object from being copied
during the current session. An excluded object is returned as-is without being registered in the session's clone map,
keeping it consistent for subsequent copy operations in the same session.
This is particularly useful when an object holds a reference to a parent that should not be recursively copied:
public interface Child extends CopyCallback {
Child getParent();
void setParent(Child parent);
@Override
default void preCopy(CopyMode mode, CopyContext context) {
if (context.isRoot()) {
context.exclude(getParent()); // parent returned as-is, not copied
}
}
@Override
default void postCopy(CopyMode mode, CopyContext context) {
if (context.isRoot()) {
context.include(getParent()); // re-enable normal copying for parent
setParent(null); // clone is detached from the original parent
}
}
}Note that exclude differs from withSelfCopy on CopyContext: exclude is temporary and
does not register the excluded object in the clone map, while withSelfCopy permanently registers
it for the lifetime of the session.
The framework is compatible with the Java module system. Classes in named modules must open their packages to allow reflection-based field access:
// module-info.java of the module containing classes to be copied
module com.example.myapp {
opens com.example.myapp.model to org.xmlobjects.copy; // adjust to actual module name
}Alternatively, implement Copyable or register a TypeCloner – both bypass reflection entirely.
object-copier requires Java 17 or higher. The project uses Gradle as build system. To build the library from source, clone the repository to your local machine and run the following command from the root of the repository.
> gradlew installDist
The script automatically downloads all required dependencies for building the module. So make sure you are connected to the internet.
The build process creates the output files in the folder build/install/object-copier. Simply put the
object-copier-<version>.jar library file and its mandatory dependencies from the lib folder on your modulepath to
start developing with object-copier. Have fun :-)
- To file bugs found in the software create a GitHub issue.
- To contribute code for fixing reported issues create a pull request with the issue id.
- To propose a new feature create a GitHub issue and open a discussion.
object-copier is licensed under the Apache License, Version 2.0.
See the LICENSE file for more details.