9

I am building an object with a simple loop:

WebTarget target = getClient().target(u);

for (Entry<String, String> queryParam : queryParams.entrySet()) {
    target = target.queryParam(queryParam.getKey(), queryParam.getValue());
}

I want to do the same thing using the Java8 Stream API but I cannot figure out how to do it. What makes me struggle is that target is reassigned every time, so a simple .forEach() will not work. I guess I need to use a .collect() or reduce() since I am looking for a single return value but I am lost at the moment!

aioobe
  • 413,195
  • 112
  • 811
  • 826
Francesco
  • 857
  • 1
  • 11
  • 26

2 Answers2

11

It's not very difficult to implement a correct foldLeft for Java 8 streams:

@SuppressWarnings("unchecked")
public static <T, U> U foldLeft(Stream<T> stream, 
                                U identity, BiFunction<U, ? super T, U> accumulator) {
    Object[] result = new Object[] { identity };
    stream.forEachOrdered(t -> result[0] = accumulator.apply((U) result[0], t));
    return (U) result[0];
}

Or in type-safe manner:

public static <T, U> U foldLeft(Stream<T> stream, 
                                U identity, BiFunction<U, ? super T, U> accumulator) {
    class Box {
        U value;
        Box(U value) { this.value = value; }
    }
    Box result = new Box(identity);
    stream.forEachOrdered(t -> result.value = accumulator.apply(result.value, t));
    return result.value;
}

This works correctly for sequential and parallel streams. You can even have a speed gain using parallel streams if your stream has some CPU-consuming stateless intermediate operations like map: in this case the next element can be processed by map in the parallel with the current element processed by foldLeft. I don't agree that such operation is not suitable for Stream API, because it can be correctly expressed via already existing forEachOrdered.

I have this operation in my StreamEx library, so you can use it like this:

WebTarget target = EntryStream.of(queryParams).foldLeft(getClient().target(u), 
        (t, entry) -> t.queryParam(entry.getKey(), entry.getValue()))
Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334
  • 1
    Nice. Still though, your 3 lines of stream code looks extremely complicated to me, compared to OP's for loop. The `Object[]` container thing doesn't really look lambda idiomatic, maybe I'm wrong. – aioobe Jun 09 '15 at 20:57
  • 3
    An AtomicReference can be used instead, if you want to have type safety, though I'm not sure if it would make the resulting lambda idiomatic either: AtomicReference result = new AtomicReference<>(identity); stream.forEachOrdered(t -> result.updateAndGet(u -> accumulator.apply(u, t)); return result.get(); – srborlongan Jun 10 '15 at 01:58
  • 1
    @aioobe: the `foldLeft` implementation is a part of low-level library code, so for me it's ok if it looks not very pretty. See, for example, `Collectors` class: there are plenty of similar stuff there. For type safety additional class can be created like `class Box {U value; Box(U v) {value = v;}}`. – Tagir Valeev Jun 10 '15 at 02:35
  • 1
    @srborlongan: this will slow down the reduction as additional ordering constraints will be added by CAS operation (`forEachOrdered` already provides a happens-before ordering). Better to introduce additional mutable class (see my previous comment). – Tagir Valeev Jun 10 '15 at 02:39
6

There's unfortunately no foldLeft method in the stream API. The reason for this is explained by Stuart Marks in this answer:

[...] Finally, Java doesn't provide foldLeft and foldRight operations because they imply a particular ordering of operations that is inherently sequential. This clashes with the design principle stated above of providing APIs that support sequential and parallel operation equally.

Ultimately what you're trying to do here is something procedural / sequential so I don't think the stream API is a good fit for this use case. I think the for-each loop that you have posted yourself is as good as it gets.

Update:

As @TagirValeev points out below you can in fact solve it with the stream API (using forEachOrdered. Your code would then look something like

WebTarget[] arr = { getClient().target(u) };
queryParams.entrySet()
           .stream()
           .forEachOrdered(e -> arr[0] = arr[0].queryParam(e.getKey(),
                                                           e.getValue()));
WebTarget target = arr[0];

I stand by my original answer though, and claim that your good old for-loop is a better approach in this case.

Community
  • 1
  • 1
aioobe
  • 413,195
  • 112
  • 811
  • 826
  • I'm nervous about that last combiner, even if you added `sequential()`. – Louis Wasserman Jun 09 '15 at 15:48
  • 1
    Me too. I'm not sure it's correct. Without any equivalent to `foldLeft`, I doubt the stream API is suitable in this situation. – aioobe Jun 09 '15 at 15:51
  • 2
    I think I'd prefer an answer that outright stated that streams don't really support this well. – Louis Wasserman Jun 09 '15 at 15:52
  • 1
    Thanks for the answer, there is a syntax error in the second-last line (missing braket). Anyway it doesn't seem to work, after this statement target hasn't been built correctly (the query field in target should be populated, but it's null) – Francesco Jun 09 '15 at 15:58
  • @LouisWasserman, me too. Thanks for your comments. (Making this a community wiki. Feel free to improve it further if you see a way to do so.) – aioobe Jun 09 '15 at 15:59
  • I think your example from before was fine, you were just returning `t1` instead of `t2` in the combiner. – Sotirios Delimanolis Jun 09 '15 at 16:07
  • 1
    As far as I can tell there's no guarantee that reduce will work sequentially. This means that both t1 and t2 may be relevant (i.e. the combiner would have to merge the query params from both WebTargets, and at this point it's already a mess) – aioobe Jun 09 '15 at 16:19