Functional Programming in Java

Java 8 introduced functional programming, which helped developers simplify and clean code. Developers new to Java or used to using Java older than Java Development Kit(JDK) version 8 should learn functional programming.

Functional Interfaces and Lambda Expressions

Java 8 introduced functional interfaces, and the FunctionalInterface annotation creates an interface that receives a parameter. The developer can manipulate processes inside the function’s operation.

@FunctionalInterface
interface Operation {
    int apply(int a, int b);
}
public class FunctionalProgramming {
    public static void main(String[] args) {
        Operation addition = (a, b) -> a + b;
        Operation subtraction = (a, b) -> a - b;

        System.out.println("Addition: " + addition.apply(5, 3));
        System.out.println("Subtraction: " + subtraction.apply(5, 3));
    }
}

The operation interface has an “apply” method with two parameters.

Addition: 8
Subtraction: 2

The developer can add default methods or static methods inside the operation interface.

@FunctionalInterface
interface MathOperation {
    int operate(int a, int b);

    default int add(int a, int b) {
        return a + b;
    }

    default int subtract(int a, int b) {
        return a - b;
    }

    // Static method
    static void greet() {
        System.out.println("Welcome to Math Operations!");
    }
}
public class FunctionalProgramming {
    public static void main(String[] args) {
         // Lambda expression implementing the operate method
        MathOperation multiplication = (a, b) -> a * b;

        System.out.println("Multiplication: " + multiplication.operate(5, 3));

        // Accessing default methods
        MathOperation operation = (a, b) -> a + b;
        System.out.println("Addition: " + operation.add(5, 3));
        System.out.println("Subtraction: " + operation.subtract(5, 3));

        // Calling static method without an instance
        MathOperation.greet();
    }
}

The MathOperation interface has an “operate” method with two parameters: a default “add” and “subtract” method, and a static “greet” method.

Multiplication: 15
Addition: 8
Subtraction: 2
Welcome to Math Operations!

Method References

Allow the developer to call the constructor method without invoking the class. There are four types of method references.

  1. Static method reference
  2. Instance method reference
  3. Constructor reference
  4. Arbitrary object method reference

Static method reference

import java.util.function.IntBinaryOperator;
class MathUtils {
    // Static method to add two numbers
    static int add(int a, int b) {
        return a + b;
    }
}

public class MethodReference {
    public static void main(String[] args) {
        // Using a static method reference
        IntBinaryOperator adder = MathUtils::add;

        int result = adder.applyAsInt(5, 3);
        System.out.println("Result of addition: " + result);
    }
}
Result of addition: 8

Instance method reference

import java.util.function.BiFunction;
class Calculator {
    // Instance method to multiply numbers
    int multiply(int a) {
        return a*a;
    }
}
public class MethodReference {
    public static void main(String[] args) {
        MethodReference m = new MethodReference();
        m.instanceMethod();
    }

    public void instanceMethod(){
        // Creating an instance of the Calculator class
        Calculator calculator = new Calculator();

        // Using an instance method reference
        BiFunction<Calculator, Integer, Integer> multiplier = Calculator::multiply;

        // Calling the instance method reference with the specified arguments
        int result = multiplier.apply(calculator, 5);
        System.out.println("Result of multiplication: " + result);
    }
}
Result of multiplication: 25

Constructor reference

Constructor references provide a concise and readable way to refer to constructors when using functional interfaces, enhancing code clarity and reducing verbosity.

import java.util.function.Function;
class Person {
    private String name;

    // Constructor
    public Person(String name) {
        this.name = name;
    }

    // Getter
    public String getName() {
        return name;
    }
}
public class MethodReference {
    public static void main(String[] args) {
        // Using constructor reference
        Function<String, Person> personCreator = Person::new;

        // Creating a new Person instance using the constructor reference
        Person person = personCreator.apply("John Doe");

        // Accessing the name using the getter method
        System.out.println("Person's name: " + person.getName());
    }
}

The Person class contains the constructor method. The developer can create a new class instance by “Person::new” and call the constructor with “personCreator.apply(“John Doe”) that creates a new person with the name “John Doe”.

Person's name: John Doe

Arbitrary object method reference

Arbitrary object method references provide a convenient way to refer to instance methods on specific objects when using functional interfaces, making the code more readable and expressive.

import java.util.function.Function;

class StringUtils {
    // Instance method to convert a string to uppercase
    String toUpperCase(String str) {
        return str.toUpperCase();
    }
}

public class MethodReference {
    public static void main(String[] args) {
        // Creating an instance of StringUtils
        StringUtils stringUtils = new StringUtils();

        // Using an arbitrary object method reference
        Function<String, String> stringConverter = stringUtils::toUpperCase;

        // Calling the instance method using method reference
        String result = stringConverter.apply("hello");
        System.out.println("Result: " + result);
    }
}
Result: HELLO

Custom function: The developer can use a method reference for any parameter.

@FunctionalInterface
interface TriFunction<A, B, C, D, R> {
    R apply(A a, B b, C c, D d);
}

class Calculator {
    // Instance method to perform a calculation with four parameters
    public int calculate(int a, int b, int c) {
        return a + b +c;
    }
}

public class MethodReference {
    public static void main(String[] args) {
        // Creating an instance of Calculator
        Calculator calculator = new Calculator();

        // Using an arbitrary object method reference with four parameters
        TriFunction<Calculator, Integer, Integer, Integer, Integer> calculatorFunction = Calculator::calculate;

        // Calling the instance method using method reference
        int result = calculatorFunction.apply(calculator, 5, 3, 8);
        System.out.println("Result of calculation: " + result);
    }
}

The developer creates a custom function interface, “TriFunction”, to receive three parameters, as shown in an example.

Streams and Functional Programming

Streams in Java provide a fluent API for processing sequences of elements. They allow you to perform aggregate operations on collections, such as filtering, mapping, and reducing.

Stream: print all words in uppercase

 public static void main(String[] args) {
    List<String> words = Arrays.asList("hello", "world", "java");

    // Using streams to convert words to uppercase and print them
    words.stream()
            .map(String::toUpperCase)
            .forEach(System.out::println);

    //Not functional
    for(String s : words){
       System.out.println(s.toUpperCase());
    }
}
HELLO
WORLD
JAVA

Stream: Manipulate values inside the list using Java Streams

public void manipulate(){
    // Create a list of integers
    List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));

    // Manipulate values inside the list using Java Streams
    List<Integer> squaredNumbers = numbers.stream()
            .map(num -> num * num)
            .collect(Collectors.toList());

    // Printing the squared numbers
    System.out.println("Squared numbers: " + squaredNumbers);

    //Not functional programming
    squaredNumbers = new ArrayList<>();
    for(int i = 0 ; i < numbers.size() ; i++){
        squaredNumbers.add(numbers.get(i) * numbers.get(i));
    }
    System.out.println("Squared numbers: " + squaredNumbers);
}
Squared numbers: [1, 4, 9, 16, 25]

Stream: Manipulate values inside the list using Java Streams and method reference

public class FuncStream {

  public void reference() {
      // Create a list of integers
      List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));

      // Manipulate values inside the list using Java Streams and method reference
      List<Integer> doubledNumbers = numbers.stream()
              .map(FuncStream::doubleNumber)
              .collect(Collectors.toList());

      // Printing the doubled numbers
      System.out.println("Doubled numbers: " + doubledNumbers);
  }

  // Method to double a number
  private static int doubleNumber(int num) {
      return num * 2;
  }

}
Doubled numbers: [2, 4, 6, 8, 10]

Stream finds the first match

Using findFirst() For Java stream, find the first match.

  • findFirst() Retrieves the first element in the stream that matches the given condition.
  • Returns an Optional element containing it if found, or an empty Optional element if no match is found.
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

public class FindFirstExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");

        // Find the first name starting with 'C'
        Optional<String> firstMatch = names.stream()
                                           .filter(name -> name.startsWith("C"))
                                           .findFirst();

        // Print the result
        firstMatch.ifPresentOrElse(
            match -> System.out.println("Found: " + match),
            () -> System.out.println("No match found")
        );
    }
}
Found: Charlie

Filter: filter even numbers.

public void filter(){
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

    // Using stream and filter to get even numbers
    List<Integer> evenNumbers = numbers.stream()
            .filter(num -> num % 2 == 0)
            .collect(Collectors.toList());

    // Printing the even numbers
    System.out.println("Even numbers: " + evenNumbers);

    //Not functional
    evenNumbers = new ArrayList<>();
    for(int num : numbers){
        if(num % 2 == 0){
            evenNumbers.add(num);
        }
    }
    System.out.println("Even numbers: " + evenNumbers);
}
Even numbers: [2, 4, 6, 8, 10]

The result is the same, but the code looks more elegant and clean.

Predicate: find the name letter starting with “A”.

public void predicate(){
    List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve", "Alan");

    // Create a Predicate to filter names starting with 'A'
    Predicate<String> startsWithA = name -> name.startsWith("A");

    // Filter names based on the Predicate
    List<String> filteredNames = names.stream()
            .filter(startsWithA)
            .collect(Collectors.toList());

    // Print filtered names
    System.out.println("Names starting with 'A': " + filteredNames);
}
Names starting with 'A': [Alice, Alan]

Predicate: Filter numbers greater than 1000 using Java Streams and Predicate

 public void predicateNumbers(){
    // Create a list of integers
    List<Integer> numbers = new ArrayList<>(Arrays.asList(500, 1200, 800, 1500, 2000));

    // Define a Predicate to check if a number is greater than 1000
    Predicate<Integer> greaterThan1000 = num -> num > 1000;

    // Filter numbers greater than 1000 using Java Streams and Predicate
    List<Integer> filteredNumbers = numbers.stream()
            .filter(greaterThan1000)
            .collect(Collectors.toList());

    // Printing the filtered numbers
    System.out.println("Numbers greater than 1000: " + filteredNumbers);
}
Numbers greater than 1000: [1200, 1500, 2000]

Reduce summary numbers in an ArrayList.

public void reduce(){
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

    // Using stream and reduce to find the sum of all numbers
    int sum = numbers.stream()
            .reduce(0, (subtotal, num) -> subtotal + num);

    // Printing the sum
    System.out.println("Sum of all numbers: " + sum);

    //Not functional
    sum = 0;
    for(int num : numbers){
        sum += num;
    }
    System.out.println("Sum of all numbers: " + sum);
}
Sum of all numbers: 15

Reduce: find the maximum number in an array list.

public void max(){
    List<Integer> numbers = Arrays.asList(7, 2, 9, 4, 5);

    // Using stream and reduce to find the maximum number
    int max = numbers.stream()
            .reduce(Integer.MIN_VALUE, Integer::max);

    // Printing the maximum number
    System.out.println("Maximum number: " + max);

    //Not functional
    max = Integer.MIN_VALUE;
    for(int num : numbers){
       max = Math.max(num, max);
    }
    System.out.println("Maximum number: " + max);
}
Maximum number: 9

Reduce: concatenate the string.

public static void main(String[] args) {
    // Create a list of strings
    List<String> words = new ArrayList<>(Arrays.asList("Hello", "world", "Java", "Streams"));

    // Concatenate strings using Java Streams and reduce
    String concatenatedString = words.stream()
                                     .reduce("", (str1, str2) -> str1 + str2);

    // Printing the concatenated string
    System.out.println("Concatenated string: " + concatenatedString);
}
Concatenated string: HelloworldJavaStreams

Optional and Functional Error Handling.

Java’s Optional class provides a way to express the absence of a value instead of using null references.

public void optional(){
    divide(4, 2).ifPresent(result -> System.out.println("Result: " + result));
    divide(4, 0).ifPresentOrElse(
            result -> System.out.println("Result: " + result),
            () -> System.out.println("Division by zero!")
    );
}

public static Optional<Double> divide(int a, int b) {
    if (b == 0) {
        return Optional.empty();
    }
    return Optional.of((double) a / b);
}
Result: 2.0
Division by zero!

Finally

Functional programming represents a contemporary approach to Java coding. Developers should familiarize themselves with its principles to enhance coding efficiency and maintain code cleanliness.

Leave a Comment

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