Java 8:如何使用lambda将列表转换为列表的列表

10
我想把一个列表分成多个列表,每个列表的最大大小为4。
我想知道如何使用lambda函数实现这个目标。
目前我正在按以下方式进行操作:
List<List<Object>> listOfList = new ArrayList<>();

final int MAX_ROW_LENGTH = 4;
int startIndex =0;
while(startIndex <= listToSplit.size() )    
{
    int endIndex = ( ( startIndex+MAX_ROW_LENGTH ) <  listToSplit.size() ) ? startIndex+MAX_ROW_LENGTH : listToSplit.size();
    listOfList.add(new ArrayList<>(listToSplit.subList(startIndex, endIndex)));
    startIndex = startIndex+MAX_ROW_LENGTH;
}

更新

似乎没有一种简单的方法可以使用lambda函数来分割列表。虽然感谢所有答案,但它们也是当lambda函数不能简化事情的绝佳例子。


7
需要注意的是,这可以通过使用Lists.partition(origList, 3);来完成。无需使用Lambda表达式(但需要使用Guava库)。 - Carcigenicate
2
这不是直接回答你的问题,但是当你放弃 Lambda 要求后,使用 Guava 的 Iterables.partition(list, size) 可以让你立即获得乐趣。 - Artur Biesiadowski
2
@azro 对于数组列表,.size() 具有常量访问权限,不是吗?我希望对它的调用非常便宜。它可能只是获取一个私有字段。 - Carcigenicate
2
此外,为什么需要lambda?即使使用Clojure,它广泛使用高阶函数,使用lambda也没有意义。只需使用(partition size your-list)即可。您希望lambda用于什么? - Carcigenicate
3
希望你不要因为lambda函数很潮而随意使用它。你可以尝试使用,但不要滥用lambda函数去完成其本身不适合的任务。把你写的代码放进一个方法里,然后调用这个方法即可。 - KarelG
显示剩余8条评论
6个回答

6
尝试这种方法:
static <T> List<List<T>> listSplitter(List<T> incoming, int size) {
    // add validation if needed
    return incoming.stream()
            .collect(Collector.of(
                    ArrayList::new,
                    (accumulator, item) -> {
                        if(accumulator.isEmpty()) {
                            accumulator.add(new ArrayList<>(singletonList(item)));
                        } else {
                            List<T> last = accumulator.get(accumulator.size() - 1);
                            if(last.size() == size) {
                                accumulator.add(new ArrayList<>(singletonList(item)));
                            } else {
                                last.add(item);
                            }
                        }
                    },
                    (li1, li2) -> {
                        li1.addAll(li2);
                        return li1;
                    }
            ));
}
System.out.println(
        listSplitter(
                Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9),
                4
        )
);

请注意,这段代码可以进行优化,而不是:
new ArrayList<>(Collections.singletonList(item))

使用这个:

List<List<T>> newList = new ArrayList<>(size);
newList.add(item);
return newList;

1
如果您将Collector提取到单独的类/变量/字段中,此代码可重复使用。 - alex.b
1
这很不错,完全符合 OP 的要求。但是我对它进行了分析,发现它比明显的非 lambda 子列表抓取 for 循环慢了约 5 倍。 - slim
是的... 我尝试过 JMH 分析器,但它更慢。但是作者是否注意到性能很重要?几乎所有基于流的操作都更慢,但它们被引入是为了使代码更易读、可链接和简单并行化。如果将此 Collector 提取为单独的内容,则可以在链接操作中使用,例如java.util.stream.Collectors#collectingAndThen - alex.b

5
如果你真的需要一个lambda函数,可以这样做。否则,前面的答案更好。
    List<List<Object>> lists = new ArrayList<>();
    AtomicInteger counter = new AtomicInteger();
    final int MAX_ROW_LENGTH = 4;
    listToSplit.forEach(pO -> {
        if(counter.getAndIncrement() % MAX_ROW_LENGTH == 0) {
            lists.add(new ArrayList<>());
        }
        lists.get(lists.size()-1).add(pO);
    });

谢谢@Jotunacom,我其实不需要一个lambda。我只是想知道是否有一种使用lambda的简单方法来完成它。如果没有一种简单直接的方法来使用lambda做事情,那也没关系。我碰巧喜欢它们,并且我知道它们并不是万能的解决方案。无论如何,感谢您的回答。 - Lucas T

3
当然,以下内容足够了。
final List<List<Object>> listOfList = new ArrayList<>(
            listToSplit.stream()
                    .collect(Collectors.groupingBy(el -> listToSplit.indexOf(el) / MAX_ROW_LENGTH))
                    .values()
    );

使用流和分组进行收集:这将给出一个对象映射到列表的Map,提取地图的值并直接传递到任何构造函数(map.values()给出的是一个集合而不是列表)。


这看起来很简单,但我猜indexOf的复杂度是O(n),如果初始列表很大,这可能会很糟糕。 - Prateek Thakur

2

2
要求有点奇怪,但你可以这样做:
final int[] counter = new int[] {0};

List<List<Object>> listOfLists = in.stream()
   .collect(Collectors.groupingBy( x -> counter[0]++ / MAX_ROW_LENGTH ))
   .entrySet().stream()
   .sorted(Map.Entry.comparingByKey())
   .map(Map.Entry::getValue)
   .collect(Collectors.toList());

您可以使用带有mapSupplier Lambda的groupingBy变体,并提供一个SortedMap来简化此过程。应返回按顺序迭代的EntrySet。我将其留给您作为一个练习。
我们正在做的是:
  • 使用计数器将您的列表项收集到一个Map<Integer,Object>中进行分组。 计数器保存在单元素数组中,因为Lambda只能使用final本地变量。
  • 将地图条目作为流获取,并按Integer键排序。
  • 使用Stream :: map()Map.Entry<Integer,Object>流转换为Object值流。
  • 将其收集到列表中。
这不会从任何“免费”并行处理中受益。它在中间Map中具有内存开销。它不是特别容易阅读的代码。
然而,我不会仅仅为了使用Lambda而这样做。 我会做一些类似于:
for(int i=0; i<in.size(); i += MAX_ROW_LENGTH) {
    listOfList.add(
        listToSplit.subList(i, Math.min(i + MAX_ROW_LENGTH, in.size());
}

您的代码使用了防御性拷贝:new ArrayList<>(listToSplit.subList(...))。我没有复制它,因为它并不总是必要的——例如,如果输入列表是不可修改的且输出列表也不打算进行修改。但如果您决定在您的情况下需要它,请将它放回去。

这对于任何内存中的列表都非常快。您很可能不想并行化它。


或者,您可以编写自己的(不可修改的)List实现,它是基于底层List<Object>的视图:

public class PartitionedList<T> extends AbstractList<List<T>> {

    private final List<T> source;
    private final int sublistSize;

    public PartitionedList(T source, int sublistSize) {
       this.source = source;
       this.sublistSize = sublistSize;
    }

    @Override
    public int size() {
       return source.size() / sublistSize;
    }

    @Override
    public List<T> get(int index) {
       int sourceIndex = index * sublistSize
       return source.subList(sourceIndex, 
                             Math.min(sourceIndex + sublistSize, source.size());
    }
}

这里是否需要进行防御性复制取决于你。

这将具有与底层列表相同的大O访问时间。


1
另一个SO答案使用Spliterator实现了类似于我的PartitionedList的功能:https://dev59.com/vF8e5IYBdhLWcg3wwMhW#25507602 - slim

2
也许你可以使用类似的东西。
 BiFunction<List,Integer,List> splitter= (list2, count)->{
            //temporary list of lists
            List<List> listOfLists=new ArrayList<>();

            //helper implicit recursive function
            BiConsumer<Integer,BiConsumer> splitterHelper = (offset, func) -> {
                if(list2.size()> offset+count){
                    listOfLists.add(list2.subList(offset,offset+count));

                    //implicit self call
                    func.accept(offset+count,func);
                }
                else if(list2.size()>offset){
                    listOfLists.add(list2.subList(offset,list2.size()));

                    //implicit self call
                    func.accept(offset+count,func);
                }
            };

            //pass self reference
            splitterHelper.accept(0,splitterHelper);

            return listOfLists;
        };

使用示例

List<Integer> list=new ArrayList<Integer>(){{
            add(1);
            add(2);
            add(3);
            add(4);
            add(5);
            add(6);
            add(7);
            add(8);
            add(8);
        }};

        //calling splitter function
        List listOfLists = splitter.apply(list, 3 /*max sublist size*/);

        System.out.println(listOfLists);

因此,我们拥有了

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

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