Java SE 8 was a major release, it brought so many features and enhancements to the java language (e.g. Functional interfaces, Lambdas, Method references, Stream API, ...etc). in this article we will mainly focus on Functional interfaces, lambdas, and method references.
but before jumping to these features, let's first see why we need them in the first place by taking an example:
Imagine we have a Product class
public class Product {
...
private String label;
private String description;
private double price;
private double weight;
private int quantity;
...
// Constructors
...
// Getters and Setters
...
}
and we want to implement an API to filter products based on their price (e.g. filter products whose price is greater than a specific price), a naive solution would be:
public static List<Product> filterByPriceGreaterThan(List<Product> products, double price) {
List<Product> result = new ArrayList<>(); // a List to store filtered products
for (Product product : products) {
if (product.getPrice() > price) {
result.add(product);
}
}
return result;
}
Sometime later, requirements change and now we also want to be able to filter products based on their weight (e.g. we want to filter heavy products whose weight is greater than a specific weight), a simple and naive solution would be to copy the method we created earlier and make the required changes:
public static List<Product> filterByWeightGreaterThan(List<Product> products, double weight) {
List<Product> result = new ArrayList<>(); // a List to store filtered products
for (Product product : products) {
if (product.getWeight() > weight) {
result.add(product);
}
}
return result;
}
As we can see the two methods are similar with minor differences in behavior(i.e. the first one filters based on price, the second one based on weight).
Wouldn't it be nice if we could have one single filter method that can do all the filtering work?
The answer is, fortunately, a YES, and this is possible thanks to behavior parameterization
Behavior parameterization is a software development pattern that makes it possible to handle frequent requirement changes. Behavior parameterization adds the ability to a method to receive multiple different behaviors as its parameter and use them internally to accomplish the task.
Let's refactor our previous example to use behavior parameterization, if we observe the previous implementations we will notice that the only change to the logic of the filter method happens at product.getX() > X
test, so we will need to abstract away this test, to do so, we will create the following interface:
public interface ProductPredicate {
boolean test(Product product, double value);
}
The filter method becomes:
public static List<Product> filterProducts(List<Product> products, ProductPredicate predicate, double value) {
List<Product> result = new ArrayList<>();
for (Product product : products) {
if (predicate.test(product, value)) {
result.add(product);
}
}
return result;
}
Now, to introduce a new filter implementation, all we have to do is to create a class that implements the ProductPredicate interface, for instance:
public class GreaterThanPriceProductPredicate implements ProductPredicate {
@Override
public boolean test(Product product, double price) {
return product.getPrice() > price;
}
}
and use the filter method as follows:
List<Product> expensiveProducts = filterProducts(products, new GreaterThanPriceProductPredicate(), 200.0);
and we can add as many implementations as needed without altering the logic of the filter method, which conforms well with the Open-Closed principle (i.e. the filter method is open for extension but closed for modification).
The problem with this approach is, a class is required each time we need to add an implementation, even when that implementation will only be used once, to solve this problem, anonymous classes can be used.
The official documentation states that:
Anonymous classes enable you to make your code more concise. They enable you to declare and instantiate a class at the same time. They are like local classes except that they do not have a name. Use them if you need to use a local class only once.
meaning we can define an implementation of the ProductPredicate interface and instantiate it at the same time:
List<Product> expensiveProducts = filterProducts(products, new ProductPredicate() {
@Override
public boolean test(Product product, double price) {
return product.getPrice() > price;
}
}, 200.0);
The drawback here is verbosity, even if the implementation is simple (e.g. implementing an interface with a single abstract method), which is the case here, we still have to write a lot of boilerplate code.
Lambda Expressions(aka. Lambdas)
Java 8 introduced a concise and compact way to pass implementations around; using lambda expressions
Lambda expressions can be thought of as anonymous functions, since they don't have a name and are not associated with a class, they are also considered first-class citizens because they can be stored in a variable, passed as method argument, or returned as a result (e.g. similar to how functions behave in a functional programming language).
The syntax of a lambda expression is as follows:
(Parameter list) -> { Lambda body }
Notes:
- The data types in the parameter list are optional thanks to type inference (i.e. the compiler deduces the types from the context).
- The parentheses around the parameter list can be omitted when the lambda has one single parameter
- The curly braces around the lambda body can be omitted when the body consists of one single line of code.
Example :
Let's say we want a predicate to filter products that are out of stock, the implementation can be written as follows:
Predicate<Product> outOfStockProducts = (Product product) -> {
return product.getQuantity() == 0;
};
let's focus on the right-hand side of the assignment and not worry about the left-hand side part as we will get to it soon.
the code above above can be shortened to:
Predicate<Product> outOfStockProducts = product -> product.getQuantity() == 0;
Now let's go back to our previous example and refactor it to use lambdas instead of anonymous classes, the code becomes:
List<Product> expensiveProducts = filterProducts(products,
(product, price) -> product.getPrice() > price, 200.0);
Ahaah! much better.
But... does that mean lambdas replace anonymous classes?
No, not really, lambdas can only be used in the context of a functional interface, whereas anonymous classes can be used whenever an anonymous implementation for an interface/abstract class is required, it's safe to say that any lambda expression can be replaced by an anonymous class but the opposite is not always true.
Functional interfaces
Simply put, a functional interface is an interface that has one single abstract method, the Java API has several functional interfaces, some examples are Runnable, Comparator, ...etc.
recall the ProductPredicate interface:
public interface ProductPredicate {
boolean test(Product product, double value);
}
it's a functional interface since it only specifies one abstract method (non-static/default methods in an interface are implicitly abstract, there is no need to add the abstract keyword).
Notes :
- Functional interfaces can optionally specify default and static methods, as long as they specify one single abstract method they are considered functional interfaces.
- Functional interfaces can be annotated with the @FunctionalInterface annotation to convey their purpose and is considered a good practice.
- The abstract method in a functional interface is called function descriptor because it describes the signature (i.e. parameter list and return type) of the lambda expression that should be used as an implementation.
- In Java 8, several functional interfaces were introduced inside the java.util.function package, examples are: Predicate, Consumer and Function, ...etc ( check this link for a complete list)
Letβs for example take the Predicate interface:
Itβs declared as follows:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T obj);
//Other default/static methods
...
}
It has one single abstract method test that takes an object of generic type T and returns a boolean, and as we have seen before it's especially useful when dealing with filtering functionality (i.e. we take an object and test to see if it passes the predicate we specified and return true or false respectively).
now back to lambdas, as we have seen before, lambdas eliminate the verbosity of anonymous classes by introducing a concise way to pass implementations around, however, this is not the only difference between anonymous classes and lambdas, the following table summarizes the difference between them:
Anonymous Classes | Lambdas |
---|---|
The compiler will generate a separate .class file. | The compiler will translate the lambda to a private static/instance method, and binds it dynamically using the invokedynamic instruction . |
Creates a new scope | Doesnβt create a new scope |
The this keyword represents the instance we are instantiating | The this keyword is inherited from the enclosing scope |
Can be used in the context of an interface/abstract class with one or multiple abstract methods | Can only be used in the context of an interface with a single abstract method (i.e. functional interface) |
Let's elaborate on some of the differences above:
Bytecode generation:
Each time the compiler encounters an anonymous class, it will generate a separate .class file containing the implementation we are providing, the file name is usually something like ClassName$number; where ClassName is the class in which the anonymous class was used, and number denotes the order (starting from 1), this means more memory will be required and the application's runtime performance will be impacted negatively (i.e. the JVM has to load and verify each class when needed).
In the other hand, when the compiler encounters a lambda expression, it will translate it to a method, and there are three scenarios here:
- If the lambda doesn't access variables in the enclosing scope(aka. non-capturing lambda), the compiler will generate a static method with same signature as the lambda.
- If the lambda accesses local/static variables in the enclosing scope, the compiler will generate a static method and preappend the accessed variables to the arguments list of the generated method.
- If the lambda accesses instance variables, this, or super of the enclosing scope, the compiler will generate an instance method.
N.b: we can verify that by inspecting the generated bytecode with the javap -c -p ClassName
command.
Generating a static/instance method is more efficient than creating a separate class.
Scope:
Anonymous classes create a new scope, this means redeclaration of variables of the enclosing scope in the anonymous class is valid (i.e. the declarations in the enclosing scope will be shadowed), this is not the case with lambdas, since they don't create a new scope, the compiler will generate an error if we try to redeclare a variable of the enclosing scope in the lambda parameter list or body;
for instance, the following code won't compile:
...
Product product = ...;
...
ProductPredicate predicate = (Product product) -> product.getPrice() > 200.0;
the compiler will complain that the variable product is already defined.
Whereas, if we use an anonymous class, it will compile:
ProductPredicate predicate = new ProductPredicate() {
@Override
public boolean test(Product product) {
return product.getPrice() > 200.0;
}
};
In fact, this is not everything when it comes to scope, lambdas and anonymous classes share some similarities in that they both have access to local, static, and instance variables of the enclosing scope, there is, however, a restriction on local variables access; for a lambda/anonymous class to capture a local variable, this latter should be final or effectively final (i.e. never assigned a new value after being initialized), this means lambdas and anonymous classes cannot mutate the state of local variables, consider the following snippet:
...
int counter = 0;
btn.setOnClickListener(view -> {
counter++; // this line causes the compilation to fail
counterView.setText("Count: " + counter);
});
...
We are attaching an event listener to the button btn, so that whenever it's clicked we will increment the counter and display the updated value, however, this snippet will fail to compile.
And we can fix it by declaring the counter variable as a static/instance member variable, the following code will compile and work as expected:
...
private static int counter;
...
counter = 0;
btn.setOnclickListener(view -> {
counter++;
counterView.setText("Count: " + counter);
});
...
the reason behind such restriction is due to the fact that local variables, unlike static/instance variables, reside on the stack, and if we observe the previous example, the lambda was used as a callback, meaning its execution will be deferred until runtime (i.e. when the botton is clicked), and chances are, by that time, the method that declared the local variable counter would have returned and counter get popped off the stack, and the lambda has no way to access it, hence the restriction.
you may be wondering, if that's the case then why lambdas always have access to final/effectively final local variables?
Actually, when a lambda captures a local variable of the outer scope, it makes a copy of that variable rather than taking a reference to it, the final/effectively final restriction takes place so we don't get the impression that we are modifying the local variable where, in fact, we are just modifying its copy.
There are actually other reasons behind that restriction, for instance, consider the following piece of code:
...
boolean isTaskDone = false;
Runnable task = () -> {
...
// some processing
...
isTaskDone = true;
};
...
In the code above, the task we created will probably run on a separate thread, and since each thread has its own private stack, the thread in which the task will be running has no access to the stack of the main thread, meaning it has no access to isTaskDone variable.
Method References:
Lambdas are great, they are concise, compact, and reduce a lot of boilerplate code, especially when their body is a few lines of code, but that's not always the case, and there are also times when a lambda simply calls an existing method, or implements the same logic as an existing method, in such cases, it would be better if we refer to the method by its name, method references are a special kind of lambdas that allow us to do exactly that, let's take an example:
Let's say we have a list of products that we want to sort based on their price:
...
List<Product> products = new ArrayList<>();
products.add(new Product("ProductB", "ProductB description", 200.0, 250.0, 0));
products.add(new Product("ProductD", "ProductD description", 400.0, 450.0, 1400));
products.add(new Product("ProductA", "ProductA description", 100.0, 150.0, 0));
products.add(new Product("ProductE", "ProductE description", 500.0, 550.0, 1500));
products.add(new Product("ProductC", "ProductC description", 300.0, 350.0, 1300));
...
Now suppose the Product class has a static helper method compareByPrice that takes two products and compares them based on their price:
public static int compareByPrice(Product product1, Product product2) {
return Double.compare(product1.getPrice(), product2.getPrice());
}
we can then sort the list of the products as follows:
...
// Using a Lambda
products.sort((product1, product2) -> Double.compare(product1.getPrice(), product2.getPrice()));
// Using a Method Reference
products.sort(Product::compareByPrice);
...
Alternatively, if we didn't have the compareByPrice method, we can make use of the comparing method of the Comparator interface that was also introduced in Java 8:
...
import static java.util.Comparator.comparing;
...
products.sort(comparing(Product::getPrice));
...
As we can see method references are more compact, more concise, and read like the problem statement(i.e. we are sorting the products list by comparing their price).
Method References Types:
There are four types of method references, the table below summarizes them:
And this is it for this article, if you made it to the end congrats, please feel free to share your comments and insights below.
Happy learning!
References
Java 8 in Action Book
Oracle Java Docs
Top comments (3)
really nice, to complex for me for now but i'm almost there
Good to hear, I recommend you give Java 8 in Action a look.
I utilized EchoAPI for API testing in my Java project, and it proved to be extremely valuable for backend testing. The platform offers various methods for sending and receiving data from APIs, making the process highly efficient