Switching from Java 7 to Java 8: Part 4

Cognizant
Cognizant Softvision Insights
9 min readMay 9, 2023

--

Lambdas Concepts and Structures

By Vlad Ionescu, Java Developer, Cognizant Softvision

So far in this series, we discussed how we can define interfaces as a type for lambda expressions, how to actually construct the lambdas themselves, and dove further into functional interfaces and what functional interfaces Java 8 provides for us. In this final article, we cover method references and collections for each iteration, and give a brief introduction to streams.

Let’s get started with method references. View the example below.

In the above image, we’ve defined a static method called “printMessage” which prints out to the console the text “Hello.” In the “main” method, we created a thread and passed it as an argument to a lambda expression that executes our previously defined method. In the previous articles we revealed that, instead of a “Runnable,” we can pass in a lambda since “Runnable” is an interface with only one abstract method called “run.” The code we want to execute in the lambda expression is the method call for “printMessage.” Running the above code would print out “Hello” from a new thread.

Now, a method reference means that, if we have a lambda expression that takes no parameters and just calls a method as its body, then we can use an alternate way to achieve the same thing. See below.

In the example above, instead of the lambda expression, we used the class name, “MethodReference,” followed by two colon symbols and then the method we want to call. This would look like: MethodReference::printMessage

Now let’s look at some other examples. Consider the below class.

Remember this example from our previous articles? We defined a list of cars for which we want to print out all the cars in it. We’re doing that by calling the “perform” method which takes in the list of cars, the condition (which is always true in our case, because we want to print out all the cars), and the action we want to execute (in our case, printing the cars to the console). Running the above would print:

The lambda that we pass in to the “perform” method looks like this: car -> System.out.println(car). So, the lambda takes in one parameter and executes an action on it (the printing to the console). Similar to the previous case where we used a method reference for a lambda with no input parameters and just a method call, we can use a method reference here too. The difference here is we’re not invoking a static method, but the method “println” on the “System.out” object. If we have a look over the documentation we can see that “System.out” is an object of type “PrintStream.”

In addition, looking into the PrintStream class, we can see a “println” method defined there which takes in an “Object” as the input parameter.

Switching back to our code, the method reference would look like this:

The method reference call from the above is composed of the instance of the class we want to execute the method on, “System.out,” followed by two colons, followed by the method itself we want to execute, “println.” We can see that there are no input parameters passed to this syntax as they are when using lambdas. This is because the “perform” method takes in as the last parameter a “Consumer” of “Cars” which is a functional interface that defines a method that takes in an input parameter of type “Car” and returns nothing. Thus, when using the method reference, the compiler automatically deduces that the action passed in, the “System.out::println”, has to be executed over a “car” object.

Now let’s talk about looping through a list of objects in Java. Prior to Java 8, there were two ways of doing that. Let’s see an example below.

We will be using the same list of “car” objects as we did in our previous examples. The first method for looping through this list is using the “for in” loop.

In the above picture, we start with an index equal to zero and iterate through all the items from the list, one by one. For each of these elements, we will print them out to the console.

Another method we could use for looping is the “for each” loop. See below.

In the above picture, for each car in the “cars” list, we will also print it out to the console. This form is a shortened one compared to the first one, achieving the same result.

Java 8 introduced a new way of iterating through a collection of items, the “forEach” one. See below as an example.

Each “Iterable” contains now a “forEach” method which is used to iterate through the elements of a collection. The method receives as an input parameter a “consumer.” In the picture above, the consumer is a lambda that consists of an input parameter of type “car” — the current element being iterated from the list, and the action — printing out to the console the current “car” object.

The first two methods of looping through a list of elements are called external iteration and this third method is called internal iteration. With the external iteration, the user who writes the code has full control of the iteration, meaning: stating where it should start, where it should end, etc. All of this is done in a sequential order.

In the internal iteration, one specifies only the intent of the iteration and not how to actually do it. The runtime could choose to use several threads to compute this, one of them handling the first two elements of the list, the second thread handling the next five elements, for example, and so on. This way facilitates a parallel approach of iterating through a list of elements, thus leveraging the multi-core processors computers have nowadays.

The above lambda can also be replaced with a method reference as shown below.

Lastly, let’s talk about “streams.” The best way to explain this concept is through an example. Let’s take our well known “cars” example. Let’s say we have a list of “cars” for which we want some work done. For this, we employ three people — one for fixing the engine, one for changing the tires, and one for painting each car red. So what we can do is to talk to each person and say: here are the cars. First, go fix the engine. And, so, the employee will go and fix the engine for each of the cars. Then, we can talk with the second person and tell them to change the tires for each of the cars. And last, talk with the third employee and ask them to paint each car red. We could take this approach, but we made three iterations, one for each employee to go through the list of cars and do their one job. This doesn’t seem very efficient. Another way to think of this is to have some people in a specific place (each one performing its own unit of work) and all the cars lined up like in an assembly line. Then, each of the cars passes by each individual, whereby they perform their duty. When a car passes through all of the employees (aka, each unit of work) then the process of work on it is done. See the picture below where the squares represent the cars.

This is a good analogy of what streams are. They let you build this “assembly line” on a collection of elements. Then, you can call different operations, aka units of work, to perform different tasks on each element of the list (these are the employees who repair the car from the above example). Let’s switch to code and see how that looks.

In the above image we have the same list of “cars” objects on which we want to perform two operations: one for choosing only the cars that have the model starting with “X” and one for printing these cars to the console. We want the output of the first operation, filtering out the cars, to be stored in a list. The first approach we discussed would mean that first, we would iterate through the list of cars to take into consideration only the cars that start with “X” and then we would iterate again through this new returned list in order to print them out to the console. Again, this doesn’t seem very efficient. What we can do instead is to build that “assembly line.” Refer to the picture below.

In Java 8, each collection has an additional method called “stream()” which creates the “assembly line.” What follows after “stream()” are the operations that we want to perform on each element of the list. The first operation is the “filter” one which accepts a “Predicate.” We talked about predicates early in this series of articles, and established that this is a functional interface with a method that accepts one input parameter and returns a boolean. So, we will define a lambda expression that, in our example, takes in a “car” object and returns whether or not that car model starts with “X.” Then, the result of the first operation goes as the input for the second operation. The first operation returns a stream containing the filtered cars. Then, “forEach” of these cars, we will print them out to the console — the second operation.

We can think of the “stream” as a view of the collection on which we invoke it. The stream will always have a source, the backing collection itself. A “stream” is composed of three parts: the collection on which is executed, the set of operations we want to use to process the collection and a terminal operation. The terminal operation is the one that triggers the stream to be executed. In our example, the terminal operation we used is “forEach.” There are others, as well. If the terminal operation wasn’t invoked, then nothing would have happened with the collection, even though we called the “stream()” method and did several processings on it.

Let’s have a look at another example.

In the above picture, we used the same collection on which we invoked the “stream()” method, did the same filtering, but instead of printing to the console the cars that start with “X,” we added them to a new list using the “collect” method. This method is also a terminal operation.

In the example below, we use another terminal operation, called “count.” We count the elements that result after applying the filter on the stream.

In this article series, we saw how lambdas enable the use of functional programming in Java by passing in directly the behavior we want to execute instead of an object containing the behavior. Using lambdas transforms our code into a more readable and concise code, compared to the use of anonymous inner classes which look more like code spaghetti. The use of out-of-the-box functional interfaces that Java 8 brings to the table facilitates the declaration of lambda expressions without the need for the programmer to create one each time. Last but not least, lambdas play a huge role when dealing with streams, enabling us to use them as inputs for different kinds of processings like filtering, adding elements to a new list, and much more.

Article Sources

https://www.youtube.com/@Java.Brains

Series Sections

Part one — fundamentals

Part two — more on lambdas

Part three — lambdas and functional interfaces

--

--