Is there a concise way to iterate over a stream with indices in Java 8?

后端 未结 22 2306
天命终不由人
天命终不由人 2020-11-22 01:42

Is there a concise way to iterate over a stream whilst having access to the index in the stream?

String[] names = {\"Sam\",\"Pamela\", \"Dave\", \"Pascal\",          


        
相关标签:
22条回答
  • 2020-11-22 02:04

    If you don't mind using a third-party library, Eclipse Collections has zipWithIndex and forEachWithIndex available for use across many types. Here's a set of solutions to this challenge for both JDK types and Eclipse Collections types using 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);
    

    Here's a solution using forEachWithIndex instead.

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

    If you change the lambdas to anonymous inner classes above, then all of these code examples will work in Java 5 - 7 as well.

    Note: I am a committer for Eclipse Collections

    0 讨论(0)
  • 2020-11-22 02:06

    The Java 8 streams API lacks the features of getting the index of a stream element as well as the ability to zip streams together. This is unfortunate, as it makes certain applications (like the LINQ challenges) more difficult than they would be otherwise.

    There are often workarounds, however. Usually this can be done by "driving" the stream with an integer range, and taking advantage of the fact that the original elements are often in an array or in a collection accessible by index. For example, the Challenge 2 problem can be solved this way:

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

    As I mentioned above, this takes advantage of the fact that the data source (the names array) is directly indexable. If it weren't, this technique wouldn't work.

    I'll admit that this doesn't satisfy the intent of Challenge 2. Nonetheless it does solve the problem reasonably effectively.

    EDIT

    My previous code example used flatMap to fuse the filter and map operations, but this was cumbersome and provided no advantage. I've updated the example per the comment from Holger.

    0 讨论(0)
  • 2020-11-22 02:06

    If you are trying to get an index based on a predicate, try this:

    If you only care about the first index:

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

    Or if you want to find multiple indexes:

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

    Add .orElse(-1); in case you want to return a value if it doesn't find it.

    0 讨论(0)
  • 2020-11-22 02:10

    I've used the following solution in my project. I think it is better than using mutable objects or integer ranges.

    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));
        }
    }
    
    0 讨论(0)
  • 2020-11-22 02:10

    You can create a static inner class to encapsulate the indexer as I needed to do in example below:

    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 讨论(0)
  • 2020-11-22 02:11

    In addition to protonpack, jOOλ's Seq provides this functionality (and by extension libraries that build on it like cyclops-react, I am the author of this library).

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

    Seq also supports just Seq.of(names) and will build a JDK Stream under the covers.

    The simple-react equivalent would similarly look like

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

    The simple-react version is more tailored for asynchronous / concurrent processing.

    0 讨论(0)
提交回复
热议问题