Soulevoker
Soulevoker

Reputation: 163

Explanation of this Lambda Expression

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.

Upvotes: 6

Views: 289

Answers (3)

Tunaki
Tunaki

Reputation: 137259

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.

Upvotes: 8

Alex Silkovsky
Alex Silkovsky

Reputation: 561

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.

Upvotes: 3

erickson
erickson

Reputation: 269847

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.

Upvotes: 6

Related Questions