Skip to content

Commit 8c2cd02

Browse files
author
nicolaiparlog
committed
Implement 'EqualityTransformingSet'
1 parent 3220b29 commit 8c2cd02

File tree

2 files changed

+259
-0
lines changed

2 files changed

+259
-0
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package org.codefx.libfx.collection.transform;
2+
3+
import java.util.HashSet;
4+
import java.util.Set;
5+
import java.util.function.BiPredicate;
6+
import java.util.function.ToIntFunction;
7+
8+
/**
9+
* An equality transforming set allows to define the implementations of {@link Object#equals(Object) equals} and
10+
* {@link Object#hashCode() hashCode} which are used by the set.
11+
* <p>
12+
* It does so by storing the entries in an inner set and providing a transforming view on them. See the
13+
* {@link org.codefx.libfx.collection.transform package} documentation for general comments on that. Note that instances
14+
* of {@code EqualityTransformingSet}s are created with a {@link EqualityTransformingBuilder builder}.
15+
* <p>
16+
* This implementation mitigates the type safety problems by optionally using a token of the outer type to check
17+
* instances against them. This solves some of the critical situations but not all of them. In those other cases
18+
* {@link ClassCastException}s might still occur.
19+
* <p>
20+
* By default the inner set will be a {@link HashSet} but another map can be provided to the builder. Such instances
21+
* must be empty and not be referenced anywhere else. The implementations of {@code equals} and {@code hashCode} are
22+
* provided as functions to the builder - see there for details.
23+
* <p>
24+
* The transformations used by this set preserve object identity of outer values. This means if values are added to this
25+
* set, an iteration over it will return the same instances.
26+
*
27+
* @param <E>
28+
* the type of elements in this set
29+
*/
30+
public class EqualityTransformingSet<E> extends AbstractTransformingSet<EqHash<E>, E> {
31+
32+
// #begin FIELDS
33+
34+
private final Set<EqHash<E>> innerSet;
35+
36+
private final Class<? super E> outerTypeToken;
37+
38+
/**
39+
* Compares two outer elements for equality.
40+
*/
41+
private final BiPredicate<? super E, ? super E> equals;
42+
43+
/**
44+
* Computes a hashCode for an outer element.
45+
*/
46+
private final ToIntFunction<? super E> hash;
47+
48+
// #end FIELDS
49+
50+
// #begin CONSTRUCTION
51+
52+
EqualityTransformingSet(Set<?> innerSet, Class<? super E> outerTypeToken,
53+
BiPredicate<? super E, ? super E> equals, ToIntFunction<? super E> hash) {
54+
this.innerSet = castInnerSet(innerSet);
55+
this.outerTypeToken = outerTypeToken;
56+
this.equals = equals;
57+
this.hash = hash;
58+
}
59+
60+
private static <E> Set<EqHash<E>> castInnerSet(Set<?> untypedInnerSet) {
61+
@SuppressWarnings("unchecked")
62+
// This class' contract states that the 'innerSet' must be empty and that no other
63+
// references to it must exist. This implies that only this class can ever access or mutate it.
64+
// Thanks to erasure its generic element type can hence be cast to any other type.
65+
Set<EqHash<E>> innerMap = (Set<EqHash<E>>) untypedInnerSet;
66+
return innerMap;
67+
}
68+
69+
// #end CONSTRUCTION
70+
71+
@Override
72+
protected Set<EqHash<E>> getInnerSet() {
73+
return innerSet;
74+
}
75+
76+
@Override
77+
protected boolean isInnerElement(Object object) {
78+
// this excludes null objects from being inner element which is correct because even null will be wrapped in EqHash
79+
return object instanceof EqHash;
80+
}
81+
82+
@Override
83+
protected E transformToOuter(EqHash<E> innerElement) throws ClassCastException {
84+
return innerElement.getElement();
85+
}
86+
87+
@Override
88+
protected boolean isOuterElement(Object object) {
89+
return object == null || outerTypeToken.isInstance(object);
90+
}
91+
92+
@Override
93+
protected EqHash<E> transformToInner(E outerElement) throws ClassCastException {
94+
return new EqHash<E>(outerElement, equals, hash);
95+
}
96+
97+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package org.codefx.libfx.collection.transform;
2+
3+
import static org.junit.Assert.assertTrue;
4+
5+
import java.util.Arrays;
6+
import java.util.List;
7+
import java.util.Set;
8+
import java.util.function.BiPredicate;
9+
import java.util.function.ToIntFunction;
10+
11+
import junit.framework.JUnit4TestAdapter;
12+
import junit.framework.Test;
13+
import junit.framework.TestSuite;
14+
15+
import org.junit.Before;
16+
17+
import com.google.common.collect.testing.SampleElements;
18+
import com.google.common.collect.testing.SetTestSuiteBuilder;
19+
import com.google.common.collect.testing.TestSetGenerator;
20+
import com.google.common.collect.testing.features.CollectionFeature;
21+
import com.google.common.collect.testing.features.CollectionSize;
22+
import com.google.common.collect.testing.features.Feature;
23+
24+
/**
25+
* Tests {@link EqualityTransformingSet}.
26+
*/
27+
public class EqualityTransformingSetTest {
28+
29+
/**
30+
* JUnit-3-style method to create the tests run for this class.
31+
*
32+
* @return the tests to run
33+
*/
34+
public static Test suite() {
35+
TestSuite suite = new TestSuite("org.codefx.libfx.collection.transform.TransformingSet");
36+
suite.addTest(originalEquality());
37+
suite.addTest(lengthBasedEquality());
38+
return suite;
39+
}
40+
41+
private static Feature<?>[] features() {
42+
return new Feature<?>[] {
43+
// since 'EqualityTransformingSet' passes all calls along,
44+
// the features are determined by the backing data structure (which is a 'HashSet')
45+
CollectionSize.ANY,
46+
CollectionFeature.ALLOWS_NULL_VALUES,
47+
CollectionFeature.FAILS_FAST_ON_CONCURRENT_MODIFICATION,
48+
CollectionFeature.SUPPORTS_ADD,
49+
CollectionFeature.SUPPORTS_ITERATOR_REMOVE,
50+
CollectionFeature.SUPPORTS_REMOVE,
51+
};
52+
}
53+
54+
/**
55+
* Creates a test which uses hashCode and equals of the original keys.
56+
*
57+
* @return the test case
58+
*/
59+
private static Test originalEquality() {
60+
return SetTestSuiteBuilder
61+
.using(new TransformingSetGenerator(String::equals, String::hashCode))
62+
.named("original equality and hashCode")
63+
.withFeatures(features())
64+
.createTestSuite();
65+
}
66+
67+
/**
68+
* Creates a test which uses hashCode and equals based on the string's lengths.
69+
*
70+
* @return the test case
71+
*/
72+
private static Test lengthBasedEquality() {
73+
BiPredicate<String, String> equals = (s1, s2) -> s1.length() == s2.length();
74+
ToIntFunction<String> hash = s -> s.length();
75+
76+
Test generalTests = SetTestSuiteBuilder
77+
.using(new TransformingSetGenerator(equals, hash))
78+
.named("length-based equality and hashCode - general tests")
79+
.withFeatures(features())
80+
.createTestSuite();
81+
TestSuite specificTests = new TestSuite("length-based equality and hashCode - specific tests");
82+
specificTests.addTest(new JUnit4TestAdapter(LengthBasedEqualityAndHashCodeTests.class));
83+
84+
TestSuite tests = new TestSuite("length-based equality and hashCode");
85+
tests.addTest(generalTests);
86+
tests.addTest(specificTests);
87+
return tests;
88+
}
89+
90+
/**
91+
* Tests {@link EqualityTransformingSet} with a specific set of tests geared towards its special functionality, i.e.
92+
* transforming equals and hashCode.
93+
*/
94+
public static class LengthBasedEqualityAndHashCodeTests {
95+
96+
private Set<String> testedSet;
97+
98+
private final BiPredicate<String, String> equals = (s1, s2) -> s1.length() == s2.length();
99+
100+
private final ToIntFunction<String> hash = s -> s.length();
101+
102+
@Before
103+
@SuppressWarnings("javadoc")
104+
public void createSet() {
105+
testedSet = EqualityTransformingBuilder
106+
.forKeyType(String.class)
107+
.withEquals(equals)
108+
.withHash(hash)
109+
.buildSet();
110+
}
111+
112+
@org.junit.Test
113+
@SuppressWarnings("javadoc")
114+
public void add_containsWithSameLengthElement_true() {
115+
testedSet.add("aaa");
116+
117+
assertTrue(testedSet.contains("bbb"));
118+
}
119+
120+
}
121+
122+
private static class TransformingSetGenerator implements TestSetGenerator<String> {
123+
124+
private final BiPredicate<String, String> equals;
125+
private final ToIntFunction<String> hash;
126+
127+
public TransformingSetGenerator(BiPredicate<String, String> equals, ToIntFunction<String> hash) {
128+
this.equals = equals;
129+
this.hash = hash;
130+
}
131+
132+
@Override
133+
public Set<String> create(Object... elements) {
134+
Set<String> transformingSet = EqualityTransformingBuilder
135+
.forKeyType(String.class)
136+
.withEquals(equals)
137+
.withHash(hash)
138+
.buildSet();
139+
Arrays.stream(elements)
140+
.map(String.class::cast)
141+
.forEach(transformingSet::add);
142+
return transformingSet;
143+
}
144+
145+
@Override
146+
public SampleElements<String> samples() {
147+
return new SampleElements<String>("A", "AA", "AAA", "AAAA", "AAAAA");
148+
}
149+
150+
@Override
151+
public String[] createArray(int length) {
152+
return new String[length];
153+
}
154+
155+
@Override
156+
public Iterable<String> order(List<String> insertionOrder) {
157+
return insertionOrder;
158+
}
159+
160+
}
161+
162+
}

0 commit comments

Comments
 (0)