Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Split a list up into 3 sub lists Java 8+ [closed]

I have a list of objects. I want to do a groupBy so that there is:

  • One Group for Number types e.g. Integer, Long, Strings or Booleans
  • One Group for Maps

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?

like image 693
More Than Five Avatar asked Mar 13 '20 19:03

More Than Five


3 Answers

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));
}
like image 68
Stuart Marks Avatar answered Nov 19 '22 04:11

Stuart Marks


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);
}
like image 1
Eugene Avatar answered Nov 19 '22 04:11

Eugene


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.

like image 1
WJS Avatar answered Nov 19 '22 02:11

WJS