3

I have an expensive method that I only want to call it when necessary in a stream. Here is an example:

public static Optional<MyObject> findTarget(String input, List<MyObject> myList) {
    return Stream.concat(myList.stream(), expensive().stream()).filter(o -> o.hasName(input)).findFirst();
}

The goal is to find the target MyObject from myList based on the input value, but if its not in myList ONLY then it will call expensive() to return a bigger list and look from there.

The above example does not do that, as it seems Stream.concat will call expensive() already before consuming all of myList.

An ugly solution I can think of is to do it in two steps, e.g.:

return myList.stream().filter(o -> o.hasName(input)).findFirst().or(
    () -> expensive().stream().filter(o -> o.hasName(input)).findFirst());

But then I will have to repeat the filter and the rest twice.

Is there any better solution or even a single liner of Stream that does that?

shmosel
  • 49,289
  • 6
  • 73
  • 138
user1589188
  • 5,316
  • 17
  • 67
  • 130

2 Answers2

7

You can lazily evaluate by concatenating Supplier<List<MyObject>> instead of List<MyObject>.

public static Optional<MyObject> findTarget(String input, List<MyObject> myList) {
    List<Supplier<List<MyObject>>> concat = List.of(() -> myList, () -> expensive());
    return concat.stream()
        .flatMap(supplier -> supplier.get().stream())
        .filter(o -> o.hasName(input))
        .findFirst();
}

Test:

record MyObject(String s) {
    public boolean hasName(String in) {
        return s.equals(in);
    }
}

static List<MyObject> expensive() {
    System.out.println("expensive() called");
    return List.of(new MyObject("z"));
}

public static void main(String[] args) {
    List<MyObject> myList = List.of(new MyObject("a"));
    System.out.println("case 1: " + findTarget("a", myList));
    System.out.println("case 2: " + findTarget("x", myList));
}

Output:

case 1: Optional[MyObject[s=a]]
expensive() called
case 2: Optional.empty

Alternatively you can do this:

public static Optional<MyObject> findTarget(String input, List<MyObject> myList) {
    return Stream.of(
            (Supplier<List<MyObject>>) () -> myList,
            (Supplier<List<MyObject>>) () -> expensive())
        .flatMap(supplier -> supplier.get().stream())
        .filter(o -> o.hasName(input))
        .findFirst();
}
モキャデ
  • 385
  • 1
  • 4
  • Ah nice! Thanks for the test as well. But why is this the case? Why it will not evaluate the expensive call in this way? – user1589188 Jan 17 '23 at 02:01
  • 1
    @user1589188 Streams, as implemented in the JDK, are lazy and process elements one-by-one. First, the `flatMap` is called with the `() -> myList` supplier. The elements of the stream returned by that flat-map operation are processed until there are no more. Only if the stream does not terminate with a successful `findFirst()` will the _second_ supplier be given to `flatMap` (the `() -> expensive()` supplier). And the use of `Supplier` delays the call to `expensive()` until it is needed. – Slaw Jan 17 '23 at 02:05
  • @user1589188 If `myList` contains the desired result, `() -> expensive()` will not be executed because the stream will immediately terminate. – モキャデ Jan 17 '23 at 02:06
  • 2
    Note that there was a [bug](https://stackoverflow.com/questions/46288915/is-flatmap-guaranteed-to-be-lazy) in earlier versions of Java where `flatMap()` was only partially lazy. but I think it would still avoid the expensive supplier in this case. – shmosel Jan 17 '23 at 02:08
  • @モキャデInstead of `List.of` then `stream()`, I was able to just use `Stream.of`. – user1589188 Jan 18 '23 at 05:45
2

Another alternative, which might be simpler to understand, is to extract the stream logic in a separate method:

private static Optional<MyObject> findInternal(String input, List<MyObject> myList) {
    return myList.stream().filter(o -> o.hasName(input)).findFirst();
}

and then simply call it twice:

public static Optional<MyObject> findTarget(String input, List<MyObject> myList) {
    return findInternal(input, myList).or(() -> findInternal(input, expensive()));
}
Didier L
  • 18,905
  • 10
  • 61
  • 103