Lambda Expressions with Functional Interfaces in Java – Complete Guide

Overview

Lambda expressions provide a clear and concise way to represent instances of functional interfaces (interfaces with a single abstract method).

Basic Lambda Syntax

@FunctionalInterface interface MyFunction { int operate(int a, int b); } public class BasicLambdaSyntax { public static void main(String[] args) { // Traditional anonymous class MyFunction traditional = new MyFunction() { @Override public int operate(int a, int b) { return a + b; } }; // Lambda expression equivalent MyFunction lambda = (a, b) -> a + b; System.out.println("Traditional: " + traditional.operate(5, 3)); System.out.println("Lambda: " + lambda.operate(5, 3)); // Different lambda syntax variations MyFunction add = (a, b) -> a + b; MyFunction multiply = (int a, int b) -> a * b; MyFunction subtract = (a, b) -> { return a - b; }; System.out.println("Add: " + add.operate(10, 5)); System.out.println("Multiply: " + multiply.operate(10, 5)); System.out.println("Subtract: " + subtract.operate(10, 5)); } }

Built-in Functional Interfaces

1. Predicate

import java.util.*; import java.util.function.Predicate; public class PredicateExample { public static void main(String[] args) { // Predicate - takes one argument, returns boolean Predicate<String> isLong = s -> s.length() > 5; Predicate<Integer> isEven = n -> n % 2 == 0; Predicate<String> startsWithA = s -> s.startsWith("A"); List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Anna"); List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // Using predicates System.out.println("Is 'Hello' long? " + isLong.test("Hello")); System.out.println("Is 4 even? " + isEven.test(4)); // Filter names with predicate System.out.println("Long names:"); names.stream() .filter(isLong) .forEach(System.out::println); // Even numbers System.out.println("Even numbers:"); numbers.stream() .filter(isEven) .forEach(System.out::println); // Predicate composition Predicate<String> longAndStartsWithA = isLong.and(startsWithA); System.out.println("Long names starting with A:"); names.stream() .filter(longAndStartsWithA) .forEach(System.out::println); // Negate Predicate<String> notLong = isLong.negate(); System.out.println("Short names:"); names.stream() .filter(notLong) .forEach(System.out::println); } }

2. Function

import java.util.*; import java.util.function.Function; public class FunctionExample { public static void main(String[] args) { // Function - takes one argument, produces result Function<String, Integer> stringLength = s -> s.length(); Function<Integer, Integer> square = n -> n * n; Function<String, String> toUpperCase = String::toUpperCase; Function<String, String> addPrefix = s -> "Mr. " + s; List<String> names = Arrays.asList("alice", "bob", "charlie"); // Apply functions System.out.println("Length of 'Hello': " + stringLength.apply("Hello")); System.out.println("Square of 5: " + square.apply(5)); // Transform names System.out.println("Uppercase names:"); names.stream() .map(toUpperCase) .forEach(System.out::println); // Function composition Function<String, String> formatName = addPrefix.andThen(toUpperCase); System.out.println("Formatted names:"); names.stream() .map(formatName) .forEach(System.out::println); // Complex transformation Function<String, String> processName = toUpperCase .andThen(s -> s + "!") .andThen(s -> "Hello " + s); System.out.println("Processed names:"); names.stream() .map(processName) .forEach(System.out::println); } }

3. Consumer

import java.util.*; import java.util.function.Consumer; public class ConsumerExample { public static void main(String[] args) { // Consumer - takes one argument, returns void Consumer<String> print = s -> System.out.println(s); Consumer<String> printUpperCase = s -> System.out.println(s.toUpperCase()); Consumer<Integer> printSquare = n -> System.out.println(n * n); List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); // Using consumers print.accept("Hello World"); printUpperCase.accept("hello world"); // Process collections System.out.println("Names:"); names.forEach(print); System.out.println("Names in uppercase:"); names.forEach(printUpperCase); // Consumer chaining Consumer<String> printAndUpperCase = print.andThen(printUpperCase); System.out.println("Chained consumers:"); printAndUpperCase.accept("test"); // Complex consumer Consumer<String> detailedPrint = s -> { System.out.println("Length: " + s.length()); System.out.println("First char: " + s.charAt(0)); System.out.println("Value: " + s); System.out.println("---"); }; System.out.println("Detailed print:"); names.forEach(detailedPrint); } }

4. Supplier

import java.util.*; import java.util.function.Supplier; import java.time.LocalDateTime; public class SupplierExample { public static void main(String[] args) { // Supplier - takes no arguments, produces result Supplier<String> currentTime = () -> LocalDateTime.now().toString(); Supplier<Double> randomNumber = () -> Math.random(); Supplier<List<String>> emptyList = () -> new ArrayList<>(); // Using suppliers System.out.println("Current time: " + currentTime.get()); System.out.println("Random number: " + randomNumber.get()); System.out.println("Empty list: " + emptyList.get()); // Lazy initialization Supplier<String> expensiveOperation = () -> { System.out.println("Performing expensive operation..."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } return "Expensive Result"; }; System.out.println("Supplier created, but not called yet"); System.out.println("Now calling: " + expensiveOperation.get()); // Supplier for default values Supplier<String> defaultName = () -> "Unknown"; String name = null; String result = (name != null) ? name : defaultName.get(); System.out.println("Result: " + result); // Generate random data Supplier<Integer> randomInt = () -> new Random().nextInt(100); System.out.println("Random numbers:"); for (int i = 0; i < 5; i++) { System.out.println(randomInt.get()); } } }

5. UnaryOperator and BinaryOperator

import java.util.*; import java.util.function.UnaryOperator; import java.util.function.BinaryOperator; public class OperatorExamples { public static void main(String[] args) { // UnaryOperator - takes one argument of type T, returns T UnaryOperator<String> toUpperCase = String::toUpperCase; UnaryOperator<Integer> square = n -> n * n; UnaryOperator<String> addExclamation = s -> s + "!"; // BinaryOperator - takes two arguments of type T, returns T BinaryOperator<Integer> add = (a, b) -> a + b; BinaryOperator<Integer> multiply = (a, b) -> a * b; BinaryOperator<String> concat = (s1, s2) -> s1 + s2; List<String> words = Arrays.asList("hello", "world", "java"); List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); // Using operators System.out.println("Uppercase: " + toUpperCase.apply("hello")); System.out.println("Square: " + square.apply(5)); System.out.println("Add: " + add.apply(10, 20)); System.out.println("Concat: " + concat.apply("Hello", "World")); // Transform list System.out.println("Uppercase words:"); words.stream() .map(toUpperCase) .forEach(System.out::println); // Reduce with binary operator Integer sum = numbers.stream() .reduce(0, add); Integer product = numbers.stream() .reduce(1, multiply); System.out.println("Sum: " + sum); System.out.println("Product: " + product); // Complex transformation chain UnaryOperator<String> processString = toUpperCase .andThen(addExclamation) .andThen(s -> "*** " + s + " ***"); System.out.println("Processed strings:"); words.stream() .map(processString) .forEach(System.out::println); } }

Custom Functional Interfaces

import java.util.*; import java.util.function.*; // Custom functional interfaces @FunctionalInterface interface StringProcessor { String process(String input); // Can have default methods default StringProcessor andThen(StringProcessor after) { return input -> after.process(this.process(input)); } } @FunctionalInterface interface Validator<T> { boolean validate(T value); default Validator<T> and(Validator<T> other) { return value -> this.validate(value) && other.validate(value); } default Validator<T> or(Validator<T> other) { return value -> this.validate(value) || other.validate(value); } } @FunctionalInterface interface Transformer<T, R> { R transform(T input); default <V> Transformer<T, V> andThen(Transformer<R, V> after) { return input -> after.transform(this.transform(input)); } } public class CustomFunctionalInterfaces { public static void main(String[] args) { // Using custom StringProcessor StringProcessor toUpper = String::toUpperCase; StringProcessor addExclamation = s -> s + "!"; StringProcessor addPrefix = s -> "Hello " + s; StringProcessor processor = addPrefix.andThen(toUpper).andThen(addExclamation); System.out.println("Processed: " + processor.process("world")); // Using custom Validator Validator<String> notEmpty = s -> s != null && !s.trim().isEmpty(); Validator<String> isEmail = s -> s.contains("@"); Validator<String> isLongEnough = s -> s.length() >= 5; Validator<String> emailValidator = notEmpty.and(isEmail).and(isLongEnough); System.out.println("Valid email '[email protected]': " + emailValidator.validate("[email protected]")); System.out.println("Valid email 'test': " + emailValidator.validate("test")); // Using custom Transformer Transformer<String, Integer> stringToLength = String::length; Transformer<Integer, String> intToString = Object::toString; Transformer<String, String> toUpperTransformer = String::toUpperCase; Transformer<String, String> complexTransform = toUpperTransformer.andThen(stringToLength).andThen(intToString); System.out.println("Transformed: " + complexTransform.transform("hello")); } }

Method References

import java.util.*; import java.util.function.*; public class MethodReferences { public static void main(String[] args) { List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David"); // 1. Static method reference Function<String, Integer> parseInt = Integer::parseInt; Consumer<String> staticPrint = System.out::println; // 2. Instance method reference of particular object String prefix = "Mr. "; Function<String, String> addPrefix = prefix::concat; // 3. Instance method reference of arbitrary object Function<String, String> toUpperCase = String::toUpperCase; Function<String, Integer> stringLength = String::length; // 4. Constructor reference Supplier<List<String>> listSupplier = ArrayList::new; Function<Integer, String[]> arrayCreator = String[]::new; // Using method references System.out.println("Uppercase names:"); names.stream() .map(String::toUpperCase) .forEach(System.out::println); System.out.println("Name lengths:"); names.stream() .map(String::length) .forEach(System.out::println); // More examples List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); // Static method reference numbers.forEach(System.out::println); // Constructor reference Supplier<HashMap<String, Integer>> mapSupplier = HashMap::new; HashMap<String, Integer> map = mapSupplier.get(); // Complex example with custom class List<Person> people = Arrays.asList( new Person("Alice", 25), new Person("Bob", 30), new Person("Charlie", 35) ); System.out.println("People names:"); people.stream() .map(Person::getName) // Instance method reference .forEach(System.out::println); System.out.println("Sorted by age:"); people.stream() .sorted(Comparator.comparing(Person::getAge)) // Instance method reference .forEach(System.out::println); } static class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } @Override public String toString() { return name + " (" + age + ")"; } } }

Real-World Examples

1. Filtering and Processing Data

import java.util.*; import java.util.function.*; import java.util.stream.*; public class DataProcessing { static class Product { String name; double price; String category; int stock; public Product(String name, double price, String category, int stock) { this.name = name; this.price = price; this.category = category; this.stock = stock; } // Getters public String getName() { return name; } public double getPrice() { return price; } public String getCategory() { return category; } public int getStock() { return stock; } @Override public String toString() { return String.format("%s - $%.2f (%s, stock: %d)", name, price, category, stock); } } public static void main(String[] args) { List<Product> products = Arrays.asList( new Product("Laptop", 999.99, "Electronics", 10), new Product("Phone", 699.99, "Electronics", 25), new Product("Book", 19.99, "Education", 100), new Product("Chair", 149.99, "Furniture", 15), new Product("Desk", 299.99, "Furniture", 8), new Product("Pen", 1.99, "Education", 200) ); // Predicates for filtering Predicate<Product> isElectronics = p -> "Electronics".equals(p.getCategory()); Predicate<Product> isExpensive = p -> p.getPrice() > 100; Predicate<Product> isLowStock = p -> p.getStock() < 20; // Functions for transformation Function<Product, String> getName = Product::getName; Function<Product, Double> getPrice = Product::getPrice; Function<Product, String> toUpperCaseName = p -> p.getName().toUpperCase(); // Consumers for actions Consumer<Product> printProduct = System.out::println; Consumer<Product> printNameAndPrice = p -> System.out.println(p.getName() + " - $" + p.getPrice()); // Process products System.out.println("All Electronics:"); products.stream() .filter(isElectronics) .forEach(printProduct); System.out.println("\nExpensive products:"); products.stream() .filter(isExpensive) .map(toUpperCaseName) .forEach(System.out::println); System.out.println("\nLow stock products:"); products.stream() .filter(isLowStock) .forEach(printNameAndPrice); // Complex processing System.out.println("\nElectronics with low stock:"); products.stream() .filter(isElectronics.and(isLowStock)) .forEach(printProduct); // Calculate total value Double totalValue = products.stream() .mapToDouble(Product::getPrice) .sum(); System.out.println("\nTotal value: $" + totalValue); // Group by category Map<String, List<Product>> byCategory = products.stream() .collect(Collectors.groupingBy(Product::getCategory)); System.out.println("\nProducts by category:"); byCategory.forEach((category, prods) -> { System.out.println(category + ":"); prods.forEach(p -> System.out.println(" " + p.getName())); }); } }

2. Event Handling System

import java.util.*; import java.util.function.*; public class EventHandlingSystem { @FunctionalInterface interface EventListener<T> { void onEvent(T event); default EventListener<T> andThen(EventListener<T> after) { return event -> { this.onEvent(event); after.onEvent(event); }; } } static class EventBus { private Map<Class<?>, List<EventListener<?>>> listeners = new HashMap<>(); public <T> void subscribe(Class<T> eventType, EventListener<T> listener) { listeners.computeIfAbsent(eventType, k -> new ArrayList<>()).add(listener); } @SuppressWarnings("unchecked") public <T> void publish(T event) { List<EventListener<?>> eventListeners = listeners.get(event.getClass()); if (eventListeners != null) { eventListeners.forEach(listener -> ((EventListener<T>) listener).onEvent(event)); } } } // Event classes static class UserLoginEvent { String username; LocalDateTime timestamp; public UserLoginEvent(String username) { this.username = username; this.timestamp = LocalDateTime.now(); } @Override public String toString() { return "UserLoginEvent{username='" + username + "', timestamp=" + timestamp + "}"; } } static class PaymentEvent { String userId; double amount; public PaymentEvent(String userId, double amount) { this.userId = userId; this.amount = amount; } @Override public String toString() { return "PaymentEvent{userId='" + userId + "', amount=" + amount + "}"; } } public static void main(String[] args) { EventBus eventBus = new EventBus(); // Create event listeners using lambdas EventListener<UserLoginEvent> loginLogger = event -> System.out.println("LOG: " + event); EventListener<UserLoginEvent> loginNotifier = event -> System.out.println("NOTIFICATION: User " + event.username + " logged in"); EventListener<UserLoginEvent> loginAuditor = event -> System.out.println("AUDIT: Login recorded for " + event.username); EventListener<PaymentEvent> paymentProcessor = event -> System.out.println("PROCESSING: Payment of $" + event.amount + " from " + event.userId); EventListener<PaymentEvent> paymentLogger = event -> System.out.println("PAYMENT_LOG: " + event); // Subscribe listeners eventBus.subscribe(UserLoginEvent.class, loginLogger); eventBus.subscribe(UserLoginEvent.class, loginNotifier.andThen(loginAuditor)); eventBus.subscribe(PaymentEvent.class, paymentProcessor.andThen(paymentLogger)); // Publish events System.out.println("Publishing events:"); eventBus.publish(new UserLoginEvent("alice123")); System.out.println(); eventBus.publish(new PaymentEvent("bob456", 99.99)); } }

3. Configuration System

import java.util.*; import java.util.function.*; import java.util.concurrent.*; public class ConfigurationSystem { static class ConfigManager { private Map<String, String> config = new ConcurrentHashMap<>(); private List<Consumer<Map<String, String>>> changeListeners = new CopyOnWriteArrayList<>(); public void set(String key, String value) { config.put(key, value); notifyListeners(); } public String get(String key) { return config.get(key); } public void addChangeListener(Consumer<Map<String, String>> listener) { changeListeners.add(listener); } private void notifyListeners() { Map<String, String> snapshot = new HashMap<>(config); changeListeners.forEach(listener -> listener.accept(snapshot)); } } public static void main(String[] args) { ConfigManager configManager = new ConfigManager(); // Configuration validators Predicate<String> isNotEmpty = s -> s != null && !s.trim().isEmpty(); Predicate<String> isNumeric = s -> s.matches("\\d+"); Predicate<String> isBoolean = s -> "true".equals(s) || "false".equals(s); // Configuration transformers Function<String, Integer> toInt = Integer::parseInt; Function<String, Boolean> toBoolean = Boolean::parseBoolean; // Change listeners using lambdas configManager.addChangeListener(config -> { System.out.println("Config changed: " + config); }); configManager.addChangeListener(config -> { String appName = config.get("app.name"); if (appName != null) { System.out.println("App name is now: " + appName); } }); configManager.addChangeListener(config -> { String port = config.get("server.port"); if (port != null && isNumeric.test(port)) { int portNum = toInt.apply(port); if (portNum > 0 && portNum < 65536) { System.out.println("Valid port configured: " + portNum); } else { System.out.println("Invalid port number: " + portNum); } } }); // Set configuration values System.out.println("Setting configuration values:"); configManager.set("app.name", "MyApplication"); configManager.set("server.port", "8080"); configManager.set("debug.mode", "true"); configManager.set("database.url", "jdbc:mysql://localhost:3306/mydb"); // Process configuration with functional interfaces processConfig(configManager, "server.port", isNumeric, toInt, port -> System.out.println("Server port: " + port)); processConfig(configManager, "debug.mode", isBoolean, toBoolean, debug -> System.out.println("Debug mode: " + debug)); } private static <T> void processConfig(ConfigManager config, String key, Predicate<String> validator, Function<String, T> transformer, Consumer<T> processor) { String value = config.get(key); if (value != null && validator.test(value)) { T transformed = transformer.apply(value); processor.accept(transformed); } } }

Best Practices and Common Patterns

import java.util.*; import java.util.function.*; import java.util.stream.*; public class BestPractices { public static void main(String[] args) { List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Diana"); // 1. Use method references when possible names.forEach(System.out::println); // Good names.forEach(s -> System.out.println(s)); // Okay, but verbose // 2. Keep lambdas short and focused Function<String, String> goodLambda = String::toUpperCase; Function<String, String> badLambda = s -> { // Too much logic in lambda if (s == null) return ""; String result = s.trim(); if (result.isEmpty()) return ""; return result.toUpperCase(); }; // 3. Use meaningful parameter names BinaryOperator<Integer> add = (a, b) -> a + b; // Good BinaryOperator<Integer> addBad = (x, y) -> x + y; // Less clear // 4. Combine simple lambdas for complex operations Predicate<String> isLong = s -> s.length() > 5; Predicate<String> startsWithA = s -> s.startsWith("A"); Predicate<String> longAndStartsWithA = isLong.and(startsWithA); // 5. Use existing functional interfaces when possible Consumer<String> printer = System.out::println; // Good // Avoid creating custom functional interfaces for common cases // 6. Consider using streams with lambdas List<String> result = names.stream() .filter(s -> s.length() > 3) .map(String::toUpperCase) .collect(Collectors.toList()); // 7. Handle exceptions properly List<String> numbers = Arrays.asList("1", "2", "three", "4"); numbers.forEach(s -> { try { int num = Integer.parseInt(s); System.out.println("Number: " + num); } catch (NumberFormatException e) { System.out.println("Invalid number: " + s); } }); // 8. Use final or effectively final variables final String prefix = "Hello "; names.forEach(name -> System.out.println(prefix + name)); // 9. Avoid side effects in streams List<String> collected = new ArrayList<>(); names.stream() .filter(s -> s.length() > 3) .forEach(collected::add); // Bad - side effect List<String> collectedGood = names.stream() .filter(s -> s.length() > 3) .collect(Collectors.toList()); // Good // 10. Use composition for complex logic Function<String, String> trim = String::trim; Function<String, String> toUpper = String::toUpperCase; Function<String, String> addExclamation = s -> s + "!"; Function<String, String> process = trim.andThen(toUpper).andThen(addExclamation); System.out.println("Processed: " + process.apply(" hello ")); } }

Key Features Summary

  1. Concise Syntax: Lambdas reduce boilerplate code
  2. Functional Programming: Enable functional programming patterns in Java
  3. Method References: Provide readable shorthand for common operations
  4. Built-in Interfaces: Predicate, Function, Consumer, Supplier, etc.
  5. Composition: Methods can be chained for complex operations
  6. Lazy Evaluation: Suppliers enable lazy initialization
  7. Parallel Processing: Work well with Streams for parallel operations

Lambda expressions and functional interfaces have revolutionized Java programming, making code more readable, maintainable, and expressive while enabling functional programming paradigms.

Leave a Reply

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


Macro Nepal Helper