02 Functional Programming
02 Functional Programming
programming in Java 8
A pragmatic overview
Sample: create a list of square values of particular elements of integer number array
Imperative code:
List<Integer> src = Arrays.asList(5, 72, 10, 11, 9);
We focus on algorithm: List<Integer> target = new ArrayList<>();
(1) iterate over the source for (Integer n : src) {
collection; if (n < 10) { // filtering
(2) square element and add target.add(n * n); // processing and merging
result to the target. } // result
}
We focus on goals:
(1) specify predicate to filter the elements we want to
process;
(2) specify processing of an element.
Functional programming scheme:
List<Type> target = create( src,
a condition for filtering elements from src list,
specify the operation to perform on the selected elements );
Interface Filter declares a predicate – a method test() responsible for selecting elements – returns
true if an element meets the criteria, false – otherwise.
public interface Filter<V> {
boolean test(V v);
}
We have separated particular stages and realized intermediate phases of processing we can achieve
the ultimate goal by combining them.
static <T, S> List<T> create(List<S> src, Filter<S> f, Transformer<T, S> t) {
List<T> target = new ArrayList<>();
for (S e : src) {
if (f.test(e)) { // invoke filtering to be specified in usage
target.add(t.transform(e)); // invoke processing to be specified in usage }
} // and then merge the result
return target;
}
We had to provide some so called boilerplate code – i.e. pieces of code which do not introduce
much added value and solely result from the syntax of the programming language.
In the above sample the boilerplate parts are highlighted in blue, the parts which introduce
significant value are highlighted in green.
@Override
public String toString() {
return lname + " " + fname;
}
}
System.out.println(
create(num, n -> n % 2 != 0, n -> n * 100)
);
System.out.println(
create(txt,
s -> s.startsWith(“t"),
s -> s.toUpperCase() + " " + s.length())
);
List<Employee> givePayRise =
create(emp,
e -> e.getAge() > 30 && e.getSalary() < 4000, [100, 300, 500, 900, 700]
e -> e [TWO 3, THREE 5]
); Pay rise should be given to:
System.out.println(“Pay rise should be given to:"); [Smith John, Allen Sofia, Owen
System.out.println(givePayRise); Michael]
The result of method create() does not need to be list of the same type as the source list.
For instance we can easily get a list of employee salaries.
List<Double> sal =
create(emp,
e -> true,
e -> e.getSalary() [3400.0, 4100.0, 3700.0, 3600.0]
);
System.out.println(sal);
Instead of defining lamba expression we can also refer methods (method reference) defined in a
class.
e -> e.getSalary() is equivalent to Employee::getSalary
create(emp,
e -> true,
Employee::getSalary
);
Let us define method change(), which modifies elements of the source list if they meet some given
predicate.
static <S> void change(List<S> list, Filter<S> f, Modifier<S> mod) {
for (S e : list) {
if (f.test(e)) {
mod.modify(e);
}
}
}
change(
emp,
e -> e.getAge() > 30 && e.getSalary() < 4000,
e -> {
double oldSalary = e.getSalary();
double newSalary = oldSalary + 200;
e.setSalary(newSalary);
});
No, we do not.
There are interfaces for frequent lambda expression usage schemes available out of the box in
package java.util.function.
Interfaces:
(1) java.util.function.Predicate and
(2) java.util.function.Function
static <T, S> List<T> create(List<S> src, Predicate<S> filter, Function<S, T> func) {
List<T> target = new ArrayList<>();
for (S e : src) { Method Predicate.test() evaluates boolean
if (filter.test(e)) {
target.add(func.apply(e)); result based on value of type S (generic
} parameter)
}
return target;
}
Method Function.apply() evaluates result of
type T based on value of type S
Apart from Function<S, T> which returns a value java.util.function package provides interface
Consumer<S> whose single method accept() simply processes the input of type S.
NOTE: Collection processing scheme based on lambda expression resembles the one described in
Visitor design pattern which also defines method accept().
Our custom method are not very flexible – we always assume the same processing scheme:
first (1) selecting the elements and then (2) processing one by one.
Inversing the above order would require providing another implementation
We may also want to further process the output – unfortunately our simple methods create() and
change().
We can use much more generic paradigm – streams (do not confuse with I/O streams).
In general a programming language construct when multiple methods invoked one after another as a
single statement and the consecutive methods consume the output of the preceding ones is called
method chaining.
.map
List Stream Stream
.filter
.filter
Stream ... Stream
.map
Method map() accepts a lambda expression returning a value of specified type based on input
parameter – exactly in the same way as Function<S, T>.apply() does.
Method filter() accepts a predicate as a lambda expression – exactly in the same way as
Predicate<S>.test() does.
Method filter() filters out the elements of the stream which do not satisfy the predicate.
Method collect() is the ultimate step of stream processing responsible for creating a collection (e.g.
list or array) based on object of Collector class instance (e.g. Collectors.toList()).
Method forEach() enables processing values one by one (e.g. printing them out to the console).
Getting a list of squared elements of src which are less than 10.
import static java.util.stream.Collectors.toList;
// ...
List<Integer> src = Arrays.asList(5, 72, 10, 11, 9);
List<Integer> target = src
.stream()
.filter(n -> n < 10)
.map(n -> n * n)
.collect(toList()); [25, 81]
Inverting the order of operations – first compute a square of the element and then filter values
greater than 80.
List<Integer> num = Arrays.asList(1, 3, 5, 10, 9, 12, 7);
List<Integer> out = num
.stream()
.map(n -> n * n)
.filter(n -> n > 80)
.collect(Collectors.toList()); [100, 81, 144]
Sample:
(1) filter 3-digit strings representing numbers in decimal notation;
(2) convert filtered elements into numbers;
(3) filter even numbers;
(4) create a new list of strings in decimal notation.
.collect(toList());
In the first example we use forEach() in order to process each element returned by filter().
In the second example we illustrate how to use forEach() with Iterable interface implementations
(i.e. not a stream) – a sample of one of new default methods introduced in Iterable since Java 8
We can aggregate the result of stream operations performed on collections with reduce() method.
reduce(initial, function)
initial – initial value;
function – function which accepts two arguments:
(1) part the intermediate result and
(2) next consecutive stream element
part = initial
for each stream element {
reduce() processing scheme part = func.apply(part, next)
}
return part
System.out.println(sum);
2. we do not need to create custom interfaces for operations on collections – most common
usages are supported by interfaces delivered out of the box in java.util.function package;
3. streams in Java 8 is a very flexible implementation which covers most of scenarios for
collection processing.
Streams enable “lazy evaluation” – i.e. evaluation of the given stream method in a
pipeline when the result for all the pipeline is requested – e.g. during iteration or
when we want to materialize the result in a list of elements.