jdcmp is an open-source library for Java™ that helps developers implement consistent hashCode()
,
equals(Object)
and compareTo(Object)
methods in a declarative, programmatic and compile-safe manner.
- Consistent implementations of
hashCode()
,equals(Object)
andcompareTo(Object)
- Declaration instead of implementation
- Drop-in replacement for
Comparator<T>
- Null-safety (optional feature)
- Supports
Serializable
(optional feature) - Faster than many alternatives such as
Comparator.comparing
The following table lists runtimes that are known to be compatible with the default implementation
comparison-impl-codegen
.
Flavor | Versions |
---|---|
OpenJDK | 8 - 22 |
IBM Semeru (OpenJ9) | 17 - 20 |
GraalVM CE | 17, 20, 21 |
Oracle GraalVM | 17, 20, 21 |
The following features are untested:
- Creating comparators for Records.
- Creating comparators for VM-anonymous or hidden classes.
Check the Releases page and replace
<version>VERSION</version>
with an appropriate entry.
Quickstart
Add this to <dependencies>
inside your POM (e.g. pom.xml
) and replace VERSION
:
<dependency>
<groupId>io.github.jdcmp</groupId>
<artifactId>comparison-impl-codegen</artifactId>
<version>VERSION</version> <!-- See https://github.com/jdcmp/jdcmp/releases -->
</dependency>
With dependencyManagement
Users of <dependencyManagement>
may copy some XML.
Replace VERSION
with an appropriate entry:
dependencies {
implementation group: 'io.github.jdcmp', name: 'comparison-impl-codegen', version: 'VERSION'
}
The API project comparison-api
requires the following dependencies:
org.jetbrains:annotations
The default implementation comparison-impl-codegen
requires the following additional
dependencies:
Licensing information is available here.
The example below demonstrates the implementation of hashCode()
and equals(Object)
.
class Person {
private static final EqualityComparator<Person> COMPARATOR = Comparators.equality()
.nonSerializable()
.requireAtLeastOneGetter(Person.class)
.use(ObjectGetter.of(Person::getFirstName))
.use(ObjectGetter.of(Person::getLastName))
.build();
private String firstName;
private String lastName;
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public int hashCode() {
return COMPARATOR.hash(this);
}
public boolean equals(Object obj) {
return COMPARATOR.areEqual(this, obj);
}
}
The example below demonstrates the implementation of hashCode()
, equals(Object)
and
compareTo(Object)
.
class Person implements Comparable<Person> {
private static final OrderingComparator<Person> COMPARATOR = Comparators.ordering()
.nonSerializable()
.requireAtLeastOneGetter(Person.class)
.use(ComparableGetter.of(Person::getFirstName))
.use(ComparableGetter.of(Person::getLastName))
.build();
private String firstName;
private String lastName;
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public int hashCode() {
return COMPARATOR.hash(this);
}
public boolean equals(Object obj) {
return COMPARATOR.areEqual(this, obj);
}
public int compareTo(Person other) {
return COMPARATOR.compare(this, other);
}
}
By default, null
method arguments are not permitted in compare(Object, Object)
.
Handling nullable arguments
The following example uses the builder's optional nullsFirst()
method to create a comparator
that accepts null
arguments in its compare(Object, Object)
method.
Comparators.ordering()
.nonSerializable()
.requireAtLeastOneGetter(X.class)
.use(ComparableArrayGetter.of(X::getStringArray))
.nullsFirst()
.build();
Note, however, that this does not affect the ability of each criterion to handle null
. If
X::getStringArray
returns null
, or if the returned array contains null
elements, a
NullPointerException
is still thrown.
Handling nullable criteria
When using multi-element criteria such as String[]
, there are two cases where null
must be
handled:
- The
String[]
reference could benull
. - A non-null
String[]
could still containnull
elements.
The following example uses a null
-safe getter:
Comparators.ordering()
.nonSerializable()
.requireAtLeastOneGetter(X.class)
.use(ComparableArrayGetter.nullsFirst(X::getStringArray))
.nullsFirst()
.build();
The following sections describe the most important types.
hashCode, equals, compareTo
Comparators
: Main API for obtaining instancesEqualityComparator<T>
: Comparator forhashCode
&equals
OrderingComparator<T>
: Comparator forhashCode
,equals
andcompareTo
EqualityCriterion<T>
: ProvideshashCode
&equals
based on a single property ofT
OrderingCriterion<T>
: ProvideshashCode
,equals
&compareTo
based on a single property of a sortableT
(e.g.String
,int
,long[]
, ...)
Serialization
SerializationSupport
: Static utilities for serializationSerializableEqualityComparator<T>
: Serializable equivalent ofEqualityComparator<T>
SerializableOrderingComparator<T>
: Serializable equivalent ofOrderingComparator<T>
Serializable*Getter
: Serializable equivalents of getters
ServiceLoader
ComparatorProviders
: Static factory to obtain concrete implementations such ascomparison-impl-codegen
ComparatorProvider
: Interface whose implementation lies incomparison-impl-codegen
and similar implementations. A provider creates comparators and is called viaComparators
upon callingbuild()
on one of the builders.
Methods such as ComparatorProviders.load()
are called from various places to obtain a default
ComparatorProvider
, one example being the deserialization process. Frequent
invocations may cause a lot of instance creation and garbage collection overhead. Therefore,
it is recommended to manage the lifecycle of a specific ComparatorProvider
via some
application-specific mechanism. It is recommended to use an existing IoC-Container in environments
such as Spring
. Here is a plain Java solution with no frameworks:
class MyApplication {
private static final ComparatorProvider PROVIDER = initializeProvider();
public static void main(String[] args) {
// ...
}
private static ComparatorProvider initializeProvider() {
CodegenProvider provider = ComparatorProviders.load(CodegenProvider.class);
provider.setSerializationMode(AvailableSerializationMode.COMPATIBLE);
ComparatorProviders.setDefaultProvider(provider);
ComparatorProviders.setSerializationProvider(provider);
return provider;
}
}
There are two ways to load a ComparatorProvider
.
Automatic instantiation
This method scans the class path for implementations of comparison-api
and returns a random
provider. Implementations such as comparison-impl-codegen
are automatically detected, if they
provide a service configuration in src/main/resources/META-INF.services
.
ComparatorProvider provider = ComparatorProviders.load();
Explicit instantiation
This method loads a specific provider. The provider must have a public no-args constructor.
MyComparatorProvider provider = ComparatorProviders.load(MyComparatorProvider.class);
Implementations are designed to be compatible with Java's serialization mechanism. All types
have Serializable counterparts (i.e. OrderingComparator<T>
is extended by
SerializableOrderingComparator<T>
). This gives users free choice over whether they wish to
introduce serialization and its security implications into their projects, or use slimmer,
safer implementations.
All supplied information such as getters must be serializable as well. The example below
demonstrates how to create the serializable lambda Person::getFirstName
by using the helper
method SerializableComparableGetter.of(getter)
:
class Person implements Comparable<Person> {
private static final SerializableOrderingComparator<Person> COMPARATOR = Comparators.ordering()
.serializable()
.requireAtLeastOneGetter(Person.class)
.use(SerializableComparableGetter.of(Person::getFirstName))
.use(SerializableComparableGetter.of(Person::getLastName))
.build();
private String firstName;
private String lastName;
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public int hashCode() {
return COMPARATOR.hash(this);
}
public boolean equals(Object obj) {
return COMPARATOR.areEqual(this, obj);
}
public int compareTo(Person other) {
return COMPARATOR.compare(this, other);
}
}
The docs directory contains further information about licenses, configuration, implementation and development.
Please file bug reports, feature requests or questions via GitHub issues, or contact me privately
via e-mail at jari.schaefer@gmail.com
.
This project is based on smaller, less complex implementations that I have been using in some of my
private projects. It serves as a learning environment for several things such as API design, Java
version & flavor compatibility, Java Object Serialization, internal APIs (e.g. sun.*
),
optimizations such as static final
, @Stable
, -XX:+TrustFinalNonStaticFields
, bytecode
generation and class loading & instantiation.
The project is available under the MIT license. See LICENSE.txt or LICENSING docs for details.