Introduction
In the previous post we had discussed about the Why and What of Lambda expressions. In this post we will take an working example to understand Lambda expressions better. It will be a step by step interactive discussion. We will start with having our own custom implementation and then will be replacing with Java 8 functional interfaces. You can access, download and run all these examples from this git repo. Java 8 Lambda Expressions - Examples. The package java8.account is the one we will be discussing here. Just use mvn test to run all the examples. Lets begin!
Example Application Structure
The example application is all about creating different types of Bank accounts and searching the accounts based on different criteria. The classes, interfaces and the relationship between them are given in the below figures.
Application Structure - Classes |
Here, the Account, an abstract class is inherited by two different types of account called as SavingsAccount and CheckingAccount. Gender is an Enum type.
Application Structure - Utility & Interfaces |
Searching Accounts
The main discussion here is how can we have better search method for a list of accounts in an easiest and efficient way. Lets assume that we have a method to create and return list of accounts in the BankUtil as below.
public static List getAccounts() {
List accounts = Arrays.asList (
new SavingsAccount("A", 2000, 500, Gender.FEMALE),
new CheckingAccount("B", 4000, 5000, Gender.MALE),
new SavingsAccount("C", 3000, 500, Gender.MALE),
new CheckingAccount("D", 5000, 5000, Gender.FEMALE));
return accounts;
}
The constructor parameters are accountName, balance, minimumBalance(in case of SA), creditLimit(in case of CA) and Gender. This can be represented as below for easy understanding.
Representation of List of Accounts
Now, as we have a list of accounts, we want to search these based on some criteria say, the available balance in an account. The normal way we search the list of accounts is just by using the forEach loop as given below.
//simple search criteria
public static List searchAccounts(List accounts, double balance){
List searchedAccounts = new ArrayList();
for(Account a : accounts) {
if(a.getBalance() >= balance) {
searchedAccounts.add(a);
System.out.println(a);
}
}
return searchedAccounts;
}
Here, we search the accounts based on the available balance and then returns the searched accounts. And, we can call this method as below.
public static void main(String args[]) {
Listaccounts = BankUtil.getAccounts(); }
BankUtil.searchAccounts(accounts, 1000);
This looks fine for simple search with balance, but what if we wanted to search the accounts within some balance range? We may need to modify our search method as below.
public static void searchAccounts(Listaccounts, double minBal, double maxBal) { }
Change is the only constant thing. Hence, we may change our requirement at anytime and want to search based on different different criteria. Every time we change our criteria we need to update our search method. Yes, we see a maintainability issue here.
Hence, the generic solution would be moving the test logic out of our main code and the new definition will be as below.
//with custom Functional interface
public static List searchAccounts(List accounts, TestAccount tester) {
List searchedAccounts = new ArrayList();
for (Account a : accounts) {
if (tester.test(a)) {
searchedAccounts.add(a);
System.out.println(a);
}
}
return searchedAccounts;
}
Ok, this looks great now. How about the caller? The caller should pass two parameters to this search method, List of accounts and the instance of TestAccount. As we are familiar with Lambda expressions no need to use any Inner class or Anonymous Inner class for the purpose of passing the TestAccount instance. We can use Lambda expression here and it would look like as below.
//using Lambda
Listaccounts = BankUtil.getAccounts();
BankUtil.searchAccounts (accounts,
a -> a.getBalance() >= 2000 && a.getBalance() <= 5000);
I think we are good now. But, still we can improve the above search method by moving the line which prints the value of account to an interface so that the searched data can be consumed and utilized for different purpose other than just printing the value.
To do the same we can create another interface called PrintAccount which will have a method to accept a parameter and do something on that.
Hence, the search method will be as below now.
//with custom Functional interfaceand the method call will be
public static List searchAccounts(List accounts, TestAccount tester, PrintAccount consumer) {
List searchedAccounts = new ArrayList();
for (Account a : accounts) {
if (tester.test(a)) {
searchedAccounts.add(a);
consumer.accept(a);
}
}
return searchedAccounts;
}
//using Lambda
List accounts = BankUtil.getAccounts();
BankUtil.searchAccounts (accounts,
a -> a.getBalance() >= 2000 && a.getBalance() <= 5000,
a -> System.out.println(a) );
Cool. We are moving fast on Lambda expressions and have used it properly. But, still there is a room for improvement. Lets go back to the search method again. What we do here is that we retrieve the account based on some balance, we print the entire account details and then we return the list of searched accounts for further use.
What if we wanted to access specific details from Account and manipulate that? For example, I may want to retrieve the account name of each account and to display it in upper case? How can I do that here?
Yes, you were right. We need to create an interface and keep that logic there. Lets define another interface called FetchAccountInfo which will have method to accept account and apply our logic into the details.
Now, our latest search method will be
//with custom Functional interfaceand similarly our method call will be updated as below.
public static List searchAccounts(List accounts, TestAccount tester, FetchAccountInfo mapper, PrintAccount consumer) {
List searchedAccounts = new ArrayList();
for (Account a : accounts) {
if (tester.test(a)) {
searchedAccounts.add(a);
String data = mapper.apply(a);
consumer.accept(data);
}
}
return searchedAccounts;
}
//using Lambda
List accounts = BankUtil.getAccounts();
BankUtil.searchAccounts (accounts,
a -> a.getBalance() >= 2000 && a.getBalance() <= 5000,
a->a.getAccountName(),
accName -> System.out.println(accName) );
Excellent! we are done with our example discussion to understand the use of Lambda Expressions better. One thing we noticed here is that the method call would become so simple with the help of Lambda expressions. No more Anonymous classes!
But, did you notice use of interfaces? They are functional interfaces(have only one abstract method) which is what required for Lambda usage. Whenever we wanted more customization and flexibility we continue creating the functional interfaces. This might be repeated for multiple projects. How to avoid these creation of interfaces? Java 8 helps here. It gives you the package java.util.function with many functional interfaces for different purposes. Java 8 - Functional Interfaces.
I would recommend you to go thro' this package. When you visit this page you might wonder that we have already used some those interfaces. Yes, you are right. You will be able to map our interfaces to those as below.
TestAccount --> PredicateHence, we may replace our interface usage with these Java 8 Functional interfaces. So, before start creating your own functional interface, please check this package.
FetchAccountInfo --> Function
PrintAccount --> Consumer
The replaced code will be as below
//with custom Functional interfaceHmmm...lot of code! I can hear your mind. As we mentioned already there is always a room for improvement and Java 8 helps in that.
public static List searchAccounts(List accounts, Predicate tester, Function mapper, Consumer consumer) {
List searchedAccounts = new ArrayList();
for (Account a : accounts) {
if (tester.test(a)) {
searchedAccounts.add(a);
String data = mapper.apply(a);
consumer.accept(data);
}
}
return searchedAccounts;
}
The above all can be replaced by Java 8 Streams aggregate methods. We can discuss about Streams in another post. But, for now just we can have the equivalent code.
public static ListThis is simple, declarative and performance oriented. You can exactly match with our previous search method implementation. If you look at the parameter type of filter() which is Predicate, the parameter type of map() is of type Function and finally the parameter of forEach() is of Consumer type. This is the power of Streams!searchUsingStreams() {
//using aggregate functions without referencing/using any functional interface directly
ListsearchedAccountNames = new ArrayList ();
BankUtil.getAccounts().stream()
.filter(a -> a.getBalance() >= 2000 && a.getBalance() <=
5000 && a.getGender() == Gender.MALE)
.map(a -> a.getAccountName())
.forEach(accName -> { System.out.println(accName);
searchedAccountNames.add(accName);});
return searchedAccountNames;
}
Want to Practice?
Practice makes us perfect. So, to conclude our understanding of Lambda expressions lets take one simple exercise.
Create a simple calculator application which should accept three parameters. The first two parameters are integers, which need to be computed and the third parameter is the operation type. Use lambda expressions to call/use this calculator for different types of operation such as addition, subtraction, division and multiplication.
You can refer the git repo Java 8 Lambda Examples package java8.calculator from src/main/java and src/test/java for the solution.
Conclusion
I hope you were able to understand the usage of Lambda expressions better. Will connect later on next post. Bye for now!
I should admit that, this representation is absolutely perfect :D Thanks so much!
ReplyDelete