0% found this document useful (0 votes)
2 views17 pages

Core Java Data Handling Streams and Lambdas

The document outlines learning objectives related to Java streams and lambda functions, emphasizing the creation and manipulation of streams from various data sources. It explains the concepts of intermediate and terminal operations, as well as functional interfaces, and provides examples of how to implement these using lambda expressions. Additionally, it covers common operations such as filter, map, and collect, demonstrating their usage in stream processing.

Uploaded by

learnwithoa
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views17 pages

Core Java Data Handling Streams and Lambdas

The document outlines learning objectives related to Java streams and lambda functions, emphasizing the creation and manipulation of streams from various data sources. It explains the concepts of intermediate and terminal operations, as well as functional interfaces, and provides examples of how to implement these using lambda expressions. Additionally, it covers common operations such as filter, map, and collect, demonstrating their usage in stream processing.

Uploaded by

learnwithoa
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 17

Learning Objectives: Streams and

lambdas

Learners will be able to…

Create lambda functions

Create a stream from different sources

Use intermediate stream operations such as filter and


map

Use terminal stream operations such as count, reduce,


collect, and forEach

info

Make Sure You Know


Java syntax
Streams
Streams are wrappers around a data source to perform computations on
data. A stream is not a data structure; it receives data from data structures
such as Collections or other data sources such as files and databases.

Streams are typically represented as pipeline to illustrate the flow of data


from a stream source through stream intermediate operations, ending in a
terminal operation.

Streams start at some data referred to as a stream source which


is then fed into the created stream. The stream passes through
any number of intermediate operations before reaching the
terminal operation which outputs a result.

Stream interfaces and their implementations in the java.util.stream


package (introduced in Java 8) allow programs to operate on data from
different sources, making bulk processing easy and efficient.

Creating Streams
There are many ways to create a stream because there are many sources
we might want to create a stream out of. We cover some basic methods
below but there are many more.

Stream from Collection

You can create a stream from collections(Collection,List, Set) by calling


.stream() method:

final Stream<String> stream1 = List.of("a", "b", "c").stream();


final Stream<String> stream2 = Set.of("a", "b", "c").stream();
final Stream<String> stream3 = Map.of("key",
"value").values().stream();

Try out the stream on the left being created from a list:
challenge

Try these variations:


Replace the stream with a Set-based stream:

final Stream<String> stream = Set.of("a", "b",


"c").stream();

Replace the stream with a Map-based stream:

final Stream<String> stream = Map.of("key",


"value").values().stream();

Stream from Array

You can create a stream from Arrays by calling Stream.of or Arrays.stream


helpers.

final Stream<String> abcStream = Stream.of("a", "b", "c");


final Stream<String> abcStreamFromArray = Arrays.stream(new
String[]{"a", "b", "c"});

Stream from file

You can create a stream from Files by calling Files.lines.


challenge

Try it out

Try this example which reads in a text file containing the words to the
children’s book “Goodnight Moon”:

Path path = Paths.get("GoodnightMoon.txt");


Stream<String> stream = Stream.empty();
try {
stream = Files.lines(path);
} catch (Exception e) {
e.printStackTrace();
}

Notice in the example above to ensure that stream is instantiated before we


attempt to print it, we create an empty stream using Stream.empty() which
is replaced if the file is found.

Stream from Primitives

There are three interfaces used for streams of primitives: IntStream,


LongStream, DoubleStream.

IntStream integerFrom0To9 = IntStream.range(0, 10);


LongStream longFrom0To10 = LongStream.rangeClosed(0, 10);

Unlike IntStream and LongStream, DoubleStream does not have a range or a


rangeClosed method since there are infinite decimal values within a
specified range.
challenge

Try it out

To try out number based streams like the primitive streams, we need to
change how we are printing out the stream. Comment out the current
System.out.println and then insert the following code to make a
primitive stream and output it:

IntStream stream = IntStream.range(0, 10);


stream.forEach(System.out::println);

Build a Stream

If you are working with large data sets, especially when you want to
combine data from multiple sources into a single stream, Java 8 or later
provides the Stream.Builder interface. Unlike creating other streams we
have created so far, Stream.Builder has two phases:
1. A building phase where elements are added
1. A built phase, triggered by the calling of .build(), where elements can
no longer be added.

final Stream.Builder<Integer> streamBuilder = Stream.builder();

streamBuilder.accept(1);
streamBuilder.accept(2);
streamBuilder.accept(3);

final Stream<Integer> stream = streamBuilder.build();

Operations on Streams
There are two types of operations performed on streams:

1. Intermediate operations set the rules for changing the stream,


returning the transformed data stream. For example, an intermediate
operation can filter elements of the stream according to a given criteria.
A stream can go through any number of intermediate operations –
including zero.

2. Terminal operations are the final operation that completes work on the
stream. A stream can only have one terminal operation.
In order to dive into these operations, we need to first understand the
Functional Interfaces they are implementing.
Functional Interfaces
Functional interfaces are how Java, despite it’s Object-Oriented stance,
allows functional programming. Functional programming practices are
useful within the context of streams because it means we can do things like
use high-order functions such as map, filter and fold.

A functional interface is an interface that contains only one abstract


method.

To write a functional interface we use syntax similar to any other interface:

@FunctionalInterface
interface Action {
void act();
}

The main difference is the @FunctionalInterface annotation which tells the


compiler that the given interface is meant to be functional – so the
compiler will ensure that the interface has exactly one method.

The standard Java library contains a large set of ready-made functional


interfaces for different cases so in most cases we won’t need to declare our
own. Note that when implementing a built-in functional interface from the
Java library, you don’t need the @FunctionalInterface annotation.

Functional Interfaces in the Java Library


Functional interfaces are placed under java.util.function package.

### interface Supplier


Represents a supplier of results. This is a functional interface whose
functional method is get().
import java.util.function.Supplier;

class HelloSupplier implements Supplier<String> {


@Override
public String get() {
// we could generate any required for business logic
result here
return "hello";
}
}

------

// get generated result


System.out.println(new HelloSupplier().get());

Supplier is implemented as the collect operation we used on the last page


to collect all the strings in a stream.

### interface Consumer


Represents an operation that accepts a single input argument and returns
no result. Unlike most other functional interfaces, Consumer is expected to
operate via side-effects. This is a functional interface whose functional
method is accept(Object).

import java.util.function.Consumer;

class PrintConsumer implements Consumer<String> {


@Override
public void accept(final String line) {
// consumer accept a line and do some action,
// action could be different in different consumer
implementations
System.out.println(line);
}
}

-----

new PrintConsumer().accept("hello");

We have actually seen an example of this on the previous page with


Stream.Builder.
Consumer is implemented as the forEach operation for Streams, which we
used on the previous page to print all the numbers in a stream.

### interface Predicate


Represents a predicate (boolean-valued function) of one argument. This is a
functional interface whose functional method is test(Object).

import java.util.function.Predicate;

class IsSmallPredicate implements Predicate<String> {


@Override
public boolean test(final String line) {
return line == null || line.length() < 10;
}
}

-----

System.out.println(new IsSmallPredicate().test("hello")); //
true, line too small
System.out.println(new IsSmallPredicate().test("hellohello"));
// false, line is ok

Predicate is often implemented as Stream filters.

### interface Function<T,R>


Represents a function that accepts one argument and produces a result.
This is a functional interface whose functional method is apply(Object).

import java.util.function.Function;

class StringToLengthFunction implements Function<String,


Integer> {
@Override
public Integer apply(final String line) {
return line == null ? 0 : line.length();
}
}

----

System.out.println(new StringToLengthFunction().apply("hello"));
// 5
System.out.println(new
StringToLengthFunction().apply("hellohello")); // 10
Function is often implemented as Stream maps.
Implementing Functional
Interfaces: Anonymous Classes and
Lambdas
On the previous page, the examples implemented a full, named class to
override a single method. However, there are a couple more common ways
to implement Functional Interfaces to avoid the clunkiness of complying
with Java’s Object-Oriented approach.

Anonymous Classes
Anonymous classes are inner classes with no name and are used when you
need to use a class only once. Anonymous classes enable you to make your
code more concise by letting you declare and instantiate a class at the same
time.

Consider the implementation on the left of the Predicate functional


interfaces test function. Click each description below to see the line(s) of
code it is referring to:
1.
1.
1.
1.
1.

Try running the code on the left:

While in the example on the left we override only one method, anonymous
classes let you override/implement any number of methods.

Lambda Expressions (Java 8 or later)


One issue with anonymous classes is that if the implementation of your
anonymous class is very simple, such as an interface that contains only one
method, then the syntax of anonymous classes may seem unwieldy.

Lambda expressions are a special Java language syntax that allows you to
quickly and compactly implement a functional interface.

Lambda expressions consist of two parts, separated by the operator ->.


1. The left part is a comma-separated list of formal parameters enclosed in
parentheses.
2. The right part is a body, which consists of a single expression or a
statement block.

Take a look at the same implementation of the Predicate Functional


interface using lambda expressions:

final Predicate<Integer> isAdult = age -> age >= 18;

Try replacing the anonymous class implementation which takes up 6 lines


of code with the single line lambda implementation and run the file again:

If you need more than a single statement within your lambda expression,
you need wrap the right part with curly braces {} and add return
statement:

final Predicate<Integer> someCheck = age -> {


if (age >= 18) {
return false
}
if (age <= 90) {
return false
}
return true
}
Stream Intermediate Operations
Now that we know how to implement Functional Interfaces, let’s return to
our Stream pipeline:

Intermediate operations set the rules for changing the stream, returning
the transformed data stream. A stream can go through any number of
intermediate operations – including zero.

Intermediate operations are lazy in nature, so they start producing new


stream elements and send it to the next operation (because intermediate
operations are never the final result producing operations).

Commonly used intermediate operations are filter, map and sorted.

Filter
filter() returns a stream consisting of the elements of this stream that
match the given Predicate.

In the file to the left, start by creating a numerical stream and printing it
out:

Stream<Integer> numberStream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8,


9, 10);

numberStream.forEach(System.out::println);

Check that you see a list from 1 to 10:

Between the lines you added, create a filter using a lambda expression:

final Predicate<Integer> isOdd = num -> num % 2 != 0;

Then apply the filter to your stream:

numberStream = numberStream.filter(isOdd);
Check that you see only odd numbers:

You can even add the lambda syntax directly into the .filter() method.

Let’s add a second filter to filter out all numbers lower than 4 since we can
have as many intermediate operations as we want:

numberStream = numberStream.filter(num -> num > 4);

Check that you see only odd numbers greater than 4:

The benefit of lambdas and functional interfaces is the ability to use


functional programming approaches. We can combine these 5 separate
lines into the following:

final Predicate<Integer> isOdd = num -> num % 2 != 0;


Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
.filter(isOdd)
.filter(num -> num > 4)
.forEach(System.out::println);

Map
map returns a stream consisting of the results of applying the given
Function to the elements of this stream.

For example, we can apply the .toUpperCase() function to all the Strings in
a stream:

final String result = Stream.of("Hello", "there", "crazy",


"world")
.map(String::toUpperCase)
.collect(Collectors.joining(" "));
System.out.println(result);

Replace the previous example in the file to the left and try the code
example above:

With map we can even transform the stream’s data type. For example, the
code below sums the string lengths:

final Integer result = Stream.of("Hello", "there", "crazy",


"world")
.map(String::length)
.reduce(0, Integer::sum);
System.out.println(result);
Replace the previous example in the file to the left and try the code
example above:

Sorted
sorted returns a stream consisting of the elements of this stream, sorted
according to natural order.

For example:

final String result = Stream.of("c", "a", "z", "f")


.sorted()
.collect(Collectors.joining(", "));
System.out.println(result);

Replace the previous example in the file to the left and try the code
example above:

You could use Comparator interface to implement your own sort or use
exists helpers.

final String result = Stream.of("c", "a", "z", "f")


.sorted(Comparator.reverseOrder())
.collect(Collectors.joining(", "));
System.out.println(result);

Replace the previous example in the file to the left and try the code
example above:
Stream Terminal Operations
Terminal operations are eager in nature i.e they process all the elements in
the stream before returning the result. You can identify terminal methods
from the return type, they will never return a Stream.

The most known terminal methods are:


* forEach
* min/max
* anyMatch/allMatch/noneMatch
* collect

forEach

forEach performs an action for each element of this stream. It is an


implementation of the Consumer Functional Interface.

A simple example we have seen before:

final IntStream integerFrom0To9 = IntStream.range(0, 10);


integerFrom0To9.forEach(number -> System.out.println(number));
// or just
// integerFrom0To9.forEach(System.out::println);

Paste the example above in the file to the left and check that it prints
numbers from 0 to 9:

min and max

min and max return the minimum or maximum element of the stream
(according to the provided Comparator).

For example:

final Stream<Integer> intStream = Stream.of(10, 3, 6, 8);


System.out.println(intStream.min(Comparator.comparingInt(e ->
e)).orElse(0));

Replace the previous example in the file to the left and try the code
example above:

Try replacing the .min with .max and re-running the code:
allMatch, anyMatch and noneMatch

allMatch, anyMatch and noneMatch return whether all/any/none of the


elements of the stream match the provided predicate.

For example:

final Predicate<Integer> isOdd = num -> num % 2 != 0;


System.out.println(Stream.of(1, 2, 3, 7, 9).allMatch(isOdd));

Replace the previous example in the file to the left and try the code
example above:

Try replacing the .allMatch with .anyMatch and re-running the code:

Try replacing the .anyMatch with .noneMatch and re-running the code:

collect

collect performs a mutable reduction operation on the elements of this


stream using a Collector. collect is most commonly used to convert a
stream to collections or other objects.

Collectors helper class contains a lot of collectors, but the most used are:
- toList() - accumulates elements into a new List
- toSet() - accumulates elements into a new Set
- toMap() - accumulates elements into a new Map whose keys and values
are the result of applying the provided mapping functions to the elements
- joining() - concatenates the elements into a String, in encounter order

final Integer[] numbers = {1, 3, 7, 9, 9};


System.out.println(Stream.of(numbers).collect(Collectors.toList(
)));
System.out.println(Stream.of(numbers).collect(Collectors.toSet()
));

Replace the previous example in the file to the left and try the code
example above:

You might also like