在Java 8中将列表拆分为具有固定元素数量的多个列表

12

我希望能找到类似于 Scala grouped 函数的东西。基本上,每次挑选两个元素进行处理。这里有一个相关的参考链接:

将列表拆分为具有固定元素数量的多个列表

Lambda 提供了类似于 groupingBy 和 partitioningBy 的东西,但似乎没有像 Scala 中 grouped 函数一样的功能。欢迎提供任何指导。

6个回答

27

您可以使用Guava库。

List<Integer> bigList = ... List<List<Integer>> smallerLists = Lists.partition(bigList, 10);


18

听起来这个问题最好像Stream操作一样处理,就像Stream API本身提供的操作一样。一个(相对)简单的解决方案可能是:

public static <T> Stream<List<T>> chunked(Stream<T> s, int chunkSize) {
    if(chunkSize<1) throw new IllegalArgumentException("chunkSize=="+chunkSize);
    if(chunkSize==1) return s.map(Collections::singletonList);
    Spliterator<T> src=s.spliterator();
    long size=src.estimateSize();
    if(size!=Long.MAX_VALUE) size=(size+chunkSize-1)/chunkSize;
    int ch=src.characteristics();
    ch&=Spliterator.SIZED|Spliterator.ORDERED|Spliterator.DISTINCT|Spliterator.IMMUTABLE;
    ch|=Spliterator.NONNULL;
    return StreamSupport.stream(new Spliterators.AbstractSpliterator<List<T>>(size, ch)
    {
        private List<T> current;
        @Override
        public boolean tryAdvance(Consumer<? super List<T>> action) {
            if(current==null) current=new ArrayList<>(chunkSize);
            while(current.size()<chunkSize && src.tryAdvance(current::add));
            if(!current.isEmpty()) {
                action.accept(current);
                current=null;
                return true;
            }
            return false;
        }
    }, s.isParallel());
}

简单测试:

chunked(Stream.of(1, 2, 3, 4, 5, 6, 7), 3)
  .parallel().forEachOrdered(System.out::println);

优点在于您不需要完整的所有项目集进行后续流处理,例如。

chunked(
    IntStream.range(0, 1000).mapToObj(i -> {
        System.out.println("processing item "+i);
        return i;
    }), 2).anyMatch(list->list.toString().equals("[6, 7]")));

将会打印:

processing item 0
processing item 1
processing item 2
processing item 3
processing item 4
processing item 5
processing item 6
processing item 7
true

不要处理一千个 IntStream.range(0, 1000) 的项目。这也使得可以使用无限的源 Stream

chunked(Stream.iterate(0, i->i+1), 2).anyMatch(list->list.toString().equals("[6, 7]")));

如果你对一个完全实现的集合感兴趣,而不是应用后续的Stream操作,你可以简单地使用以下操作:

List<Integer> list=Arrays.asList(1, 2, 3, 4, 5, 6, 7);
int listSize=list.size(), chunkSize=2;
List<List<Integer>> list2=
    IntStream.range(0, (listSize-1)/chunkSize+1)
             .mapToObj(i->list.subList(i*=chunkSize,
                                       listSize-chunkSize>=i? i+chunkSize: listSize))
             .collect(Collectors.toList());

一旦tryAdvance返回false,它以后总是会返回false,那么为什么需要在调用之间缓存列表呢?在正常使用情况下,这意味着Spliterator在消耗完列表后始终保持对其的引用。 - Marko Topolnik
1
@Marko Topolnik:老实说,我不记得了。也许我遇到了一个没有正确行为的Spliterator,也许这是以前实现尝试或forEachRemaining方法的产物...但它在消耗后不保留引用,因为它明确地被设置为null - Holger
我已经考虑到了这一点:每当它要返回“true”时,它都会将其置空,但最后一次调用总是返回“false”,在这种情况下列表被保留。 - Marko Topolnik
1
@Marko Topolnik:但是这样列表就是空的,没有被使用。我会看看是否能找出这样做的原因,否则我会进行编辑... - Holger
2
@Lyubomyr Shaydariv:确实,这是一个不同的问题。这个答案的重点是提供一个返回Stream的操作,可以用于链接更多的Stream操作,保持惰性。collect操作是一个终端操作,启动实际处理。这样的Collector应该是可能的,我很确定,在SO上已经存在这样的解决方案。 - Holger
显示剩余5条评论

1
您可以创建自己的收集器。就像这样:
class GroupingCollector<T> implements Collector<T, List<List<T>>, List<List<T>>> {
    private final int elementCountInGroup;

    public GroupingCollector(int elementCountInGroup) {
        this.elementCountInGroup = elementCountInGroup;
    }

    @Override
    public Supplier<List<List<T>>> supplier() {
        return ArrayList::new;
    }

    @Override
    public BiConsumer<List<List<T>>, T> accumulator() {
        return (lists, integer) -> {
            if (!lists.isEmpty()) {
                List<T> integers = lists.get(lists.size() - 1);
                if (integers.size() < elementCountInGroup) {
                    integers.add(integer);
                    return;
                }
            }

            List<T> list = new ArrayList<>();
            list.add(integer);
            lists.add(list);
        };
    }

    @Override
    public BinaryOperator<List<List<T>>> combiner() {
        return (lists, lists2) -> {
            List<List<T>> r = new ArrayList<>();
            r.addAll(lists);
            r.addAll(lists2);
            return r;
        };
    }

    @Override
    public Function<List<List<T>>, List<List<T>>> finisher() {
        return lists -> lists;
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Collections.emptySet();
    }
}

然后你可以像这样使用它:

    List<List<Integer>> collect = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).collect(new GroupingCollector<>(3));
    System.out.println(collect);

将会打印:

[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]


这个收集器在并行流中的工作是不正确的,因此combiner()实现是无用的(最好抛出UnsupportedOperationException)。如果不知道源元素的索引,则无法对此任务执行有效的并行收集。 - Tagir Valeev
是的,我知道。作者没有提到并行性。 - Ιναη ßαbαηιη

1
将列表转换为列表的递归解决方案也是可能的。
int chunkSize = 2;

private <T> List<List<T>> process(List<T> list) {
    if (list.size() > chunkSize) {
        List<T> chunk = list.subList(0, chunkSize);
        List<T> rest = list.subList(chunkSize, list.size());
        List<List<T>> lists = process(rest);
        return concat(chunk, lists);
    } else {
        ArrayList<List<T>> retVal = new ArrayList<>();
        retVal.add(list);
        return retVal;
    }
}

private <T> List<List<T>> concat(List<T> chunk, List<List<T>> rest) {
    rest.add(0, chunk);
    return rest;
}

我还没有尝试过。目前我已经使用了上面的解决方案。无论如何,还是谢谢你。 - vamosrafa

0

你可以编写自己的收集器完成器,类似于

final List<String> strings = Arrays.asList("Hello", "World", "I", "Am", "You");
final int size = 3;

final List<List<String>> stringLists = strings.stream()
        .collect(Collectors.collectingAndThen(Collectors.toList(), new Function<List<String>, List<List<String>>>() {
            @Override
            public List<List<String>> apply(List<String> strings) {
                final List<List<String>> result = new ArrayList<>();
                int counter = 0;
                List<String> stringsToAdd = new ArrayList<>();

                for (final String string : strings) {
                    if (counter == 0) {
                        result.add(stringsToAdd);
                    } else {
                        if (counter == size) {
                            stringsToAdd = new ArrayList<>();
                            result.add(stringsToAdd);
                            counter = 0;
                        }
                    }

                    ++counter;
                    stringsToAdd.add(string);
                }

                return result;
            }
        }));

System.out.println("stringLists = " + stringLists); // stringLists = [[Hello, World, I], [Am, You]]

谢谢回复。我已经按照这些线路做了一些事情..只是想知道,使用lambda是否是最好的选择?我在想是否有更优雅的方法来完成这个任务.. - vamosrafa

0

使用Java 8 Streams API的简单版本:

static <T> List<List<T>> partition(List<T> list, Integer partitionSize) {
    int numberOfLists = BigDecimal.valueOf(list.size())
        .divide(BigDecimal.valueOf(partitionSize), 0, CEILING)
        .intValue();

    return IntStream.range(0, numberOfLists)
        .mapToObj(it -> list.subList(it * partitionSize, Math.min((it+1) * partitionSize, list.size())))
        .collect(Collectors.toList());
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接