【问题标题】:Is it possible to get next element in the Stream?是否可以在 Stream 中获取下一个元素?
【发布时间】:2015-10-17 11:10:09
【问题描述】:

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

【问题讨论】:

  • 我认为这是不可能的,无论如何有趣的问题。
  • 这真是扭曲的代码。这很难理解,我仍然不确定它是否做对了。我认为,您应该先尝试使用普通循环编写一个不重复代码的适当解决方案。

标签: java java-8 functional-programming java-stream


【解决方案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】:

      根据流的性质,除非您阅读它,否则您不知道 next 元素。因此,在处理当前元素时,无法直接获取 next 元素。但是,由于您正在阅读 current 元素,因此您显然知道之前阅读过的内容,因此要实现“访问 previous 元素”和“访问 next 元素”这样的目标> element”,您可以依赖已处理的元素的历史记录。

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

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

      解决方案 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(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 在大多数情况下都可以完成这项工作并且相当容易遵循,但也有一些用例可以检查下一个元素和前一个元素确实很方便。在这种情况下,我们只对三个元素元组(前一个、当前、下一个)感兴趣,并且只有一个元素并不重要(例如,考虑以下谜语:“给定一个数字流返回一个由三个后续数字组成的元组,这给出了最高金额”)。为了解决此类用例,我们可能希望拥有比 StreamHistory 类更方便的 api。

      对于这个场景,我们引入了 StreamHistory 类的新变体(我们称之为 StreamNeighbours)。该类将允许直接检查 previousnext 元素。处理将在时间“T-1”完成(即:当前处理的原始元素被视为next元素,之前处理的原始元素被视为current元素)。这样,在某种意义上,我们可以检查前面的一个元素。

      修改后的类如下:

      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 类更容易跟踪前一个/下一个元素(因为我们有具有适当名称的方法),而在 StreamHistory 类中这更麻烦,因为我们需要手动“反转”队列的顺序来实现这一点.

      【讨论】:

        猜你喜欢
        • 2017-01-27
        • 1970-01-01
        • 2023-03-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-12-21
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多