在Java 8中,有没有一种简洁的方法可以迭代带有索引的流?


382

是否有一种简洁的方法可以在访问流中的索引的同时对流进行迭代?

String[] names = {"Sam","Pamela", "Dave", "Pascal", "Erik"};

List<String> nameList;
Stream<Integer> indices = intRange(1, names.length).boxed();
nameList = zip(indices, stream(names), SimpleEntry::new)
        .filter(e -> e.getValue().length() <= e.getKey())
        .map(Entry::getValue)
        .collect(toList());

与那里给出的LINQ示例相比,这似乎令人失望

string[] names = { "Sam", "Pamela", "Dave", "Pascal", "Erik" };
var nameList = names.Where((c, index) => c.Length <= index + 1).ToList();

有没有更简洁的方法?

此外,似乎拉链已移动或已被移除...


2
什么intRange()啊 到目前为止,在Java 8中还没有遇到过这种方法。
罗希特·贾因

@RohitJain可能是一个IntStream.rangeClosed(x, y)
assylias,2013年

2
List<String> allCities = map.values().stream().flatMap(list -> list.stream()).collect(Collectors.toList());
顺带

3
是的,zip已与各种称为BiStream或的实验性二值流一起被删除MapStream。主要问题是要有效地执行此操作,Java确实需要结构类型对(或元组)类型。缺少一个,创建一个通用的Pair或Tuple类很容易-已经完成了很多次-但它们都被擦除为相同的类型。
Stuart Marks 2013年

3
哦,通用的Pair或Tuple类的另一个问题是,它要求将所有原语装箱。
Stuart Marks

Answers:


435

最干净的方法是从索引流开始:

String[] names = {"Sam", "Pamela", "Dave", "Pascal", "Erik"};
IntStream.range(0, names.length)
         .filter(i -> names[i].length() <= i)
         .mapToObj(i -> names[i])
         .collect(Collectors.toList());

结果列表仅包含“ Erik”。


当您习惯于循环时,一种看起来更熟悉的替代方法是使用可变对象维护临时计数器,例如AtomicInteger

String[] names = {"Sam", "Pamela", "Dave", "Pascal", "Erik"};
AtomicInteger index = new AtomicInteger();
List<String> list = Arrays.stream(names)
                          .filter(n -> n.length() <= index.incrementAndGet())
                          .collect(Collectors.toList());

注意,在并行流上使用后一种方法可能会中断,因为不必“按顺序”处理项目


28
这种方式使用原子对于并行流是有问题的。首先,元素的处理顺序不一定与初始数组中元素出现的顺序相同。因此,使用原子分配的“索引”可能与实际的数组索引不匹配。其次,虽然原子是线程安全的,但您可能会在多个更新原子的线程之间遇到争用,从而降低了并行性。
斯图尔特·马克

1
我开发了一种类似于@assylias的解决方案。为了用提到的并行流@StuartMarks来解决问题,我首先使给定的并行流顺序执行,执行映射并恢复并行状态。 public static <T> Stream<Tuple2<Integer, T>> zipWithIndex(Stream<T> stream) { final AtomicInteger index = new AtomicInteger(); final Function<T, Tuple2<Integer, T>> zipper = e -> Tuples.of(index.getAndIncrement(), e); if (stream.isParallel()) { return stream.sequential().map(zipper).parallel(); } else { return stream.map(zipper); } }
Daniel Dietrich 2014年

4
@DanielDietrich如果您认为它可以解决问题,则应将其发布为答案而不是评论(并且代码也将更具可读性!)。
assylias 2014年

3
@DanielDietrich对不起,如果我正确阅读该代码,它将无法正常工作。您不能有并行运行的管道与顺序运行的管道的不同部分。终端操作开始时,仅接受parallel或接受最后一个sequential
斯图尔特(Stuart Marks)

4
为了公正起见,@ Stuart的回答中窃取了“最干净的方式”。
Vadzim

70

Java 8流API缺少获取流元素的索引以及将流压缩在一起的功能。这是不幸的,因为它使某些应用程序(如LINQ挑战)比其他情况下更加困难。

但是,通常有解决方法。通常,这可以通过“驱动”具有整数范围的流并利用原始元素通常位于数组或索引可访问的集合中这一事实来实现。例如,可以通过以下方式解决“挑战2”问题:

String[] names = {"Sam", "Pamela", "Dave", "Pascal", "Erik"};

List<String> nameList =
    IntStream.range(0, names.length)
        .filter(i -> names[i].length() <= i)
        .mapToObj(i -> names[i])
        .collect(toList());

正如我上面提到的,这利用了数据源(名称数组)可直接索引的事实。如果不是这样,则此技术将无效。

我承认这不能满足挑战2的意图。尽管如此,它确实可以有效地解决问题。

编辑

我之前的代码示例曾flatMap用于融合filter和map操作,但这很麻烦并且没有任何优势。根据Holger的评论,我已经更新了示例。


7
怎么IntStream.range(0, names.length).filter(i->names[i].length()<=i).mapToObj(i->names[i])样 它不需要拳击就可以工作……
Holger 2014年

1
嗯,是的,我为什么仍然需要使用flatMap
斯图尔特(Stuart)标记2014年

2
最终重新讨论一下……我可能使用过,flatMap因为它的排序将过滤和映射操作融合为一个操作,但这实际上没有任何优势。我将编辑示例。
Stuart Marks 2014年

Stream.of(Array)将为数组创建一个流接口。有效地使其Stream.of( names ).filter( n -> n.length() <= 1).collect( Collectors.toList() );减少拆箱和内存分配;因为我们不再创建范围流。
Code Eyez

44

从番石榴21开始,您可以使用

Streams.mapWithIndex()

示例(来自官方文档):

Streams.mapWithIndex(
    Stream.of("a", "b", "c"),
    (str, index) -> str + ":" + index)
) // will return Stream.of("a:0", "b:1", "c:2")

3
另外,番石榴公司的人们还没有实现forEachWithIndex(使用使用者而不是函数),但这是一个分配的问题:github.com/google/guava/issues/2913
John Glassmyer '18年

25

我在项目中使用了以下解决方案。我认为这比使用可变对象或整数范围更好。

import java.util.*;
import java.util.function.*;
import java.util.stream.Collector;
import java.util.stream.Collector.Characteristics;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static java.util.Objects.requireNonNull;


public class CollectionUtils {
    private CollectionUtils() { }

    /**
     * Converts an {@link java.util.Iterator} to {@link java.util.stream.Stream}.
     */
    public static <T> Stream<T> iterate(Iterator<? extends T> iterator) {
        int characteristics = Spliterator.ORDERED | Spliterator.IMMUTABLE;
        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, characteristics), false);
    }

    /**
     * Zips the specified stream with its indices.
     */
    public static <T> Stream<Map.Entry<Integer, T>> zipWithIndex(Stream<? extends T> stream) {
        return iterate(new Iterator<Map.Entry<Integer, T>>() {
            private final Iterator<? extends T> streamIterator = stream.iterator();
            private int index = 0;

            @Override
            public boolean hasNext() {
                return streamIterator.hasNext();
            }

            @Override
            public Map.Entry<Integer, T> next() {
                return new AbstractMap.SimpleImmutableEntry<>(index++, streamIterator.next());
            }
        });
    }

    /**
     * Returns a stream consisting of the results of applying the given two-arguments function to the elements of this stream.
     * The first argument of the function is the element index and the second one - the element value. 
     */
    public static <T, R> Stream<R> mapWithIndex(Stream<? extends T> stream, BiFunction<Integer, ? super T, ? extends R> mapper) {
        return zipWithIndex(stream).map(entry -> mapper.apply(entry.getKey(), entry.getValue()));
    }

    public static void main(String[] args) {
        String[] names = {"Sam", "Pamela", "Dave", "Pascal", "Erik"};

        System.out.println("Test zipWithIndex");
        zipWithIndex(Arrays.stream(names)).forEach(entry -> System.out.println(entry));

        System.out.println();
        System.out.println("Test mapWithIndex");
        mapWithIndex(Arrays.stream(names), (Integer index, String name) -> index+"="+name).forEach((String s) -> System.out.println(s));
    }
}

+1-能够实现一个功能,该功能使用StreamSupport.stream()自定义迭代器每N个索引“插入”一个元素。
2014年

13

除了protonpack之外,jOOλ的Seq还提供了此功能(以及构建在其上的扩展库,例如cyclops-react,我是该库的作者)。

Seq.seq(Stream.of(names)).zipWithIndex()
                         .filter( namesWithIndex -> namesWithIndex.v1.length() <= namesWithIndex.v2 + 1)
                         .toList();

Seq也仅支持Seq.of(names)并将在幕后构建一个JDK Stream。

类似的简单反应类似

 LazyFutureStream.of(names)
                 .zipWithIndex()
                 .filter( namesWithIndex -> namesWithIndex.v1.length() <= namesWithIndex.v2 + 1)
                 .toList();

简单反应版本更适合异步/并发处理。


约翰,我今天看到你的图书馆,我既惊讶又困惑。
GOXR3PLUS

12

为了完整起见,以下是涉及我的StreamEx库的解决方案:

String[] names = {"Sam","Pamela", "Dave", "Pascal", "Erik"};
EntryStream.of(names)
    .filterKeyValue((idx, str) -> str.length() <= idx+1)
    .values().toList();

在这里,我们创建了一个EntryStream<Integer, String>,扩展Stream<Entry<Integer, String>>并添加了一些特定的操作,例如filterKeyValuevalues。还使用toList()快捷方式。


做得好; 有捷径.forEach(entry -> {}) 吗?
史蒂夫·奥

2
@SteveOh,如果我理解您的问题正确,那么可以,可以写.forKeyValue((key, value) -> {})
塔吉尔·瓦列夫18'Mar

8

在创建列表或数组的流(并且知道大小)后,在这里找到了解决方案。但是,如果Stream的大小未知,该怎么办?在这种情况下,请尝试以下变体:

public class WithIndex<T> {
    private int index;
    private T value;

    WithIndex(int index, T value) {
        this.index = index;
        this.value = value;
    }

    public int index() {
        return index;
    }

    public T value() {
        return value;
    }

    @Override
    public String toString() {
        return value + "(" + index + ")";
    }

    public static <T> Function<T, WithIndex<T>> indexed() {
        return new Function<T, WithIndex<T>>() {
            int index = 0;
            @Override
            public WithIndex<T> apply(T t) {
                return new WithIndex<>(index++, t);
            }
        };
    }
}

用法:

public static void main(String[] args) {
    Stream<String> stream = Stream.of("a", "b", "c", "d", "e");
    stream.map(WithIndex.indexed()).forEachOrdered(e -> {
        System.out.println(e.index() + " -> " + e.value());
    });
}

6

有了清单,您可以尝试

List<String> strings = new ArrayList<>(Arrays.asList("First", "Second", "Third", "Fourth", "Fifth")); // An example list of Strings
strings.stream() // Turn the list into a Stream
    .collect(HashMap::new, (h, o) -> h.put(h.size(), o), (h, o) -> {}) // Create a map of the index to the object
        .forEach((i, o) -> { // Now we can use a BiConsumer forEach!
            System.out.println(String.format("%d => %s", i, o));
        });

输出:

0 => First
1 => Second
2 => Third
3 => Fourth
4 => Fifth

1
其实是个不错的主意,但是string :: indexOf可能有点贵。我的建议是改用:.collect(HashMap :: new,(h,s)-> h.put(h.size(),s),(h,s)-> {})。您可以简单地使用size()方法创建索引。
gil.fernandes

@ gil.fernandes谢谢您的建议。我将进行编辑。
V0idst4r

3

Stream由于a Stream与其他索引不同,因此无法在访问索引的同时进行迭代Collection。如文档中Stream所述,A 仅仅是用于将数据从一个地方传输到另一个地方的管道:

没有存储空间。流不是存储元素的数据结构。相反,它们通过计算操作流水线从源(可能是数据结构,生成器,IO通道等)中携带值。

当然,正如您在问题中所暗示的那样,您始终可以将转换Stream<V>Collection<V>,例如List<V>,可以在其中访问索引。


2
这可用其他语言/工具提供。这是简单的通过地图功能的增加值
李·坎贝尔

您到文档的链接已损坏。
Usman Mutawakil

3

使用https://github.com/poetix/protonpack,您 可以执行以下zip:

String[] names = {"Sam","Pamela", "Dave", "Pascal", "Erik"};

List<String> nameList;
Stream<Integer> indices = IntStream.range(0, names.length).boxed(); 

nameList = StreamUtils.zip(indices, stream(names),SimpleEntry::new)
        .filter(e -> e.getValue().length() <= e.getKey()).map(Entry::getValue).collect(toList());                   

System.out.println(nameList);

3

如果您不介意使用第三方库,则Eclipse Collections拥有zipWithIndex并且forEachWithIndex可以在多种类型中使用。这是针对使用JDK类型和Eclipse Collections类型的挑战的一系列解决方案zipWithIndex

String[] names = { "Sam", "Pamela", "Dave", "Pascal", "Erik" };
ImmutableList<String> expected = Lists.immutable.with("Erik");
Predicate<Pair<String, Integer>> predicate =
    pair -> pair.getOne().length() <= pair.getTwo() + 1;

// JDK Types
List<String> strings1 = ArrayIterate.zipWithIndex(names)
    .collectIf(predicate, Pair::getOne);
Assert.assertEquals(expected, strings1);

List<String> list = Arrays.asList(names);
List<String> strings2 = ListAdapter.adapt(list)
    .zipWithIndex()
    .collectIf(predicate, Pair::getOne);
Assert.assertEquals(expected, strings2);

// Eclipse Collections types
MutableList<String> mutableNames = Lists.mutable.with(names);
MutableList<String> strings3 = mutableNames.zipWithIndex()
    .collectIf(predicate, Pair::getOne);
Assert.assertEquals(expected, strings3);

ImmutableList<String> immutableNames = Lists.immutable.with(names);
ImmutableList<String> strings4 = immutableNames.zipWithIndex()
    .collectIf(predicate, Pair::getOne);
Assert.assertEquals(expected, strings4);

MutableList<String> strings5 = mutableNames.asLazy()
    .zipWithIndex()
    .collectIf(predicate, Pair::getOne, Lists.mutable.empty());
Assert.assertEquals(expected, strings5);

这是使用的解决方案forEachWithIndex

MutableList<String> mutableNames =
    Lists.mutable.with("Sam", "Pamela", "Dave", "Pascal", "Erik");
ImmutableList<String> expected = Lists.immutable.with("Erik");

List<String> actual = Lists.mutable.empty();
mutableNames.forEachWithIndex((name, index) -> {
        if (name.length() <= index + 1)
            actual.add(name);
    });
Assert.assertEquals(expected, actual);

如果将lambda更改为上面的匿名内部类,那么所有这些代码示例也将在Java 5-7中运行。

注意:我是Eclipse Collections的提交者


2

如果碰巧使用Vavr(以前称为Javaslang),则可以利用专用方法:

Stream.of("A", "B", "C")
  .zipWithIndex();

如果我们打印出内容,我们将看到一些有趣的东西:

Stream((A, 0), ?)

这是因为Streams很懒,我们对流中的下一个项目一无所知。


1

如果您尝试获取基于谓词的索引,请尝试以下操作:

如果您只关心第一个索引:

OptionalInt index = IntStream.range(0, list.size())
    .filter(i -> list.get(i) == 3)
    .findFirst();

或者,如果您要查找多个索引:

IntStream.range(0, list.size())
   .filter(i -> list.get(i) == 3)
   .collect(Collectors.toList());

.orElse(-1);如果找不到该值,请添加以防万一。


1

这是AbacusUtil的代码

Stream.of(names).indexed()
      .filter(e -> e.value().length() <= e.index())
      .map(Indexed::value).toList();

披露:我是AbacusUtil的开发人员。


1

您可以IntStream.iterate()用来获取索引:

String[] names = {"Sam","Pamela", "Dave", "Pascal", "Erik"};
List<String> nameList = IntStream.iterate(0, i -> i < names.length, i -> i + 1)
        .filter(i -> names[i].length() <= i)
        .mapToObj(i -> names[i])
        .collect(Collectors.toList());

这仅适用于Java 8或更高版本的Java 9,您可以使用以下命令:

String[] names = {"Sam","Pamela", "Dave", "Pascal", "Erik"};
List<String> nameList = IntStream.iterate(0, i -> i + 1)
        .limit(names.length)
        .filter(i -> names[i].length() <= i)
        .mapToObj(i -> names[i])
        .collect(Collectors.toList());

0

您可以按照下面的示例中的操作,创建一个静态内部类来封装索引器:

static class Indexer {
    int i = 0;
}

public static String getRegex() {
    EnumSet<MeasureUnit> range = EnumSet.allOf(MeasureUnit.class);
    StringBuilder sb = new StringBuilder();
    Indexer indexer = new Indexer();
    range.stream().forEach(
            measureUnit -> {
                sb.append(measureUnit.acronym);
                if (indexer.i < range.size() - 1)
                    sb.append("|");

                indexer.i++;
            }
    );
    return sb.toString();
}

0

这个问题(获取与布尔值匹配的第一个元素的索引的流方式)已将当前问题标记为重复,因此我无法在此处回答。我在这里回答。

这是获取不需要外部库的匹配索引的通用解决方案。

如果您有清单。

public static <T> int indexOf(List<T> items, Predicate<T> matches) {
        return IntStream.range(0, items.size())
                .filter(index -> matches.test(items.get(index)))
                .findFirst().orElse(-1);
}

并这样称呼它:

int index = indexOf(myList, item->item.getId()==100);

如果使用的是收藏夹,请尝试使用此收藏夹。

   public static <T> int indexOf(Collection<T> items, Predicate<T> matches) {
        int index = -1;
        Iterator<T> it = items.iterator();
        while (it.hasNext()) {
            index++;
            if (matches.test(it.next())) {
                return index;
            }
        }
        return -1;
    }

0

一种可能的方法是索引流中的每个元素:

AtomicInteger index = new AtomicInteger();
Stream.of(names)
  .map(e->new Object() { String n=e; public i=index.getAndIncrement(); })
  .filter(o->o.n.length()<=o.i) // or do whatever you want with pairs...
  .forEach(o->System.out.println("idx:"+o.i+" nam:"+o.n));

在流中使用匿名类虽然没有用,但却非常有用。


0

您不需要 map 一定
是最接近LINQ示例的lambda:

int[] idx = new int[] { 0 };
Stream.of( names ).filter( name -> name.length() <= idx[0]++ ).collect( Collectors.toList() );

0
String[] namesArray = {"Sam","Pamela", "Dave", "Pascal", "Erik"};
String completeString
         =  IntStream.range(0,namesArray.length)
           .mapToObj(i -> namesArray[i]) // Converting each array element into Object
           .map(String::valueOf) // Converting object to String again
           .collect(Collectors.joining(",")); // getting a Concat String of all values
        System.out.println(completeString);

输出:Sam,Pamela,Dave,Pascal,Erik

String[] namesArray = {"Sam","Pamela", "Dave", "Pascal", "Erik"};

IntStream.range(0,namesArray.length)
               .mapToObj(i -> namesArray[i]) // Converting each array element into Object
               .map(String::valueOf) // Converting object to String again
               .forEach(s -> {
                //You can do various operation on each element here
                System.out.println(s);
               }); // getting a Concat String of all 

收集到列表中:

String[] namesArray = {"Sam","Pamela", "Dave", "Pascal", "Erik"};
 List<String> namesList
                =  IntStream.range(0,namesArray.length)
                .mapToObj(i -> namesArray[i]) // Converting each array element into Object
                .map(String::valueOf) // Converting object to String again
                .collect(Collectors.toList()); // collecting elements in List
        System.out.println(listWithIndex);

上述问题的解决方案应该是List包含一个元素Erik
卡普兰

我还添加了一个示例以收集到列表中。
阿潘·塞尼

0

正如jean-baptiste-yunès所说,如果您的流是基于Java List的,则使用AtomicInteger,并且使用其invokeAndGet方法可以很好地解决此问题,并且只要您返回的整数确实与原始List中的索引相对应不要使用并行流。


0

如果您需要forEach中的索引,那么这提供了一种方法。

  public class IndexedValue {

    private final int    index;
    private final Object value;

    public IndexedValue(final int index, final Object value) { 
        this.index = index;
        this.value = value;
    }

    public int getIndex() {
        return index;
    }

    public Object getValue() {
        return value;
    }
}

然后按如下方式使用它。

@Test
public void withIndex() {
    final List<String> list = Arrays.asList("a", "b");
    IntStream.range(0, list.size())
             .mapToObj(index -> new IndexedValue(index, list.get(index)))
             .forEach(indexValue -> {
                 System.out.println(String.format("%d, %s",
                                                  indexValue.getIndex(),
                                                  indexValue.getValue().toString()));
             });
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.