6

I am creating a Word Comparison class and it will count the occurrences of words as well. (This is Java)

This was my original method:

/**
 * @param map The map of words to search
 * @param num The number of words you want printed
 * @return list of words
 */
public static List<String> findMaxOccurrence(Map<String, Integer> map, int num) {
    List<WordComparable> l = new ArrayList<>();
    for (Map.Entry<String, Integer> entry : map.entrySet())
        l.add(new WordComparable(entry.getKey(), entry.getValue()));

My IDE suggested that the loop and list assignment could be replaced with a "collect call": "stream api calls"

In which it generated this code:

    List<WordComparable> l =
            map.entrySet().stream()
                    .map(entry -> new WordComparable
                            (entry.getKey(), entry.getValue())).collect(Collectors.toList());

I am kinda confused on how the lambda math works. If my memory serves correctly, the -> is the for each loop, but the other calls are completely confusing.

My IDE can also expand the code into these two snippets:

    List<WordComparable> l =
            map.entrySet().stream()
                    .map(entry -> {
                        return new WordComparable
                                (entry.getKey(), entry.getValue());
                    }).collect(Collectors.toList());

And

    List<WordComparable> l =
            map.entrySet().stream()
                    .map(new Function<Map.Entry<String, Integer>, WordComparable>() {
                        @Override
                        public WordComparable apply(Map.Entry<String, Integer> entry) {
                            return new WordComparable
                                    (entry.getKey(), entry.getValue());
                        }
                    }).collect(Collectors.toList());

Any light-shedding would be awesome.

3 Answers 3

8

Let's take a look at the for loop a bit closer to see how we can write it functionally:

List<WordComparable> l = new ArrayList<>();
for (Map.Entry<String, Integer> entry : map.entrySet())
    l.add(new WordComparable(entry.getKey(), entry.getValue()));

If we read that code in plain English, we might say "for each entry of my map, let's convert it to a WordComparable and add it to a list".

Now, we can rephrase that sentence to "for each entry of my map, let's convert it to a WordComparable, and when we have converted it all, let's make a list out of it".

Using that sentence, we see that we need to create a function: one that takes an entry of the map and converts it to a WordComparable. So let's build one! Java 8 introduces a new type named Function, which has one important method: apply. This method takes one input, transforms it and returns one output.

Writing good old Java, since Function is an interface, we can implement it to write our conversion code:

public class EntryConverter implements Function<Map.Entry<String, Integer>, WordComparable> {

    public WordComparable apply(Map.Entry<String, Integer> entry) {
        return new WordComparable(entry.getKey(), entry.getValue());
    }

}

Now that we have this converter, we need to use it on all the entries. Java 8 also introduces the notion of Stream, that is to say, a sequence of elements (note that this sequence can be infinite). Using this sequence, we can finally write into code what we said earlier, i.e. "for each entry, let's convert it to a WordComparable". We make use of the map method, whose goal is to apply a method on each element of the stream.

We have the method: EntryConverter, and we build a Stream of our entries using the stream method.

So, we get:

map.entrySet().stream().map(new EntryConverter());

What remains is the last part of the sentence: "make a List out of it", i.e. collect all the elements into a List. This is done using the collect method. This method takes a Collector as argument, i.e. an object capable of reducing a stream into a final container. Java 8 comes with a lot of prebuilt collectors; one of them being Collectors.toList().

Finally, we get:

map.entrySet().stream().map(new EntryConverter()).collect(Collectors.toList());

Now, if we remove the temporary class EntryConverter and make it anonymous, we get what your IDE is proposing:

List<WordComparable> l = map.entrySet()
                            .stream() //make a Stream of our entries
                            .map(new Function<Map.Entry<String, Integer>, WordComparable>() {
                                 @Override
                                 public WordComparable apply(Map.Entry<String, Integer> entry) {
                                     return new WordComparable(entry.getKey(), entry.getValue());
                                 }
                             }) //let's convert each entry to a WordComparable
                             .collect(Collectors.toList()); //and make a List out of it

Now, writing all that code is a bit cumbersome, especially the declaration of the anonymous class. Java 8 comes to the rescue with the new -> operator. This operator allows the creation of a Function much more painlessly than before: the left side corresponds to the argument of the function and the right side corresponds to the result. This is called a lambda expression.

In our case, we get:

entry -> new WordComparable(entry.getKey(), entry.getValue())

It is also possible to write this lambda expression using a block body and a return statement:

entry -> {
    return new WordComparable(entry.getKey(), entry.getValue());
}

Notice how that corresponds to what we had written earlier in EntryConverter.

This means we can refactor our code to:

List<WordComparable> l = map.entrySet()
                            .stream()
                            .map(entry -> new WordComparable(entry.getKey(), entry.getValue()))
                            .collect(Collectors.toList());

which is much more readable, and is what your IDE proposes.

You can find more about lambda expressions on Oracle site.

Sign up to request clarification or add additional context in comments.

2 Comments

I LOVE this explanation, hopefully you are finishing it.
You, sir, are the man. By all means of that idiom. I can't believe how well you explained that. Thank you.
6

This is a lambda expression for a Function. It takes an object and returns an object. In this case, it takes a Map.Entry<String, Integer>, and returns a WordComparable.

entry -> new WordComparable(entry.getKey(), entry.getValue())

You could write the equivalent code by hand:

final class ConversionFunction 
  implements Function<Map.Entry<String, Integer>, WordComparable>
{
  @Override
  public WordComparable apply(Map.Entry<String, Integer> entry) {
    return new WordComparable(entry.getKey(), entry.getValue());
  }
}

map.entrySet().stream().map(new ConversionFunction()).collect(...);

The Stream.map() method takes a Function that can be applied to each element (Map.Entry) in the stream, and produces a stream of elements of a new type (WordComparable).

The Stream.collect() method uses a Collector to condense all elements of a stream to a single object. Usually it's a collection, like it is here, but it could be any sort of aggregate function.

1 Comment

Thank you so much for this explanation. :) If the newest answer posted by Tunaki isn't finished then I'll have to mark this as accepted by default.
3
List<WordComparable> l = map.entrySet().stream()
   .map(entry -> new WordComparable(entry.getKey(), entry.getValue()))
   .collect(Collectors.toList());

"->" is a part of lambda itself. In this snippet .stream() is like foreach loop and then begins the set of data processing "directives" (map, collect, etc).

map means than you map each element of current collection to some new collection with some rule:

entry -> new WordComparable(entry.getKey(), entry.getValue())

your rule means that you use each element (with "entry" alias) to create the new elements for the map() result collection. then you should collect your elements to appropriate collection by using suitable collector.

note, that collect applies to map() result.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.