Splitting List into sublists along elements

后端 未结 13 1725
野趣味
野趣味 2020-11-28 21:51

I have this list (List):

[\"a\", \"b\", null, \"c\", null, \"d\", \"e\"]

And I\'d like something like this:

相关标签:
13条回答
  • 2020-11-28 22:30

    In my StreamEx library there's a groupRuns method which can help you to solve this:

    List<String> input = Arrays.asList("a", "b", null, "c", null, "d", "e");
    List<List<String>> result = StreamEx.of(input)
            .groupRuns((a, b) -> a != null && b != null)
            .remove(list -> list.get(0) == null).toList();
    

    The groupRuns method takes a BiPredicate which for the pair of adjacent elements returns true if they should be grouped. After that we remove groups containing nulls and collect the rest to the List.

    This solution is parallel-friendly: you may use it for parallel stream as well. Also it works nice with any stream source (not only random access lists like in some other solutions) and it's somewhat better than collector-based solutions as here you can use any terminal operation you want without intermediate memory waste.

    0 讨论(0)
  • 2020-11-28 22:31

    Well, after a bit of work U have come up with a one-line stream-based solution. It ultimately uses reduce() to do the grouping, which seemed the natural choice, but it was a bit ugly getting the strings into the List<List<String>> required by reduce:

    List<List<String>> result = list.stream()
      .map(Arrays::asList)
      .map(x -> new LinkedList<String>(x))
      .map(Arrays::asList)
      .map(x -> new LinkedList<List<String>>(x))
      .reduce( (a, b) -> {
        if (b.getFirst().get(0) == null) 
          a.add(new LinkedList<String>());
        else
          a.getLast().addAll(b.getFirst());
        return a;}).get();
    

    It is however 1 line!

    When run with input from the question,

    System.out.println(result);
    

    Produces:

    [[a, b], [c], [d, e]]
    
    0 讨论(0)
  • 2020-11-28 22:32

    Group by different token whenever you find a null (or separator). I used here a different integer (used atomic just as holder)

    Then remap the generated map to transform it into a list of lists.

    AtomicInteger i = new AtomicInteger();
    List<List<String>> x = Stream.of("A", "B", null, "C", "D", "E", null, "H", "K")
          .collect(Collectors.groupingBy(s -> s == null ? i.incrementAndGet() : i.get()))
          .entrySet().stream().map(e -> e.getValue().stream().filter(v -> v != null).collect(Collectors.toList()))
          .collect(Collectors.toList());
    
    System.out.println(x);
    
    0 讨论(0)
  • 2020-11-28 22:34

    With String one can do:

    String s = ....;
    String[] parts = s.split("sth");
    

    If all sequential collections (as the String is a sequence of chars) had this abstraction this could be doable for them too:

    List<T> l = ...
    List<List<T>> parts = l.split(condition) (possibly with several overloaded variants)
    

    If we restrict the original problem to List of Strings (and imposing some restrictions on it's elements contents) we could hack it like this:

    String als = Arrays.toString(new String[]{"a", "b", null, "c", null, "d", "e"});
    String[] sa = als.substring(1, als.length() - 1).split("null, ");
    List<List<String>> res = Stream.of(sa).map(s -> Arrays.asList(s.split(", "))).collect(Collectors.toList());
    

    (please don't take it seriously though :))

    Otherwise, plain old recursion also works:

    List<List<String>> part(List<String> input, List<List<String>> acc, List<String> cur, int i) {
        if (i == input.size()) return acc;
        if (input.get(i) != null) {
            cur.add(input.get(i));
        } else if (!cur.isEmpty()) {
            acc.add(cur);
            cur = new ArrayList<>();
        }
        return part(input, acc, cur, i + 1);
    }
    

    (note in this case null has to be appended to the input list)

    part(input, new ArrayList<>(), new ArrayList<>(), 0)
    
    0 讨论(0)
  • 2020-11-28 22:35

    The only solution I come up with for the moment is by implementing your own custom collector.

    Before reading the solution, I want to add a few notes about this. I took this question more as a programming exercise, I'm not sure if it can be done with a parallel stream.

    So you have to be aware that it'll silently break if the pipeline is run in parallel.

    This is not a desirable behavior and should be avoided. This is why I throw an exception in the combiner part (instead of (l1, l2) -> {l1.addAll(l2); return l1;}), as it's used in parallel when combining the two lists, so that you have an exception instead of a wrong result.

    Also this is not very efficient due to list copying (although it uses a native method to copy the underlying array).

    So here's the collector implementation:

    private static Collector<String, List<List<String>>, List<List<String>>> splitBySeparator(Predicate<String> sep) {
        final List<String> current = new ArrayList<>();
        return Collector.of(() -> new ArrayList<List<String>>(),
            (l, elem) -> {
                if (sep.test(elem)) {
                    l.add(new ArrayList<>(current));
                    current.clear();
                }
                else {
                    current.add(elem);
                }
            },
            (l1, l2) -> {
                throw new RuntimeException("Should not run this in parallel");
            },
            l -> {
                if (current.size() != 0) {
                    l.add(current);
                    return l;
                }
            );
    }
    

    and how to use it:

    List<List<String>> ll = list.stream().collect(splitBySeparator(Objects::isNull));
    

    Output:

    [[a, b], [c], [d, e]]
    


    As the answer of Joop Eggen is out, it appears that it can be done in parallel (give him credit for that!). With that it reduces the custom collector implementation to:

    private static Collector<String, List<List<String>>, List<List<String>>> splitBySeparator(Predicate<String> sep) {
        return Collector.of(() -> new ArrayList<List<String>>(Arrays.asList(new ArrayList<>())),
                            (l, elem) -> {if(sep.test(elem)){l.add(new ArrayList<>());} else l.get(l.size()-1).add(elem);},
                            (l1, l2) -> {l1.get(l1.size() - 1).addAll(l2.remove(0)); l1.addAll(l2); return l1;});
    }
    

    which let the paragraph about parallelism a bit obsolete, however I let it as it can be a good reminder.


    Note that the Stream API is not always a substitute. There are tasks that are easier and more suitable using the streams and there are tasks that are not. In your case, you could also create a utility method for that:

    private static <T> List<List<T>> splitBySeparator(List<T> list, Predicate<? super T> predicate) {
        final List<List<T>> finalList = new ArrayList<>();
        int fromIndex = 0;
        int toIndex = 0;
        for(T elem : list) {
            if(predicate.test(elem)) {
                finalList.add(list.subList(fromIndex, toIndex));
                fromIndex = toIndex + 1;
            }
            toIndex++;
        }
        if(fromIndex != toIndex) {
            finalList.add(list.subList(fromIndex, toIndex));
        }
        return finalList;
    }
    

    and call it like List<List<String>> list = splitBySeparator(originalList, Objects::isNull);.

    It can be improved for checking edge-cases.

    0 讨论(0)
  • 2020-11-28 22:37

    Please do not vote. I do not have enough place to explain this in comments.

    This is a solution with a Stream and a foreach but this is strictly equivalent to Alexis's solution or a foreach loop (and less clear, and I could not get rid of the copy constructor) :

    List<List<String>> result = new ArrayList<>();
    final List<String> current = new ArrayList<>();
    list.stream().forEach(s -> {
          if (s == null) {
            result.add(new ArrayList<>(current));
            current.clear();
          } else {
            current.add(s);
          }
        }
    );
    result.add(current);
    
    System.out.println(result);
    

    I understand that you want to find a more elegant solution with Java 8 but I truly think that it has not been designed for this case. And as said by Mr spoon, highly prefer the naive way in this case.

    0 讨论(0)
提交回复
热议问题