1. Favor Immutability

One of the core principles of functional programming is immutability. Immutable objects are those whose state cannot be modified after creation. Immutable objects offer several benefits, such as thread safety, predictability, and easier reasoning about the code.

In Java, you can achieve immutability by following these guidelines:

  • Make classes final to prevent inheritance.
  • Make fields final and private.
  • Don’t provide setter methods.
  • If a class needs to be mutable, consider creating a new instance with the updated state instead of modifying the existing object.

Here’s an example of an immutable class in Java:

public final class Person {
    private final String name;
    private final int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

2. Use Pure Functions

Pure functions are a fundamental concept in functional programming. A pure function is a function that always produces the same output for the same input and has no side effects. Pure functions are predictable, testable, and easier to reason about.

When writing functional code in Java, strive to create pure functions whenever possible. Here are some guidelines to follow:

  • Functions should depend only on their input parameters and not on any external state.
  • Functions should not modify any external state or have side effects.
  • Functions should return a value based solely on their input.

Here’s an example of a pure function in Java:

public static int square(int x) {
    return x * x;
}

3. Functional Interfaces and Lambda Expressions

Java provides functional interfaces and lambda expressions to enable functional programming. Functional interfaces are interfaces that have a single abstract method (SAM). Lambda expressions provide a concise way to implement functional interfaces.

When working with functional code in Java, make use of the built-in functional interfaces such as Function, Predicate, Consumer, and Supplier. These interfaces cover common use cases and provide a standard way to represent functions.

Here’s an example of using a functional interface and lambda expression:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squaredNumbers = numbers.stream()
                                      .map(x -> x * x)
                                      .collect(Collectors.toList());

4. Avoid Mutating State

In functional programming, it’s important to avoid mutating state. Mutable state can lead to unexpected behavior, make code harder to reason about, and introduce concurrency issues.

When working with collections or data structures, prefer using immutable versions or creating new instances instead of modifying existing ones. Java provides immutable collections in the java.util.Collections class, such as emptyList(), singletonList(), and unmodifiableList().

Here’s an example of using an immutable list:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> immutableNames = Collections.unmodifiableList(names);

5. Favor Composition over Inheritance

Composition is a powerful technique in functional programming that allows you to create complex behavior by combining smaller, reusable functions. In Java, you can achieve composition using functional interfaces and lambda expressions.

Instead of relying heavily on inheritance, which can lead to tight coupling and inflexible code, favor composition. Create small, focused functions and combine them to build more complex behavior.

Here’s an example of composition using functional interfaces:

Function<Integer, Integer> square = x -> x * x;
Function<Integer, Integer> addOne = x -> x + 1;

Function<Integer, Integer> squareAndAddOne = square.andThen(addOne);

int result = squareAndAddOne.apply(3); // Output: 10

6. Use the Stream API for Data Processing

Java’s Stream API is a powerful tool for processing collections in a functional and declarative manner. It provides a fluent and expressive way to perform operations like filtering, mapping, reducing, and collecting.

When working with collections, consider using the Stream API instead of traditional imperative loops. Streams allow you to focus on the “what” rather than the “how” of data processing, making the code more readable and maintainable.

Here’s an example of using the Stream API to process a list of numbers:

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

int sum = numbers.stream()
                 .filter(x -> x % 2 == 0)
                 .mapToInt(x -> x * x)
                 .sum();

System.out.println(sum); // Output: 20

7. Handle Exceptions Functionally

Exception handling is an important aspect of any Java program. In functional programming, it’s recommended to handle exceptions in a functional manner using techniques like the Optional class or functional exception handling libraries.

The Optional class provides a way to represent the presence or absence of a value, avoiding null checks and reducing the chances of NullPointerException. It offers methods like map(), flatMap(), and filter() to operate on the contained value functionally.

Here’s an example of using Optional to handle a potentially null value:

public static Optional<String> getUserName(User user) {
    return Optional.ofNullable(user)
                   .map(User::getName)
                   .filter(name -> !name.isEmpty());
}

8. Lazy Evaluation

Lazy evaluation is a technique in functional programming where the evaluation of an expression is deferred until its value is needed. Java’s Stream API supports lazy evaluation, allowing you to chain multiple operations without actually performing them until a terminal operation is invoked.

Lazy evaluation can provide performance benefits by avoiding unnecessary computations and allowing for infinite or large data structures.

Here’s an example of lazy evaluation using the Stream API:

Stream<Integer> infiniteStream = Stream.iterate(0, x -> x + 1);

List<Integer> firstTenNumbers = infiniteStream
                                    .limit(10)
                                    .collect(Collectors.toList());