Crash Course on Lambda Expressions and Streams
Functional programming has been on the rise for quite some time now, and rightly so. Many non-functional programming languages had long adopted at least some amount of functional programming principles, and while Java had been lagging behind, it’s finally jumped on the bandwagon with the latest version, Java 8.
Functional programming is closer to mathematics than other programming paradigms such as procedural or object-oriented programming, so as one gains more and more experience on one of them, it gets more difficult for them to grasp the functional programming concepts. Even though I had a C/C++ background using function pointers, and a lot of Javascript experience using callbacks and methods such as Array.map()
, I had a hard time understanding LINQ in C#. In that sense, it’s perfectly understandable for me that most Java programmers, even seasoned ones, are shying away from lambdas and streams and prefer to stick to good old lists and for
loops in their daily usage.
In the light of that observation, I decided to write this article to help programmers establish a ground on what these “strange” facilities are. I will be explaining what streams and lambda expressions are, and which functional operations are executed and what does it mean to be executed lazily. Hopefully, this article will be helpful to both newcoming Java 8 programmers, and C# programmers who are not familiar with the LINQ API.
For a downloadable version of the examples in this article, visit the corresponding Gists at:
Java: https://gist.github.com/ygunayer/c8941775a36cc2c60ad4
C#: https://gist.github.com/ygunayer/9e2a67b020a3613900e3
Concepts
Lambda Expressions (aka Anonymous Functions)
A lambda expression (or an anonymous function) is an unbound function that can be defined and used anywhere, be it method parameters or even return values. Programmers with a C/C++ background might feel a bit more familiar with this concept as they’re probably used to passing around function pointers, and it’s safe to assume that every Javascript developer has at some point used an anonymous function as a callback.
To handle a lambda expression, Java first maps it to a special type called a functional interface and then executes a specific method found on that interface. These interfaces are defined as follows:
Functional Interface* | Input Types | Output Types | Primary Method | Description |
---|---|---|---|---|
Supplier<T> | None | T | get() | Returns a value of type T |
Consumer<T> | T | None | accept(T) | Consumes a value of type T |
BiConsumer<T, U> | T, U | None | accept(T, U) | Consumes values of types T and U |
Function<T, R> | T | R | apply(T) | Consumes a value of type T and returns a value of type R |
BiFunction<T, U, R> | T, U | R | apply(T, U) | Consumes values of types T and U, and returns a value of type R |
UnaryOperator<T> | T | T | apply(T) | Consumes a value of type T and returns a value of type T |
BinaryOperator<T, T> | T | T | apply(T, T) | Consumes two values of type T and returns a value of type T |
Predicate<T> | T | boolean | test(T) | Consumes a value of type T and returns a boolean |
BiPredicate<T, U> | T, U | boolean | test(T, U) | Consumes values of types T and U, and returns a boolean |
*: Since primitive types cannot be specified as type parameters, these interfaces all have overrides for primitive types, such as IntConsumer
.
As you might have noticed, Java’s built-in functional interfaces only accept up to 2 input parameters. For more, you can either use a technique called Currying, which basically means nesting functional interfaces in each other, or you can also create your own functional interfaces. Here’s an example:
1 |
|
C#, on the other hand, maps them to two types: Action<T>
and Func<T, R>
, both of which are delegate
s themselves and can receive up to 16 parameters. A delegate in C# is like a function pointer from C/C++ but it’s type-safe and contains a built-in iterator for callees. This iterator allows multiple functions to register themselves on delegates so that they’re invoked when the delegate itself is invoked. If you want to learn more about delegates, simply visit the MSDN article about delegates.
Delegate | Input Types | Output Types | Description |
---|---|---|---|
Action<T1..T16> | T1..T16 | None | Consumes up to 16 values of type T1 to T16 |
Func<T1..T16, R> | T1..T16 | R | Consumes up to 16 values of type T1 to T16 and returns a value of type R |
Predicate<T> | T | bool | Consumes a value of type returns a boolean |
Code-wise, a lambda expression is defined inside another method or function’s scope, so it doesn’t have an access modifier. Since its types can be inferred, it doesn’t need to explicitly define its parameter and return types either. Furthermore, if the expression is a one-liner and contains a single expression (i.e. a sum or product), it doesn’t even have to have a return statement and curly braces.
The short-hand method to define a lambda expression in Java is as follows (notice how Java uses the same arrow notation used in lambda calculus):
Java
1 | ... |
C#
1 | ... |
And a few actual implementations:
Java
1 | public void someMethod() { |
C#
1 | public void SomeMethod() |
Now that we now how to express an anonymous function, it’s trivial to compose a method that takes an anonymous function as a parameter:
Java
1 | public void someLambdaMethodExecutor(BiFunction<Integer, Integer, Integer> fn) { |
C#
1 | public void SomeLambdaMethodExecutor(Func<int, int, int> fn) |
And if you want to generate and return a lambda expression:
Java
1 | public BiFunction<Integer, Integer, Integer> SomeLambdaGenerator(String which) { |
C#
1 | public Func<int, int, int> SomeLambdaGenerator(string which) |
Streams
A stream is a special kind of collection that only evaluates its values when they’re requested. In other words, it’s a lazy collection. This laziness allows them to be unrestricted by the time factor and thus able to be used on collection of infinite or at least unknown sizes, without worrying too much about concurrency (more of that in a later topic!). The C#’s term for a stream is enumerable. Here’s the definition:
Definition | |
---|---|
Java | Stream<T> |
C# | IEnumerable<T> |
Streams can be explored in a much more detailed fashion, but for the sake of simplicity I’ll pass the subject to a future article.
Creating a Stream
Since the functional operations are only defined on the stream class. As before, we won’t go into too much detail and will stick to turning generic collections into streams instead. To do that in Java, simply call the stream()
method of a generic collection and you’ll get an appropriate Stream<T>
instance. In the case of C#, all collections implement the IEnumerable
interface, and since this is where functional operations are declared, there’s nothing you need to do before using them. If somehow you stumble upon a collection that doesn’t extend IEnumerable and therefore not contain any functional operations, try calling the AsEnumerable()
extension method.
Java
1 | ... |
Materializing a Stream
Since streams are conceptually lazy, you’ll need to materialize them into generic collections when you want to do some constant-time operations with them (i.e. count their items). In Java, you’ll need to use the Collectors
class to achieve this, and in C#, you can simply call the appropriate extension methods defined on IEnumerable<T>. Here’s the method to materialize a stream into a list (there’s more, of course, but they vary too much from Java to C#, for more information visit Oracle Docs page for Java 8 Collectors or MSDN page for C#’s IEnumerable methods):
Definition | |
---|---|
Java | collect(Collectors::toList()) |
C# | ToList() |
Optional
In Java, every instance of Object
and its sub-classes can be null
, so you have to manually check for null values in your business. The downside of traditional null-checking is that it’s error-prone because it’s incredibly easy to forget to do. The Optional<T>
class introduced in Java 8 aims to overcome that by wrapping objects and requiring you to be explicit about null-checking. C# does not have a direct equivalent, but the ?
suffix which is the equivalent of using Nullable<T>
can be used to some extent.
In a functional programming perspective, Optional is a Stream with a single element, so the functional operations listed in the Functional Operations section does apply to it as well. One extra method that Optional defines is the ifPresent(Consumer<T> fn)
method which invokes the provided lambda expression when the contained value is present, which can be extremely useful.
To wrap an object in an Optional, simply call the Optional.ofNullable(T t)
method. And to create an empty Optional, do Optional.ofNullable(null)
.
1 | public void optionalExample() { |
Functional Operations
These functional operations are used to transform a stream into another without changing its integrity. This is essentially what makes streams inherently concurrent and thread-safe. Also, since streams are lazily evaluated, it is possible to queue up multiple functional operations without any interference. The queued operations will only be executed when the stream is collected, therefore will not cause too much performance hit. In fact, this property is what made LINQ-to-SQL possible in the first place.
Map
Based on a given transformation function, returns a new stream (not necessarily to different types or values) from a stream.
Definition | |
---|---|
Java | Stream<R> map(Function<T, R> mapper) |
C# | IEnumerable<R> Select(Func<T, R> mapper) |
T: input, R: output |

Java
1 | public void mapExample() { |
C#
1 | public void MapExample() |
Flat Map
Applies map
to a stream and unfolds the returning stream. This is helpful in cases where your map function produces a collection but you want to group all output into a single collection.
Definition | |
---|---|
Java | Stream<R> flatMap(Function<T, Stream<R>> mapper) |
C# | IEnumerable<R> SelectMany(Func<T, IEnumerable<R>> mapper) |
T: input, R: output |

Java
1 | public void flatMapExample() { |
C#
1 | public void FlatMapExample() |
Filter
Returns a new stream containing only the elements on a stream that passes the given predicate function.
Definition | |
---|---|
Java | Stream<T> filter(Function<T, Boolean> predicate) |
C# | IEnumerable<T> Where(Func<T, bool> predicate) |

Java
1 | public void filterExample() { |
C#
1 | public void FilterExample() |
Skip
Offsets a stream, returning a new stream containing the remainder of a stream after a given number of elements. Not technically a functional operation, but still useful with streams.
Definition | |
---|---|
Java | Stream<T> skip(long count) |
C# | IEnumerable<T> Skip(int count) |

Java
1 | public void skipExample() { |
C#
1 | public void SkipExample() |
Limit
Limits a stream, returns a new stream containing the given number of elements taken from a stream.
Definition | |
---|---|
Java | Stream<T> limit(long count) |
C# | IEnumerable<T> Take(int count) |
Java
1 | public void limitExample() { |
C#
1 | public void TakeExample() |
Distinct
Returns a new stream containing only the unique elements in a stream. Used the the default equality comparer defined on the type T
(equals
in Java, Equals
in C#).
Definition | |
---|---|
Java | Stream<T> distinct() |
C# | IEnumerable<T> Distinct() |

Java
1 | public void distinctExample() { |
C#
1 | public void DistinctExample() |
Sort
Java and C# handle this differently. The sort
method in Java, given a comparator function, sorts the elements in a stream into a new stream. In C#, however, the equivalent OrderBy
method does not accept a comparative lambda expression, but rather, expects a lambda expression that returns a comparable data type. Since all types in C# have an implicit comparator function, it’s possible to simply provide which property to sort on, given a custom class.
Java
1 | public void sortExample() { |
C#
1 | public void SortExample() |
Reduce
Also known as aggregate. Given an initial value and a combinator function, iterates over a stream and produces a final, scalar result. For example, the sum of a stream is a reduce operation with an initial value of 0 and a combinator function that adds two values to each other. The idea behind collector methods such as Collectors.toList()
is also this, they start with an initially empty collection and add to it while they iterate over the stream.
Java
1 | public void reduceExample() { |
C#
1 | public void ReduceExample() |
Conclusion
Well, that’s it for this article. I hope it’s been helpful. There’s more to talk about functional programming, of course, and especially regarding how it’s inherently more suited for concurrent/parallel programming, but that topic’s for another article, hopefully.