In Java threads, is it viewed really only for debugging?

I read about Java threads and discover new things when I go. One of the new things I found was the peek() function. Almost everything I read at peek says that it should be used to debug your threads.

What if I have a Stream in which each account has a username, password and login () and loggedIn () method.

I also have

 Consumer<Account> login = account -> account.login(); 

and

 Predicate<Account> loggedIn = account -> account.loggedIn(); 

Why is it so bad?

 List<Account> accounts; //assume it been setup List<Account> loggedInAccount = accounts.stream() .peek(login) .filter(loggedIn) .collect(Collectors.toList()); 

Now, as far as I can tell, this does exactly what he intended to do. it

  • accepts a list of accounts
  • Tries to log into each account
  • Filters any account that does not log in.
  • Collects registered accounts in a new list

What is the disadvantage of doing something like this? For some reason I should not continue? Finally, if not this decision, then what?

The original version of this method used the .filter () method as follows:

 .filter(account -> { account.login(); return account.loggedIn(); }) 
+83
java java-8 java-stream peek
Nov 10 '15 at 17:12
source share
5 answers

The key to this:

Do not use the API inadvertently, even if it reaches your immediate goal. This approach may break in the future, and it is also unclear to the future maintainer.




There is no harm in breaking it down into several operations, since they are different operations. There is harm in using the API in an unclear and unintended way that could have consequences if this particular behavior is changed in future versions of Java.

Using forEach in this operation, you should tell the specialist that there is a certain side effect for each accounts element, and that you are doing some operation that might mutate it.

This is also more arbitrary in the sense that peek is an intermediate operation that does not work in the entire collection until the terminal operation completes, but forEach indeed a terminal operation. That way, you can make strong arguments around the behavior and flow of your code, and not ask questions about whether peek behave the same way as forEach in this context.

 accounts.forEach(a -> a.login()); List<Account> loggedInAccounts = accounts.stream() .filter(Account::loggedIn) .collect(Collectors.toList()); 
+53
Nov 10 '15 at 17:55
source share

The important thing you need to understand is that threads are controlled by a terminal operation. The terminal operation determines whether all elements should be processed or any at all. Thus, collect is an operation that processes each element, whereas findAny can stop processing elements after it encounters the corresponding element.

And count() cannot handle any elements at all when it can determine the size of the stream without processing the elements. Since this is an optimization not made in Java 8, but which will be in Java 9, there may be surprises when switching to Java 9 and have code based on count() processing of all elements. It is also associated with other implementation-dependent details, for example, even in Java 9, the reference implementation will not be able to predict the size of the infinite stream source in combination with limit while there is no fundamental restriction to prevent such a prediction.

Because peek allows you to "perform the provided action for each element as the elements are consumed from the resulting stream", it does not require processing of the elements, but it will perform an action depending on what the terminal operation requires. This implies that you should use it with great care if you need specific processing, for example, you want to apply an action to all elements. It works if the terminal operation is guaranteed to handle all elements, but even then you must be sure that not the next developer will not change the terminal operation (or you will forget this subtle aspect).

In addition, while threads guarantee meeting order maintenance for a specific combination of operations, even for parallel threads, these guarantees do not apply to peek . When compiling into a list, the resulting list will have the correct order for ordered parallel threads, but the peek action can be called in arbitrary order and at the same time.

So the most useful thing you can do with peek is to find out if a thread element has been processed, which exactly matches the API documentation:

This method exists mainly to support debugging when you want to see elements when they pass a certain point in the pipeline

+73
Nov 10 '15 at 17:50
source share

Perhaps a rule of thumb should be that if you use a look beyond the scope of the "debug" scenario, you should only do this if you are sure that the conditions for completion and intermediate filtering are met. For example:

 return list.stream().map(foo->foo.getBar()) .peek(bar->bar.publish("HELLO")) .collect(Collectors.toList()); 

seems to be a valid case when you want to convert all Foos to Bars in one operation and tell them about all the greetings.

Seems more efficient and elegant than something like:

 List<Bar> bars = list.stream().map(foo->foo.getBar()).collect(Collectors.toList()); bars.forEach(bar->bar.publish("HELLO")); return bars; 

and you don’t stop repeating the assembly twice.

+11
Nov 11 '16 at 12:52
source share

Although I agree with most of the answers above, I have one case where using peek actually seems to be the cleanest way.

As with use, assume that you want to filter only active accounts and then log in to these accounts.

 accounts.stream() .filter(Account::isActive) .peek(login) .collect(Collectors.toList()); 

Peek helps to avoid redundant calls by not having to iterate the collection twice twice:

 accounts.stream() .filter(Account::isActive) .map(account -> { account.login(); return account; }) .collect(Collectors.toList()); 
+3
Oct 26 '17 at 14:45
source share

I would say that peek provides the ability to decentralize code that can mutate stream objects or change the global state (based on them), instead of stuffing everything into a simple or folded function passed to the terminal method.

Now the question may arise: should we mutate stream objects or change the global state from functions in functional java programming ?

If the answer to any of the above two questions is yes (or: in some cases, yes), then peek() definitely not just for debugging purposes , for the same reason that forEach() is not just for debugging purposes .

For me, when you choose between forEach() and peek() , you choose the following: do I want code snippets that mutate stream objects that need to be attached to the compound, or do I want them to be directly attached to the stream?

I think peek() would be better combined with java9 methods. for example, takeWhile() might need to decide when to stop the iteration based on an already mutated object, so processing it with forEach() will not have the same effect.

PS I did not refer to map() anywhere, because if we want to mutate objects (or global state), and not generate new objects, it works exactly like peek() .

+1
Jun 21 '18 at 14:14
source share



All Articles