Java stream reduce

后端 未结 4 2562
南旧
南旧 2021-02-20 11:53

I have the following example data set that I want to transform / reduce using Java stream api based on direction\'s value

Direction    int[]
IN           1, 2
O         


        
相关标签:
4条回答
  • 2021-02-20 12:30

    How about this. First define a small helper method:

    private static Tuple mergeTwo(Tuple left, Tuple right) {
        int[] leftArray = left.getData();
        int[] rightArray = right.getData();
        int[] result = new int[leftArray.length + rightArray.length];
        System.arraycopy(leftArray, 0, result, 0, leftArray.length);
        System.arraycopy(rightArray, 0, result, leftArray.length, rightArray.length);
        return new Tuple(left.getDirection(), result);
    }
    

    This is close to your concat/merge I guess, but a single one. Basically a way to merge two Tuple(s) together.

    And a helper method to produce the needed Collector, you can put this into a utility so that it can be re-used:

    private static Collector<Tuple, ?, List<Tuple>> mergedTuplesCollector() {
        class Acc {
    
            ArrayDeque<Tuple> deque = new ArrayDeque<>();
    
            void add(Tuple elem) {
                Tuple head = deque.peek();
                if (head == null || head.getDirection() != elem.getDirection()) {
                    deque.offerFirst(elem);
                } else {
                    deque.offerFirst(mergeTwo(deque.poll(), elem));
                }
            }
    
            Acc merge(Acc right) {
    
                Tuple lastLeft = deque.peekLast();
                Tuple firstRight = right.deque.peekFirst();
    
                if (lastLeft.getDirection() == firstRight.getDirection()) {
                    deque.offerLast(mergeTwo(deque.pollLast(), right.deque.pollFirst()));
                } else {
                    deque.addAll(right.deque);
                }
    
                return this;
            }
    
            public List<Tuple> finisher() {
                return new ArrayList<>(deque);
            }
    
        }
        return Collector.of(Acc::new, Acc::add, Acc::merge, Acc::finisher);
    }
    

    And usage would be, for example:

    List<Tuple> merged = tuples.stream()
                .parallel()
                .collect(mergedTuplesCollector());
    
    0 讨论(0)
  • 2021-02-20 12:32

    I've got two ideas on this topic. First one is getting the indices like in this answer and group it accordingly.

    The second idea - if you already got a Stream a custom Collector should be used (similar to the other solutions, though using Deque):

    private Collector<Tuple, ?, List<Tuple>> squashTuples() {
      return new Collector<Tuple, Deque<Tuple>, List<Tuple>>() {
        @Override
        public Supplier<Deque<Tuple>> supplier() {
          return ArrayDeque::new;
        }
    
        @Override
        public BiConsumer<Deque<Tuple>, Tuple> accumulator() {
          return (acc, e) -> {
            Objects.requireNonNull(e);
            if (!acc.isEmpty() && acc.peekLast().getDirection() == e.getDirection()) {
              acc.offerLast(acc.pollLast().merge(e));
            } else {
              acc.offerLast(e);
            }
          };
        }
    
        @Override
        public BinaryOperator<Deque<Tuple>> combiner() {
          return (left, right) -> {
            if (!left.isEmpty() && !right.isEmpty() && left.peekLast().getDirection() == right.peekFirst().getDirection()) {
              left.offerLast(left.pollLast().merge(right.pollFirst()));
            }
            left.addAll(right);
            return left;
          };
        }
    
        @Override
        public Function<Deque<Tuple>, List<Tuple>> finisher() {
          return ArrayList::new;
        }
    
        @Override
        public Set<Characteristics> characteristics() {
          return EnumSet.noneOf(Characteristics.class);
        }
      };
    }
    
    0 讨论(0)
  • 2021-02-20 12:34

    This is an alternative approach that uses slightly different data structures.

    If this is an option, changing from int[] to List<Integer> allows for more flexibility (not to mention avoiding creating/copying arrays multiple times):

    class Tuple {
        Direction direction;
        List<Integer> data;
    }
    

    And the following function does the merging on a Deque collection:

    private static List<Integer> next(Deque<Tuple> t, Direction d) {
        if (!t.isEmpty() && t.peekLast().getDirection() == d) {
            return t.peekLast().getData();
        } else {
            Tuple next = new Tuple();
            next.direction = d;
            next.data = new ArrayList<>();
            t.addLast(next);
            return next.data;
        }
    }
    

    And with that, the stream can look as simple as:

    Deque<Tuple> deq = new LinkedList<>(); //the final collection of tuples
    
    tuples.stream()
    .flatMap(tp -> tp.getData().stream()
                     .map(d -> Pair.of(tp.getDirection(), Integer.valueOf(d))))
    .forEach(el -> next(deq, el.getLeft()).add(el.getRight()));
    
    0 讨论(0)
  • 2021-02-20 12:39

    I think your solution is pretty nice already, especially as using a reduction enables parallelism easily compared to collecting into a shared outside container. But it's easier to use collect instead of reduce as Holger pointed out. Furthermore, the conditions in the accumulator can be simplified a bit, and you forgot to merge the last and first elements in the combiner:

    List<Tuple> reduce = tupleStream.collect(ArrayList::new, WDParser::add, WDParser::combine);
    
    private static List<Tuple> combine(List<Tuple> list1, List<Tuple> list2)
    {
        if (!list2.isEmpty())
        {
            add(list1, list2.remove(0)); // merge lists in the middle if necessary
            list1.addAll(list2);         // add all the rest
        }
        return list1;
    }
    
    private static List<Tuple> add(List<Tuple> list, Tuple t)
    {
        int lastIndex = list.size() - 1;
        if (list.isEmpty() || list.get(lastIndex).getDirection() != t.getDirection())
        {
            list.add(t);
        }
        else
        {
            list.set(lastIndex, list.get(lastIndex).merge(t));
        }
        return list;
    }
    

    Instead of using indexes to access the first/last element you could even use LinkedList and the methods add/removeFirst/Last().

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