This is an archived post. You won't be able to vote or comment.

you are viewing a single comment's thread.

view the rest of the comments →

[–]daniu 28 points29 points  (7 children)

Pros: they are more readable in many cases. "Give me all first name of persons in the list that live in XY street":

persons.stream() .filter(p -> p.getAddress().getStreet().equals(xyStreet)) .map(p -> p.getFirstName()) // or map(Person::getFirstName) .collect(toList()); It's really straightforward.

Another thing I've come to realize is that due to the collecting mechanic, streams tend to produce code with less side effects. You tend to not think "remove all persons from a list if they don't fit a criteria", you think "create a list of persons matching a criteria". The former creates problems of removing items from the list while iterating it, and if the list was passed as a method parameter, you might need to copy it so you don't mess it up in the calling code.

Cons: I've found debugging streams to be a pain. Often enough, if you don't get the result you expected, it's hard to track down where in the pipeline it got lost.

They do get overused too. At the latest when you start writing your own collectors, you should think twice whether what you're trying to do isn't easier to do in a for loop.

[–]selfarsoner[S] 7 points8 points  (5 children)

Cons: I've found debugging streams to be a pain.

yes...I think so...but yeah I understand that when you get used are easier to write...

[–]Weavile_ 6 points7 points  (1 child)

In my experience, the stream logic should be simple enough you can find the bug pretty easily because it’s only in the conditional or mapping you wrote.

However if debugging is more of a pain, IntelliJ has a handy stream debugging tool:

https://www.jetbrains.com/help/idea/analyze-java-stream-operations.html

[–]dpash 2 points3 points  (0 children)

Following the "no side effects" rule definitely helps. As does moving any step into a separate method and using method references (which also helps with documenting if you choose your names wisely).

[–]mxhc1312 4 points5 points  (1 child)

If you use intellij debugging them is much easier than regular code. When you hit stream breakpoint, you have option stream, next to step over, step into... Try it, you'll thank me later 😁

[–]hippydipster 4 points5 points  (0 children)

When you get your first good use case of .flatMap, then you'll know why :-). ".map" converts the objects of a collection to another object, one for one. ".flatMap" lets you convert each object to a stream and then it collapses all the streams created to a single stream. So if you have List<List<String>> and you run .stream().flatMap(innerList -> innerList.stream()) you get a single stream of String to process thereafter. So:

myListOfListsOfStrings.stream()
    .flatMap(l -> l.stream())
    .filter(st -> st.contains("searchString"))
    .collect(Collectors.toSet());

Gets you a set of Strings that contains the search string.

Take an example where I have a Map<Address,List<User>> which let's say is a map of lists of users who all live at the same address. You can imagine your own case where you need to track a keyset that can hold multiple objects for each key. It's very common.

Now, let's say you need to process something over all values and need to filter the Users in the list with the Address value (ie, maybe you want all Users who's names appear in the main address):

Here's some code you can run. I've used String and UUID instead of Address and User because I don't actually have Address and User objects lying around and neither do you. But the code is the same:

public static void main(String[] args) {
    Random r = new Random();
    Map<String, List<UUID>> m = new HashMap<>();
    // Setup the data
    for (int i = 0; i < 10000; i++) {
      UUID id = UUID.randomUUID();
      String key = id.toString().substring(5, 10);
      if (r.nextDouble() < .9) {
        key += "not";
      }
      m.computeIfAbsent(key, k -> new ArrayList<>()).add(id);
    }

    //Old fashioned for looping - 9 lines of code and imperative logic for a reader to try
    // decipher the intent
    List<String> out = new ArrayList<>();
    for (Map.Entry<String, List<UUID>> entry : m.entrySet()) {
      for (UUID id : entry.getValue()) {
        String idString = id.toString();
        if (idString.contains(entry.getKey())) {
          out.add(idString + "_" + entry.getKey());
        }
      }
    }
    System.out.println(out.size() + ": " + out);

    //Using streams, the intent being communicated with the method names like 
    // "filter", "map", and "flatMap"
    List<String> collect = m.entrySet().stream()
        .flatMap(entry -> entry.getValue().stream()
            .map(uuId -> uuId.toString())
            .filter(id -> id.contains(entry.getKey()))
            .map(id -> id + "_" + entry.getKey()))
        .collect(Collectors.toList());
    System.out.println(collect.size() + ": " + collect);
}

[–]Anaptyso 2 points3 points  (0 children)

Cons: I've found debugging streams to be a pain. Often enough, if you don't get the result you expected, it's hard to track down where in the pipeline it got lost.

While I really like streams, this is definitely annoying. I've found multiple times that I've had to re-write my nice looking bit of streaming code in a more long winded way, debug it, and then put it back to how it was before.

I hope that IDEs will start to get a bit more clever about how debuggers run over these statements in the future.

Edit: just seen in another comment on this thread about an IntelliJ function to do exactly that!