Skip to content

Commit 6482823

Browse files
committed
#63 FieldUtils: Shorten method names, return streams, util to group by name
1 parent fb2bfcd commit 6482823

File tree

2 files changed

+117
-53
lines changed

2 files changed

+117
-53
lines changed

src/main/java/ch/jalu/typeresolver/FieldUtils.java

+41-29
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55
import java.lang.reflect.Field;
66
import java.lang.reflect.Modifier;
77
import java.util.Arrays;
8+
import java.util.LinkedHashMap;
89
import java.util.LinkedList;
910
import java.util.List;
1011
import java.util.Optional;
12+
import java.util.function.BiConsumer;
1113
import java.util.function.Consumer;
12-
import java.util.function.Predicate;
14+
import java.util.stream.Collector;
1315
import java.util.stream.Collectors;
16+
import java.util.stream.Stream;
1417

1518
/**
1619
* Class with utilities for processing fields.
@@ -57,43 +60,26 @@ public static boolean isRegularStaticField(Field field) {
5760
* in this class's hierarchy are returned first.
5861
*
5962
* @param clazz the class whose fields (incl. its parents' fields) should be returned
60-
* @return all fields, with top-most parent's fields first, and this class's fields last
63+
* @return a stream of all fields, with top-most parent's fields first, and this class's fields last
6164
*/
62-
public static List<Field> getFieldsIncludingParents(Class<?> clazz) {
63-
return getFieldsIncludingParents(clazz, f -> true, true);
65+
public static Stream<Field> getAllFields(Class<?> clazz) {
66+
return getAllFields(clazz, true);
6467
}
6568

6669
/**
67-
* Returns all fields from the given class and its parents, recursively, that match the provided filter.
68-
* The fields of the top-most parent in this class's hierarchy are returned first.
70+
* Returns all fields from the given class and its parents, recursively. Depending on the parameter, fields are
71+
* either returned from top-to-bottom or bottom-to-top relative to the class's hierarchy.
6972
*
70-
* @param clazz the class whose fields (incl. its parents' fields) should be returned that match the filter
71-
* @param fieldFilter the condition a field must fulfill in order to be part of the result
72-
* @return all fields matching the filter, with the top-most parent's fields first, and this class's fields last
73-
*/
74-
public static List<Field> getFieldsIncludingParents(Class<?> clazz, Predicate<Field> fieldFilter) {
75-
return getFieldsIncludingParents(clazz, fieldFilter, true);
76-
}
77-
78-
/**
79-
* Returns all fields from the given class and its parents, recursively, that match the provided filter. Depending
80-
* on the parameter, fields are either returned from top-to-bottom or bottom-to-top relative to the class's
81-
* hierarchy.
82-
*
83-
* @param clazz the class whose fields (incl. its parents' fields) should be returned that match the filter
84-
* @param fieldFilter the condition a field must fulfill in order to be part of the result
73+
* @param clazz the class whose fields (incl. its parents' fields) should be returned
8574
* @param topParentFirst true if the top-most parent's fields should come first, false for last
86-
* @return all fields matching the filter, in the specified order
75+
* @return a stream of all fields, in the specified order
8776
*/
88-
public static List<Field> getFieldsIncludingParents(Class<?> clazz, Predicate<Field> fieldFilter,
89-
boolean topParentFirst) {
77+
public static Stream<Field> getAllFields(Class<?> clazz, boolean topParentFirst) {
9078
LinkedList<Class<?>> classes = new LinkedList<>();
9179
collectParents(clazz, (topParentFirst ? classes::addFirst : classes::addLast));
9280

9381
return classes.stream()
94-
.flatMap(clz -> Arrays.stream(clz.getDeclaredFields()))
95-
.filter(fieldFilter)
96-
.collect(Collectors.toList());
82+
.flatMap(clz -> Arrays.stream(clz.getDeclaredFields()));
9783
}
9884

9985
/**
@@ -102,8 +88,10 @@ public static List<Field> getFieldsIncludingParents(Class<?> clazz, Predicate<Fi
10288
* @param clazz the class whose instance fields should be retrieved
10389
* @return all non-synthetic instance fields
10490
*/
105-
public static List<Field> getRegularInstanceFieldsIncludingParents(Class<?> clazz) {
106-
return getFieldsIncludingParents(clazz, FieldUtils::isRegularInstanceField, true);
91+
public static List<Field> collectAllRegularInstanceFields(Class<?> clazz) {
92+
return getAllFields(clazz, true)
93+
.filter(FieldUtils::isRegularInstanceField)
94+
.collect(Collectors.toList());
10795
}
10896

10997
/**
@@ -143,6 +131,30 @@ public static Optional<Field> tryFindFieldInClassOrParent(Class<?> clazz, String
143131
return Optional.empty();
144132
}
145133

134+
/**
135+
* Collector for a stream of fields to group them by name, with the parameter indicating whether the first
136+
* encountered field with a given name should be retained, or the last one. This collector does not support
137+
* parallel streams.
138+
* <p>
139+
* Note that this collector is especially useful if you are only dealing with instance fields: for a mechanism
140+
* that processes the instance fields of classes (e.g. for serialization), you may want to only consider the
141+
* lowest-most declared field per any given name so that behavior can be overridden.
142+
* <p>
143+
* To get <b>all fields</b> grouped by name, use {@code stream.collect(Collectors.groupingBy(Field::getName))}.
144+
*
145+
* @param firstFieldWins true if the first encountered field with a given name should be kept;
146+
* false to keep the last one
147+
* @return collector to collect names by field
148+
*/
149+
public static Collector<Field, ?, LinkedHashMap<String, Field>> collectByName(boolean firstFieldWins) {
150+
BiConsumer<LinkedHashMap<String, Field>, Field> accumulator = firstFieldWins
151+
? (map, field) -> map.putIfAbsent(field.getName(), field)
152+
: (map, field) -> map.put(field.getName(), field);
153+
154+
return Collector.of(LinkedHashMap::new, accumulator,
155+
(a, b) -> { throw new UnsupportedOperationException(); });
156+
}
157+
146158
private static void collectParents(Class<?> clazz, Consumer<Class<?>> classAdder) {
147159
Class<?> currentClass = clazz;
148160
while (currentClass != null) {

src/test/java/ch/jalu/typeresolver/FieldUtilsTest.java

+76-24
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,17 @@
77
import java.util.Arrays;
88
import java.util.HashMap;
99
import java.util.Iterator;
10+
import java.util.LinkedHashMap;
1011
import java.util.List;
12+
import java.util.Map;
1113
import java.util.Optional;
1214
import java.util.concurrent.TimeUnit;
15+
import java.util.stream.Collectors;
16+
import java.util.stream.Stream;
1317

1418
import static org.hamcrest.MatcherAssert.assertThat;
19+
import static org.hamcrest.Matchers.contains;
20+
import static org.hamcrest.Matchers.containsInAnyOrder;
1521
import static org.hamcrest.Matchers.empty;
1622
import static org.hamcrest.Matchers.equalTo;
1723
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
@@ -68,7 +74,7 @@ void shouldReturnIfIsNonSyntheticStaticField() throws NoSuchFieldException {
6874
@Test
6975
void shouldGetFieldsIncludingParents() throws NoSuchFieldException {
7076
// given / when
71-
List<Field> allFields = FieldUtils.getFieldsIncludingParents(Class3.class);
77+
List<Field> allFields = FieldUtils.getAllFields(Class3.class).collect(Collectors.toList());
7278

7379
// then
7480
assertThat(allFields.size(), greaterThanOrEqualTo(6));
@@ -96,39 +102,28 @@ void shouldGetFieldsIncludingParents() throws NoSuchFieldException {
96102
}
97103
}
98104

99-
@Test
100-
void shouldGetAllFieldsSatisfyingFilter() throws NoSuchFieldException {
101-
// given / when
102-
List<Field> allFields = FieldUtils.getFieldsIncludingParents(Class3.class,
103-
field -> field.getType().isPrimitive());
104-
105-
// then
106-
assertThat(allFields, hasSize(4));
107-
assertThat(allFields.get(0), equalTo(Class1.class.getDeclaredField("C1A")));
108-
assertThat(allFields.get(1), equalTo(Class1.class.getDeclaredField("c1b")));
109-
assertThat(allFields.get(2), equalTo(Class2.class.getDeclaredField("c2a")));
110-
assertThat(allFields.get(3), equalTo(Class3.class.getDeclaredField("c3b")));
111-
}
112-
113105
@Test
114106
void shouldGetAllFieldsWithFilterAndParentsLast() throws NoSuchFieldException {
115107
// given / when
116-
List<Field> allFields = FieldUtils.getFieldsIncludingParents(Class3.class,
117-
field -> !field.getName().endsWith("a") && !field.isSynthetic(), false);
108+
List<Field> allFields = FieldUtils.getAllFields(Class3.class, false)
109+
.filter(f -> !f.isSynthetic())
110+
.collect(Collectors.toList());
118111

119112
// then
120-
assertThat(allFields, hasSize(4));
121-
assertThat(allFields.get(0), equalTo(Class3.class.getDeclaredField("c3b")));
122-
assertThat(allFields.get(1), equalTo(Class2.class.getDeclaredField("c2b")));
123-
assertThat(allFields.get(2), equalTo(Class1.class.getDeclaredField("C1A")));
124-
assertThat(allFields.get(3), equalTo(Class1.class.getDeclaredField("c1b")));
113+
assertThat(allFields, hasSize(6));
114+
assertThat(allFields.get(0), equalTo(Class3.class.getDeclaredField("c3a")));
115+
assertThat(allFields.get(1), equalTo(Class3.class.getDeclaredField("c3b")));
116+
assertThat(allFields.get(2), equalTo(Class2.class.getDeclaredField("c2a")));
117+
assertThat(allFields.get(3), equalTo(Class2.class.getDeclaredField("c2b")));
118+
assertThat(allFields.get(4), equalTo(Class1.class.getDeclaredField("C1A")));
119+
assertThat(allFields.get(5), equalTo(Class1.class.getDeclaredField("c1b")));
125120
}
126121

127122
@Test
128123
void shouldGetAllRegularInstanceFieldsIncludingParents() throws NoSuchFieldException {
129124
// given / when
130-
List<Field> allFields1 = FieldUtils.getRegularInstanceFieldsIncludingParents(Class3.class);
131-
List<Field> allFields2 = FieldUtils.getRegularInstanceFieldsIncludingParents(InnerClass.class);
125+
List<Field> allFields1 = FieldUtils.collectAllRegularInstanceFields(Class3.class);
126+
List<Field> allFields2 = FieldUtils.collectAllRegularInstanceFields(InnerClass.class);
132127

133128
// then
134129
assertThat(allFields1, hasSize(4));
@@ -165,6 +160,63 @@ void shouldReturnFieldFromClassOrParentIfExists() throws NoSuchFieldException {
165160
assertThat(FieldUtils.tryFindFieldInClassOrParent(Class3.class, ""), equalTo(Optional.empty()));
166161
}
167162

163+
@Test
164+
void shouldCollectFieldsByName() throws NoSuchFieldException {
165+
// given
166+
Field c1a = Class1.class.getDeclaredField("C1A");
167+
Field c1b = Class1.class.getDeclaredField("c1b");
168+
Field c2a = Class2.class.getDeclaredField("c2a");
169+
Field c2b = Class2.class.getDeclaredField("c2b");
170+
Field c3a = Class3.class.getDeclaredField("c3a");
171+
Field c3b = Class3.class.getDeclaredField("c3b");
172+
173+
Field x1b = ClassWithSameFieldNames.class.getDeclaredField("c1b");
174+
Field x2b = ClassWithSameFieldNames.class.getDeclaredField("c2b");
175+
176+
// when
177+
LinkedHashMap<String, Field> firstFieldsParentFirst = FieldUtils.getAllFields(ClassWithSameFieldNames.class, true)
178+
.filter(field -> !field.isSynthetic())
179+
.collect(FieldUtils.collectByName(true));
180+
LinkedHashMap<String, Field> firstFieldsParentLast = FieldUtils.getAllFields(ClassWithSameFieldNames.class, false)
181+
.filter(field -> !field.isSynthetic())
182+
.collect(FieldUtils.collectByName(true));
183+
LinkedHashMap<String, Field> lastFieldsParentFirst = FieldUtils.getAllFields(ClassWithSameFieldNames.class, true)
184+
.filter(field -> !field.isSynthetic())
185+
.collect(FieldUtils.collectByName(false));
186+
LinkedHashMap<String, Field> lastFieldsParentLast = FieldUtils.getAllFields(ClassWithSameFieldNames.class, false)
187+
.filter(field -> !field.isSynthetic())
188+
.collect(FieldUtils.collectByName(false));
189+
190+
// then
191+
assertThat(firstFieldsParentFirst.keySet(), contains("C1A", "c1b", "c2a", "c2b", "c3a", "c3b"));
192+
assertThat(firstFieldsParentFirst.values(), contains( c1a, c1b, c2a, c2b, c3a, c3b));
193+
assertThat(firstFieldsParentLast.keySet(), contains("c1b", "c2b", "c3a", "c3b", "c2a", "C1A"));
194+
assertThat(firstFieldsParentLast.values(), contains( x1b, x2b, c3a, c3b, c2a, c1a ));
195+
assertThat(lastFieldsParentFirst.keySet(), contains("C1A", "c1b", "c2a", "c2b", "c3a", "c3b"));
196+
assertThat(lastFieldsParentFirst.values(), contains( c1a, x1b, c2a, x2b, c3a, c3b ));
197+
assertThat(lastFieldsParentLast.keySet(), contains("c1b", "c2b", "c3a", "c3b", "c2a", "C1A"));
198+
assertThat(lastFieldsParentLast.values(), contains( c1b, c2b, c3a, c3b, c2a, c1a ));
199+
}
200+
201+
@Test
202+
void shouldHaveValidJavadoc_collectByName() throws NoSuchFieldException {
203+
// given
204+
Field x2b = ClassWithSameFieldNames.class.getDeclaredField("c2b");
205+
Field c2a = Class2.class.getDeclaredField("c2a");
206+
Field c2b = Class2.class.getDeclaredField("c2b");
207+
208+
209+
Stream<Field> stream = Stream.of(x2b, c2a, c2b);
210+
211+
// when
212+
Map<String, List<Field>> result = stream.collect(Collectors.groupingBy(Field::getName));
213+
214+
// then
215+
assertThat(result.keySet(), containsInAnyOrder("c2a", "c2b"));
216+
assertThat(result.get("c2a"), contains(c2a));
217+
assertThat(result.get("c2b"), contains(x2b, c2b));
218+
}
219+
168220
private class InnerClass {
169221
}
170222

0 commit comments

Comments
 (0)