Behaviour Parameterization with lambda expressions
This write up is about passing behaviour as parameter using lambdas in Java.
Little Background:
Java’s lambda expressions are just anonymous functions which you can define on the go. These anonymous functions are the bread and butter for functional programming in java.
Let’s assume we have a class for Apple
public enum Color {
red,
green;
}
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public class Apple {
private Integer weight;
private Color color;
private Integer price;
}
Let’s also assume that there’s a farmer who cultivates apples and you are supposed to create an application for him where based on the attribute of choice we should let the farmer filter apples.
One fine day the farmer turns up and informs you that he needs filtering on Color, so you come up with a filter method who’s definition is like this
public List<Apple> filterByColor(final Color color, final List<Apple> apples){
final List<Apple> filteredApples = new ArrayList<>();
for(Apple apple: apples){
if(apples.getColor().equals(color)){
filteredApples.add(apple);
}
}
return filteredApples;
}
But what you forgot to perceive is the golden rule of software, requirements keep changing :D, so another fine day the farmer turns up
and tells you that the application needs filter by weight support as well, so after this you go into thinking mode and think of all the
scenarios for filter given the Apple class has three attributes weight
, color
, price
so your first shot at solving this problem is
something like
private void filterByColor(final Color color, final List<Apple> filteredApples){
for(Apple apple: apples){
if(apples.getColor().equals(color)){
filteredApples.add(apple);
}
}
}
private void filterByWeight(final Integer weight, final List<Apple> filteredApples){
for(Apple apple: apples){
if(apples.getWeight() >= weight){
filteredApples.add(apple);
}
}
}
private void filterByPrice(final Integer price, final List<Apple> filteredApples){
for(Apple apple: apples){
if(apples.getPrice() >= price){
filteredApples.add(apple);
}
}
}
public List<Apple> filter(final Color color, final Integer weight, final Integer price, final List<Apple> apples){
final List<Apple> filteredApples = new ArrayList<>();
for(int i=0; i<apples.size(); i++){
if(color != null){
filterByColor(color, filteredApples);
}
if(weight != null){
filterByWeight(weight, filteredApples);
}
if(price != null){
filterByPrice(price, filteredApples);
}
}
return filteredApples;
}
so the requirement is satisfied and the farmer is happy, but there is a serious issue with this kind of implementation, notice it ?
- All the attributes are passed as parameters to the filter method, so let’s say you want to filter by only color, you will end up passing color to the filter method and the rest two parameters being null.
- Let’s say that in the future you add another attribute in the Apple class which is also supports filter behaviour then the filter method needs to change and the clumsy if block needs to be added.
- Code duplication, observe that most of the code is same except for the if clause in the filterBy
methods.
Can we do better? Definitely yes, Java 8 lambda expressions to the rescue
I assume you are aware of Functional Interfaces in java, if not i recommend you to have a in depth read about it.
One such functional interface is called Predicate
interface Predicate<T>{
boolean test(T t);
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
default Predicate<T> negate() {
return (t) -> !test(t);
}
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
our method of interest here is the test
method which takes an object of Type T
, so let’s see how this can be used to
filter our Collection of apples efficiently
Let’s define three implementations of the Predicate
class with parameter T
being Apple
public RedApplePredicate implements Predicate<Apple> {
@Override
boolean test(Apple apple){
return red.equals(apple.getColor());
}
}
public HeayApplePredicate implements Predicate<Apple> {
@override
boolean test(Apple apple){
return apple.getWeight() >= 150;
}
}
now you may be thinking that since the parameters to filter are hard coded in the above implementation even this seems daunting because you will have to create classes for each category of filter, we can avoid this as well through an in line implementation for the Predicate class like this
final Predicate<Apple> redApplePredicate = (a) -> a.getColor().equals(red);
final Predicate<Apple> greenApplePredicate = (a) -> a.getColor().equals(green);
clearly the above code seems very elegant now let’s apply this to our filter method
public List<Apple> filter(final List<Apple> apples, final Predicate<Apple> filterBehaviour){
final List<Apple> filteredApples = new ArrayList();
for(Apple apple: apples){
if(filterBehaviour.test(apple)){
filteredApples.add(apple);
}
}
return filteredApples;
}
so now our filter method accepts a predicate, say now we have to filter for green apples with weight > 150, we could easily do this by
public static void main(String[] args){
final Predicate<Apple> greenApplePredicate = (a) -> a.getColor().equals(green);
final Predicate<Apple> heavyApplePredicate = (a) -> a.getWeight() >= 150;
final Apple redApple = new Apple(150, red, 150);
final Apple greenApple = new Apple(100, green, 150);
final Apple greenHeavyApple = new Apple(150, green, 150);
final List<Apple> apples = Arrays.asList(redApple, greenApple, greenHeavyApple);
final List<Apple> filteredApples = this.filter(apples, greenApplePredicate.and(heavyApplePredicate));
}
You can clearly see that the implementation above is very elegant and easily modifyable without requiring major code changes. That’s the power of behaviour parameterization.
Happy Coding!!!