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