是否有可能在 Stream 中获取下一个元素?

Is it possible to get next element in the Stream?

我正在尝试将 for loop 转换为功能代码。我需要向前看一个值,也需要向后看一个值。是否可以使用流? 下面的代码是将罗马文本转换为数值。 不确定带有 two/three 参数的 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();
        }
            
    }
        
}

我还没有看到流的这种用例,所以我不能说它是否可能。但是当我需要使用带索引的流时,我选择 IntStream#range(0, table.length),然后在 lambdas 中我从这个 table/list 中获取值。

例如

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

不,使用流是不可能的,至少不容易。流 API 从处理元素的顺序中抽象出来:流可以并行处理,也可以按相反的顺序处理。所以 "the next element" 和 "previous element" 不存在于流抽象中。

您应该使用最适合该工作的 API:如果您需要对集合的 所有 元素应用一些操作而您不是对订单感兴趣。如果您需要按特定顺序处理元素,则必须使用迭代器或者通过索引访问列表元素。

根据流的性质,您不知道 next 元素,除非您阅读它。因此在处理当前元素时,直接获取next元素是不可能的。然而,由于您正在阅读 current 元素,您显然知道之前阅读了什么,因此要实现 "accesing previous element" 和 "accessing next element" 等目标,您可以依赖历史已处理的元素。

您的问题可能有以下两种解决方案:

  1. 获取对以前阅读的元素的访问权限。这样你就知道 current 元素和定义的先前读取元素的数量
  2. 假设在流处理时您读取了 next 元素并且在上一次迭代中读取了 current 元素。换句话说,您将先前读取的元素视为 "current",将当前处理的元素视为 next(见下文)。

解决方案 1 - 实施

首先我们需要一个数据结构,它可以跟踪流经流的数据。好的选择可能是 Queue 的一个实例,因为队列本质上允许数据流过它们。我们只需要将队列绑定到我们想知道的最后一个元素的数量(对于您的用例,这将是 3 个元素)。为此,我们创建了一个 "bounded" 队列来保存历史,如下所示:

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 returns 对当前 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(character) 的调用都会更新 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 在大多数情况下都可以完成工作并且相当容易遵循,但也有一些用例可以检查下一个元素和上一个元素,这真的很方便。在这种情况下,我们只对三个元素元组(上一个、当前、下一个)感兴趣,只有一个元素并不重要(对于简单的示例,请考虑以下谜语:"given a stream of numbers return a tupple of three subsequent numbers which gives the highest sum")。为了解决此类用例,我们可能希望 api 比 StreamHistory class.

更方便

对于这种情况,我们引入了 StreamHistory class 的新变体(我们称之为 StreamNeighbours)。 class 将允许直接检查 previousnext 元素。处理将在"T-1"时间完成(即:当前处理的原始元素被认为是下一个元素,之前处理的原始元素被认为是当前 元素)。这样,我们在某种意义上提前检查了一个元素。

修改后的class如下:

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

}

现在,假设已经读取了三个元素,我们可以直接访问current元素(即在时间T-1通过流的元素),我们可以访问next 元素(这是当前通过流的元素)和 previous(这是在时间 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 class 更容易跟踪 previous/next 元素(因为我们有适当名称的方法),而在 StreamHistory class 中这更麻烦,因为我们需要手动"reverse"队列的顺序来实现。