0

I'm learning about Java 8 streams API by trying different examples. I'm trying to form a HashMap from an ArrayList using streams .map() method. (I know about .groupingBy method but i am trying to understand the underlying logic of stream operations.) When i run my code without calling the forEach() method (or any terminal operation) changes are not being applied to my HashMap<> and it stays empty. But calling a terminal operation enable to protect changes i made. I was thinking that terminal operations used only to return non-stream values and end streams. Can anyone explain why this is happening like this ?

class Main {

    public static void main(String[] args) {

    // Create a list of arrays then form a HasMap based on entries of list
    List<String[]> list3 = new ArrayList<String[]>();
    String[] entry1 = {"a","b","c"};
    String[] entry2 = {"d","e","f"};
    String[] entry3 = {"g","h","i"};
    list3.add(entry1);
    list3.add(entry2);
    list3.add(entry3);

    // hashmap
    HashMap<String, String[]> map1 = new HashMap<String, String[]>();

    // streamize the list to form hashmap
    list3.stream()
         .map(entry ->  {
                            String key = entry[0];
                            String[] value = {entry[1],entry[2]};
                            map1.put(key, value);
                            // must return a String[]
                            return entry;
                        })
         // CHANGES ARE ONLY APPLIED WHEN TERMINAL OPERATIONS CALLED ???
         .forEach(entry -> {});

    // print elements
    for (Map.Entry<String,String[]> entry : map1.entrySet()) {
        System.out.println(entry);
    }

}

// Without terminal op

                              //Empty

// With terminal op

a=[Ljava.lang.String;@7921b0a2

d=[Ljava.lang.String;@174d20a

g=[Ljava.lang.String;@66d2e7d9

Mert Beşiktepe
  • 155
  • 1
  • 5
  • 1
    nothing is done without ta terminal operation - as simple as that – Eugene Mar 29 '19 at 09:55
  • 3
    Possible duplicate of [What is the difference between intermediate and terminal operations?](https://stackoverflow.com/questions/47688418/what-is-the-difference-between-intermediate-and-terminal-operations) – Ravindra Ranwala Mar 29 '19 at 09:56
  • 3
    Streams are lazy, they won't process more than their terminal operation require. Good examples are `anyMatch()` and such were the operation will stop at the first positive – Aaron Mar 29 '19 at 09:56
  • 1
    Also, don't make an habit of coding side-effects into a `map`, that step should be pure – Aaron Mar 29 '19 at 09:57
  • So i better look for understanding lazy methods first i guess ? – Mert Beşiktepe Mar 29 '19 at 10:05
  • 3
    So why are you using `map` when you do not intent to *map* elements to other elements, but rather perform an action per element? Why don’t you write that action into the consumer passed to `forEach` and get rid of the obsolete `map` step? Of course, a real Stream solution, i.e. `Map map1 = list3.stream() .collect(Collectors.toMap(a -> a[0], a -> new String[] { a[1], a[2] }));` would be better, but *if* you need to perform an action, use the method intended to perform an action. – Holger Mar 29 '19 at 10:13
  • @MertBeşiktepe "lazy" can be used in multiple ways in programming, but it generally means avoiding to do work you might not require. With streams it means they won't process their whole datasource's content but only as much as their terminal operation requires. If it's `forEach` they must process all the datasource, but if it's `findFirst` they can stop once they've reached the first non-filtered element. – Aaron Mar 29 '19 at 10:27
  • Thanks for clarfication! Your last comment would be a good answer to this question i think. I was imagining that map is for applying a function element-wise on stream real misunderstanding originates from here. – Mert Beşiktepe Mar 29 '19 at 11:02

1 Answers1

0

I think it is good to note that the main reason why your HashMap is not filled, is because your code is put into the body of a lambda expression. A lambda is similar to an anonymous inner class:

jbutton.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent event) {
        doSomething();
    }
});

If you define something like this, the method doSomething() is not instantly executed, instead, the addActionListener method stores the ActionListener instance somewhere as a instance variable, so it can be used later. That means you're effectlively only defining a method, but you're not calling it yet.

The map method of the Stream interface (whichever implementation you're using) accepts an instance of Function, which is only a definition. It only stores this Function somewhere, but does not execute the body of your lambda expression.

Stream implementations are lazy. The're designed to do nothing until they absolutely have to. That is only the case when the caller actually need some result (i.e. collect).

Once you call a terminal operation, then the Stream implementation will actually apply all necessary transformation and filtering steps, and return some form of result.

Related:


Note: stream operations should not have side effects. That means that you should avoid doing things like map1.put(key, value).

MC Emperor
  • 22,334
  • 15
  • 80
  • 130