Есть ли краткий способ перебора потока с индексами в 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();
Есть ли более лаконичный способ?
Далее кажется, что почтовый индекс либо сдвинулся, либо был удален...
26 ответов
Самый чистый способ - начать с потока индексов:
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());
Полученный список содержит только "Эрик".
Одна из альтернатив, которая выглядит более знакомой, когда вы привыкли к циклам 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());
Обратите внимание, что использование последнего метода в параллельном потоке может прерваться, поскольку элементы не обязательно будут обрабатываться "по порядку".
В API потоков Java 8 отсутствуют функции получения индекса элемента потока, а также возможность объединения потоков. Это прискорбно, так как делает некоторые приложения (например, задачи 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
объединить операции фильтра и карты, но это было громоздко и не дало никаких преимуществ. Я обновил пример согласно комментарию от Хольгера.
Начиная с гуавы 21, вы можете использовать
Streams.mapWithIndex()
Пример (из официального документа):
Streams.mapWithIndex(
Stream.of("a", "b", "c"),
(str, index) -> str + ":" + index)
) // will return Stream.of("a:0", "b:1", "c:2")
Я использовал следующее решение в моем проекте. Я думаю, что это лучше, чем использование изменяемых объектов или целочисленных диапазонов.
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));
}
}
В дополнение к protonpack, SeQ в jOOλs обеспечивает эту функциональность (и благодаря библиотекам расширений, которые основаны на нем, как cyclops-реагировать, я являюсь автором этой библиотеки).
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>>
и добавляет некоторые конкретные операции, такие как filterKeyValue
или же values
, Также toList()
ярлык используется.
Я нашел решения здесь, когда поток создается из списка или массива (и вы знаете размер). Но что, если 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());
});
}
С помощью списка вы можете попробовать
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
Если вы используете Vavr(ранее известный как Javaslang), вы можете использовать выделенный метод:
Stream.of("A", "B", "C")
.zipWithIndex();
Если мы распечатаем контент, мы увидим что-то интересное:
Stream((A, 0), ?)
Это потому что Streams
ленивы, и мы понятия не имеем о следующих элементах в потоке.
Вот код от AbacusUtil
Stream.of(names).indexed()
.filter(e -> e.value().length() <= e.index())
.map(Indexed::value).toList();
Раскрытие информации: я разработчик AbacusUtil.
Нет способа перебрать Stream
имея доступ к индексу, потому что Stream
в отличие от любого Collection
, Stream
это просто конвейер для переноса данных из одного места в другое, как указано в документации:
Нет хранения Поток не является структурой данных, которая хранит элементы; вместо этого они переносят значения из источника (который может быть структурой данных, генератором, каналом ввода-вывода и т. д.) через конвейер вычислительных операций.
Конечно, как вы, похоже, намекаете на свой вопрос, вы всегда можете Stream<V>
к Collection<V>
такой как List<V>
, в котором вы будете иметь доступ к индексам.
Вы можете использовать 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());
С 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);
Если вы не возражаете против использования сторонней библиотеки, Eclipse Collections zipWithIndex
а также forEachWithIndex
доступны для использования во многих типах. Вот набор решений этой проблемы для типов JDK и коллекций Eclipse с использованием 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);
Если вы измените лямбда-выражения на анонимные внутренние классы выше, то все эти примеры кода будут работать и в Java 5 - 7.
Примечание: я являюсь коммиттером для коллекций Eclipse
Если вы пытаетесь получить индекс, основанный на предикате, попробуйте это:
Если вам важен только первый индекс:
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);
в случае, если вы хотите вернуть значение, если оно не находит его.
тебе не нужен map
обязательно
, это ближайшая лямбда к примеру LINQ:
int[] idx = new int[] { 0 };
Stream.of( names ).filter( name -> name.length() <= idx[0]++ ).collect( Collectors.toList() );
Одним из возможных способов является индексирование каждого элемента в потоке:
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));
Использование анонимного класса в потоке не очень полезно, хотя и очень полезно.
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 (будьте проще...).
Самый производительный, читабельный и предсказуемый способ написания. Просто вставьте метод и будьте счастливы, без ошибок и непредсказуемости.
Если вам нужен индекс в 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);
});
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);
Как сказал jean-baptiste-yunès, если ваш поток основан на java List, то использование AtomicInteger и его метода incrementAndGet является очень хорошим решением проблемы, и возвращаемое целое число действительно соответствует индексу в исходном List, пока поскольку вы не используете параллельный поток.
Есть ли краткий способ перебора потока, имея доступ к индексу в потоке?
Если вы думаете об итерации как о терминальной операции (например,forEach
), тогда да, есть простой способ доступа к индексу:
Stream<String> names = Stream.of("Sam","Pamela", "Dave", "Pascal", "Erik");
List<String> filtered = new ArrayList<>();
names.reduce(0L, (index, item) -> {
log.info("Iteration over items [index: {}, item: {}]", index, item);
if (item.length() < index) {
filtered.add(item);
}
return index + 1;
}, Long::sum);
Это работает только для потоков.
Parallel
потоки должны быть сначала преобразованы вsequential
те:
names.sequential().reduce(0L, (index, item) -> {
log.info("Iteration over items [index: {}, item: {}]", index, item);
if (item.length() < index) {
filtered.add(item);
}
return index + 1;
}, Long::sum);
Этот вопрос ( Stream Way для получения индекса первого элемента, совпадающего с логическим значением) пометил текущий вопрос как дубликат, поэтому я не могу ответить на него там; Я отвечаю на это здесь.
Вот общее решение для получения соответствующего индекса, который не требует внешней библиотеки.
Если у вас есть список.
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;
}
Вы можете создать статический внутренний класс для инкапсуляции индексатора, как мне нужно было сделать в примере ниже:
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();
}