Partition a Stream by a discriminator function

前端 未结 3 673
有刺的猬
有刺的猬 2020-12-05 05:19

One of the missing features in the Streams API is the \"partition by\" transformation, for example as defined in Clojure. Say I want to reproduce Hibernate\'s fetch join

相关标签:
3条回答
  • 2020-12-05 05:33

    The solution requires us to define a custom Spliterator which can be used to construct the partitioned stream. We shall need to access the input stream through its own spliterator and wrap it into ours. The output stream is then constructed from our custom spliterator.

    The following Spliterator will turn any Stream<E> into a Stream<List<E>> provided a Function<E, ?> as the discriminator function. Note that the input stream must be ordered for this operation to make sense.

    public class PartitionBySpliterator<E> extends AbstractSpliterator<List<E>> {
      private final Spliterator<E> spliterator;
      private final Function<? super E, ?> partitionBy;
      private HoldingConsumer<E> holder;
      private Comparator<List<E>> comparator;
    
      public PartitionBySpliterator(Spliterator<E> toWrap, Function<? super E, ?> partitionBy) {
        super(Long.MAX_VALUE, toWrap.characteristics() & ~SIZED | NONNULL);
        this.spliterator = toWrap;
        this.partitionBy = partitionBy;
      }
    
      public static <E> Stream<List<E>> partitionBy(Function<E, ?> partitionBy, Stream<E> in) {
        return StreamSupport.stream(new PartitionBySpliterator<>(in.spliterator(), partitionBy), false);
      }
    
      @Override public boolean tryAdvance(Consumer<? super List<E>> action) {
        final HoldingConsumer<E> h;
        if (holder == null) {
          h = new HoldingConsumer<>();
          if (!spliterator.tryAdvance(h)) return false;
          holder = h;
        }
        else h = holder;
        final ArrayList<E> partition = new ArrayList<>();
        final Object partitionKey = partitionBy.apply(h.value);
        boolean didAdvance;
        do partition.add(h.value);
        while ((didAdvance = spliterator.tryAdvance(h))
            && Objects.equals(partitionBy.apply(h.value), partitionKey));
        if (!didAdvance) holder = null;
        action.accept(partition);
        return true;
      }
    
      static final class HoldingConsumer<T> implements Consumer<T> {
        T value;
        @Override public void accept(T value) { this.value = value; }
      }
    
      @Override public Comparator<? super List<E>> getComparator() {
        final Comparator<List<E>> c = this.comparator;
        return c != null? c : (this.comparator = comparator());
      }
    
      private Comparator<List<E>> comparator() {
        @SuppressWarnings({"unchecked","rawtypes"})
        final Comparator<? super E> innerComparator =
            Optional.ofNullable(spliterator.getComparator())
                    .orElse((Comparator) naturalOrder());
        return (left, right) -> {
          final int c = innerComparator.compare(left.get(0), right.get(0));
          return c != 0? c : innerComparator.compare(
              left.get(left.size() - 1), right.get(right.size() - 1));
        };
      }
    }
    
    0 讨论(0)
  • 2020-12-05 05:34

    For those of you who just want to partition a stream, there are mappers and collectors for that.

    class Person {
    
        String surname;
        String forename;
    
        public Person(String surname, String forename) {
            this.surname = surname;
            this.forename = forename;
        }
    
        @Override
        public String toString() {
            return forename;
        }
    
    }
    
    class Family {
    
        String surname;
        List<Person> members;
    
        public Family(String surname, List<Person> members) {
            this.surname = surname;
            this.members = members;
        }
    
        @Override
        public String toString() {
            return "Family{" + "surname=" + surname + ", members=" + members + '}';
        }
    
    }
    
    private void test() {
        String[][] data = {
            {"Kray", "Ronald"},
            {"Kray", "Reginald"},
            {"Dors", "Diana"},};
        // Their families.
        Stream<Family> families = Arrays.stream(data)
                // Build people
                .map(a -> new Person(a[0], a[1]))
                // Collect into a Map<String,List<Person>> as families
                .collect(Collectors.groupingBy(p -> p.surname))
                // Convert them to families.
                .entrySet().stream()
                .map(p -> new Family(p.getKey(), p.getValue()));
        families.forEach(f -> System.out.println(f));
    }
    
    0 讨论(0)
  • 2020-12-05 05:34

    It can be done by collapse with StreamEx

    StreamEx.of(queryBuilder.createQuery(
        "SELECT f.name, m.name"
        + " FROM Family f JOIN Member m on m.family_id = f.id"
        + " ORDER BY f.name"))
            .collapse((a, b) -> a.string(0).equals(b.string(0)), Collectors.toList())
            .map(l -> new Family(l.get(0).string(0), StreamEx.of(l).map(r -> r.string(1)).toList())) 
            .forEach(System.out::println);
    
    0 讨论(0)
提交回复
热议问题