In this post, we will look at the Java spliterator. They introduced the Java spliterator in Java 8. We will look at the different method of the spliterator in Java along with how to create a custom spliterator in Java 8.
Introduction
Java spliterator, as its name suggests, used to iterate through a list of source items. It can split the source into different parts and process them in a parallel or sequential manner. In this post, we will learn different use cases of spliterator, how to create a custom spliterator and benefits of a custom spliterator.
1. Spliterator in Java 8 and Stream API
Spliterator introduced in Java 8 and they define it in ‘java.util
‘ package. Java already has Iterators for iterating a source of elements. Spliterator was developed mainly to support parallel traversal. We can use spliterator with both Collection and Stream API. Parallel execution is faster than sequential execution. It divides the main source into smaller sub-sources, executes them in parallel and combines the results of all to create the result.
Note that spliterator supports the parallel processing, but it doesn’t provide it. We can use fork/join with spliterator to create the parallelism. Also, spliterator is not dependent on fork/join, we will use it only to implement the parallel processing. Spliterator can also do sequential traversal like any other Iterators. The BaseStream
class includes a method called spliterator()
to use spliterator with streams.
Spliterator spliterator()
It returns one spliterator for the elements of the caller stream. Once we get that spliterator object, we can perform different operations on it. Similar to stream, the collection class also introduced one new method to initialize spliterator in Java 8:
default Spliterator spliterator()
This method returns one spliterator over the elements of the collection.
2. Spliterator Methods
Following are the list of methods available in the Java Spliterator interface:
2.1. Characteristics
Defined as int characteristics()
, This method returns a set of characteristics of this spliterator and its elements. It can be any of the following 8 values. All the below values are static final integer values:
- ORDERED: This value used to signify that an encounter order is defined for the spliterator elements.
- DISTINCT: This value signifies if the elements are not equal for each pair it encounters. If we create a spliterator from a set, it will be DISTINCT always.
- IMMUTABLE: This value signifies if the source of the elements can’t be modified i.e. we can’t add, replace or delete any element.
- NONNULL: This value signifies that the encountered elements will not be null.
- SIZED: It signifies that the value returned from the
estimateSize()
method represents a finite size. - SORTED: Signifies that the elements are always in sorted order.
- SUBSIZED: Signifies that the all spliterator returned by
trySplit()
will beSIZED
andSUBSIZED
. - CONCURRENT: Signifies that the source can be modified concurrently i.e. we can add, remove or delete elements concurrently using multiple threads with no synchronization.
2.2. The hasCharacteristics Method
It defines the hasCharacteristics
as below:
boolean hasCharacteristics(int characteristics)
characteristics()
method, that we have explained above is used to find out the characteristics of a spliterator. hasCharacteristics
is used to check if the spliterator contains all the characteristics. It returns one boolean value. 'true'
if all characteristics are present in the spliterator, false otherwise.
2.3. tryAdvance
We use this method to iterate through the source elements. It returns one boolean value and takes one Consumer as a parameter. If any element exists, it performs the Consumer we are passing as the argument and returns ‘true’ and moves to the next element. If it traverses all elements, it returns 'false'
. For example:
import java.util.ArrayList;
import java.util.List;
import java.util.Spliterator;
public class Main {
public static void main(String[] args){
List values = new ArrayList<>();
values.add("one");
values.add("two");
values.add("three");
Spliterator stringSpliterator = values.spliterator();
while(stringSpliterator.tryAdvance(System.out::println)){
//inside while loop
}
}
}
If you run this program, it will print: one, two three
. Here, we have created one ArrayList with three string values. This is a collection variable. So, we have used the spliterator()
method to create one spliterator from it. We use the while loop to check if the current return value of tryAdvance
is true or not. If it is true, it prints the current value inside the while condition. The body of the while loop is empty. But we can use it for any other operations.
2.4. trySplit Method
We use the trySplit
in possibly-parallel operations. If we call trySplit
on a Spliterator, it returns a spliterator with elements the caller spliterator will not cover. That means, we use it to divide a spliterator into multiple parts. The main benefit of trySplit
is that we can process the divided elements in parallel. Calling trySplit
repeatedly returns null if the source contains a finite number of elements. Let’s look at one example for a better understanding:
import java.util.ArrayList;
import java.util.List;
import java.util.Spliterator;
public class Main {
public static void main(String[] args){
List values = new ArrayList<>();
values.add("one");
values.add("two");
values.add("three");
values.add("four");
values.add("five");
values.add("six");
values.add("seven");
values.add("eight");
values.add("nine");
values.add("ten");
Spliterator firstSpliterator = values.spliterator();
Spliterator secondSpliterator = firstSpliterator.trySplit();
Spliterator thirdSpliterator = firstSpliterator.trySplit();
System.out.println("firstSpliterator : ");
while(firstSpliterator.tryAdvance(System.out::println)){
//inside while loop
}
System.out.println("secondSpliterator : ");
while(secondSpliterator.tryAdvance(System.out::println)){
//inside while loop
}
System.out.println("thirdSpliterator : ");
while(thirdSpliterator.tryAdvance(System.out::println)){
//inside while loop
}
}
}
If you run the above example, it will print the below output:
firstSpliterator : eight nine ten
secondSpliterator : one two three four five
thirdSpliterator : six seven
As you can see, all spliterators have different elements.
2.5. estimateSize()
It returns an estimation of the total number of elements. The return value is of type long. Here is the method signature for the estimateSize()
:
long estimateSize()
2.6. getExactSizeIfKnown()
If the spliterator has SIZED
characteristics, it returns the result of estimateSize()
. Else, it returns -1
. Here is the method signature.
default long getExactSizeIfKnown()
2.7. forEachRemaining()
It takes one Consumer action as the argument. Sequentially, it performs that action on all elements. It stops if all elements are processed or if it throws an exception.
import java.util.ArrayList;
import java.util.List;
import java.util.Spliterator;
public class Main {
public static void main(String[] args){
List values = new ArrayList<>();
values.add(1);
values.add(2);
values.add(3);
Spliterator integerSpliterator = values.spliterator();
integerSpliterator.forEachRemaining(System.out::println);
}
}
It will print :1 2 3
2.8. getComparator
It returns a Comparator if the source is SORTED
by a comparator and if the source is SORTED
in a natural order returns null. It throws IllegalStateException
if not SORTED
.
3. Custom Java Spliterator
We can also create one custom spliterator by implementing the Spliterator class. Let’s create one model class Student first:
public class Student {
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public int getAge() {
return age;
}
}
We can create one custom spliterator with this Student class as like below:
import java.util.List;
import java.util.Spliterator;
import java.util.function.Consumer;
public class StudentSpliterator implements Spliterator {
private final List studentList;
private int endIndex;
private int startIndex;
public StudentSpliterator(List studentList) {
this(studentList, 0, studentList.size());
}
private StudentSpliterator(List studentList, int startIndex, int endIndex) {
this.studentList = studentList;
this.startIndex = startIndex;
this.endIndex = endIndex;
}
@Override
public boolean tryAdvance(Consumer<? super Student> action) {
if (getSize() > 0) {
action.accept(studentList.get(startIndex));
startIndex++;
return true;
}
return false;
}
@Override
public Spliterator trySplit() {
if (getSize() == 1) {
return null;
}
int newStartIndex;
int newEndIndex = endIndex;
if (getSize() % 2 == 0) {
newStartIndex = startIndex + getSize() / 2;
this.endIndex -= getSize() / 2;
} else {
newStartIndex = startIndex + getSize() / 2 + 1;
this.endIndex -= getSize() / 2;
}
return new StudentSpliterator(studentList, newStartIndex, newEndIndex);
}
private int getSize() {
return endIndex - startIndex;
}
@Override
public long estimateSize() {
return studentList.size();
}
@Override
public int characteristics() {
return NONNULL;
}
}
Let’s discuss some important points:
- This is a custom spliterator class that implements
Spliterator<Student>
. - We need to override four methods:
tryAdvance, trySplit, estimateSize
andcharacteristics
. - The characteristics methods return
NONNULL
i.e. all elements arenonnull
. - The
estimateSize
is returning the size of the list studentList.
Apart from these changes, the main important methods that we need to implement are trySplit
and tryAdvance
. tryAdvance is straight forward. If the current length is greater than 0, we are calling the ‘accept’ method on the current method defined by the startIndex. We are also incrementing the current value of startIndex.
Inside the trySplit
method, we are splitting the list studentList. This method returns one new StudentSpliterator. The start and end-index of the split spliterator are different based on the size of the studentList.This is how we can use our custom Java spliterator.
public class Main {
public static void main(String[] args) {
List studentList = Arrays.asList(new Student("Alex", 23), new Student("Albert", 29), new Student("Brad", 33), new Student("Fred", 38), new Student("Max", 43));
StreamSupport.stream(new StudentSpliterator(studentList), true).forEach(e -> System.out.println("====>" + e.getAge()));
System.out.println(StreamSupport.stream(new StudentSpliterator(studentList), true).mapToInt(Student::getAge).sum());
StreamSupport.stream(new StudentSpliterator(studentList), false).forEach(e -> System.out.println("====>" + e.getAge()));
System.out.println(StreamSupport.stream(new StudentSpliterator(studentList), false).mapToInt(Student::getAge).sum());
}
}
Here, the first and the second statements used to create one parallel stream and to find out the total age of all Student objects. Similarly, the third and the fourth statements used to create one sequential stream and the total age of all student objects.
<pre>====>43
====>38
====>23
====>29
====>33
166
====>23
====>29
====>33
====>38
====>43
166
You can see that we process the elements in the same order for the sequential stream, but for the parallel stream, we process these items in random order.
4. Why custom spliterator
We can use a spliterator with a stream in a sequential or parallel way. As you have seen in the above example,tryAdvance
and trySplit are two important methods that we need to override. Using a custom spliterator, we can write our logic on how to process the next element and also how to divide the source for a parallel stream. e.g. if you have a group of items with different categories, you can split the items with similar categories so that while processing with a parallel stream, it processes the same category items in the same thread. Using a custom spliterator, we can customize it for an infinite amount of elements.
Summary
In this post, we saw the different features of the Java spliterator. We also saw the different method of the Java spliterator. In the last section of the post, we created a custom Java spliterator. As always, the source code for this post is available on the GitHub.