To add an illustration to Giovanni's answer, we can highlight the difference between f::handle and obj -> f.handle(obj) if we replace f with a method call:
static Set<String> f() {
System.out.println(" f() called");
return new HashSet<>();
}
public static void main(String[] args) {
List<String> empty = Collections.emptyList();
List<String> strings = Arrays.asList("foo", "bar");
System.out.println("method reference, no invocations");
empty.forEach(f()::add);
System.out.println("method reference, 2 invocations");
strings.forEach(f()::add);
System.out.println("lambda, no invocations");
empty.forEach(str -> f().add(str));
System.out.println("lambda, 2 invocations");
strings.forEach(str -> f().add(str));
}
Output:
method reference, no invocations
f() called
method reference, 2 invocations
f() called
lambda, no invocations
lambda, 2 invocations
f() called
f() called
So, as you see .forEach(f()::add) will evaluate f() right away and then call add(...) on the result as many times as the lambda is called.
On the other hand, str -> f().add(str) will not do anything upfront but will call f() every time the lambda is invoked.