Friends of OpenJDK Today

Java: Functional Programming f(x) – Part2

June 03, 2024

Author(s)

  • Avatar photo
    Mahendra Rao B

    Senior Technical Architect hailing from India with over 17 years of experience in the field. I hold a degree in Computer Science and specialize in Java, Spring, Microservices, and AWS ... Learn more

In the preceding article, we explored the significance of Functional Programming, Lambda Calculus, and other related concepts.

In this article, we delve deeper into the essential aspects of Functional Programming, such as…​

1. Lambda Expressions
2. Method References
3. Functional Interfaces

And we will discuss about the relationship between each of the features.

1. Lambda Expressions

JSR 335 has greatly facilitated programming in a multicore environment by introducing Lambda Expressions/closures/anonymous method, and related features.

If Lambda Expressions had been integrated into the Collections API from the beginning, their development could have taken a different path. However, they have enriched the Collections API by introducing new methods to current interfaces and introducing new Interfaces such as 'Stream'.

Why Lambda Expressions?

Before the introduction of Lambda Expressions, developers commonly used ubiquitous anonymous inner classes, which were more verbose and overwhelming.

However, Lambda expressions are simplified, made more concise, and made it elegant and concise way to write a block of code that was previously written in an anonymous class.

So,

What is Lambda Expression?

According to Oracle documentation, a lambda expression is just a shorter way of writing an implementation of a method for later execution.

At compile time, the lambda expression determines its type (variable, field, method, or return type of a method) and it must be a functional interface. And you cannot write a lambda expression for an anonymous class that does not implement a functional interface.

After we determine the lambda expression’s type from the functional interface class, we can conveniently locate the appropriate method, namely the abstract method, for the implementation. As a result, the lambda expression only executes the abstract method from the functional interface.

Composing a lambda expression involves three components:

() -> { }
  1. A block of parameters
  2. An Arrow and Java uses meager arrows(->)
  3. Providing a block of code that serves as the method’s body

Nevertheless, we can streamline the syntax from () -> {} to argument -> block of code which gives more concise, simple, and readable. We can omit () if you have one argument.

For example, for the below list, we can convert the anonymous inner class syntax which takes too many lines to express the basic concept.

List names = List.of("Foojay", "Java", "Steve", "Mahi");

Collections.sort(names, new Comparator() {
  @Override
  public int compare(String str1, String str2) {
      return str1.compareTo(str2);
  }
});

Using the lamda expression, we can simplify the above code snippet like the below,

List names = List.of("Foojay", "Java", "Steve", "Mahi");

names.sort((str1, str2) -> str1.compareTo(str2));

The provided code snippet describes the lambda expression and sorting method used for the list of names as follows:

Type of Lambda Expression:

The lambda expression (str1, str2) -> str1.compareTo(str2) serves as a Comparator lambda expression. It defines the comparison logic used for sorting the elements in the list.
This lambda expression is of type Comparator, which is a functional interface in the java.util package.

Method Used for Implementation:

The list is sorted using the method List.sort(Comparator c). This method takes a Comparator as an argument to establish the order of the elements.

Finally, using Lambda expressions allows you to invoke any method specified in the interface.

This means that invoking an abstract method will execute the code within the lambda expression itself, as the lambda acts as an implementation of the method.

In contrast, invoking a default method will execute the code defined in the interface and cannot be changed by the lambda expression.

And, Lambdas cannot modify local variables that are defined outside of their body, but they can read them as long as they are final, meaning immutable.

This concept of accessing variables is known as capturing: lambdas have the ability to capture values, not variables. A final variable essentially represents a value.

2. Method References

Typically, developers utilize Lambda Expressions for writing business logic within parentheses or for invoking custom defined methods. On the other hand, Method References can serve as replacements for lambda expressions when there is an existing method available.

Using Method References can make your code more concise and often improve its readability compare to Lambda Expressions.

A lambda expression can be substituted with a method reference in the following situations:

1. Static Method References

Suppose you have the following Lambda Expression code:

List<String> names = List.of("Foojay", "Java", "Steve", "Mahi");
names.sort((str1, str2) -> str1.compareTo(str2));

The above code can be rewritten as

List<String> names = List.of("Foojay", "Java", "Steve", "Mahi");
names.sort(String::compareTo);

In this instance, the compareTo method within the String class serves as a Method Reference for String::compareTo, effectively substituting the lambda expression (str1, str2) -> str1.compareTo(str2).

2. Instance Methods of a Particular Object or Unbounded Method Reference

The general syntax of instance methods of a particular object:

Lambda Expression: (args) -> instance.method(args)

Method Reference: instance::method

When you demonstrate a suitable example of a specific object’s instance method, you should think about an instance of a custom comparator class.

Create custom comparator class

public class CustomComparator {
    public int compareStrings(String str1, String str2) {
        return str1.compareToIgnoreCase(str2);
    }
}

Lambda Expression Code

List<String> names = List.of("Foojay", "Java", "Steve", "Mahi");
var comparator = new CustomComparator();
names.sort((str1, str2) -> comparator.compareStrings(str1, str2));

Using Method References

List<String> names = List.of("Foojay", "Java", "Steve", "Mahi");
var comparator = new CustomComparator();
names.sort(comparator::compareStrings);

The method reference comparator::compareStrings refers to the compareStrings method of the comparator object, replacing the lambda expression (str1, str2) -> comparator.compareStrings(str1, str2. This substitution enhances the code’s conciseness and readability.

3. Instance Methods of a Particular Object or Bounded Method References

The general syntax for a bound method reference is expression:instanceMethod. In this syntax, expression represents an expression that yields an object, and instanceMethod denotes the name of an instance method.

Let’s consider the following example

Lambda Expression

Consumer<String> result = (s) -> System.out.println(s);

Using Bounded Method References

Consumer<String> printer = System.out::println;

4. Constructor Method References

This is pretty straigthforward, for example

Lambda Expression code : (args) -> new ClassName(args)

Supplier<List<String>> supplier = () -> new ArrayList<>();

Constructor Method References : ClassName::new

Supplier<List<String>> supplier = ArrayList::new;

3. Functional Interfaces

A functional interface is an interface that has only Single Abstract Method (SAM).

Within the JDK, the java.util.function package includes a wide array of functional interfaces.

The JDK API extensively uses these functional interfaces, particularly in the Collections Frameworks and the Stream API.

The functional interfaces can be categorized as follows:

Supplier interface: The supplier function does not accept any parameters and yields an object.

This interface is extremely straightforward: it solely consists of a get() method and does not include any default or static methods. The interface is presented below.

@FunctionalInterface
public interface Supplier<T> {
     T get();
 }

The subsequent lambda implements the aforementioned interface.

Supplier<String> supplier = () -> "Hello Foojay!";`

The JDK comes with four of specialized suppliers which will avoid unneccessary boxing / unboxing

  • IntSupplier
  • BooleanSupplier
  • LongSupplier
  • DoubleSupplier

Consumer interface: The consumer does the opposite of the supplier: it takes an argument and does not return anything.

It does contain one abstract method, and many default methods

@FunctionalInterface
public interface Consumer<T> {

     void accept(T t);
    // default methods ....
}

The subsequent lambda implements the aforementioned interface.

Consumer<String> printer = str -> System.out.println(str);

There are specialized versions of BiConsumer interface which handles the primitive types

  • ObjIntConsumer
  • ObjLongConsumer
  • ObjDoubleConsumer

Predicate interface: A predicate is utilized to examine an object, serving as a filter for streams within the Stream API.

It contains an abstract method which takes an object and returns a boolean value.

@FunctionalInterface
public interface Predicate<T> {

     boolean test(T t);
     // default and static methods
}

The subsequent lambda implements the aforementioned interface.

Predicate<String> lengthCheck = s -> s.length() == 9;

It also contains specialized versions of interfaces

  • IntPredicate
  • LongPredicate
  • DoublePredicate

Function interface:

The abstract function’s method takes an object of type T as input and produces a transformation of that object to a different type U. Furthermore, this interface contains default and static methods.

@FunctionalInterface
public interface Function<T, R> {

    R apply(U u);

    // default and static methods
}

Example

// Function that converts a String to its length
 Function<String, Integer> stringLength = String::length;

// Example string
String example = "Hello, Foojay Friends!";

// Applying the function to get the length
int length = stringLength.apply(example);

// Printing the result
System.out.println("The length of the string is: " + length);

Key Benefits

  • Improving Readability: Reducing boilerplate code enhances the clarity of the logic.
  • Increasing Efficiency: Writing less code accelerates the development process.
  • Enhancing Maintainability: Concise code facilitates easier debugging and maintenance.
  • Optimizing Parallel Processing: It seamlessly integrates with parallel processing frameworks such as the Stream API.

Conclusion

Understanding and learning, Lambda Expressions, Method References, and Functional Interfaces can enhance the efficiency and readability of your Java code.

Lambda Expressions help simplify code by eliminating the need for verbose anonymous inner classes, particularly beneficial in enhancing the clarity of the Collections framework.

Method References enhance both readability and maintainability by providing a more concise syntax that directly points to pre-existing methods.

Functional Interfaces have only one abstract method, which allows for the use of lambda expressions and method references. Important functional interfaces include Function, Consumer, Supplier, and Predicate.

In the next article, we will discuss about StreamAPI extensively.

Thanks for reading.

Happy Learning!

Related Articles

View All

Author(s)

  • Avatar photo
    Mahendra Rao B

    Senior Technical Architect hailing from India with over 17 years of experience in the field. I hold a degree in Computer Science and specialize in Java, Spring, Microservices, and AWS ... Learn more

Comments (0)

Your email address will not be published. Required fields are marked *

Highlight your code snippets using [code lang="language name"] shortcode. Just insert your code between opening and closing tag: [code lang="java"] code [/code]. Or specify another language.

Save my name, email, and website in this browser for the next time I comment.

Subscribe to foojay updates:

https://foojay.io/feed/
Copied to the clipboard