• Home
  • LLMs
  • Docker
  • Kubernetes
  • Java
  • All
  • About
Java | Lambda Expressions
  1. Lambda Expressions
  2. Using Lambda Expressions with Collections

  1. Lambda Expressions
    A lambda expression is an anonymous function that can be treated as a value - it can be assigned to variables, passed as arguments, or returned from methods. Lambda expressions provide a concise way to represent functional interfaces (interfaces with a single abstract method).

    A lambda expression can be used in several contexts:
    • Initializing a variable of a functional interface type.
    • As method or constructor arguments where a functional interface is expected.
    • As method return values.
    • In conditional expressions "?:" (ternary operator).

    The syntax of a lambda expression is as follows: parameters -> body

    The arrow operator -> separates the parameter list from the body of the lambda expression.

    • Parameters: Define the input variables for the lambda expression.
      Parameter types can be omitted if the compiler can infer them from the context (target typing). When types are specified, they must be specified for all parameters or none.

      Parentheses around parameters are:
      • Required for zero parameters: ()
      • Optional for exactly one parameter: x or (x)
      • Required for multiple parameters: (x, y)

      Examples:
      • explicit types: (Integer i, Integer j) -> i + j
      • inferred types: (i, j) -> i + j
      • no parameters: () -> Math.random()
      • single parameter: i -> i * 2

    • Body: Can be either a single expression or a block of statements.
      For single expressions, the return keyword is implicit. For statement blocks, you must explicitly use return if the lambda should return a value.

      Examples:
      • expression body: (String s) -> s.toUpperCase()
      • statement block: (String s) -> { return s.toUpperCase(); }
      • void lambda: () -> System.out.println("Hello!")

    • Return Type: The return type is inferred by the compiler based on the lambda body and target type.
      Examples:
      • returning String: () -> "hello!"
      • with explicit return: () -> { return "hello!"; }
      • void return: () -> System.out.println("Hello!")
      • higher-order function: x -> y -> x + y (returns a function)

    Lambda expressions have some limitations compared to regular methods:
    • Cannot use break or continue statements unless inside a loop within the lambda.
    • Cannot throw checked exceptions unless the functional interface's method declares them.
    • Must be compatible with the target functional interface's method signature.
    import java.util.function.Predicate;
    
    public class TestLambda {
        public Predicate<Integer> testCheckedException() {
            Predicate<Integer> predicate = t -> {
                if (t == 0) {
                    // Runtime exceptions are allowed
                    throw new IllegalStateException("t should not be equal to 0");
                }
    
                // Checked exceptions are NOT allowed unless declared in the functional interface
                if (t % 2 == 0) {
                    throw new Exception("t should be odd"); // Compiler error: Unhandled exception type Exception
                }
    
                return t >= 1;
            };
    
            return predicate;
        }
    }
    Lambda expressions can access variables from their enclosing scope:
    • Local variables: Must be effectively final (not reassigned after initialization).
    • Instance variables: Can be accessed and modified freely.
    • Static variables: Can be accessed and modified freely.
    import java.util.function.Predicate;
    
    public class TestLambda {
        private Integer instanceVar = 1;
        private static Integer classVar = 1;
    
        public Predicate<Integer> testVariableCapture() {
            final Integer finalVar = 1;
            Integer effectivelyFinalVar = 1; // must not be modified after this point
    
            Predicate<Integer> predicate = t -> {
                // Can modify instance and static variables
                instanceVar = t;
                classVar = t * 2;
    
                // Can read final and effectively final local variables
                return t > finalVar && t < effectivelyFinalVar;
            };
    
            // effectivelyFinalVar = 1; // Un-commenting this line will cause a compile error: Local variable effectivelyFinalVar defined in an enclosing scope must be final or effectively final
    
            return predicate;
        }
    }
    Functional Interfaces:
    Lambda expressions can only be used where a functional interface is expected. A functional interface has exactly one abstract method.

    Examples:
    • Runnable: () -> void
    • Supplier<T>: () -> T
    • Consumer<T>: T -> void
    • Function<T,R>: T -> R
    • Predicate<T>: T -> boolean

    Runnable Example:
    Runnable task = () -> System.out.println("Task is running...");
    task.run(); // Executes the lambda
  2. Using Lambda Expressions with Collections
    • forEach Method
      The java.lang.Iterable interface provides a default forEach method that enables functional-style iteration over collections.
      public interface Iterable<T> {
          default void forEach(Consumer<? super T> action) {
              Objects.requireNonNull(action);
              for (T t : this) {
                  action.accept(t);
              }
          }
      }
      The forEach method accepts a Consumer<T> functional interface, allowing you to specify an action to perform on each element.

      Since java.util.Collection extends Iterable, all collection classes (List, Set, etc.) inherit this method.

      Anonymous Classes vs Lambda Expressions:
      import java.util.Arrays;
      import java.util.Collection;
      import java.util.function.Consumer;
      
      public class MainClass {
          public static void main(String[] args) {
              Collection<String> list = Arrays.asList("A", "B", "C");
      
              // 1. anonymous inner class
              list.forEach(new Consumer<String>() {
                  @Override
                  public void accept(String t) {
                      System.out.println(t);
                  }
              });
      
              // 2. Lambda with explicit type
              list.forEach((String t) -> System.out.println(t));
      
              // 3. Lambda with type inference
              list.forEach(t -> System.out.println(t));
      
              // 4. Method reference
              list.forEach(System.out::println);
          }
      }
      Stream Operations with Lambda Expressions:
      import java.util.Arrays;
      import java.util.List;
      import java.util.Optional;
      import java.util.stream.Collectors;
      
      public class MainClass {
          public static void main(String[] args) {
              List<String> names = Arrays.asList("Java", "Lambda", "Expressions");
      
              // Filter and collect
              List<String> longNames = names.stream().filter(name -> name.length() > 4).collect(Collectors.toList());
      
              // Transform and collect
              List<String> upperNames = names.stream().map(name -> name.toUpperCase()).collect(Collectors.toList());
      
              // Find first match
              Optional<String> firstLongName = names.stream().filter(name -> name.length() > 4).findFirst();
      
              // Reduce operation
              int totalLength = names.stream().mapToInt(name -> name.length()).sum();
          }
      }
© 2025  mtitek