1. Overview
In this article, we will be looking at the jOOL library – another product from jOOQ.
2. Maven Dependency
Let’s start by adding a Maven dependency to your pom.xml:
<dependency>
<groupId>org.jooq</groupId>
<artifactId>jool</artifactId>
<version>0.9.12</version>
</dependency>
You can find the latest version here.
3. Functional Interfaces
In Java 8, functional interfaces are quite limited. They accept the maximum number of two parameters and do not have many additional features.
jOOL fixes that by proving a set of new functional interfaces that can accept even 16 parameters (from Function1 up to Function16) and are enriched with additional handy methods.
For example, to create a function that takes three arguments, we can use Function3:
Function3<String, String, String, Integer> lengthSum
= (v1, v2, v3) -> v1.length() + v2.length() + v3.length();
In pure Java, you would need to implement it by yourself. Besides that, functional interfaces from jOOL have a method applyPartially() that allows us to perform a partial application easily:
Function2<Integer, Integer, Integer> addTwoNumbers = (v1, v2) -> v1 + v2;
Function1<Integer, Integer> addToTwo = addTwoNumbers.applyPartially(2);
Integer result = addToTwo.apply(5);
assertEquals(result, (Integer) 7);
When we have a method that is of a Function2 type, we can transform it easily to a standard Java BiFunction by using a toBiFunction() method:
BiFunction biFunc = addTwoNumbers.toBiFunction();
Similarly, there is a toFunction() method in Function1 type.
4. Tuples
A tuple is a very important construct in a functional programming world. It’s a typed container for values where each value can have a different type. Tuples are often used as function arguments.
They’re also very useful when doing transformations on a stream of events. In jOOL, we have tuples that can wrap from one up to sixteen values, provided by Tuple1 up to Tuple16 types:
tuple(2, 2)
And for four values:
tuple(1,2,3,4);
Let’s consider an example when we have a sequence of tuples that carried 3 values:
Seq<Tuple3<String, String, Integer>> personDetails = Seq.of(
tuple("michael", "similar", 49),
tuple("jodie", "variable", 43));
Tuple2<String, String> tuple = tuple("winter", "summer");
List<Tuple4<String, String, String, String>> result = personDetails
.map(t -> t.limit2().concat(tuple)).toList();
assertEquals(
result,
Arrays.asList(tuple("michael", "similar", "winter", "summer"), tuple("jodie", "variable", "winter", "summer"))
);
We can use different kinds of transformations on tuples. First, we call a limit2() method to take only two values from Tuple3. Then, we are calling a concat() method to concatenate two tuples.
In the result, we get values that are of a Tuple4 type.
5. Seq
The Seq construct adds higher-level methods on a Stream while often uses its methods underneath.
5.1. Contains Operations
We can find a couple variants of methods checking for a presence of elements in a Seq. Some of those methods use an anyMatch() method from a Stream class:
assertTrue(Seq.of(1, 2, 3, 4).contains(2));
assertTrue(Seq.of(1, 2, 3, 4).containsAll(2, 3));
assertTrue(Seq.of(1, 2, 3, 4).containsAny(2, 5));
5.2. Join Operations
When we have two streams and we want to join them (similar to a SQL join operation of two datasets), using a standard Stream class is not a very elegant way to do this:
Stream<Integer> left = Stream.of(1, 2, 4);
Stream<Integer> right = Stream.of(1, 2, 3);
List<Integer> rightCollected = right.collect(Collectors.toList());
List<Integer> collect = left
.filter(rightCollected::contains)
.collect(Collectors.toList());
assertEquals(collect, Arrays.asList(1, 2));
We need to collect right stream to a list, to prevent java.lang.IllegalStateException: stream has already been operated upon or closed. Next, we need to make a side effect operation by accessing a rightCollected list from a filter method. It is error prone and not elegant way to join two data sets.
Fortunately, Seq has useful methods to do inner, left and right joins on data sets. Those methods hide an implementation of it exposing elegant API.
We can do an inner join by using an innerJoin() method:
assertEquals(
Seq.of(1, 2, 4).innerJoin(Seq.of(1, 2, 3), (a, b) -> a == b).toList(),
Arrays.asList(tuple(1, 1), tuple(2, 2))
);
We can do right and left joins accordingly:
assertEquals(
Seq.of(1, 2, 4).leftOuterJoin(Seq.of(1, 2, 3), (a, b) -> a == b).toList(),
Arrays.asList(tuple(1, 1), tuple(2, 2), tuple(4, null))
);
assertEquals(
Seq.of(1, 2, 4).rightOuterJoin(Seq.of(1, 2, 3), (a, b) -> a == b).toList(),
Arrays.asList(tuple(1, 1), tuple(2, 2), tuple(null, 3))
);
There is even a crossJoin() method that makes possible to make a cartesian join of two datasets:
assertEquals(
Seq.of(1, 2).crossJoin(Seq.of("A", "B")).toList(),
Arrays.asList(tuple(1, "A"), tuple(1, "B"), tuple(2, "A"), tuple(2, "B"))
);
5.3. Manipulating a Seq
Seq has many useful methods for manipulating sequences of elements. Let’s look at some of them.
We can use a cycle() method to take repeatedly elements from a source sequence. It will create an infinite stream, so we need to be careful when collecting results to a list thus we need to use a limit() method to transform infinite sequence into finite one:
assertEquals(
Seq.of(1, 2, 3).cycle().limit(9).toList(),
Arrays.asList(1, 2, 3, 1, 2, 3, 1, 2, 3)
);
Let’s say that we want to duplicate all elements from one sequence to the second sequence. The duplicate() method does exactly that:
assertEquals(
Seq.of(1, 2, 3).duplicate().map((first, second) -> tuple(first.toList(), second.toList())),
tuple(Arrays.asList(1, 2, 3), Arrays.asList(1, 2, 3))
);
Returning type of a duplicate() method is a tuple of two sequences.
Let’s say that we have a sequence of integers and we want to split that sequence into two sequences using some predicate. We can use a partition() method:
assertEquals(
Seq.of(1, 2, 3, 4).partition(i -> i > 2)
.map((first, second) -> tuple(first.toList(), second.toList())),
tuple(Arrays.asList(3, 4), Arrays.asList(1, 2))
);
5.4. Grouping Elements
Grouping elements by a key using the Stream API is cumbersome and non-intuitive – because we need to use collect() method with a Collectors.groupingBy collector.
Seq hides that code behind a groupBy() method that returns Map so there is no need to use a collect() method explicitly:
Map<Integer, List<Integer>> expectedAfterGroupBy = new HashMap<>();
expectedAfterGroupBy.put(1, Arrays.asList(1, 3));
expectedAfterGroupBy.put(0, Arrays.asList(2, 4));
assertEquals(
Seq.of(1, 2, 3, 4).groupBy(i -> i % 2),
expectedAfterGroupBy
);
5.5. Skipping Elements
Let’s say that we have a sequence of elements and we want to skip elements while a predicate is not matched. When a predicate is satisfied, elements should land in a resulting sequence.
We can use a skipWhile() method for that:
assertEquals(
Seq.of(1, 2, 3, 4, 5).skipWhile(i -> i < 3).toList(),
Arrays.asList(3, 4, 5)
);
We can achieve the same result using a skipUntil() method:
assertEquals(
Seq.of(1, 2, 3, 4, 5).skipUntil(i -> i == 3).toList(),
Arrays.asList(3, 4, 5)
);
5.6. Zipping Sequences
When we’re processing sequences of elements, often there is a need to zip them into one sequence.
The zip() API that could be used to zip two sequences into one:
assertEquals(
Seq.of(1, 2, 3).zip(Seq.of("a", "b", "c")).toList(),
Arrays.asList(tuple(1, "a"), tuple(2, "b"), tuple(3, "c"))
);
The resulting sequence contains tuples of two elements.
When we are zipping two sequences, but we want to zip them in a specific way we can pass a BiFunction to a zip() method that defines the way of zipping elements:
assertEquals(
Seq.of(1, 2, 3).zip(Seq.of("a", "b", "c"), (x, y) -> x + ":" + y).toList(),
Arrays.asList("1:a", "2:b", "3:c")
);
Sometimes, it is useful to zip sequence with an index of elements in this sequence, via the zipWithIndex() API:
assertEquals(
Seq.of("a", "b", "c").zipWithIndex().toList(),
Arrays.asList(tuple("a", 0L), tuple("b", 1L), tuple("c", 2L))
);
6. Converting Checked Exceptions to Unchecked
Let’s say that we have a method that takes a string and can throw a checked exception:
public Integer methodThatThrowsChecked(String arg) throws Exception {
return arg.length();
}
Then we want to map elements of a Stream applying that method to each element. There is no way to handle that exception higher so we need to handle that exception in a map() method:
List<Integer> collect = Stream.of("a", "b", "c").map(elem -> {
try {
return methodThatThrowsChecked(elem);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}).collect(Collectors.toList());
assertEquals(
collect,
Arrays.asList(1, 1, 1)
);
There is not much we can do with that exception because of the design of functional interfaces in Java so in a catch clause, we are converting a checked exception into unchecked one.
Fortunately, in a jOOL there is an Unchecked class that has methods that can convert checked exceptions into unchecked exceptions:
List<Integer> collect = Stream.of("a", "b", "c")
.map(Unchecked.function(elem -> methodThatThrowsChecked(elem)))
.collect(Collectors.toList());
assertEquals(
collect,
Arrays.asList(1, 1, 1)
);
We are wrapping a call to a methodThatThrowsChecked() into an Unchecked.function() method that handles converting of exceptions underneath.
7. Conclusion
This article shows how to use the jOOL library that adds useful additional methods to the Java standard Stream API.
The implementation of all these examples and code snippets can be found in the GitHub project – this is a Maven project, so it should be easy to import and run as it is.