As you all know, Java 8 is a big change. Making Java syntax slimmer and easier to read.
But many of you have a hard time getting started, so I will write a thorough article why using Java lambda Java 8, it does not help us.
Note: Due to thorough analysis of the article, the code will be long. You can use the clips function to save and read slowly.
I want to help you read only once and understand thoroughly, instead of reading many times, many places but still do not understand how to use.
There are three things ignorant: Not knowing what we need to know; Know not know what you know; Know what you shouldn’t know. (La Rochefoucauld)
Coping with change
Writing code to adapt to changing requirements is a difficult task. Let’s take a look at the examples, step by step improving the code, making your code more flexible.
In the context of the farm management application, you perform the following function:
- Search for the green apples in the list.
Method 1: filter the green apples
The method you often use will be as follows:
1 2 3 4 5 6 7 8 9 10 | public static List<Apple> filterGreenApple(List<Apple> inventory) { List<Apple> result = new ArrayList<>(); for (Apple apple : inventory) { if ( "green".equals(apple.getColor())) { result.add(apple); } } return result; } |
Filter the green apple with the condition as follows: "green".equals(apple.getColor())
. But the farmer wants to change his mind and wants to search for the apple in red.
What will you do with this problem? What we often do is copy the current code, rename the function from filterGreenApple
to filterRedApple
, and change the search condition to "red".equals(apple.getColor())
.
However, this way will not meet the requirements of the farm owner, who can search for a variety of colors such as blue, red black, yellow.
There is a rule to solve this problem: when encountering duplicate code, abstract it.
Method 2: parameterize colors
What you can do is add a parameter to the function to parameterize the color to help the code more flexible with the change:
1 2 3 4 5 6 7 8 9 10 | public static List<Apple> filterApplesByColor(List<Apple> inventory, String color) { List<Apple> result = new ArrayList<>(); for (Apple apple : inventory) { if ( "color".equals(apple.getColor())) { result.add(apple); } } return result; } |
And now, the rancher is very happy because he can search by all colors:
List<Apple> greenApples = filterApplesByColor(inventory, "green");
List<Apple> redApples = filterApplesByColor(inventory, "red");
It’s easy, isn’t it? We will now explore with a harder example.
The owner of the farm meets you and tells you that
It would be great to separate apples based on weight.
As you know the answer will be like:
1 2 3 4 5 6 7 8 9 10 | public static List<Apple> filterApplesByWeight(List<Apple> inventory, int weight) { List<Apple> result = new ArrayList<>(); for (Apple apple: inventory){ if ( apple.getWeight() > weight ){ result.add(apple); } } return result; } |
It looks fine, but after a closer look you’ll realize that only the search term is changed if ( apple.getWeight() > weight )
. All that code repeats.
This breaks the DRY programming principle (don’t repeat yourself).
If you want to speed up the search terms, then you have to fix the entire executable code, instead of just fixing one place. This is very maintenance time, and very easy to create bugs.
And you want to combine two search terms by color and weight into one place. But you need to have an attribute that distinguishes a search term (by color or weight) by creating a flag variable.
Method 3: Search with multiple attributes
You can execute the following code: (but it looks confusing, quite confusing)
1 2 3 4 5 6 7 8 9 10 11 | public static List<Apple> filterApples(List<Apple> inventory, String color, int weight, boolean flag) { List<Apple> result = new ArrayList<>(); for (Apple apple: inventory){ if ( (flag && apple.getColor().equals(color)) || (!flag && apple.getWeight() > weight ) ){ result.add(apple); } } return result; } |
When flag is true
will search by color.
List<Apple> greenApples = filterApples(inventory, "green", 0, true);
When flag is false
will search by size.
List<Apple> heavyApples = filterApples(inventory, "", 150, false);
This solution is very bad.
- The code client looks horrible. What is
true
false
? - Not responding when request changes. What if the farmer asked to search by place of production, type, etc.?
- If the farmer asks for a combination of conditions: Looking for green apples weighing over 600g? You have to copy the code somewhere else and then edit the code, or create a very complicated function like creating a flag variable as above.
Parameterization by value can be solved in some specific cases. But in this case, you need a better way, and the next part I will mention it.
Behavior parameterization
As you saw in the previous section, you need a better solution instead of adding many parameters.
You just need to change the search conditions and the remaining code remains the same. An object only returns true
or false
.
We will create an interface with a function that is a search criteria
1 2 3 4 | public interface ApplePredicate { boolean test(Apple apple); } |
And now you define your search term the way you want:
Look for apples that weigh more than 150g:
1 2 3 4 5 6 | public class AppleHeavyWeightPredicate implements ApplePredicate { public boolean test(Apple apple) { return apple.getWeight() > 150 } } |
Search for green apples:
1 2 3 4 5 6 | public class AppleGreenColorPredicate implements ApplePredicate { public boolean test(Apple apple) { return "green".equals(apple.getColor()); } } |
As you can see, we have divided two types of searches, based on user needs.
What you have just seen is the Strategy Design Pattern, which helps pack the changes, and can run in real time.
The second part is the search conditions, so we created the ApplePredicate
interface with two implementation classes: AppleHeavyWeightPredicate
and AppleGreenColorPredicate
.
You can refer to the following article about Strategy Design Pattern, introduce what is Design Pattern, and how to design Strategy Design Pattern:
https://viblo.asia/p/design-pattern-la-gi-V3m5WPbyKO7
But how to implement ApplePredicate
?
You need a filterApples function, whose parameter is the ApplePredicate object to check the condition of the apple.
The meaning of parameterizing behavior: allows the function to accept multiple behaviors (strategies) as a parameter, and use them to execute.
Method 4: Search by statistic field behavior
1 2 3 4 5 6 7 8 9 10 11 | public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p) { List<Apple> result = new ArrayList<>(); for (Apple apple : inventory) { //Điều kiện tìm kiếm đã được đóng gói if (p.test(apple)) { result.add(apple); } } return result; } |
The code becomes more flexible than previous times, the code is easy to read and use.
You can create additional classes that implement the ApplePredicate
interface and pass the filterApples
function.
If the rancher asks you to look for red apples and weighs over 150g, all you need to do is create the corresponding ApplePredicate class.
Your code is now very flexible to apply to the new request:
1 2 3 4 5 6 7 8 9 | public class AppleRedAndHeavyPredicate implements ApplePredicate{ public boolean test(Apple apple){ return "red".equals(apple.getColor()) && apple.getWeight() > 150; } } //Tìm kiếm táo màu đỏ và trọng lượng > 150g List<Apple> redAndHeavyApples = filterApples(inventory, new AppleRedAndHeavyPredicate()); |
You’ve just done something amazing: the behavior of the filterApples
function depends on the object you pass in. In other words, you’ve just parameterized the behavior of the filterApples
function.
Many behaviors, one parameter
As explained earlier, the parameterization of the behavior is great, because it allows you to separate the “browse each part of the for loop” and “search behavior”.
With this application you can reuse the function, and can pass all the desired behavior.
This is why parameterizing behavior is a good concept, so you should keep it as a tool to generate flexible lines of code.
Parameterize the behavior of filterApples () and pass different strategies (behaviors)
To give you a better understanding of behavior parameterization, we will look at the following example:
Write the function that outputs the apple information
We will create a function named prettyPrintApple
- Input: apple list (Apple).
- Output: export to many different formats.
In this example I will give 2 types of output, in fact you can create 5 or 10 or 100 types:
- Output type of apple: heavy or light and their color.
- Export the weight of the apple.
To help you easily visualize yourself as a model frame for you:
1 2 3 4 5 6 7 | public static void prettyPrintApple(List<Apple> apples, ???) { for(Apple apple : apples) { String output = ???.???(apple); System.out.print(output); } } |
String output = ???.???(apple);
. As you can see we need a handler function, the input value is Apple and the result is a processed string.
You will do the same when you create an ApplePredicate
interface:
1 2 3 4 | public interface AppleFormatter{ String accept(Apple a); } |
And now we will create classes to implement the AppleFormatter
interface
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class AppleFancyFormatter implements AppleFormatter{ public String accept(Apple apple){ String characteristic = apple.getWeight() > 150 ? "heavy" : "light"; return "A " + characteristic + " " + apple.getColor() +" apple"; } } public class AppleSimpleFormatter implements AppleFormatter{ public String accept(Apple apple){ return "An apple of " + apple.getWeight() + "g"; } } |
And finally we will modify the prettyPrintApple
function
1 2 3 4 5 6 7 | public static void prettyPrintApple(List<Apple> apples, AppleFormatter formatter){ for(Apple apple: apples){ String output = formatter.accept(apple); System.out.println(output); } } |
Great! And now we can pass any behavior into prettyPrintApple
function. Do this by initializing the implementation class of the corresponding AppleFormatter
interface.
We want to export color and apple information so we will use the behavior of AppleFancyFormatter
prettyPrintApple(inventory, new AppleFancyFormatter());
Result:
A light green apple A heavy red apple …
Or just export the size of the apple:
prettyPrintApple(inventory, new AppleSimpleFormatter());
Result:
An apple of 80g An apple of 155g …
By abstracting the behavior, it makes it possible for the code to respond to many different requirements, but the problem is that we have to create many classes. To solve this problem we will explore the next example.
Resolve redundancy
Java has a technique called anonymous class, which helps you declare and initialize the class at the same time.
As the name implies (anonymous class) this class is used just like the normal class, but there will be no class name.
Method 5: use an anonymous class
The code below will create an anonymous class that implements the ApplePredicate
interface
1 2 3 4 5 6 | List<Apple> redApples = filterApples(apples, new ApplePredicate() { public boolean test(Apple apple) { return "red".equals(apple.getColor()); } }) |
Anonymous classes are often used in the context of Java applications to create event handlers.
1 2 3 4 5 6 | button.setOnAction(new EventHandler<ActionEvent>() { public void handle(ActionEvent event) { System.out.println("Woooo a click!!"); } }); |
But the anonymous class is still not good enough to review the code to search for red apples:
1 2 3 4 5 6 7 | List<Apple> redApples = filterApples(apples, new ApplePredicate() { public boolean test(Apple apple) { //Code của chúng ta chỉ thay đổi tại đây, những đoạn còn lại vẫn giữ nguyên return "red".equals(apple.getColor()); } }) |
return "red".equals(apple.getColor());
Only this paragraph is changed. Other code we have to copy and paste, very redundant.
One more thing is that the excess code will make us confused. For example, the following scenario:
When executing the code below, what is the output: 4, 5, 6 or 42?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public class MeaningOfThis { public final int value = 4; public void doIt() { int value = 6; Runnable r = new Runnable() { public final int value = 5; public void run() { int value = 10; System.out.println(this.value); } }; r.run(); } public static void main(String ...args) { MeaningOfThis m = new MeaningOfThis(); m.doIt(); } } |
The result will be 5, because it is in the scope of the Runnable, not the MeaningOfThis class.
Excess is often not good, and is not recommended in the programming language because it takes time to write code and maintain, and makes it difficult to read the code.
A good piece of code is an easy-to-understand, interesting code for readers.
Method 6: use lambda expressions (lambda expressions)
With the lambda expression you can rewrite the following:
1 2 | List<Apple> result = filterApples(apples, (Apple apple) -> "red".equals(apple.getColor())); |
You have to admit that the above code looks easier to understand than the previous one.
It is great because it only executes code that is related to the problem it needs to solve -> I want to get red apples (Apple apple) -> "red".equals(apple.getColor())
apples (Apple apple) -> "red".equals(apple.getColor())
The graph compares the parameter with value vs acts parameterized
Method 7: Abstractizing the list type (list type)
Currently the filterApple function is specifically designed for apples, but what about oranges, grapefruit, plums and mangoes?
We will use generic to solve this problem:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public interface Predicate<T>{ boolean test(T t); } public static <T> List<T> filter(List<T> list, Predicate<T> p) { List<T> result = new ArrayList<>(); for (T e: list) { if(p.test(e)) { result.add(e); } } return result; } |
And now you can search by list of apples, bananas, oranges, Interger or String.
1 2 3 | List<Apple> redApples = filter(apples, (Apple apple) -> "red".equals(apple.getColor())); List<String> evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0); |
It is amazing, isn’t it? You have realized the brevity and concise of Java 8, something that previous versions did not.
Bonus practical examples:
Sort with Comparator:
Farmers want to rearrange the list of apples by size. Or he changed his mind about wanting to sort by color.
You need a behavior to show how different types of arrangements respond to customers’ changing requirements.
In java, the List
class has a sort
function with the parameter passed as a behavioral parameter of the java.util.Comparator
arrangement with the following interface:
1 2 3 4 | public interface Comparator<T> { public int compare(T o1, T o2); } |
And we can create the corresponding behavior according to customer requirements, using the anonymous class.
Sorting by increasing weight according to apple’s weight:
1 2 3 4 5 6 | inventory.sort(new Comparator<Apple>() { public int compare(Apple a1, Apple a2){ return a1.getWeight().compareTo(a2.getWeight()); } }); |
Lambda expression:
1 2 | inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())); |
summary
Here are the points to note:
- Behavioral parameterization allows you to pass different behaviors in response to customers’ constantly changing requirements.
- Behavioral parameterization makes your code readable, easy to maintain, and eliminates redundancy.
- Creating new classes or using anonymous classes makes your code redundant. Java 8 solves this problem by using lambda, which allows you to generate code that is only relevant to the problem.
- The Java API contains many functions that help you parameterize your behavior: search, threads, and Java app processing.
If you have time, you should learn about Strategy Design Pattern:
https://viblo.asia/p/design-pattern-la-gi-V3m5WPbyKO7
Source code
https://github.com/java8/Java8InAction/tree/master/src/main/java/lambdasinaction/chap2
Contribute
Please give me a minute to help me. Please leave your comments to help the latter to read and understand better.
Thank you for your interest in this article. I wish you a good day! ?
References from: Java 8 in Action (Raoul-Gabriel Urma, Mario Fusco, and Alan Mycroft).
Blessed.