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[]) {
List accounts = 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(List accounts, 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
List accounts = 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 interface
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;
}
and the method call will be
//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 interface
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;
}
and similarly our method call will be updated as below.
//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 --> Predicate
FetchAccountInfo --> Function
PrintAccount --> Consumer
Hence, we may replace our interface usage with these
Java 8 Functional interfaces. So, before start creating your own functional interface, please check this package.
The replaced code will be as below
//with custom Functional interface
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;
}
Hmmm...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.
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 List searchUsingStreams() {
//using aggregate functions without referencing/using any functional interface directly
List searchedAccountNames = 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;
}
This 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!
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!