Streams API中缺少的一个功能是“分区依据”转换,例如Clojure中定义的。假设我想重现Hibernate的获取连接:我想发出一个SQL SELECT语句来从结果中接收这种对象:
class Family {
String surname;
List<String> members;
}
我发出:
SELECT f.name, m.name
FROM Family f JOIN Member m on m.family_id = f.id
ORDER BY f.name
我检索了(f.name, m.name)
条记录的平面流。现在我需要将它转换为Family
个对象的流,并在其中包含其成员列表。假设我已经有Stream<ResultRow>
;现在我需要将其转换为Stream<List<ResultRow>>
,然后使用映射转换对其进行处理,将其转换为Stream<Family>
。
转换的语义如下:只要提供的鉴别器函数保持返回相同的值,就继续将流收集到List
中;一旦值发生变化,将List
作为输出流的元素发出,并开始收集新的List
。
我希望能够编写这种代码(我已经有resultStream
方法):
Stream<ResultRow> dbStream = resultStream(queryBuilder.createQuery(
"SELECT f.name, m.name"
+ " FROM Family f JOIN Member m on m.family_id = f.id"
+ " ORDER BY f.name"));
Stream<List<ResultRow> partitioned = partitionBy(r -> r.string(0), dbStream);
Stream<Family> = partitioned.map(rs -> {
Family f = new Family(rs.get(0).string(0));
f.members = rs.stream().map(r -> r.string(1)).collect(toList());
return f;
});
毋庸置疑,我希望得到的流保持延迟(非物化),因为我希望能够处理任何大小的结果集而不会遇到任何O(n)内存限制。如果没有这个关键要求,我会对提供的groupingBy
收集器感到满意。
答案 0 :(得分:12)
该解决方案要求我们定义可用于构造分区流的自定义Spliterator
。我们需要通过自己的spliterator访问输入流并将其包装到我们的。然后从我们的自定义分裂器构造输出流。
以下Spliterator会将Stream<E>
变为Stream<List<E>>
,并提供Function<E, ?>
作为鉴别器功能。请注意,必须为此操作订购输入流才有意义。
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));
};
}
}
答案 1 :(得分:1)
对于那些只想对流进行分区的人,可以使用映射器和收集器。
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));
}
答案 2 :(得分:0)
可以collapse
使用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);