Java Fundamentals

Functional Programming in Java

Lecture 10

Lambda expressions, functional interfaces, method references, and Stream API

What is Functional Programming?

Functional programming is a programming paradigm that treats computation as the evaluation of mathematical functions
  • Java 8 introduced functional programming features
  • Allows writing more concise and expressive code
  • Makes it easier to work with collections and streams
  • Reduces boilerplate code
Key concept: Functions as first-class citizens (can be passed as parameters)

Before Functional Programming: Anonymous Classes

Before Java 8, we used anonymous classes for simple operations:
import java.util.Arrays;
import java.util.List;

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

// Old way: using anonymous class
names.forEach(new Consumer<String>() {
    @Override
    public void accept(String name) {
        System.out.println(name);
    }
});
This is verbose and hard to read!

Lambda Expressions: A Better Way

Lambda expressions provide a concise way to write anonymous functions:
import java.util.Arrays;
import java.util.List;

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

// New way: using lambda expression
names.forEach(name -> System.out.println(name));

// Even shorter with method reference
names.forEach(System.out::println);
Much cleaner and easier to understand!

Lambda Syntax

Lambda expressions have a simple syntax:
// Basic syntax: (parameters) -> expression
(x, y) -> x + y

// With type declarations
(int x, int y) -> x + y

// Single parameter (parentheses optional)
x -> x * 2

// No parameters
() -> System.out.println("Hello")

// Multiple statements (need curly braces)
(x, y) -> {
    int sum = x + y;
    return sum * 2;
}

Functional Interfaces

A functional interface has exactly one abstract method:
@FunctionalInterface
public interface Calculator {
    int calculate(int a, int b);
}

// Using the functional interface with a lambda
Calculator add = (a, b) -> a + b;
Calculator multiply = (a, b) -> a * b;

System.out.println(add.calculate(5, 3));      // 8
System.out.println(multiply.calculate(5, 3)); // 15
The @FunctionalInterface annotation is optional but recommended

Common Functional Interfaces in Java

Java provides many built-in functional interfaces in java.util.function:
  • Predicate<T> - takes T, returns boolean
    Predicate<Integer> isPositive = x -> x > 0;
  • Function<T,R> - takes T, returns R
    Function<String, Integer> length = s -> s.length();
  • Consumer<T> - takes T, returns nothing
    Consumer<String> print = s -> System.out.println(s);
  • Supplier<T> - takes nothing, returns T
    Supplier<Double> random = () -> Math.random();

Predicate Example

Predicates are used for testing conditions:
import java.util.function.Predicate;

public class PredicateExample {
    public static void main(String[] args) {
        // Define predicates
        Predicate<Integer> isEven = x -> x % 2 == 0;
        Predicate<Integer> isPositive = x -> x > 0;

        // Test values
        System.out.println(isEven.test(4));      // true
        System.out.println(isPositive.test(-5)); // false

        // Combine predicates
        Predicate<Integer> isPositiveEven = isEven.and(isPositive);
        System.out.println(isPositiveEven.test(4));  // true
        System.out.println(isPositiveEven.test(-4)); // false
    }
}

Function Example

Functions transform input to output:
import java.util.function.Function;

public class FunctionExample {
    public static void main(String[] args) {
        // Define functions
        Function<String, Integer> length = s -> s.length();
        Function<Integer, Integer> square = x -> x * x;

        // Apply functions
        System.out.println(length.apply("Hello")); // 5
        System.out.println(square.apply(4));       // 16

        // Chain functions
        Function<String, Integer> lengthSquared = length.andThen(square);
        System.out.println(lengthSquared.apply("Hi")); // 4 (length=2, square=4)
    }
}

Method References

Method references are shorthand for lambda expressions that call a specific method:
import java.util.Arrays;
import java.util.List;

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

// Lambda expression
names.forEach(name -> System.out.println(name));

// Method reference (equivalent)
names.forEach(System.out::println);

// Static method reference
Function<String, Integer> parseInt = Integer::parseInt;

// Instance method reference
String str = "Hello";
Supplier<Integer> lengthSupplier = str::length;

// Constructor reference
Supplier<List<String>> listSupplier = ArrayList::new;

Introduction to Streams

Streams provide a functional way to process collections:
  • A stream is a sequence of elements that supports sequential and parallel operations
  • Streams don't store data - they operate on a source (collection, array, etc.)
  • Stream operations are either intermediate (return a stream) or terminal (return a result)
import java.util.Arrays;
import java.util.List;

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

// Create a stream and perform operations
numbers.stream()
       .filter(n -> n % 2 == 0)     // Intermediate: keep even numbers
       .map(n -> n * 2)              // Intermediate: double each number
       .forEach(System.out::println); // Terminal: print results

Stream Operations: filter()

Filter selects elements based on a predicate:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");

// Filter names starting with 'A' or 'C'
List<String> filtered = names.stream()
    .filter(name -> name.startsWith("A") || name.startsWith("C"))
    .collect(Collectors.toList());

System.out.println(filtered); // [Alice, Charlie]

// Filter numbers greater than 50
List<Integer> numbers = Arrays.asList(10, 45, 60, 75, 30, 90);
List<Integer> large = numbers.stream()
    .filter(n -> n > 50)
    .collect(Collectors.toList());

System.out.println(large); // [60, 75, 90]

Stream Operations: map()

Map transforms each element using a function:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

List<String> names = Arrays.asList("alice", "bob", "charlie");

// Convert to uppercase
List<String> uppercase = names.stream()
    .map(String::toUpperCase)
    .collect(Collectors.toList());

System.out.println(uppercase); // [ALICE, BOB, CHARLIE]

// Get string lengths
List<Integer> lengths = names.stream()
    .map(String::length)
    .collect(Collectors.toList());

System.out.println(lengths); // [5, 3, 7]

Stream Operations: reduce()

Reduce combines elements to produce a single result:
import java.util.Arrays;
import java.util.List;

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

// Sum all numbers
int sum = numbers.stream()
    .reduce(0, (a, b) -> a + b);
System.out.println("Sum: " + sum); // 15

// Find maximum
int max = numbers.stream()
    .reduce(Integer.MIN_VALUE, (a, b) -> a > b ? a : b);
System.out.println("Max: " + max); // 5

// Using method reference for sum
int sum2 = numbers.stream()
    .reduce(0, Integer::sum);
System.out.println("Sum2: " + sum2); // 15

Stream Operations: collect()

Collect accumulates stream elements into a collection:
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

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

// Collect to List
List<String> list = names.stream()
    .collect(Collectors.toList());

// Collect to Set (removes duplicates)
Set<String> set = names.stream()
    .collect(Collectors.toSet());

// Join strings
String joined = names.stream()
    .collect(Collectors.joining(", "));
System.out.println(joined); // Alice, Bob, Charlie, Alice

Practical Example: Processing Employee Data

class Employee {
    private String name;
    private int age;
    private double salary;

    // Constructor, getters, setters...
}

List<Employee> employees = Arrays.asList(
    new Employee("Alice", 25, 50000),
    new Employee("Bob", 35, 75000),
    new Employee("Charlie", 28, 60000)
);

// Find employees earning more than 55000
List<String> highEarners = employees.stream()
    .filter(e -> e.getSalary() > 55000)
    .map(Employee::getName)
    .collect(Collectors.toList());

System.out.println(highEarners); // [Bob, Charlie]

Exercise 1: Filter and Transform

Create a program that:
  • Creates a list of integers from 1 to 20
  • Filters out odd numbers (keep only even numbers)
  • Multiplies each remaining number by 3
  • Collects the results into a new list
  • Prints the final list
Hint: Use stream(), filter(), map(), and collect()

Exercise 2: String Processing

Given a list of strings, create a program that:
  • Filters strings with length greater than 5
  • Converts them to uppercase
  • Sorts them alphabetically
  • Joins them with " | " separator
  • Prints the result
List<String> words = Arrays.asList("apple", "banana", "cat",
                                   "dog", "elephant", "fish");
// Expected output: "BANANA | ELEPHANT"

Exercise 3: Calculate Statistics

Create a program that:
  • Takes a list of product prices: [19.99, 25.50, 15.00, 42.00, 8.99]
  • Calculates the total sum of all prices
  • Finds the average price
  • Counts how many products cost more than 20.00
  • Finds the most expensive product
Hint: Use reduce(), filter(), count(), and max()

Exercise 4: Student Grade Processor

Create a Student class with name and grade fields. Then:
  • Create a list of students with various grades (0-100)
  • Filter students with grades >= 60 (passing)
  • Map to their names
  • Sort alphabetically
  • Print each passing student's name
class Student {
    String name;
    int grade;
    // Constructor, getters...
}

Exercise 5: Data Processing Pipeline

Create a data processing pipeline for a list of transactions:
class Transaction {
    String id;
    String type; // "CREDIT" or "DEBIT"
    double amount;
    LocalDate date;
}
Your pipeline should:
  • Filter transactions from the last 30 days
  • Filter only "CREDIT" transactions
  • Sum the total credited amount
  • Print the result

Best Practices

  • Keep lambdas short - if they're complex, extract to a method
  • Use method references when possible for better readability
  • Prefer streams for collection processing over loops
  • Avoid side effects in lambda expressions (don't modify external state)
  • Use meaningful variable names even in short lambdas
  • Be careful with parallel streams - only use when appropriate

Summary

Today we learned:
  • Lambda expressions provide concise syntax for anonymous functions
  • Functional interfaces define single abstract methods
  • Common functional interfaces: Predicate, Function, Consumer, Supplier
  • Method references simplify lambda expressions further
  • Streams enable functional-style operations on collections
  • Key stream operations: filter, map, reduce, collect
Functional programming makes Java code more expressive and maintainable!

Slide Overview