I have a list of objects. I want to do a groupBy so that there is:
and One Group that is neither of the above.
I could do:
for (Object obj: myList) {
if (obj instanceof Long) || (obj instanceof String) {
// add to sublist1
} else if (obj instanceof Map) {
// sublist2
} else {
// sublist3
}
}
How do I this using Java 8?
When I'm confronted with a problem like this, I usually approach it "inside-out", that is, I think of little nuggets of logic that seem like they're likely to be useful. Then, I build up toward the full problem via composition.
For example, you're asking for a way to distinguish objects according to whether they are instances of one of a set of classes. (For the set of Integer, Long, String, and Boolean I'm assuming you mean primitive-like classes, not numbers.) This suggests to me a function that takes an object and a set of such classes and determines whether the object is an instance of any of them. Here's what that would look like:
boolean instanceOfAny(Object obj, Set<Class<?>> set) {
return set.stream().anyMatch(clazz -> clazz.isInstance(obj));
}
Not by coincidence, this function is shaped like a Predicate<Object>
. You could call it like this:
if (instanceOfAny(obj, Set.of(Integer.class, Long.class)) { ...
The thing is, we have several of these predicates, and we're going to want to group things by which predicate matches. This suggests a list of predicates. You wanted primitive-like classes in one group and Maps in a second group. That list would look like this:
List<Predicate<Object>> predicates = List.of(
obj -> instanceOfAny(obj, Set.of(Integer.class, Long.class, String.class, Boolean.class)),
obj -> instanceOfAny(obj, Set.of(Map.class)));
There's a question about how to handle the "other" cases. You could put a predicate obj -> true
at the end, which would guarantee a match. But the code that matches these predicates would still have a case where no predicate matched. You could throw an assertion error there, or just bake into the code that if no predicate matches, the item is put into another group.
Since we have a list of predicates, a natural value on which to group them is the index of the predicate in the list. We can somewhat arbitrarily assign a value to the "not-matched" group. Since it's listed last in the problem statement, I'll assign "not-matched" an index one beyond the last index in the list.
Given an object, we could write a loop over the list and call each predicate on the object in turn. But we want to use streams and lambdas, so let's do that instead. (I actually think it comes out quite nicely using streams anyway.) Here's how to do that, using the old IntStream-over-list-indices trick:
int grouper(Object obj) {
return IntStream.range(0, predicates.size())
.filter(i -> predicates.get(i).test(obj))
.findFirst()
.orElse(predicates.size());
}
Here, we stream over the list indices and call each predicate from a filter
operation. This gives us the indices of the predicates that matched. We only want the first, so we use findFirst
. This gives us an OptionalInt
, which is empty if none of the predicates matched, so we substitute the list size in that case.
Now, let's throw some input at it:
List<Object> input = List.of(
true, 1, 2L, "asdf", Map.of("a", "b"), new BigInteger("23456"),
Map.of(3, 4), List.of("x", "y", "z"), false, 17, 'q');
To process the input, we use this short stream:
Map<Integer, List<Object>> result = input.stream().collect(groupingBy(this::grouper));
result.forEach((k, v) -> System.out.println(k + " => " + v));
The output is:
0 => [true, 1, 2, asdf, false, 17]
1 => [{a=b}, {3=4}]
2 => [23456, [x, y, z], q]
Putting it all together, we have the following:
boolean instanceOfAny(Object obj, Set<Class<?>> set) {
return set.stream().anyMatch(clazz -> clazz.isInstance(obj));
}
List<Predicate<Object>> predicates = List.of(
obj -> instanceOfAny(obj, Set.of(Integer.class, Long.class, String.class, Boolean.class)),
obj -> instanceOfAny(obj, Set.of(Map.class)));
int grouper(Object obj) {
return IntStream.range(0, predicates.size())
.filter(i -> predicates.get(i).test(obj))
.findFirst()
.orElse(predicates.size());
}
void main() {
List<Object> input = List.of(
true, 1, 2L, "asdf", Map.of("a", "b"), new BigInteger("23456"),
Map.of(3, 4), List.of("x", "y", "z"), false, 17, 'q');
Map<Integer, List<Object>> result =
input.stream().collect(groupingBy(this::grouper));
result.forEach((k, v) -> System.out.println(k + " => " + v));
}
Something like this?
public static void main(String[] args) {
List<Object> test = new ArrayList<>();
test.add(2L);
test.add("me");
test.add(new Object());
Set<Class<?>> left = Set.of(Integer.class, Long.class, String.class, Boolean.class);
Map<Boolean, List<Object>> map =
test.stream()
.collect(Collectors.partitioningBy(
x -> left.contains(x.getClass())
));
// add these to subList1
map.get(Boolean.TRUE).forEach(System.out::println);
System.out.println("=====");
map.get(Boolean.FALSE).forEach(System.out::println);
}
EDIT
Unfortunately for more types, things get complicated. Mainly because, as you say, you have an instanceOf
check against Map
for example; but they way I showed you above would not work. As such you could build an extensive search, for example:
enum Type {
ONE,
TWO,
THREE
}
static final Map<Class<?>, Type> MAP = Map.of(
Long.class, Type.ONE,
Integer.class, Type.ONE,
String.class, Type.ONE,
Boolean.class, Type.ONE,
Map.class, Type.TWO
);
And use it in the form:
public static void main(String[] args) {
List<Object> test = new ArrayList<>();
test.add(2L);
test.add("me");
test.add(new HashMap<>());
test.add(new Object());
Map<Type, List<Object>> map =
test.stream()
.collect(Collectors.groupingBy(
x -> {
Type t = MAP.get(x.getClass());
if (t == null) {
for (Entry<Class<?>, Type> entry : MAP.entrySet()) {
if (entry.getKey().isAssignableFrom(x.getClass())) {
return entry.getValue();
}
}
return Type.THREE;
} else {
return t;
}
}
));
map.get(Type.ONE).forEach(System.out::println);
System.out.println("=====");
map.get(Type.TWO).forEach(System.out::println);
System.out.println("=====");
map.get(Type.THREE).forEach(System.out::println);
}
Is this what you wanted?
enum Categories {
NUMBER, MAP, OTHER
};
Map<String,Long> m = new HashMap<>();
m.put("Value", 23L);
LinkedHashMap<Long, Short> lhm = new LinkedHashMap<>();
lhm.put(23L, (short)23);
LocalDate date = LocalDate.now();
Object[] obs = {2.2f, 12.5, 2, 23L, m,date,
lhm, true, false, "This is a string"};
String types = "LongFloatDoubleInteger";
Map<Categories, List<Object>> map = Stream.of(obs)
.collect(Collectors.groupingBy( a-> {
String t = a.getClass().getSimpleName();
if (types.contains(t)) {
return Categories.NUMBER;
} else if(t.contains("Map")) {
return Categories.MAP;
} else {
return Categories.OTHER;
}},Collectors.toList()));
for(Categories type : map.keySet()) {
System.out.println(type + " -> " + map.get(type));
}
It prints
MAP -> [{Value=23}, {23=23}]
NUMBER -> [2.2, 12.5, 2, 23]
OTHER -> [2020-03-13, true, false, This is a string]
You can categorize them however you want.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With