在Java 8中是否有一种简洁的方法来迭代带有索引的流?

是否有一种简洁的方法在流上迭代,同时访问流中的索引?

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();

有更简洁的方式吗?

此外,似乎拉链已经移动或被拆除…

390296 次浏览

没有办法在访问索引的同时遍历Stream,因为Stream不同于任何CollectionStream只是一个将数据从一个地方传输到另一个地方的管道,如文档所述:

没有存储。流不是存储元素的数据结构;相反,它们通过计算操作管道携带来自源(可以是数据结构、生成器、IO通道等)的值。

当然,就像你在问题中暗示的那样,你总是可以将你的Stream<V>转换为Collection<V>,比如List<V>,这样你就可以访问索引。

最简洁的方法是从一系列指数开始:

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”。


当你习惯for循环时,另一种看起来更熟悉的方法是使用可变对象维护一个临时计数器,例如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());

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

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来融合过滤器和映射操作,但这很麻烦,没有任何好处。我已经根据Holger的评论更新了示例。

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

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));
}
}

With 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);

除了质子包,jOOλSeq也提供了这个功能(通过扩展库,如cyclops-react,我是这个库的作者)。

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

Seq还只支持Seq.of(names),并将在幕后构建一个JDK流。

简单反应的等效物看起来类似

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

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

为了完整起见,这里是涉及我的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()快捷方式。

你可以创建一个静态的内部类来封装索引器,就像我在下面的例子中需要做的那样:

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();
}

如果你不介意使用第三方库,Eclipse集合zipWithIndexforEachWithIndex可供多种类型使用。下面是针对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);

如果您将上述lambdas更改为匿名内部类,那么所有这些代码示例都可以在Java 5 - 7中工作。

注意:我是Eclipse集合的提交者

下面是AbacusUtil的代码

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

披露:我是AbacusUtil的开发者。

使用列表,你可以尝试一下

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

自番石榴21起,就可以使用了

Streams.mapWithIndex()

示例(来自官方文档):

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

这个问题(获取第一个匹配布尔值的元素索引的方法)已经将当前问题标记为重复,所以我不能在那里回答它;我在这里回答。

下面是获得匹配索引的通用解决方案,不需要外部库。

如果你有一个清单。

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;
}

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

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

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

Stream((A, 0), ?)

这是因为Streams是懒惰的,我们不知道流中的下一项。

我在这里找到了解决方案,当流创建的列表或数组(你知道的大小)。但是如果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());
});
}

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

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));

在流中使用匿名类虽然非常有用,但却没有得到很好的使用。

如果你试图获得一个基于谓词的索引,试试这个:

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

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);,以防你想在没有找到值的情况下返回值。

你可以使用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 9以上的Java 8,你可以使用这个:

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());
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);

输出:山姆,帕梅拉,戴夫,帕斯卡,埃里克

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);

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

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

正如jean-baptiste-yunès所说,如果您的流是基于java列表的,那么使用AtomicInteger及其incrementAndGet方法是一个很好的解决问题的方法,并且返回的整数与原始列表中的索引对应,只要您不使用并行流。

如果您需要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()));
});
}

下面是标准Java的解决方案:

在线解决方案:

Arrays.stream("zero,one,two,three,four".split(","))
.map(new Function<String, Map.Entry<Integer, String>>() {
int index;


@Override
public Map.Entry<Integer, String> apply(String s) {
return Map.entry(index++, s);
}
})
.forEach(System.out::println);

更可读的解决方案与实用方法:

static <T> Function<T, Map.Entry<Integer, T>> mapWithIntIndex() {
return new Function<T, Map.Entry<Integer, T>>() {
int index;


@Override
public Map.Entry<Integer, T> apply(T t) {
return Map.entry(index++, t);
}
};
}


...
Arrays.stream("zero,one,two,three,four".split(","))
.map(mapWithIntIndex())
.forEach(System.out::println);

如果列表是唯一的,我们可以使用indexOf方法。

List<String> names = Arrays.asList("Sam", "Pamela", "Dave", "Pascal", "Erik");


names.forEach(name ->{
System.out.println((names.indexOf(name) + 1) + ": " + name);
});