Search code examples
javajava-streamlazy-evaluation

Lazily calls a method in a java Stream


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?


Solution

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