Возможно ли получить следующий элемент в потоке?
Я пытаюсь преобразовать цикл for в функциональный код. Мне нужно смотреть вперед на одно значение, а также смотреть на одно значение. Возможно ли использование потоков? Следующий код предназначен для преобразования римского текста в числовое значение. Не уверен, что здесь может помочь метод Reduce с двумя / тремя аргументами.
int previousCharValue = 0;
int total = 0;
for (int i = 0; i < input.length(); i++) {
char current = input.charAt(i);
RomanNumeral romanNum = RomanNumeral.valueOf(Character.toString(current));
if (previousCharValue > 0) {
total += (romanNum.getNumericValue() - previousCharValue);
previousCharValue = 0;
} else {
if (i < input.length() - 1) {
char next = input.charAt(i + 1);
RomanNumeral nextNum = RomanNumeral.valueOf(Character.toString(next));
if (romanNum.getNumericValue() < nextNum.getNumericValue()) {
previousCharValue = romanNum.getNumericValue();
}
}
if (previousCharValue == 0) {
total += romanNum.getNumericValue();
}
}
}
5 ответов
Нет, это невозможно с помощью потоков, по крайней мере, не легко. API потока абстрагируется от порядка, в котором обрабатываются элементы: поток может обрабатываться параллельно или в обратном порядке. Таким образом, "следующий элемент" и "предыдущий элемент" не существуют в абстракции потока.
Вы должны использовать API, который лучше всего подходит для работы: поток отлично подходит, если вам нужно применить какую-то операцию ко всем элементам коллекции, и вас не интересует порядок. Если вам нужно обработать элементы в определенном порядке, вы должны использовать итераторы или, возможно, получить доступ к элементам списка через индексы.
Я не видел такой вариант использования с потоками, поэтому я не могу сказать, возможно это или нет. Но когда мне нужно использовать потоки с индексом, я выбираю IntStream#range(0, table.length)
, а затем в лямбдах я получаю значение из этой таблицы / списка.
Например
int[] arr = {1,2,3,4};
int result = IntStream.range(0, arr.length)
.map(idx->idx>0 ? arr[idx] + arr[idx-1]:arr[idx])
.sum();
По характеру потока вы не знаете следующий элемент, пока не прочитаете его. Поэтому непосредственное получение следующего элемента невозможно при обработке текущего элемента. Однако, поскольку вы читаете текущий элемент, вы явно знаете, что было прочитано ранее, поэтому для достижения такой цели, как "доступ к предыдущему элементу" и "доступ к следующему элементу", вы можете положиться на историю элементов, которые уже были обработаны.
Следующие два решения возможны для вашей проблемы:
- Получите доступ к ранее прочитанным элементам. Таким образом, вы знаете текущий элемент и определенное количество ранее прочитанных элементов
- Предположим, что в момент обработки потока вы читаете следующий элемент, а текущий элемент был прочитан в предыдущей итерации. Другими словами, вы считаете ранее прочитанный элемент "текущим", а текущий обрабатываемый элемент следующим (см. Ниже).
Решение 1 - реализация
Во-первых, нам нужна структура данных, которая позволит отслеживать данные, проходящие через поток. Хорошим выбором может быть экземпляр Queue, поскольку очереди по своей природе допускают прохождение через них данных. Нам нужно только привязать очередь к числу последних элементов, которые мы хотим знать (это будет 3 элемента для вашего варианта использования). Для этого мы создаем "ограниченную" очередь, сохраняющую историю следующим образом:
public class StreamHistory<T> {
private final int numberOfElementsToRemember;
private LinkedList<T> queue = new LinkedList<T>(); // queue will store at most numberOfElementsToRemember
public StreamHistory(int numberOfElementsToRemember) {
this.numberOfElementsToRemember = numberOfElementsToRemember;
}
public StreamHistory save(T curElem) {
if (queue.size() == numberOfElementsToRemember) {
queue.pollLast(); // remove last to keep only requested number of elements
}
queue.offerFirst(curElem);
return this;
}
public LinkedList<T> getLastElements() {
return queue; // or return immutable copy or immutable view on the queue. Depends on what you want.
}
}
Общий параметр T является типом фактических элементов потока. Метод save возвращает ссылку на экземпляр текущей StreamHistory для лучшей интеграции с java Stream api (см. Ниже), и это на самом деле не требуется.
Теперь единственное, что нужно сделать, - это преобразовать поток элементов в поток экземпляров StreamHistory (где каждый следующий элемент потока будет содержать последние n экземпляров реальных объектов, проходящих через поток).
public class StreamHistoryTest {
public static void main(String[] args) {
Stream<Character> charactersStream = IntStream.range(97, 123).mapToObj(code -> (char) code); // original stream
StreamHistory<Character> streamHistory = new StreamHistory<>(3); // instance of StreamHistory which will store last 3 elements
charactersStream.map(character -> streamHistory.save(character)).forEach(history -> {
history.getLastElements().forEach(System.out::print);
System.out.println();
});
}
}
В приведенном выше примере мы сначала создаем поток всех букв в алфавите. Затем мы создаем экземпляр StreamHistory, который будет передаваться на каждую итерацию вызова map() в исходном потоке. Через вызов map() мы конвертируем в поток, содержащий ссылки на наш экземпляр StreamHistory.
Обратите внимание, что каждый раз, когда данные проходят через оригинальный поток, вызов streamHistory.save(символ) обновляет содержимое объекта streamHistory, чтобы отразить текущее состояние потока.
Наконец, в каждой итерации мы печатаем последние 3 сохраненных символа. Вывод этого метода следующий:
a
ba
cba
dcb
edc
fed
gfe
hgf
ihg
jih
kji
lkj
mlk
nml
onm
pon
qpo
rqp
srq
tsr
uts
vut
wvu
xwv
yxw
zyx
Решение 2 - реализация
В то время как решение 1 в большинстве случаев выполнит свою работу и за ним будет довольно легко следовать, существуют варианты использования, в которых была возможность проверить следующий элемент, а предыдущий действительно удобен. В таком сценарии нас интересуют только три набора элементов (неявный, текущий, следующий), и наличие только одного элемента не имеет значения (для простого примера рассмотрим следующую загадку: "данный поток чисел возвращает набор из трех последовательных чисел, который дает Самая высокая сумма "). Для решения таких случаев использования нам может потребоваться более удобный API, чем класс StreamHistory.
Для этого сценария мы вводим новый вариант класса StreamHistory (который мы называем StreamNeighbours). Класс позволит проверять предыдущий и следующий элемент напрямую. Обработка будет выполнена за время "T-1" (то есть: текущий обработанный исходный элемент считается следующим элементом, а ранее обработанный исходный элемент считается текущим элементом). Таким образом, мы, в некотором смысле, проверяем один элемент впереди.
Модифицированный класс выглядит следующим образом:
public class StreamNeighbours<T> {
private LinkedList<T> queue = new LinkedList(); // queue will store one element before current and one after
private boolean threeElementsRead; // at least three items were added - only if we have three items we can inspect "next" and "previous" element
/**
* Allows to handle situation when only one element was read, so technically this instance of StreamNeighbours is not
* yet ready to return next element
*/
public boolean isFirst() {
return queue.size() == 1;
}
/**
* Allows to read first element in case less than tree elements were read, so technically this instance of StreamNeighbours is
* not yet ready to return both next and previous element
* @return
*/
public T getFirst() {
if (isFirst()) {
return queue.getFirst();
} else if (isSecond()) {
return queue.get(1);
} else {
throw new IllegalStateException("Call to getFirst() only possible when one or two elements were added. Call to getCurrent() instead. To inspect the number of elements call to isFirst() or isSecond().");
}
}
/**
* Allows to handle situation when only two element were read, so technically this instance of StreamNeighbours is not
* yet ready to return next element (because we always need 3 elements to have previos and next element)
*/
public boolean isSecond() {
return queue.size() == 2;
}
public T getSecond() {
if (!isSecond()) {
throw new IllegalStateException("Call to getSecond() only possible when one two elements were added. Call to getFirst() or getCurrent() instead.");
}
return queue.getFirst();
}
/**
* Allows to check that this instance of StreamNeighbours is ready to return both next and previous element.
* @return
*/
public boolean areThreeElementsRead() {
return threeElementsRead;
}
public StreamNeighbours<T> addNext(T nextElem) {
if (queue.size() == 3) {
queue.pollLast(); // remove last to keep only three
}
queue.offerFirst(nextElem);
if (!areThreeElementsRead() && queue.size() == 3) {
threeElementsRead = true;
}
return this;
}
public T getCurrent() {
ensureReadyForReading();
return queue.get(1); // current element is always in the middle when three elements were read
}
public T getPrevious() {
if (!isFirst()) {
return queue.getLast();
} else {
throw new IllegalStateException("Unable to read previous element of first element. Call to isFirst() to know if it first element or not.");
}
}
public T getNext() {
ensureReadyForReading();
return queue.getFirst();
}
private void ensureReadyForReading() {
if (!areThreeElementsRead()) {
throw new IllegalStateException("Queue is not threeElementsRead for reading (less than two elements were added). Call to areThreeElementsRead() to know if it's ok to call to getCurrent()");
}
}
}
Теперь, предполагая, что три элемента уже прочитаны, мы можем напрямую получить доступ к текущему элементу (который является элементом, проходящим через поток в момент времени T-1), мы можем получить доступ к следующему элементу (который является элементом, проходящим в данный момент через поток) и предыдущий (который является элементом, проходящим через поток в момент времени T-2):
public class StreamTest {
public static void main(String[] args) {
Stream<Character> charactersStream = IntStream.range(97, 123).mapToObj(code -> (char) code);
StreamNeighbours<Character> streamNeighbours = new StreamNeighbours<Character>();
charactersStream.map(character -> streamNeighbours.addNext(character)).forEach(neighbours -> {
// NOTE: if you want to have access the values before instance of StreamNeighbours is ready to serve three elements
// you can use belows methods like isFirst() -> getFirst(), isSecond() -> getSecond()
//
// if (curNeighbours.isFirst()) {
// Character currentChar = curNeighbours.getFirst();
// System.out.println("???" + " " + currentChar + " " + "???");
// } else if (curNeighbours.isSecond()) {
// Character currentChar = curNeighbours.getSecond();
// System.out.println(String.valueOf(curNeighbours.getFirst()) + " " + currentChar + " " + "???");
//
// }
//
// OTHERWISE: you are only interested in tupples consisting of three elements, so three elements needed to be read
if (neighbours.areThreeElementsRead()) {
System.out.println(neighbours.getPrevious() + " " + neighbours.getCurrent() + " " + neighbours.getNext());
}
});
}
}
Вывод этого следующий:
a b c
b c d
c d e
d e f
e f g
f g h
g h i
h i j
i j k
j k l
k l m
l m n
m n o
n o p
o p q
p q r
q r s
r s t
s t u
t u v
u v w
v w x
w x y
x y z
С помощью класса StreamNeighbours легче отслеживать предыдущий / следующий элемент (потому что у нас есть метод с соответствующими именами), в то время как в классе StreamHistory это более громоздко, так как для этого нам нужно вручную "изменить" порядок очереди.
Как заявили другие, невозможно получитьnext
элементы из повторяющегосяStream
.
ЕслиIntStream
используется как суррогат цикла, который просто действует как поставщик итерации индекса, можно использовать его индекс итерации диапазона, как и в случае с ; однако необходимо предоставить средство пропуска следующего элемента на следующей итерации, например, с помощью внешней переменной пропуска, например так:
AtomicBoolean skip = new AtomicBoolean();
List<String> patterns = IntStream.range(0, ptrnStr.length())
.mapToObj(i -> {
if (skip.get()) {
skip.set(false);
return "";
}
char c = ptrnStr.charAt(i);
if (c == '\\') {
skip.set(true);
return String.valueOf(new char[] { c, ptrnStr.charAt(++i) });
}
return String.valueOf(c);
})
Это некрасиво, но это работает. С другой стороны, с , это может быть так же просто, как:
List<String> patterns = new ArrayList();
for (char i=0, c=0; i < ptrnStr.length(); i++) {
c = ptrnStr.charAt(i);
patternList.add(
c != '\\'
? String.valueOf(c)
: String.valueOf(new char[] { c, ptrnStr.charAt(++i) })
);
}
РЕДАКТИРОВАТЬ: сжатый код и добавленныйfor
пример.
Не совсем решение для Java, но можно получить следующий элемент в Kotlin с помощью zip
функции. Вот что я придумал с функциями. Выглядит намного чище
enum class RomanNumeral(private val value: Int) {
I(1), V(5), X(10), L(50), C(100), D(500), M(1000);
operator fun minus(other: RomanNumeral): Int = value - other.value
operator fun plus(num: Int): Int = num + value
companion object {
fun toRoman(ch: Char): RomanNumeral = valueOf(ch.toString())
}
}
fun toNumber(roman: String): Int {
return roman.map { RomanNumeral.toRoman(it) }
.zipWithNext()
.foldIndexed(0) { i, currentVal, (num1, num2) ->
when {
num1 < num2 -> num2 - num1 + currentVal
i == roman.length - 2 -> num1 + (num2 + currentVal)
else -> num1 + currentVal
}
}
}