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个回答

576
最干净的方式是从索引流开始:
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());

注意在并行流上使用后一种方法可能会导致错误,因为项目不一定会按顺序进行处理

48
使用原子操作在并行流中存在问题。首先,元素的处理顺序可能与它们在初始数组中出现的顺序不同。因此,使用原子赋值的“索引”可能与实际的数组索引不匹配。其次,虽然原子操作是线程安全的,但多个线程同时更新原子操作可能会产生争用,从而降低并行性能。 - Stuart Marks
4
如果您认为它解决了问题,那么您应该将其发布为答案而不是评论(代码也会更易读!)。 - assylias
3
抱歉,如果我正确理解那段代码的话,它是行不通的。你无法同时运行管道的不同部分以并行或顺序方式。只有在终端操作开始时,parallelsequential中的最后一个会被执行。 - Stuart Marks
7
为了公正起见,“最干净的方式”这个词汇是从@Stuart的回答中抄袭来的。 - Vadzim
4
毫不冒犯地说:从函数式编程的角度来看,这是一个非常糟糕的解决方案。Java 应该能够做得更好。我们只需要让流 API 允许一个双变量回调函数即可。 - Jonathan Benn
显示剩余6条评论

96

Java 8的流API缺少获取流元素索引以及合并流的功能。这很不幸,因为它使得某些应用(如LINQ挑战)比本来更加困难。

然而,通常可以通过使用整数范围“驱动”流,并利用原始元素通常在数组或可通过索引访问的集合中的事实来实现解决办法。例如,Challenge 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的评论更新了示例。


8
IntStream.range(0, names.length).filter(i->names[i].length()<=i).mapToObj(i->names[i]) 这样怎么样?它可以正常工作而不需要装箱... - Holger
1
嗯,是啊,我为什么觉得需要使用flatMap呢? - Stuart Marks
2
最后重新审视一下这个问题...我可能使用了flatMap,因为它将过滤和映射操作合并成了一个单独的操作,但实际上并没有提供任何优势。我会编辑这个例子。 - Stuart Marks
Stream.of(Array)将为数组创建一个流接口。有效地将其转换为Stream.of(names).filter(n-> n.length()<= 1)。collect(Collectors.toList());减少了取消装箱和内存分配;因为我们不再创建范围流。 - Code Eyez

74

自从guava 21版本以后,你可以使用

Streams.mapWithIndex()

示例(摘自官方文档):

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

4
此外,Guava的开发者们尚未实现forEachWithIndex(使用消费者而不是函数),但这是一个已指派的问题:https://github.com/google/guava/issues/2913。 - John Glassmyer
2
那个Guava问题似乎仍然没有解决 :-( - Brian Agnew

29

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

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个索引处"插入"一个元素。 - ach

14

除了 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流。

简单的React等效代码类似于

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

simple-react版本更适合异步/并发处理。


13

为了完整起见,这里提供使用我的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-> {}) 的快捷方式? - Steve Oh
2
@SteveOh 如果我理解你的问题正确的话,那么是的,你可以写.forKeyValue((key, value) -> {}) - Tagir Valeev

12

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

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

9

使用列表,您可以尝试

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

3
这个想法不错,但是strings::indexOf可能有点昂贵。我的建议是改用:*.collect(HashMap::new, (h, s) -> h.put(h.size(), s), (h, s) -> {})* 。您可以简单地使用size()方法创建索引。 - gil.fernandes
@gil.fernandes 感谢您的建议,我会进行修改。 - V0idst4r

6

如果您使用Vavr(之前称为Javaslang),您可以利用专用方法:

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

如果我们打印出内容,将会看到一些有趣的东西:
Stream((A, 0), ?)

这是因为是惰性的,我们对流中下一个项目没有任何线索。

5
这是由 abacus-common 提供的代码。
Stream.of(names).indexed()
      .filter(e -> e.value().length() <= e.index())
      .map(Indexed::value).toList();

声明:我是abacus-common的开发者。


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