Можно ли получить следующий элемент в потоке?

Я пытаюсь преобразовать цикл for в функциональный код. Мне нужно посмотреть на одно значение, а также заглянуть за одно значение. Возможно ли использование потоков? Следующий код предназначен для преобразования римского текста в числовое значение. Не уверен, что здесь может помочь метод уменьшения с двумя/тремя аргументами.

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

    }

}

Ответ 1

Нет, это невозможно, используя потоки, по крайней мере, не так просто. API потока абстрагируется от порядка, в котором обрабатываются элементы: поток может обрабатываться параллельно или в обратном порядке. Таким образом, "следующий элемент" и "предыдущий элемент" не существуют в абстракции потока.

Вы должны использовать API, наиболее подходящий для задания: поток отличен, если вам нужно применить некоторую операцию ко всем элементам коллекции, и вы не заинтересованы в заказе. Если вам нужно обработать элементы в определенном порядке, вам нужно использовать итераторы или, возможно, получить доступ к элементам списка через индексы.

Ответ 2

Я не вижу такого варианта использования с потоками, поэтому не могу сказать, возможно ли это или нет. Но когда мне нужно использовать потоки с индексом, я выбираю IntStream#range(0, table.length), а затем в lambdas я получаю значение из этой таблицы/списка.

Например

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

Ответ 3

По характеру потока вы не знаете следующий элемент, если вы его не читаете. Поэтому непосредственное получение следующего элемента невозможно при обработке текущего элемента. Однако, так как вы читаете текущий элемент, вы явно знаете, что было прочитано раньше, поэтому для достижения такой цели, как "доступ к предыдущему элементу" и "доступ к следующему элементу", вы можете положиться на историю уже обработанных элементов.

Для вашей проблемы возможны два решения:

  • Получить доступ к ранее прочитанным элементам. Таким образом, вы знаете текущий элемент и определенное количество ранее прочитанных элементов
  • Предположим, что в момент обработки потока вы читаете следующий элемент и текущий элемент читается в предыдущей итерации. Другими словами, вы считаете ранее прочитанный элемент как "текущий" и текущий обработанный элемент следующим (см. Ниже).

Решение 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() на исходный поток. Через call to map() мы конвертируем в поток, содержащий ссылки на наш экземпляр StreamHistory.

Обратите внимание, что каждый раз, когда данные передаются через исходный поток, вызов stream_story.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 в большинстве случаев выполнит эту работу и будет довольно легко следовать, есть варианты использования возможности проверки следующего элемента, а предыдущий действительно удобен. В таком сценарии нас интересуют только три элементарных кортежа (pevious, current, next) и имеющие только один элемент, не имеет значения (для простого примера рассмотрим следующую загадку: "если поток чисел возвращает набор из трех последующих чисел, что дает высшая сумма" ). Чтобы решить такие варианты использования, мы могли бы иметь более удобный 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 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 это более громоздко, поскольку нам нужно вручную "отменить" порядок очереди для достижения этого.

Ответ 4

Не совсем 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
            }
        }
}