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.










