13

"peek" is to be primarily used for debugging. What if I want to call a method on a stream in the middle of a stream, something that changes the state of the streamed object.

Stream.of("Karl", "Jill", "Jack").map(Test::new).peek(t->t.setLastName("Doe"));

I could do:

Stream.of("Karl", "Jill", "Jack").map(Test::new).map(t->{t.setLastName("Doe"); return t;});

But that seems ugly. Is this something that shouldn't be done or is there a better way to do this?

EDIT: forEach works except that it's a terminal operation, and so you can't keep working on the stream afterwards. I would then expect to make a Collection, do forEach, then start streaming the Collection again.

EDIT: map(Class::processingMethod) is what I'm doing now, but since processingMethod simply returns this, it seems to be a misuse of map. Plus, it doesn't really read like business logic.

FINAL EDIT: I accepted @Holger's answer. Stream.peek cannot be expected to process all the elements on a Stream because it is not a terminal operation. The same goes for map. Even though you might have terminated your stream with something that guarantees it will process all operations, you should not be writing code that expects every user to do so. So, to do processing you should use forEach on a Collection, then start streaming the Collection again if you want to.

K.Nicholas
  • 10,956
  • 4
  • 46
  • 66
  • 3
    Are you looking for `forEach`? Map will work on every element. – matt Feb 08 '17 at 15:44
  • @matt - forEach works except that it's a terminal operation, and so you can't keep working on the stream afterwards. I would then expect to make a Collection, do `forEach`, then start streaming the Collection again. Do you think there is a reason to do processing that way? – K.Nicholas Feb 08 '17 at 17:14

3 Answers3

13

You are overusing method references. The simplicity of Test::new is not worth anything, if it complicates the rest of your stream usage.

A clear solution would be:

Stream.of("Karl", "Jill", "Jack")
      .map(first -> { Test t = new Test(first); t.setLastName("Doe"); return t; })
      …

or much better

Stream.of("Karl", "Jill", "Jack").map(first -> new Test(first, "Doe")) …

assuming that the class has the not-so-far-fetched constructor accepting both names.

The code above addresses the use case, where the action manipulates a locally constructed object, so the action is only relevant if the object will be consumed by the subsequent Stream operations. For other cases, where the action has a side effect on objects outside the Stream, abusing map has almost all of the drawbacks of peek explained in “In Java streams, is peek really only for debugging?

Community
  • 1
  • 1
Holger
  • 285,553
  • 42
  • 434
  • 765
  • Thanks @Holger. I understand what you're saying, but the question is really about doing processing during streams not just doing transformations. So, as shown in above answers, doing `map(Class::method)` is really the only way to do it, but it seems to read wrong. – K.Nicholas Feb 08 '17 at 17:12
  • 3
    @Nicholas: abusing `map` for side effects is on par with using `peek` for that. It might work, if it is restricted to manipulating the locally created object like in your example, but then, fusing the creation and manipulation into a single step, like in my answer, would be much clearer. In all other cases, abusing `map` has almost all of the drawbacks of `peek` explained in [In Java streams, is peek really only for debugging?](http://stackoverflow.com/a/33636377/2711488). – Holger Feb 08 '17 at 17:18
  • 1
    I think that was the answer that I was looking for. If I was to write a library that expected you to abuse `map` and misuse it as a terminal operation you would say that is a bad library. So, I should use `forEach` because is it a terminal operation. – K.Nicholas Feb 08 '17 at 17:22
6

You can't use a method reference, even if you create another constructor with two parameters.

The only way to do it is:

.map(token -> {Test t = new Test(token); token.setLastname("joe"); return t;})
Eugene
  • 117,005
  • 15
  • 201
  • 306
  • 2
    The class *has* a constructor that takes a string, but it is the *first* name. Adding another parameter would be useful (maybe such a constructor even exists), but it can’t be used with such a simple method reference in a Stream, as the object should be initialized with both, the stream element (first name) and the constant (last name) `"Doe"`. – Holger Feb 08 '17 at 16:25
  • 1
    It seems the answer here is "no". You're saying that map *is* the way to adding processing during streams. That's what I have done, but it seems to be implying something that isn't happening, i.e., I'm not mapping but rather just executing a Function and returning the same object. – K.Nicholas Feb 08 '17 at 17:10
1

Not in every case it makes sense to define a new method or constructor for what you are trying to achieve. You can create yourself a tool class Functions for functions featuring the following functions:

public static <T, R> Function<T,R> of(Function<T, R> function){
    return function;
}


public static <T> Function<T,T> peek(Consumer<? super T> peeker){
    return t -> {
        peeker.accept(t);
        return t;
    };
}

Then you can use them like this:

Stream.of("Karl", "Jill", "Jack").map(Functions.of(Test::new).andThen(Functions.peek(t -> t.setLastName("Doe"))));

The stream shouldn't be bothered with trivially setting the last name for every element via a mapping (which should be side effect free). Therefore, I composed it directly to the constructing function in this example.

Calculator
  • 2,769
  • 1
  • 13
  • 18