Java Functional Programming

Functional Interfaces

Functional interfaces have a single abstract method and are the basis of lambda expressions. Key interfaces include:

  • Supplier: Provides a result without input.
  • Consumer: Accepts a single input and performs an action.
  • Function: Accepts input and returns a result.
  • Predicate: Represents a boolean condition.

Example:

Supplier<String> supplier = () -> "Hello World";
System.out.println(supplier.get());

Predicate<Integer> isEven = n -> n % 2 == 0;
System.out.println(isEven.test(4)); // true

Consumer<String> print = s -> System.out.println("Input: " + s);
print.accept("Hello, World!"); // Output: Input: Hello, World!

Function<Integer, String> intToString = n -> "The number is: " + n;
String result = intToString.apply(10); // Result: "The number is: 10"
System.out.println(result);

Stream Operations

Stream operations are divided into:

  • Terminal Operations: End the stream pipeline, e.g., collect(), forEach(), reduce().
  • Intermediate Operations: Transform the stream, e.g., filter(), map(), flatMap().

Example:

List names = Arrays.asList("John", "Alice", "Bob");
List upperNames = names.stream()
     .filter(name -> name.length() > 3)
     .map(String::toUpperCase)
     .collect(Collectors.toList());
System.out.println(upperNames); // [JOHN, ALICE]

Intermediate Operations

Intermediate operations transform a stream and are lazy (executed only when terminal operations are invoked).

  • filter(): Filters elements based on a condition.
  • map(): Transforms each element.
  • flatMap(): Flattens nested structures.

Example:

List names = Arrays.asList("John", "Alice", "Bob");
names.stream()
     .filter(name -> name.startsWith("A"))
     .map(String::toUpperCase)
     .forEach(System.out::println); // ALICE
     
//flatMap 
List<List<String>> nestedList = Arrays.asList(
            Arrays.asList("A", "B"),
            Arrays.asList("C", "D"),
            Arrays.asList("E", "F")
        );

List<String> flattenedList = nestedList.stream()
         .flatMap(List::stream) // Flattens each inner list into a single stream
         .collect(Collectors.toList());

System.out.println(flattenedList); // Output: [A, B, C, D, E, F]     
     

Primitive Streams

Specialized streams for primitives avoid boxing overhead:

  • DoubleStream: For double values.
  • IntStream: For int values.
  • LongStream: For long values.

Example:

IntStream.range(1, 5).forEach(System.out::println); // 1 2 3 4

Primitive streams are specialized streams designed for primitive types to avoid the overhead of boxing and unboxing. This improves performance when working with large datasets of primitive values by reducing memory usage and computation time. They also provide specialized methods like sum(), average(), and min() for convenience.

Optional

Optional helps avoid NullPointerException by representing a value that may or may not be present.

Example:

Optional optional = Optional.ofNullable(null);
System.out.println(optional.orElse("Default Value")); // Default Value

Searching Streams

Streams provide methods to search elements based on conditions:

  • findFirst(): Retrieves the first element in the stream.
  • findAny(): Retrieves any element, useful for parallel streams.
  • anyMatch(): Checks if any element matches a condition.
  • allMatch(): Checks if all elements match a condition.
  • noneMatch(): Checks if no elements match a condition.

Example:

List numbers = Arrays.asList(1, 2, 3, 4);
boolean anyEven = numbers.stream().anyMatch(n -> n % 2 == 0); // true
boolean allPositive = numbers.stream().allMatch(n -> n > 0); // true

Sorting Streams

Streams can be sorted using the sorted() method. Sorting can be in natural order or with a custom Comparator.

  • Natural Order: Uses the elements' natural ordering.
  • Custom Comparator: Defines custom sorting logic.

Example:

// Natural order sorting
List names = Arrays.asList("John", "Alice", "Bob");
names.stream().sorted().forEach(System.out::println);

// Custom comparator
names.stream()
     .sorted((a, b) -> b.compareTo(a)) // Descending order
     .forEach(System.out::println);

Method References

Method references simplify lambda expressions by directly referring to a method by its name. There are four types:

  • Static Method Reference: ClassName::staticMethod
  • Instance Method Reference: object::instanceMethod
  • Constructor Reference: ClassName::new
  • Reference to an Instance Method of an Arbitrary Object: ClassName::instanceMethod

Example:

// Static method reference
List numbers = Arrays.asList(1, 2, 3, 4);
numbers.forEach(System.out::println);

// Constructor reference
Supplier> listSupplier = ArrayList::new;
List list = listSupplier.get();

Mutable vs Immutable Objects in Java Streams

When using Java Streams, handling mutable and immutable objects in operations like forEach leads to different outcomes. Mutable objects (e.g., StringBuilder) allow in-place modifications, which can introduce side effects. For example:


List<StringBuilder> list = Arrays.asList(new StringBuilder("one"), new StringBuilder("two"));
list.stream().forEach(sb -> sb.append(" updated")); // Modifies the original objects
    

Here, the original StringBuilder instances are altered. In contrast, immutable objects (e.g., String) cannot be changed directly, ensuring thread safety and preventing side effects:


List<String> list = Arrays.asList("one", "two");
list.stream().map(String::toUpperCase).forEach(System.out::println); // Creates new objects
    

This creates new values while keeping the original list intact, promoting a functional approach.

Post a Comment

0 Comments