简体   繁体   中英

Java 8 Stream - Get item after the last occurence of specific item

I would like to get the item after the last occurence of specific item in list. eg

List<Bean>  list = ArrayList<Bean>() {{
   add(new Bean(1, null));  // null
   add(new Bean(2, "text2"));
   add(new Bean(3, "text3"));
   add(new Bean(4, "text4"));
   add(new Bean(5, null));   // null last occurence
   add(new Bean(6, "text6");  // want this one.
}}

I want to get the item after the last occurence of a Bean with null text eg Bean id 6 in the above.

Many thanks.

As it was mentioned in the comments, Streams are not a good solution for this task. But it's still possible to do with them using Atomic :

    AtomicBoolean isPreviousNull = new AtomicBoolean(false);
    AtomicReference<Bean> lastOccurrence = new AtomicReference<>(null);
    list.forEach(item -> {
        if (item.text == null) {
            isPreviousNull.set(true);
        } else if (isPreviousNull.get()) {
            isPreviousNull.set(false);
            lastOccurrence.set(item);
        }
    });
    System.out.println(lastOccurrence.get());

I'm with RealSkeptic on streams not being the best for this problem. If you have to do it, though, you could iterate over your list in reverse, filter to find the first null text, and then pick the one preceding it. It's more practical with a stream of indices rather than the stream of list elements:

Bean firstAfterNull = IntStream.iterate(list.size() - 2, i -> i - 1)
        .limit(list.size() - 1) //or better .takeWhile(i -> i >= 0) on Java 9+
        .filter(i -> null == list.get(i).getText())
        .boxed()
        .findFirst()
        .map(i -> i + 1)
        .map(list::get)
        .orElse(null);

Without streams:

Bean lastFound = null;

Iterator<Bean> it = list.iterator();
if (it.hasNext()) {
  Bean prev = it.next();
  while (it.hasNext()) {
    Bean curr = it.next();
    if (prev.text == null) {
      lastFound = curr;
    }
    prev = curr;
  }
}

You can also iterate the list in reverse, using a ListIterator , and stop as soon as you find the first match.

One of the workarounds for using the Stream would be to keep track of last index of text s from Bean .

Map<String, Integer> lastIndexMap = IntStream.range(0, list.size())
        .boxed()
        .collect(Collectors.toMap(a -> list.get(a).getText(), a -> a, Integer::max));
    

This way you can easily then access the next element after certain text's last index.

Bean afterLastIndexOfNull = list.get(lastIndexMap.get(null) + 1);
   

The drawback especially in your case if the null text value which I am turning into the null keys in the Map . They should be highly discouraged and for that reason you can choose to implement a wrapper to convert null to some default text and then lookup based on the same as well.


Gotchas

  1. One of the gotchas with the above approach is that you could get an array index out of bounds exception while trying to access

     list.get(lastIndexMap.get(<key>) + 1)

and the key text is also present in the last element of the list.

  1. Another point as Holger has also mentioned in the comments explicitly is that this approach prepares a lot of information.

    Note there is a trade-off in terms of what your queries further might be. If you want to prepare for lookups of list of texts. This approach might be useful for a single iteration and faster access.

    But on the other hand, if it's just a one-time computation then its worth minimizing the information to just the single index. Holger has already suggested a solution to that as well.

     OptionalInt lastIndex = IntStream.range(0, list.size()).filter(ix -> list.get(ix).getText() == null).reduce((a, b) -> b);

Another solution is:

Map<String, Bean> map = new HashMap<>();
for (Iterator<Bean> it = list.iterator(); it.hasNext(); )
    if (it.next().getText() == null && it.hasNext()) 
        map.put(null, it.next());
        
System.out.println(map.get(null));

Stream version:

lastFound = IntStream.range(0, list.size())
           .collect(() -> new HashMap<String, Bean>(), (m, i) ->
                   {
                      if (list.get(i).getText() == null && (i + 1) < list.size()) 
                        m.put(null, list.get(i + 1));
                   }
                    , HashMap::putAll)
            .get(null);

Using partitioningBy .

int index = IntStream.range(0, items.size())
    .boxed()
    .collect(partitioningBy(i -> items.get(i).getText() == null, reducing((l, r) -> r)))
    .get(true).get() + 1;

Bean element = items.get(i);

This relies on positioned elements. This throws a NoSuchElementException or an ArrayIndexOutOfBoundsException if such element does not exist.


Alternatively, instead of + 1 , you could also just do .mapToObj(i -> i + 1) instead of .boxed() , which yields the same results.

I would simply start at the end and work backwards. Note that this starts on the next to last element. If the last element were null there would be nothing to return.

Optional<Bean> opt = IntStream
        .iterate(list.size() - 2, i -> i >= 0, i -> i - 1)
        .filter(i -> list.get(i).getText() == null)
        .mapToObj(i -> list.get(i + 1)).findFirst();
        
System.out.print(
        opt.isPresent() ? opt.get().getText() : "None Found");

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM