Functional Interfaces in Java
Java 8 introduced functional programming features such as lambdas, method references, and the java.util.function
package. At the heart of this ecosystem are functional interfaces.
1️⃣ What is a Functional Interface?
- A functional interface is an interface with exactly one abstract method.
- This single method defines the functional contract.
- Java provides many built-in functional interfaces in the
java.util.function
package. - Examples:
Function<T,R>
,Consumer<T>
,Supplier<T>
,Predicate<T>
,UnaryOperator<T>
,BinaryOperator<T>
.
⚡ Lambdas and method references can be used wherever a functional interface is expected.
2️⃣ Function<T, R>
– Transforming Data
- Represents a function that takes one argument of type
T
and returns a result of typeR
.
package thisisamr;
import java.util.function.Function;
public class FunctionInterfaceExample {
public static void main(String[] args) {
Function<String, String> modify = (name) -> name + " Modified";
System.out.println(modify.apply("Amr")); // Amr Modified
// Function composition
var result = modify
.compose((String p) -> p.toLowerCase()) // runs first
.apply("AAAAA");
System.out.println(result); // aaaaa Modified
}
}
📝 Key methods
apply(T t)
→ runs the function.compose(Function before)
→ run another function before.andThen(Function after)
→ run another function after.
So you can build pipelines of transformations.
3️⃣ Consumer<T>
– Consuming Data
- Represents an operation that takes one argument and returns nothing (side effects only).
- Typical use: logging, printing, collecting results.
package thisisamr;
import java.util.List;
import java.util.function.Consumer;
public class ConsumerInterfaceExample {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
Consumer<Integer> print = System.out::println;
// Chaining consumers
numbers.forEach(print.andThen(i -> System.out.println(i * 2)));
}
}
4️⃣ Supplier<T>
– Supplying Values
- Represents a function that takes no arguments but returns a value.
package thisisamr;
import java.util.function.DoubleSupplier;
import java.util.function.Supplier;
public class SupplierInterfaceExample {
public static void main(String[] args) {
Supplier<Double> randomBoxed = Math::random;
DoubleSupplier randomPrimitive = Math::random;
System.out.println(randomBoxed.get()); // Double (boxed)
System.out.println(randomPrimitive.getAsDouble()); // double (primitive)
}
}
🔑 Why DoubleSupplier
?
- To avoid the overhead of boxing/unboxing.
Supplier<Double>
works with objects (heap allocated).DoubleSupplier
works directly withdouble
(faster, no GC overhead).
👉 Boxing/unboxing exists because Java generics only work with objects, unlike modern languages (Rust, Go, C++).
5️⃣ Predicate<T>
– Filtering Data
- Represents a function that takes one argument and returns a boolean.
- Often used for filtering and conditional logic.
package thisisamr;
import java.util.List;
import java.util.function.Predicate;
public class PredicateInterfaceExample {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 9);
Predicate<Integer> divisibleBy3 = i -> i % 3 == 0;
numbers.stream()
.filter(divisibleBy3)
.forEach(System.out::println); // 3, 6, 9
// Combining predicates
Predicate<String> hasLeftBrace = str -> str.startsWith("{");
Predicate<String> hasRightBrace = str -> str.endsWith("}");
Predicate<String> both = hasLeftBrace.and(hasRightBrace);
Predicate<String> either = hasLeftBrace.or(hasRightBrace);
Predicate<String> notLeft = hasLeftBrace.negate();
System.out.println(both.test("{ASD}")); // true
System.out.println(either.test("{ASD")); // true
System.out.println(notLeft.test("ASD")); // true
}
}
6️⃣ UnaryOperator<T>
and BinaryOperator<T>
- Both are specializations of
Function
:UnaryOperator<T>
→ takes one argument and returns the same type.BinaryOperator<T>
→ takes two arguments of the same type and returns the same type.
package thisisamr;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.UnaryOperator;
public class OperatorExamples {
public static void main(String[] args) {
BinaryOperator<Integer> add = Integer::sum;
Function<Integer, Integer> square = a -> a * a;
var result = add.andThen(square).apply(1, 3);
System.out.println(result); // (1+3)^2 = 16
UnaryOperator<Integer> increment = a -> a + 1;
UnaryOperator<Integer> squareOp = a -> a * a;
System.out.println(increment.andThen(squareOp).apply(1)); // (1+1)^2 = 4
}
}
🔑 Summary
Functional interfaces allow Java to use lambdas and method references elegantly.
Core built-in interfaces:
Function<T,R>
→ transforms data.Consumer<T>
→ consumes data (side effects).Supplier<T>
→ supplies values.Predicate<T>
→ tests conditions.UnaryOperator<T>
andBinaryOperator<T>
→ operate on same-type inputs.
They can be chained, composed, and combined.
You’ll use them extensively with Streams for mapping, filtering, reducing, and collecting.
👉 Next step: apply these in Streams (map, filter, reduce, collect) — that’s where the real power shows.
Perfect 👌 what you’ve written is already a nice “playground” for students to see streams in action. Let me turn it into a structured, refined lecture on Java Streams, with clearer flow, theory + practical examples.
🚀 Java Streams – A Comprehensive Introduction
1. What are Streams?
Streams are one of the most powerful features introduced in Java 8. They allow us to process data in a declarative, functional style, instead of writing loops and manual logic.
Think of a stream as:
- A pipeline of data flowing through transformations.
- It doesn’t store data – it processes data from a source (collection, array, I/O, or infinite generator).
2. Creating Streams
You can create streams from multiple sources:
From collections:
List<String> names = List.of("Amr", "Huda", "Salwa");
names.stream().forEach(System.out::println);
From arrays:
int[] nums = {1, 2, 3, 4, 5};
Arrays.stream(nums).forEach(System.out::print);
From infinite generators:
Stream.generate(Math::random).limit(3).forEach(System.out::println);
Stream.iterate(1, n -> n * 2).limit(5).forEach(System.out::println);
3. Intermediate Operations
These transform streams but don’t produce a result until a terminal operation is called.
Some common ones:
filter
→ keep only matching elementsmap
→ transform each elementdistinct
→ remove duplicatessorted
→ order elementslimit
,skip
,takeWhile
,dropWhile
→ slicingpeek
→ debugging, view without consuming
Example:
movies.stream()
.filter(m -> m.likes > 20) // keep movies with likes > 20
.map(m -> m.title) // transform to titles
.distinct()
.forEach(System.out::println);
4. Terminal Operations
These end the stream pipeline and produce a result (number, collection, boolean, etc.).
Examples:
- Counting
long count = movies.stream().filter(m -> m.likes > 20).count();
- Matching
movies.stream().allMatch(m -> m.likes > 10); // true/false
movies.stream().anyMatch(m -> m.genre == Movie.GENRE.ACTION);
- Finding
movies.stream().findFirst().ifPresent(System.out::println);
movies.stream().findAny().ifPresent(System.out::println);
- Reducing
int totalLikes = movies.stream()
.map(m -> m.likes)
.reduce(Integer::sum)
.orElse(0);
5. Collecting Results
Streams integrate tightly with the Collectors utility class.
- To List/Set
List<String> titles = movies.stream()
.map(m -> m.title)
.toList();
- To Set (unique values)
Set<Integer> likes = movies.stream()
.map(m -> m.likes)
.collect(Collectors.toSet());
- Summarizing
var stats = movies.stream()
.collect(Collectors.summarizingInt(m -> m.likes));
System.out.println(stats.getAverage());
- Grouping
var grouped = movies.stream()
.collect(Collectors.groupingBy(m -> m.genre));
- Grouping + Mapping
var groupedTitles = movies.stream()
.collect(Collectors.groupingBy(m -> m.genre,
Collectors.mapping(m -> m.title, Collectors.joining(", "))));
- Partitioning
var partitioned = movies.stream()
.collect(Collectors.partitioningBy(m -> m.likes > 20));
6. Sorting
Streams allow easy sorting:
- With a comparator:
var sorted = movies.stream()
.sorted(Comparator.comparingInt(m -> m.likes))
.toList();
- With a custom comparator:
var sortedDesc = movies.stream()
.sorted((m1, m2) -> m2.likes - m1.likes)
.toList();
7. Primitive Streams
When working with primitives, Java provides specialized streams:
IntStream
LongStream
DoubleStream
Useful methods:
IntStream.range(1, 5).forEach(System.out::println); // 1..4
IntStream.rangeClosed(1, 5).forEach(System.out::println); // 1..5
This avoids boxing/unboxing overhead.
8. Example Recap with Movie
Class
List<Movie> movies = List.of(
new Movie("A", 12, Movie.GENRE.ACTION),
new Movie("B", 22, Movie.GENRE.ROMANCE),
new Movie("C", 32, Movie.GENRE.COMEDY)
);
// Filter + map + collect
List<String> popularMovies = movies.stream()
.filter(m -> m.likes > 20)
.map(m -> m.title)
.toList();
System.out.println(popularMovies); // [B, C]
9. Key Takeaways
- Streams = pipeline of data with intermediate + terminal operations.
- Encourages declarative programming (what to do, not how).
- Avoids manual loops and state.
- Powerful when combined with lambdas + functional interfaces.
- Collectors let you transform results into lists, sets, maps, summaries, or groups.