Menu

Functional Programming and OOP Integration

Functional Programming and OOP Integration

Java is, and remains, fundamentally an object-oriented language. But since Java 8, it has absorbed a substantial set of functional programming ideas, treating behaviour itself as a value that can be passed around, rather than always needing to be wrapped inside a full object. This chapter looks at how these two styles work together, not against each other.

Functional Interfaces

Introduced in the Intermediate tutorial's chapter on interfaces, a functional interface is simply an interface with exactly one abstract method. That single-method shape is what makes it possible to represent an implementation as a compact lambda expression rather than a full named class.

@FunctionalInterface

interface Validator<T> {

boolean isValid(T input);

}

Validator<String> notEmpty = s -> s != null && !s.isEmpty();

System.out.println(notEmpty.isValid("hello"));   // true

System.out.println(notEmpty.isValid(""));       // false

Lambda Expressions

A lambda expression is a compact, anonymous block of behaviour, an implementation of a functional interface written inline, without a class name, a method name, or even an explicit return type, all of which the compiler infers from context.

List<String> names = new ArrayList<>(List.of("Ravi", "Asha", "Meera"));

// Sorting with a lambda implementing Comparator's single abstract method

names.sort((a, b) -> a.compareTo(b));

// Filtering and printing using lambdas as Predicate and Consumer implementations

names.forEach(name -> System.out.println("Hello, " + name));

Under the hood, a lambda still creates an object implementing the relevant functional interface, the same conceptual mechanism as the anonymous inner classes covered in Chapter 1, just expressed far more concisely.

Method References

When a lambda would do nothing more than call an existing method, Java lets you reference that method directly by name, using the :: operator, which is often clearer than writing out the equivalent lambda.

List<String> names = List.of("Ravi", "Asha", "Meera");

names.forEach(System.out::println);       // equivalent to: name -> System.out.println(name)

List<Integer> lengths = names.stream()

.map(String::length)                    // equivalent to: name -> name.length()

.collect(Collectors.toList());

// Referencing a constructor directly

Supplier<ArrayList<String>> listMaker = ArrayList::new;

Method Reference Form

Example

Equivalent Lambda

Static method

Integer::parseInt

s -> Integer.parseInt(s)

Instance method, particular object

System.out::println

x -> System.out.println(x)

Instance method, arbitrary object

String::length

s -> s.length()

Constructor

ArrayList::new

() -> new ArrayList<>()

Stream API Basics

The Stream API, introduced alongside lambdas in Java 8, lets you express data-processing pipelines, filtering, transforming, and reducing collections, declaratively, describing what should happen to the data rather than manually writing the loops yourself.

List<Employee> employees = List.of(

new Employee("Asha", 60000),

new Employee("Ravi", 45000),

new Employee("Meera", 72000)

);

List<String> highEarnerNames = employees.stream()

.filter(e -> e.salary > 50000)     // keep only matching elements

.map(e -> e.name)                    // transform each remaining element

.sorted()                                // arrange the results

.collect(Collectors.toList());          // gather into a new List

System.out.println(highEarnerNames);   // [Asha, Meera]

double totalPayroll = employees.stream()

.mapToDouble(e -> e.salary)

.sum();                                  // reduce the stream to a single value

A stream pipeline always follows the same shape: a source (here, the employees list), zero or more intermediate operations (filter, map, sorted), and exactly one terminal operation (collect, sum, forEach) that actually triggers the processing and produces a result.

Combining OOP and Functional Programming

In well-designed modern Java code, OOP and functional style are not competitors, they divide responsibilities. Classes and interfaces still model your domain's nouns, the things in your system, while lambdas and streams handle the verbs, the transformations and processing applied to collections of those things.

class Order {

String customer;

double amount;

boolean shipped;

Order(String c, double a, boolean s) { customer = c; amount = a; shipped = s; }

}

List<Order> orders = List.of(

new Order("Asha", 250, true),

new Order("Ravi", 180, false),

new Order("Meera", 420, true)

);

// The Order class is plain OOP; the processing pipeline below is functional

double shippedRevenue = orders.stream()

.filter(o -> o.shipped)

.mapToDouble(o -> o.amount)

.sum();

System.out.println("Shipped revenue: " + shippedRevenue);   // 670.0

Practical Examples

Strategy pattern objects, covered in Chapter 5, often become unnecessary as full classes once a functional interface and a lambda can express the same idea more directly, especially when the 'strategy' is a single, simple operation.

// The Strategy pattern from Chapter 5, reimagined with a functional interface

interface DiscountStrategy {

double apply(double price);

}

class Checkout {

private DiscountStrategy strategy;

Checkout(DiscountStrategy s) { strategy = s; }

double finalPrice(double price) { return strategy.apply(price); }

}

// No separate NoDiscount or PercentageDiscount classes needed anymore

Checkout regular = new Checkout(price -> price);

Checkout sale = new Checkout(price -> price * 0.8);

System.out.println(sale.finalPrice(1000));   // 800.0

Performance Considerations

•        Streams add a small amount of overhead compared to a hand-written loop, due to the extra layers of abstraction; for very simple, performance-critical operations on small collections, a plain for loop may still be faster.

•        Parallel streams (.parallelStream()) can speed up processing of very large collections by splitting work across CPU cores, but they introduce real overhead of their own and are not automatically faster, they should be measured, not assumed.

•       Capturing variables in a lambda or stream pipeline can create extra object allocations under the hood; in extremely hot code paths, this is occasionally worth profiling.

•        Readability and maintainability gains from streams and lambdas usually outweigh the small performance cost for typical application code; reach for the imperative, loop-based style mainly when profiling has actually identified a real bottleneck.

Did You Know?

The Stream API was deliberately designed so that streams are lazy: intermediate operations like filter() and map() don't actually run any code until a terminal operation like collect() or sum() is called. This means you can build up a long, complex pipeline of operations cheaply, and the JVM only does the real work once, when a result is genuinely needed.