0

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

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 e.g. Bean id 6 in the above.

Many thanks.

Naman
  • 27,789
  • 26
  • 218
  • 353
solarwind
  • 327
  • 1
  • 3
  • 17
  • 3
    What would be returned if you called getAfterLast(new Bean(6, "text6"))? – NomadMaker Jun 22 '20 at 08:37
  • 3
    Streams are not a good solution for this. It requires looking ahead (to see which item is "last"), looking around (to see "item after"). Streams should only be used where they make sense and the code is not too complicated - which usually means applying operations to each item (filter, map etc.) without context. – RealSkeptic Jun 22 '20 at 08:40
  • 1
    Use an iterator rather than a stream. Given that you have an obvious ordering requirement, there is no advantage in using streams over simple iteration. – Andy Turner Jun 22 '20 at 08:42
  • 2
    By the way: **do not use the *double-brace initializer*** (which doesn't exist in reality by the way). [Here's more about it](https://stackoverflow.com/questions/1958636/what-is-double-brace-initialization-in-java). – MC Emperor Jun 22 '20 at 12:36

7 Answers7

2

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());
DDovzhenko
  • 1,295
  • 1
  • 15
  • 34
  • 3
    "_Streams are not a good solution for this task. But it's still possible to do with them using Atomic:_" – Your example does not use streams. – Slaw Jun 22 '20 at 20:21
  • @Slaw You're right, I missed the point `.forEach()` is also present in `Iterable`. Thanks! – DDovzhenko Jun 23 '20 at 09:33
1

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);
ernest_k
  • 44,416
  • 5
  • 53
  • 99
0

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.

Andy Turner
  • 137,514
  • 11
  • 162
  • 243
0

One of the workarounds for using the Stream would be to keep track of last index of texts 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);
    
Naman
  • 27,789
  • 26
  • 218
  • 353
  • Note - `list.get(lastIndexMap.get(null) + 1)` might get you AIOOBE, in case you are looking for a text that is also there for a Bean as the last element in the input. – Naman Jun 22 '20 at 10:20
  • 3
    That’s a lot of evaluations of unwanted information. Why not `OptionalInt o = IntStream.range(0, list.size()) .filter(ix -> list.get(ix).getText() == null) .reduce((a, b) -> b) .map(ix -> ix + 1);`? – Holger Jun 22 '20 at 11:16
  • @Holger I made that on purpose, to be honest, considering the use cases that could be built over it. I understand that it brings in a lot of information, but the idea to diverge from the `iterator` approach was to provide lookups for other strings without iterating again. Thank you for the suggestion, I would still adapt all of this into the answer. – Naman Jun 22 '20 at 13:39
0

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);
Hadi J
  • 16,989
  • 4
  • 36
  • 62
0

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.

MC Emperor
  • 22,334
  • 15
  • 80
  • 130
0

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");
WJS
  • 36,363
  • 4
  • 24
  • 39
  • the handling for the last element remains unclear based on the question. but in order to note, with this solution the if `null` as a text was part of two `Bean`s, one at 2nd index and another at last index, this would print the third element. (while one may expect it to be "None Found") – Naman Jun 23 '20 at 17:25