0
# Relaxed Equality API
1
2
Specialized verification for classes with relaxed equality rules where multiple instances can be equal despite different internal state. This is common in normalized representations, value objects, and classes that implement canonical forms.
3
4
## Capabilities
5
6
### Relaxed Equal Examples
7
8
Creates a verifier for classes where multiple distinct instances can be equal to each other.
9
10
```java { .api }
11
/**
12
* Factory method. Asks for a list of equal, but not identical, instances of T.
13
*
14
* For use when T is a class which has relaxed equality rules. This happens when two
15
* instances of T are equal even though the its internal state is different.
16
*
17
* This could happen, for example, in a Rational class that doesn't normalize: new
18
* Rational(1, 2).equals(new Rational(2, 4)) would return true.
19
*
20
* Using this factory method requires that andUnequalExamples be called to supply a list of
21
* unequal instances of T.
22
*
23
* This method automatically suppresses ALL_FIELDS_SHOULD_BE_USED.
24
*
25
* @param first An instance of T
26
* @param second Another instance of T, which is equal, but not identical, to first
27
* @param more More instances of T, all of which are equal, but not identical, to one another
28
* and to first and second
29
* @return A fluent API for a more relaxed EqualsVerifier
30
*/
31
@SafeVarargs
32
public static <T> RelaxedEqualsVerifierApi<T> forRelaxedEqualExamples(
33
T first,
34
T second,
35
T... more
36
);
37
```
38
39
**Usage Examples:**
40
41
```java
42
import nl.jqno.equalsverifier.EqualsVerifier;
43
44
// Example with rational numbers that don't normalize
45
Rational oneHalf = new Rational(1, 2);
46
Rational twoQuarters = new Rational(2, 4);
47
Rational fourEighths = new Rational(4, 8);
48
49
EqualsVerifier.forRelaxedEqualExamples(oneHalf, twoQuarters, fourEighths)
50
.andUnequalExamples(new Rational(1, 3), new Rational(3, 4))
51
.verify();
52
53
// Example with normalized string representations
54
NormalizedString str1 = new NormalizedString(" Hello World ");
55
NormalizedString str2 = new NormalizedString("hello world");
56
NormalizedString str3 = new NormalizedString("HELLO WORLD");
57
58
EqualsVerifier.forRelaxedEqualExamples(str1, str2, str3)
59
.andUnequalExamples(new NormalizedString("Different Text"))
60
.verify();
61
```
62
63
### Adding Unequal Examples
64
65
Provides unequal examples to complete the relaxed verification setup.
66
67
```java { .api }
68
/**
69
* Provides single unequal example and returns SingleTypeEqualsVerifierApi
70
* @param example An instance of T that is not equal to any of the equal examples
71
* @return A SingleTypeEqualsVerifierApi for further configuration and verification
72
*/
73
public SingleTypeEqualsVerifierApi<T> andUnequalExample(T example);
74
75
/**
76
* Provides multiple unequal examples and returns SingleTypeEqualsVerifierApi
77
* @param first An instance of T that is not equal to any of the equal examples
78
* @param more More instances of T that are not equal to any of the equal examples
79
* @return A SingleTypeEqualsVerifierApi for further configuration and verification
80
*/
81
@SafeVarargs
82
public final SingleTypeEqualsVerifierApi<T> andUnequalExamples(T first, T... more);
83
```
84
85
**Usage Examples:**
86
87
```java
88
// Single unequal example
89
EqualsVerifier.forRelaxedEqualExamples(oneHalf, twoQuarters)
90
.andUnequalExample(new Rational(1, 3))
91
.verify();
92
93
// Multiple unequal examples
94
EqualsVerifier.forRelaxedEqualExamples(oneHalf, twoQuarters, fourEighths)
95
.andUnequalExamples(
96
new Rational(1, 3),
97
new Rational(3, 4),
98
new Rational(2, 3)
99
)
100
.verify();
101
102
// With additional configuration
103
EqualsVerifier.forRelaxedEqualExamples(normalizedStr1, normalizedStr2)
104
.andUnequalExamples(differentStr1, differentStr2)
105
.suppress(Warning.NONFINAL_FIELDS)
106
.verify();
107
```
108
109
### Common Use Cases
110
111
**Rational Number Classes:**
112
113
```java
114
public class Rational {
115
private final int numerator;
116
private final int denominator;
117
118
public Rational(int numerator, int denominator) {
119
// Note: This implementation doesn't normalize fractions
120
this.numerator = numerator;
121
this.denominator = denominator;
122
}
123
124
@Override
125
public boolean equals(Object obj) {
126
if (!(obj instanceof Rational)) return false;
127
Rational other = (Rational) obj;
128
// Cross multiplication to check equality without normalization
129
return this.numerator * other.denominator == other.numerator * this.denominator;
130
}
131
132
@Override
133
public int hashCode() {
134
// Normalize for consistent hash codes
135
int gcd = gcd(numerator, denominator);
136
return Objects.hash(numerator / gcd, denominator / gcd);
137
}
138
}
139
140
// Test with relaxed equality
141
@Test
142
public void testRationalEquals() {
143
Rational oneHalf = new Rational(1, 2);
144
Rational twoQuarters = new Rational(2, 4);
145
Rational threeHalves = new Rational(3, 6); // Actually equals 1/2
146
147
EqualsVerifier.forRelaxedEqualExamples(oneHalf, twoQuarters, threeHalves)
148
.andUnequalExamples(new Rational(1, 3), new Rational(2, 3))
149
.verify();
150
}
151
```
152
153
**Case-Insensitive String Wrappers:**
154
155
```java
156
public class CaseInsensitiveString {
157
private final String value;
158
159
public CaseInsensitiveString(String value) {
160
this.value = value;
161
}
162
163
@Override
164
public boolean equals(Object obj) {
165
if (!(obj instanceof CaseInsensitiveString)) return false;
166
CaseInsensitiveString other = (CaseInsensitiveString) obj;
167
return this.value.equalsIgnoreCase(other.value);
168
}
169
170
@Override
171
public int hashCode() {
172
return value.toLowerCase().hashCode();
173
}
174
}
175
176
// Test with relaxed equality
177
@Test
178
public void testCaseInsensitiveStringEquals() {
179
CaseInsensitiveString lower = new CaseInsensitiveString("hello");
180
CaseInsensitiveString upper = new CaseInsensitiveString("HELLO");
181
CaseInsensitiveString mixed = new CaseInsensitiveString("HeLLo");
182
183
EqualsVerifier.forRelaxedEqualExamples(lower, upper, mixed)
184
.andUnequalExamples(new CaseInsensitiveString("world"))
185
.verify();
186
}
187
```
188
189
**Normalized Path Classes:**
190
191
```java
192
public class NormalizedPath {
193
private final String path;
194
195
public NormalizedPath(String path) {
196
this.path = path; // Store original, normalize in equals/hashCode
197
}
198
199
@Override
200
public boolean equals(Object obj) {
201
if (!(obj instanceof NormalizedPath)) return false;
202
NormalizedPath other = (NormalizedPath) obj;
203
return normalize(this.path).equals(normalize(other.path));
204
}
205
206
@Override
207
public int hashCode() {
208
return normalize(this.path).hashCode();
209
}
210
211
private String normalize(String path) {
212
return path.replaceAll("/+", "/")
213
.replaceAll("/\\./", "/")
214
.replaceAll("/$", "");
215
}
216
}
217
218
// Test with relaxed equality
219
@Test
220
public void testNormalizedPathEquals() {
221
NormalizedPath path1 = new NormalizedPath("/home/user/documents");
222
NormalizedPath path2 = new NormalizedPath("/home//user/./documents/");
223
NormalizedPath path3 = new NormalizedPath("/home/user/documents/");
224
225
EqualsVerifier.forRelaxedEqualExamples(path1, path2, path3)
226
.andUnequalExamples(new NormalizedPath("/home/user/downloads"))
227
.verify();
228
}
229
```
230
231
**URL Classes with Different Representations:**
232
233
```java
234
public class FlexibleUrl {
235
private final String scheme;
236
private final String host;
237
private final Integer port;
238
private final String path;
239
240
public FlexibleUrl(String url) {
241
// Parse URL and store components
242
// This constructor could create different internal representations
243
// for the same logical URL
244
}
245
246
@Override
247
public boolean equals(Object obj) {
248
if (!(obj instanceof FlexibleUrl)) return false;
249
FlexibleUrl other = (FlexibleUrl) obj;
250
251
// Normalize comparison - default ports, case-insensitive hosts, etc.
252
return normalizeScheme(this.scheme).equals(normalizeScheme(other.scheme)) &&
253
normalizeHost(this.host).equals(normalizeHost(other.host)) &&
254
normalizePort(this.port, this.scheme).equals(normalizePort(other.port, other.scheme)) &&
255
normalizePath(this.path).equals(normalizePath(other.path));
256
}
257
258
@Override
259
public int hashCode() {
260
return Objects.hash(
261
normalizeScheme(scheme),
262
normalizeHost(host),
263
normalizePort(port, scheme),
264
normalizePath(path)
265
);
266
}
267
}
268
269
// Test with relaxed equality
270
@Test
271
public void testFlexibleUrlEquals() {
272
FlexibleUrl url1 = new FlexibleUrl("http://Example.COM:80/path/");
273
FlexibleUrl url2 = new FlexibleUrl("HTTP://example.com/path");
274
FlexibleUrl url3 = new FlexibleUrl("http://example.com:80/path/");
275
276
EqualsVerifier.forRelaxedEqualExamples(url1, url2, url3)
277
.andUnequalExamples(new FlexibleUrl("https://example.com/path"))
278
.verify();
279
}
280
```
281
282
### Advanced Configuration
283
284
Since `RelaxedEqualsVerifierApi` returns a `SingleTypeEqualsVerifierApi`, all single-class configuration options are available:
285
286
```java
287
// Complex relaxed equality verification with full configuration
288
EqualsVerifier.forRelaxedEqualExamples(
289
new ComplexRational(1, 2, metadata1),
290
new ComplexRational(2, 4, metadata2),
291
new ComplexRational(4, 8, metadata3)
292
)
293
.andUnequalExamples(new ComplexRational(1, 3, metadata4))
294
.suppress(Warning.NONFINAL_FIELDS)
295
.withPrefabValues(Metadata.class, redMetadata, blueMetadata)
296
.withIgnoredFields("creationTime", "lastModified")
297
.verify();
298
```
299
300
### Important Notes
301
302
1. **Automatic Warning Suppression**: The `forRelaxedEqualExamples` method automatically suppresses `Warning.ALL_FIELDS_SHOULD_BE_USED` because relaxed equality typically doesn't use all fields in the same way.
303
304
2. **Equal Examples Requirement**: All examples provided to `forRelaxedEqualExamples` must be equal to each other according to the class's `equals` method.
305
306
3. **Unequal Examples Requirement**: All examples provided to `andUnequalExamples` must not be equal to any of the equal examples.
307
308
4. **hashCode Consistency**: The class must still maintain the contract that equal objects have equal hash codes, even if they have different internal representations.