Java 8中是否有一种简明的方式在流中迭代索引?

517

有没有一种简洁的方法在迭代流时访问流中的索引?

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

有更简洁的方法吗?

另外,似乎该zip文件已经移动或被删除...


3
intRange() 是什么?我在 Java 8 中还没有遇到过这个方法。 (翻译者注:原文中的 "accross" 应为 "across",已进行修正。) - Rohit Jain
@RohitJain 可能是 IntStream.rangeClosed(x, y) - assylias
2
作为旁注,我认为使用List<String> allCities = map.values().stream().flatMap(list -> list.stream()).collect(Collectors.toList());更好地完成挑战4。 - assylias
4
是的,zip 已经被移除了,还有实验性质的双值流,也称为 BiStreamMapStream。主要问题在于,为了有效地执行此操作,Java 确实需要一个结构类型的二元组(或元组)类型。由于缺乏这样一个类型,很容易创建一个通用的 Pair 或 Tuple 类 - 这已经做过很多次了 - 但它们都会被擦除到相同的类型。 - Stuart Marks
4
通用的Pair或Tuple类的另一个问题是,它需要将所有的基本类型进行装箱。 - Stuart Marks
显示剩余2条评论
26个回答

3
如果您不介意使用第三方库,Eclipse Collections提供了zipWithIndexforEachWithIndex可用于许多类型。以下是使用zipWithIndex解决此挑战的JDK类型和Eclipse Collections类型的一组解决方案。
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的提交者。

3

使用 https://github.com/poetix/protonpack,您可以压缩文件:

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

您可以使用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());

3

无法在迭代Stream时访问索引,因为Stream与任何Collection不同。如文档所述,Stream仅是一个从一个地方传输数据到另一个地方的管道:

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

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


4
可在其他语言/工具中使用。它只是一个递增的值,传递给映射函数。 - Lee Campbell
您提供的文档链接已失效。 - Usman Mutawakil

2

如果您想根据谓词获取索引,请尝试以下方法:

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

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

我真的很喜欢这个。我用它来处理通过索引相互链接的2个集合/数组:IntStream.range(0, list.size()).forEach(i -> list.get(i).setResult(resultArray[i])); - maximus

1

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

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

在流(stream)中使用匿名类(anonymous class)虽然非常有用,但并不常见。


1

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

1
你不一定需要一个地图,这是最接近LINQ示例的lambda表达式:
int[] idx = new int[] { 0 };
Stream.of(names)
    .filter(name -> name.length() <= idx[0]++)
    .collect(Collectors.toList());

请注意,如果您使用此解决方法,您应该知道自己在做什么,并避免在其他任何地方使用“idx”以避免意外行为。 - N4ppeL
@N4ppeL 增加 idx 相当简单,没有必要以任何其他方式访问 idx。目标是找到一个不那么笨拙的解决方案。 - Kaplan
1
为什么不使用AtomicInteger而要用数组呢? - OneCricketeer
@OneCricketeer 我想展示一个最接近LINQ的解决方案。改进,如在类中进行包装或使用原子整数-也是一个好主意-但可能会使其变得含混不清。 我认为原始帖子的作者可以自己完成这些改进。 - Kaplan

1
ArrayList result = new ArrayList()
for(int i = 0; i < names.length(); i++){
  if(names[i].length() < i+1) {
    result.add(names[i])
  }
}
return result;

拜托,遵循KISS原则(保持简单简洁...)。

这是编写它最高效、可读性和可预测性的方式。只需将其放入一个方法中,没有错误和不可预测性就可以愉快地使用。


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

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