1. 概述

Java Stream不是集合,无法通过索引直接访问元素。但仍有几种技巧可以实现带索引的遍历。

本文将介绍四种主流方案:使用IntStreamStreamUtilsEntryStream和Vavr的Stream实现带索引遍历。

2. 使用原生Java

通过IntStream创建索引范围,结合原始数组/集合的索引访问能力,可实现带索引遍历。

以下示例从字符串数组中筛选偶数索引元素:

public List<String> getEvenIndexedStrings(String[] names) {
    List<String> evenIndexedNames = IntStream
      .range(0, names.length)
      .filter(i -> i % 2 == 0)
      .mapToObj(i -> names[i])
      .collect(Collectors.toList());
    
    return evenIndexedNames;
}

测试验证:

@Test
public void whenCalled_thenReturnListOfEvenIndexedStrings() {
    String[] names 
      = {"Afrim", "Bashkim", "Besim", "Lulzim", "Durim", "Shpetim"};
    List<String> expectedResult 
      = Arrays.asList("Afrim", "Besim", "Durim");
    List<String> actualResult 
      = StreamIndices.getEvenIndexedStrings(names);
   
    assertEquals(expectedResult, actualResult);
}

3. 使用StreamUtils

通过proton-pack库的StreamUtils.zipWithIndex()方法实现(最新版本)。

先添加依赖:

<dependency>
    <groupId>com.codepoetics</groupId>
    <artifactId>protonpack</artifactId>
    <version>1.16</version>
</dependency>

实现代码:

public List<Indexed<String>> getEvenIndexedStrings(List<String> names) {
    List<Indexed<String>> list = StreamUtils
      .zipWithIndex(names.stream())
      .filter(i -> i.getIndex() % 2 == 0)
      .collect(Collectors.toList());
    
    return list;
}

测试用例:

@Test
public void whenCalled_thenReturnListOfEvenIndexedStrings() {
    List<String> names = Arrays.asList(
      "Afrim", "Bashkim", "Besim", "Lulzim", "Durim", "Shpetim");
    List<Indexed<String>> expectedResult = Arrays.asList(
      Indexed.index(0, "Afrim"), 
      Indexed.index(2, "Besim"), 
      Indexed.index(4, "Durim"));
    List<Indexed<String>> actualResult 
      = StreamIndices.getEvenIndexedStrings(names);
    
    assertEquals(expectedResult, actualResult);
}

4. 使用StreamEx

利用StreamEx库的EntryStream.filterKeyValue()实现(最新版本)。

添加依赖:

<dependency>
    <groupId>one.util</groupId>
    <artifactId>streamex</artifactId>
    <version>0.8.1</version>
</dependency>

实现示例:

public List<String> getEvenIndexedStringsVersionTwo(List<String> names) {
    return EntryStream.of(names)
      .filterKeyValue((index, name) -> index % 2 == 0)
      .values()
      .toList();
}

测试验证:

@Test
public void whenCalled_thenReturnListOfEvenIndexedStringsVersionTwo() {
    String[] names 
      = {"Afrim", "Bashkim", "Besim", "Lulzim", "Durim", "Shpetim"};
    List<String> expectedResult 
      = Arrays.asList("Afrim", "Besim", "Durim");
    List<String> actualResult 
      = StreamIndices.getEvenIndexedStrings(names);
   
   assertEquals(expectedResult, actualResult);
}

5. 使用Vavr的Stream

通过Vavr(原Javaslang)的Stream.zipWithIndex()方法实现。

添加依赖:

<dependency>    
     <groupId>io.vavr</groupId>    
     <artifactId>vavr</artifactId>    
    <version>0.10.4</version>    
</dependency>

实现代码:

public List<String> getOddIndexedStringsVersionTwo(String[] names) {
    return Stream
      .of(names)
      .zipWithIndex()
      .filter(tuple -> tuple._2 % 2 == 1)
      .map(tuple -> tuple._1)
      .toJavaList();
}

测试用例:

@Test
public void whenCalled_thenReturnListOfOddStringsVersionTwo() {
    String[] names 
      = {"Afrim", "Bashkim", "Besim", "Lulzim", "Durim", "Shpetim"};
    List<String> expectedResult 
      = Arrays.asList("Bashkim", "Lulzim", "Shpetim");
    List<String> actualResult 
      = StreamIndices.getOddIndexedStringsVersionTwo(names);

    assertEquals(expectedResult, actualResult);
}

✅ 想深入了解Vavr?可参考这篇文章

6. 使用AtomicInteger

通过AtomicInteger维护可变计数器,在lambda表达式中跟踪索引。仅适用于顺序流

public List<String> getEvenIndexedStringsUsingAtomicInteger(String[] names) {
    AtomicInteger index = new AtomicInteger(0);
    return Arrays.stream(names)
      .filter(name -> index.getAndIncrement() % 2 == 0)
      .collect(Collectors.toList());
}

顺序流测试:

@Test
public void whenCalledSequentially_thenReturnListOfEvenIndexedStrings() {
    String[] names = {"Afrim", "Bashkim", "Besim", "Lulzim", "Durim", "Shpetim"};
    List<String> expectedResult = Arrays.asList("Afrim", "Besim", "Durim");
    List<String> actualResult = StreamIndices.getEvenIndexedStringsUsingAtomicInteger(names);
    
    assertEquals(expectedResult, actualResult);
}

⚠️ 并行流踩坑警告
AtomicInteger在并行流中会导致竞态条件!元素处理顺序不可预测,索引分配会错乱:

public List<String> getEvenIndexedStringsAtomicIntegerParallel(String[] names) {
    AtomicInteger index = new AtomicInteger(0);
    return Arrays.stream(names)
      .parallel()  // 切换为并行流
      .filter(name -> index.getAndIncrement() % 2 == 0)
      .collect(Collectors.toList());
}

并行流测试(预期失败):

@Test
public void whenCalledInParallel_thenResultInconsistent() {
    String[] names = {"Afrim", "Bashkim", "Besim", "Lulzim", "Durim", "Shpetim"};
    List<String> result = StreamIndices.getEvenIndexedStringsAtomicIntegerParallel(names);
    
    // 并行模式下可能间歇性失败
    assertNotEquals(Arrays.asList("Afrim", "Besim", "Durim"), result); 
}

💡 并行流解决方案:改用IntStream.range()避免竞态条件。

7. 总结

本文介绍了四种带索引遍历Stream的方案:

  1. 原生Java + IntStream
  2. StreamUtils.zipWithIndex()
  3. EntryStream.filterKeyValue()
  4. Vavr的Stream.zipWithIndex()

选择建议

  • ✅ 简单场景:原生方案足够
  • ✅ 需要索引对象:选StreamUtils或Vavr
  • ❌ 避免在并行流中使用AtomicInteger

Stream作为Java 8的核心特性,掌握这些技巧能显著提升处理效率。更多Stream特性可参考Baeldung


原始标题:How to Iterate Over a Stream With Indices | Baeldung