Practical Guide to Java Collections (Iurii Mednikov)
Practical Guide to Java Collections (Iurii Mednikov)
Disclaimer
Every precaution was taken in the preparation of this e book. However, the author assumes no
responsibility for errors, omissions or for damages, that may result from the use of information
(including code listings) contained herein.
Cover image
The cover has been designed using resources from Pexels.com
These classes are provided as a part of Java Development Kit (JDK) to make our lives as developers
easier in many ways. For example, a common base reduces a learning curve for novice programmers
and protects us from reinventing a wheel, when we deal with data structures and algorithms.
This book is for Java developers of all levels. It can be equally useful for beginners, who just start to
study Java Collections. But it can also serve for more advanced software engineers as a reference
manual.
Acknowledgments
It is a huge honor for me to publish this book. It gives me a great opportunity to summarize and to
reflect my skills. I would like to dedicate this book to my father Andrei: I spent more time to study, that
it should be (I studied international economics as my first major, and only then I switched to computer
science). And without my father’s patience it would not be possible.
Chapter 1. Overview of java.util.Collection
When we talk about Java collections, we consider two things. First, a more wide concept, means
implementations of common data structures. An another, narrow understanding, corresponds to
concrete interfaces and their implementations, that are contained in Java Collections Framework, and
are provided out-of-the box as part of JDK. In that sense, we need to remember that such collections
inherit the root interface java.util.Collection, that defines all common functionality, that sub classes
should implement.
First, please observe the graph below, which sums up a hierarchy of JCF:
One of the common interview questions sounds like "why do we use JCF"? Well, there is a number of
reasons, why JCF is so useful and important:
• JCF reduces programming efforts, because you don't need to reinvent these data structures and
algorithms yourself
• JCF offers high-performance implementations of data structures and algorithms, therefore it
increases performance
• JCF establishes interoperability between various unrelated APIs
• JCF makes it easier to learn collections for developers
Let return to the illustration. The root classes are java.util.Collection and java.util.Map. That is
important to remember, as it is common to think that Map is a collection. While maps do contain
collection-view operations, which enable them to be manipulated as collections, from a technical point
of view, maps are not collections in Java.
As it was already mentioned, Java collection is a group of objects, that are known as its elements. Note,
that elements in some collections have to be unique (for example, sets), while other types permit
duplicates (for example, array-based lists). The interface java.util.Collection is not implemented
directly, rather Java has implementations of its sub interfaces. This interface is typically used to pass
collections around and manipulate them with max degree of generality.
Note, that both of these methods are defined as optional. methods From a technical point of view, that
concrete implementations are permitted to not perform one or more of these operations (in such cases
they throw UnsupportedOperationException when such operation is performed).
(Listing 1-1)
@Test
void insertTest(){
// get mock posts
List<Post> posts = getPosts();
// Add single element
Post post = new Post(6, "Phasellus scelerisque", "Phasellus scelerisque eros id
lacus auctor");
posts.add(post);
assertThat(posts).contains(post).hasSize(6);
// add multiple elements
List<Post> newPosts = new ArrayList<>();
newPosts.addAll(posts);
assertThat(newPosts).containsAll(posts);
}
Note, that in the code snippet, I use an array list. This is one of most used Java collections and it
implements both add() and addAll() methods. Array lists have two overloaded versions of add()
method, although we will concentrate here on the Collection's one.
• boolean add(Element e) adds a new element E and ensures collection contains the specified
element. So it returns either true or false depending if this collection changed as a result of the call.
• boolean addAll(Collection c) inserts all elements from collection C to the collection. Note, that
this method also returns boolean value, which stands true if this collection changed as a result of the
call
Several implementations impose restrictions on elements that they may contain, and as the result they
prohibit certain insertions. For example, if collection does not allow duplicates, you can't add an
element that already exists. Same goes for null values.
Remove elements from a collection
In contrast with two insertion operations, there are more methods to delete elements from the
collection. Let briefly observe them:
(Listing 1-2)
@Test
void removeTest(){
// get mock posts
List<Post> posts = getPosts();
// remove object
posts.remove(post);
assertThat(posts).doesNotContain(post);
// clear
posts.clear();
assertThat(posts).isEmpty();
• Stream<E> stream() creates a sequential stream with this collection as its source
• Stream<E> parallelStream() creates a possibly parallel stream with this collection as its source.
(Listing 1-3)
@Test
void streamTest(){
List<Post> posts = getPosts();
Stream<Post> stream = posts.stream();
assertThat(stream).isInstanceOf(Stream.class);
}
• Using iterators
• Using streams
• Using forEach()
(Listing 1-4)
@Test
void iterationTest(){
List<Post> posts = getPosts();
// create iterator
Iterator<Post> iterator = posts.iterator();
// using forEach
System.out.println("Iteration using forEach");
posts.forEach(System.out::println);
// using stream
System.out.println("Iteration using stream");
posts.stream().forEach(p -> System.out.println(p));
}
Basically, the iterator pattern permits all elements of the collection to be accessed sequentially, with
some operation being performed on each element. You can note that iterator has two core methods:
• hasNext() returns true value if the iteration has more elements and we use it in the while loop (it
works in similar way like next() of result sets)
• next() returns an element and we use it to access the current element of iteration
You can also use forEachRemaining() method. It accepts a consumer function that is executed for each
remaining element until all elements have been processed or the action throws an exception. Note, that
iterator() method is not optional.
• contains(Element e) returns true if this collection contains the specified element. This method
utilizes an implementation of element’s equals() method in order to check an equality
• containsAll(Collection c) returns true if this collection contains all of the elements in the
specified collection.
• isEmpty() returns true if the collection is empty
• size() returns an integer value with a number of elements in the collection
• toArray() creates an array Element[] from elements of the collection
As a summary for this chapter, these methods provide unified interfaces for all types of collections. Of
course, each particular sub class has its different underlying implementation of these methods, due to
concrete data structure's logic. But all of them inherit non-optional functionality from the root interface.
This, as we mentioned already, ensures better learning curve for developers and promotes
interoperability.
Chapter 2. How to use streams
Streams were first introduced in Java 8 and then were updated in next releases. This concept defines a
sequence of elements supporting sequential and parallel aggregate operations. Please don't confuse the
word "stream": before 8th version, Java had InputStream and OutputStream. However, these classes
have nothing in common with the hero of this chapter. Streams API, that was introduced in Java 8 is an
implementation of a monad pattern: the concept, which was brought from functional languages like
Haskell or Clojure.
The idea of streams gains its popularity with rise of parallel computations. It is based on principle of
lazy evaluation. From a technical point of view, this means, that actual evaluation is suspended until the
result is needed. The laziness of operations states, that the actual computation on the source data is
performed after the final operation will be invoked, and source elements are consumed only as needed.
Let have a look on a simple example. Imagine, that we create a program, which prints all occurrences
of a particular name in a console. First, let write it in a traditional (imperative) paradigm:
(Listing 2-1)
What we do here is that we find all occurrences of “Anna” in the list and print them. That is a simple
operation, however, it requires to write a lot of code for a small task! With streams, we can rewrite the
previous example using the declarative paradigm.
(Listing 2-2)
names.stream().filter(name→name.equalsIgnoreCase("Anna")).forEach(System.out::print
ln);
This is totally different case! This one line code (it is sometimes called one-liner among developers)
does same thing, but in a contrasting way. Instead of loop and if-else condition, declarative approach
combines functions into pipelines. In our example we have two operations – filter(), which looks for
requested entities, and forEach(), which executes some action on each occurrence.
Initialize a stream
From a technical point of view, stream is a programming abstraction, and it is not the same thing as
collection, although it can be created from collections. These concepts are often mixed by developers,
that start with functional programming in Java, and we need to distinguish them. In this section we will
observe different approaches, that are used to initialize streams.
From collections
As this book is devoted to Java Collections Framework, it may seem logically to start with collections.
I mentioned already, that stream is not a collection, but it uses elements from a collection as its data
source. In the previous chapter we noted a built-in method stream(), that is brought by the root interface
and which creates a sequential stream from the collection’s entities.
(Listing 2-3)
If you don't have a collection with defined data, you can create a stream with generated records. This
may be useful for experimenting with streams methods. In such case, you need to define a supplier
function, that will be used for generation of a random sequence. The static factory method generate()
initializes an infinite sequential unordered stream.
(Listing 2-4)
An another method to generate a stream is rangeClosed(). The difference between aforesaid methods is
if the sequence contain an upper limit number or not. On the other side, the rangeClosed() function
returns a range that includes both limits, while range() excludes a second value from results.
From nullable
This method was initially introduced in JDK 9. It creates a new stream from a single element (that is
nullable: a value can be potentially null), which either contains that element or is empty (in the case of
null value).
(Listing 2-5)
The static factory method of() creates a stream from one or more elements (in the later situation, it uses
varargs). There are two overloaded versions:
• of (T element)
• of (T... elements)
In the first version, of() method returns a sequential stream, which contains a single element T. In the
second variant, it creates a sequential ordered stream, which has elements, provided in a form of var
args.
(Listing 2-6)
Iterating
The static method iterate() was introduced in Java 9. It takes two arguments: an initial value (also
called seed) and a unary operator function, which is used to produce a sequence. The method starts with
the seed value and applies the given function to get the next element.
(Listing 2-7)
We have already mentioned ofNullable() method, which could possibly return an empty stream in case
of null value. Still, there is an another approach to get an explicitly empty stream. The static method
empty() returns as it implied from its name an empty stream.
(Listing 2-8)
Until now we observed static factory methods, that are used to initialize a new stream instance. Some
programmers tend to rely on a builder pattern to create objects. Stream class has its builder, which can
be used as an alternative to factory methods. The static method Stream.builder() allows the creation of
streams by generating elements individually and adding them to builder without temporary collections
or buffers.
(Listing 2-9)
// 1. create builder
Stream.Builder<String> builder = Stream.builder();
// 2. create stream
Stream<String> nameStreamBuilder =
builder.add("Alejandra").add("Beatriz").add("Carmen").add("Dolores").add("Juanita")
.build();
In this code example, we start with a creation of a builder’s instance. Next, with add() method, we
populate values. Finally, we transform a builder into a stream with build() method.
A concept of a pipeline
We took a broad introduction to the stream creation and observed ways to do it. Now, as we obtained a
stream instance, we can assemble a pipeline in order to do something useful with the stream. From
technical point of view, a pipeline consists of a source (collection, element, generated sequence etc.),
followed by zero or more intermediate operations and one terminal operation.
Intermediate operations
Intermediate operations return new stream and are lazy. Their laziness means that the actual
computation on the source data is performed only after the terminal operation is invoked, and source
elements are consumed only as needed. We can chain multiple intermediate operations, as each returns
a new stream instance.
Filter
In the beginning of this chapter, we did an example with names. There, we filtered a list of names in
order to find matching names. This was done using filter(). Basically, it returns a new stream consisting
of the elements, that match the given predicate (function, that specifies a logical condition).
(Listing 2-10)
@Test
void filterTest(){
Stream<String> stream = names.stream();
long result = stream.filter(n -> n.startsWith("A")).count();
assertThat(result).isEqualTo(4);
}
Map
The streams API has a number of mapping operations. It is a good idea to group them together under
one section. Let start with a general map() function. It returns a new stream, which consists of results of
an application a mapper function to the elements of the stream.
(Listing 2-11)
@Test
void mapTest(){
Stream<String> stream = names.stream();
int result = stream.mapToInt(n -> n.length()).sum();
assertThat(result).isEqualTo(43);
}
There are specific mapping methods, that map results of previous computation to numeric values:
• mapToInt() produces an IntStream instance, that consists of of the results of applying the given
mapper function
• mapToDouble() produces a DoubleStream instance, that consists of the results of applying the
given mapper function
• mapToLong() produces a LongStream instance, that consists of the results of applying the given
mapper function
Distinct
An another commonly used intermediate operation is called distinct(). It produces a stream of unique
elements from a source. Technically, the distinct() method relies on equals() implementation of the type
class in order to avoid duplicates. For ordered streams, the selection of distinct elements is stable,
however for unordered ones, Java provides no stability guarantee.
(Listing 2-12)
@Test
void distinctTest(){
List<Integer> numbers = Arrays.asList(1, 1, 2, 3, 3, 4, 5, 5);
Stream<Integer> stream = numbers.stream();
long result = stream.distinct().count();
assertThat(result).isEqualTo(5);
}
Sorting
In many situations, we need to sort elements of a collection in some way, for example in their natural
order. The method sorted() returns a stream, which elements are sorted in the natural order.
(Listing 2-13)
@Test
void sortTest(){
List<Integer> numbers = Arrays.asList(-9, -18, 0, 25, 4);
Stream<Integer> stream = numbers.stream();
List<Integer> result = stream.sorted().collect(Collectors.toList());
assertThat(result).containsAll(numbers).containsExactly(-18, -9, 0, 4, 25);
}
In the listing 2-13 I used a stream, which elements are numbers. The Integer class implements
Comparable interface, therefore provides own logic to compare values. If you use sorted() with own
classes, you have to implement Comparable and to write it yourself. Otherwise, ClassCastException
will be thrown, when a terminal operation will be executed.
While
These two methods were also added at Java 9 release: dropWhile() and takeWhile(). Each of them
accepts a predicate function, which specifies the condition.
• dropWhile() produces a stream consisting of the remaining elements of this stream after
dropping the longest prefix of elements that match the given predicate.
• takeWhile() produces a stream consisting of the longest prefix of elements taken from this
stream that match the given predicate.
Note, that both methods work with streams, that have elements in order. This means, that you first have
to sort elements, for example using sorted() method, which was described in the previous section. In
the listing 2-14 I use set.
(Listing 2-14)
@Test
void whileTest(){
Set<Integer> numbers = Set.of(1,2,3,4,5,6,7,8);
Stream<Integer> stream = numbers.stream();
Set<Integer> result = stream.takeWhile(x-> x < 5).collect(Collectors.toSet());
assertThat(result).hasSize(4);
}
Limit
The method limit() produces a stream consisting of the elements, limited to be no longer than specified
length. This method accepts one argument, which represents a desired length.
(Listing 2-15)
@Test
void limitTest(){
Stream<Integer> stream = getNumbers().stream();
Set<Integer> result = stream.sorted().limit(5).collect(Collectors.toSet());
System.out.println(result);
assertThat(result).contains(-75, -18, -9, -5, 0);
}
Peek
This method is often misunderstood among developers. The reason for this, is that it acts in a similar
way as map() method, but only in a small portion. Likewise, peek() method applies modifications on an
each element of a stream, but it these modifications exist only inside the method execution. Unlike
map(), peek() does not return a modified element to the stream flow. That is a reason, why peek() is
often used for debugging. You can think about it as intermediate counterpart for forEach().
Let take an example to distinguish between peek() and map(). Consider a list of numbers. We apply
same modification for both cases – multiply an each number on 2. However, when you will run this
snippet, you will see, that in the first case the final result contains same values as the initial sequence:
(Listing 2-16)
@Test
void peekTest() {
List<Integer> numbers = Arrays.asList(5, 2, 12, 20, 9, 8);
System.out.println("Peek:");
List<Integer> peekResults = numbers.stream().peek(number -> {
number = number *2;
System.out.println(number);
}).collect(Collectors.toList());
System.out.println("Final result:");
peekResults.stream().forEach(System.out::println);
Assertions.assertThat(peekResults).hasSameElementsAs(numbers);
System.out.println("Map");
List<Integer> mapResults = numbers.stream().map(number -> {
number = number * 2;
System.out.println(number);
return number;
}).collect(Collectors.toList());
System.out.println("Final result:");
mapResults.stream().forEach(System.out::println);
Assertions.assertThat(mapResults).contains(10, 4, 24, 40, 18, 16);
}
The final output of running this test case is presented on the screenshot below:
Note, the result sequence in the first case (after an application of the peek() operation) contains same
numbers, as the original list. This is due to the fact, that peek() method does not return modified values.
The purpose of this method is to validate elements in a certain point of pipeline. On the other hand, the
result of the pipeline 2 (where map() method was applied) is different from the initial sequence.
Terminal operations
The other group of pipeline operations corresponds to terminal operations. Different from intermediate
operations, there may be only one terminal operation that is executed on stream, because as it will be
completed, the stream pipeline will be consumed, and can no longer be used. Terminal operations
produces some result (can be either Java object or void), not a stream.
ForEach
We have already encountered the forEach() method in many examples, that illustrated intermediate
operations in the previous section. From a technical point of view, the forEach() function accepts a
consumer function, which defines an action to be performed on each element of the stream.
(Listing 2-17)
@Test
void forEachTest(){
Stream<String> stream = names.stream();
stream.filter(n->!n.equalsIgnoreCase("Anna")).map(n -> n.toUpperCase()).forEach(n -
> System.out.println(n));
}
Note, that for parallel stream pipelines, this operation does not guarantee to respect the encounter order
of the stream, as doing so would sacrifice the benefit of parallelism. For any given element, the action
may be performed at whatever time and in whatever thread the library chooses. If the action accesses
shared state, it is responsible for providing the required synchronization.
Collectors
The forEach() method has no return: it consumes data, but does not provide back result (it is of the void
return type). However, we often need to perform some operations with stream on a collection and then
get a new collection back. In these situations we use collect() method. It performs a mutable reduction
operation on the elements of this stream using collectors.
(Listing 2-18)
@Test
void collectTest(){
Stream<String> stream = names.stream();
List<Integer> result = stream.filter(n -> n.length() <= 4).map(n ->
n.length()).collect(Collectors.toList());
assertThat(result).hasSize(5);
}
In the example code, presented in the program listing 2-18, I use a built-in collector method, which
collects stream elements into a list. There are other collector methods, which Java provides out of the
box:
• Collectors.toMap()
• Collectors.toSet()
Find
Under this section, I list operations, which look for an element and return an Optional container,
because the requested element may not be presented (e.g. may be null value):
• findAny()
• findFirst()
Both of these methods do not have any arguments, so you may ask a very reasonable question: how do
they actually find the element? The answer is quite straightforward. The condition is specified in
filter(), and a terminal find operation returns a single element, which satisfies that predicate.
(Listing 2-19)
@Test
void findTest(){
Stream<String> stream = names.stream();
Optional<String> result = stream.filter(n -> n.length() < 4).findFirst();
assertThat(result).isPresent();
assertThat(result.get()).isEqualToIgnoringCase("Bob");
}
What is a difference between findFirst() and findAny()? Like it can figured out from their names,
findFirst() returns the first occurrence of the matching element. From other side, findAny() returns any
element, which satisfies a predicate. That could be possibly the first one or could be not: behavior of
this operation is explicitly nondeterministic; in other words, it is free to select any element in the
stream.
Match
An another common scenario is to validate, that elements of a sequence satisfy a logical condition. We
may need to check that either all elements, either some elements, either none of elements are valid
according some predicate. Stream API provides for this purpose a group of match methods. There are
three functions:
• allMatch()
• noneMatch()
• anyMatch()
As name imply, match operations accept a predicate (a logical condition) and return a boolean value,
that states that elements of the stream match all, do not match at all, or just several of them match. Let
see it on practical examples.
The first candidate is the anyMatch() terminal operation. It returns true if at least one element matches
a predicate. This method may not evaluate the predicate on all elements of the stream, if it is not
necessary for determining the result. On an empty stream false result is returned. Consider a list of
integers. We use anyMatch() to figure out if there are even numbers:
(Listing 2 – 20)
@Test
void anyMatchTest(){
List<Integer> numbers = Arrays.asList(10, 53, 23, 95, 30);
boolean hasEven = numbers.stream().anyMatch(n -> n%2 == 0);
Assertions.assertThat(hasEven).isTrue();
}
The other case is to evaluate that all elements satisfy condition. This can be done using the allMatch()
operation. The difference with the previous function is that the allMatch() returns true on the empty
stream. Let have an example with a list of numbers. We want to assert, that each element is even:
(Listing 2 - 21)
@Test
void allMatchTest(){
List<Integer> numbers = Arrays.asList(10, 53, 23, 95, 30);
boolean hasEven = numbers.stream().allMatch(n -> n%2 == 0);
Assertions.assertThat(hasEven).isFalse();
}
Finally, we can check, that there is no element in a stream that matches to the predicate. It is performed
with the noneMatch() method. Consider a following example: we need to assert, that all integers in a
list are negative (less than 0):
(Listing 2 - 22)
@Test
void noneMatchTest(){
List<Integer> numbers = Arrays.asList(-4, -54, -25, -8, -1);
boolean hasPositives = numbers.stream().noneMatch(n -> n>0);
Assertions.assertThat(hasPositives).isTrue();
}
Reduction
From a technical point of view, reduction is an operation, that allows to produce a single result from a
sequence of elements. A good example of a reduction is sum operation: we add each element and return
a final result. In other words, reduction is an application of some function (for instance, addition)
repeatedly on all members of a sequence.
Java provides its implementation of reduction as the reduce() terminal operation. This method takes
two arguments: an identity value and a binary operator, that specifies combining function. Identity is an
initial value (for example 0 or empty string) or a default value, in case of an empty stream. The binary
operator takes two parameters: a partial result (for instance, subtotal of total) and a next element of a
stream.
Let see how to use the reduce() function on two examples: on a list of numbers (we expect a sum) and
on a list of strings (we expect a one word):
(Listing 2 - 23)
@Test
void reduceTest(){
// numbers
List<Integer> numbers = Arrays.asList(10, 20, 30, 40, 50);
int total = numbers.stream().reduce(0, (subtotal, number) -> subtotal + number);
Assertions.assertThat(total).isEqualTo(150);
// string
List<String> letters = Arrays.asList("h", "e", "l", "l", "o");
String word = letters.stream().map(s -> s.toUpperCase()).reduce("", (partial, next)
-> partial.concat(next));
Assertions.assertThat(word).isEqualTo("HELLO");
}
To sum up, the usage of a reduction operation helps to create a single result from a sequence.
Chapter 3. Lists
Before we will begin with an overview of list implementations in the Java Collections Framework, let
define what is a list. In computer programming, list is finite sequence of elements, where each element
has its own position. In computer science are contrasted two types of lists: array-based lists and linked
lists.
An array-based list is called this way, because behind the scenes it uses a backed array to store values.
One of important characteristics of arrays is their fixed size, but, array-based lists could add as many
elements as we need and extend the size of the underlying array. Here we need to introduce concepts of
size and capacity. Take a look on the graph below, which demonstrates an array list, which has 5 cells
and 3 of them have actual values:
Technically, when size becomes equal to capacity, in order to add new elements, Java has to increase a
capacity. It is performed with a mechanism, that is called dynamic memory allocation, which reserves
memory cells for prospective elements as needed.
All lists in Java implement java.util.List interface as well have all core collection methods. Array-based
list implementation is defined in java.util.ArrayList class. The capacity of list’s array is grown
automatically, and is always at least large than its size (number of elements).
It is important to note here, that while Java collections are mutable, there are two ways to initialize a
new array list object: the first approach produces a mutable collection and the another one produces an
immutable list:
• Using constructor, like List<Integer> numbers = new ArrayList<>(); this version creates
mutable lists
• Using the Arrays.asList() static method, which creates immutable lists
An another option is to use the List.of() static method, which accepts arguments as var args. This
operation is available since Java 10. It also produces an immutable list.
• add(E e) is the basic collection's method, that inserts a new element to the end of the list
• addAll(Collection c) is an another method from the root java.util.Collection interface, that
appends all elements of the collection C to the end of the list
• add(int position, E e) this method inserts a new element E to the specified position position. It
shifts the element currently at that position and any subsequent elements to the right. This method
could throw IndexOutOfBoundsException in a case the position value is higher than the size of list
• addAll(int position, Collection c) the operation which is similar to the previous one, but adds all
elements from collection c starting from position position. Can throw IndexOutOfBoundsException
with same conditions as the add() function
(Listing 3-1)
@Test
void addElementsToListTest () {
// Approach 1. Create new list and use add() MUTABLE!
List<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(15);
numbers.add(20);
numbers.add(25);
numbers.add(30);
assertThat(numbers.add(66)).isTrue();
Remove elements
A deletion of elements is an another important thing we need to do on the day-to-day basis. There are
two overloaded remove methods to remove elements from an array list:
• remove(E e) removes a single instance of the specified element e from the list (if that element is
presented)
• remove(int position) removes an element in index position.
This is important to distinguish these methods, as it is very common questions in interviews and
certifications. For example, you have a list of numbers:
(Listing 3-2)
@Test
void removeElementFromListTest() {
List<String> names = new ArrayList<>();
names.add("Alejandra");
names.add("Beatriz");
names.add("Carmen");
names.add("Dolores");
names.add("Juanita");
When you run this code, you will see that was removed the element with index 1, or "Beatriz". To
delete element by its value (for example “Juanita”), we pass it as an argument:
(Listing 3-3)
names.remove("Juanita")
(Listing 3-4)
@Test
void accessElementTest() {
List<Integer> numbers = Arrays.asList(1, 52, 12, 39, 45, 98, 100, 565, 6, 13);
int beginning = numbers.get(0);
int value = numbers.get(5);
int end = numbers.get(numbers.size()-1);
assertThat(beginning).isEqualTo(1);
assertThat(value).isEqualTo(98);
assertThat(end).isEqualTo(13);
}
(Listing 3-5)
@Test
void createSublistTest(){
List<String> original = Arrays.asList("Alejandra", "Beatriz", "Carmen", "Dolores",
"Juanita", "Katarina", "Maria");
List<String> sublist = original.subList(0, 5);
assertThat(sublist).contains("Juanita").doesNotContain("Katarina");
}
• indexOf(E e) returns an index of the first occurrence of the element E or -1 if nothing found
• lastIndexOf(E e) returns an index of the last occurrence of the element E or -1 if nothing found
(Listing 3-6)
@Test
void searchForElementTest(){
List<Integer> numbers = Arrays.asList(1, 52, 12, 39, 45, 98, 100, 565, 6, 13);
assertThat(numbers.indexOf(45)).isEqualTo(4);
// using lastIndexOf
List<Integer> numbers2 = Arrays.asList(1, 52, 12, 39, 45, 98, 100, 565, 45, 6, 13);
assertThat(numbers2.lastIndexOf(45)).isEqualTo(8);
}
• Original list
• Predicate
It returns the index of the first element, that matches the given predicate. Or -1 the element is not
present or a list is empty. This allows to find not only element by value, but also the one that satisfies a
condition. Take a look on the example code below:
(Listing 3 - 7)
@Test
void searchCommonsTest(){
List<Integer> numbers = Arrays.asList(1, 52, 12, 39, 45, 98, 100, 565, 6, 13);
int index = ListUtils.indexOf(numbers, n -> n == 39);
Assertions.assertThat(index).isEqualTo(3);
}
Filter
When we filter a list, we basically create a new list, which contains all elements from the original list,
that satisfy a logical condition (predicate). The List interface does not have a standard filter() method,
like in other programming languages (for example, in JavaScript). We can implement it manually by
creating a stream, applying a filter() intermediate operation and then collecting results as a list.
(Listing 3 - 8)
@Test
void filterStreamTest() {
List<Integer> numbers = Arrays.asList(45, 12, 80, 77, 95, 4);
List<Integer> results = numbers.stream().filter(n -> n%2 ==
0).collect(Collectors.toList());
Assertions.assertThat(results).hasSize(3);
}
We can also use Apache Commons Collections to achieve this task. It provides a static method select()
as a part of ListUtils. The idea of this operation similar to filtering: it applies a predicate on each
element of an original list and returns a new list, which contains only those elements, that satisfy this
condition.
(Listing 3 - 9)
@Test
void filterCommonsTest(){
List<Integer> numbers = Arrays.asList(45, 12, 80, 77, 95, 4);
List<Integer> results = ListUtils.select(numbers, n -> n%2 == 0);
Assertions.assertThat(results).hasSize(3);
}
Replace an element
In this section we observe how to replace (change) a specific element. Recall, that array lists store
items in arrays, so by replacing we understand changing a value of the cell with concrete index. To do it
we can use two approaches:
• set(int position, E e) replaces the element at the specified position in this list with the value e
• Using static method Collections.replaceAll(collection, old, new) which accepts three arguments:
a list itself, old value and a new element.
Take a look on the code snippet, presented in the listing 3-10, which demonstrates both approaches:
(Listing 3 - 10)
@Test
void replaceElementTest(){
List<String> names = new ArrayList<>();
names.add("Alejandra");
names.add("Beatriz");
names.add("Carmen");
names.add("Dolores");
names.add("Juanita");
// Appraoch 1 By index
names.set(1, "Maria");
assertThat(names.get(1)).isEqualToIgnoringCase("Maria");
There are alternative approaches. First way is to use streams. In some situations we want to check that
lists contain same elements, but the order of elements is different. Technically, this is out of the equality
definition, and these lists are not strictly equal. In that case we can utilize streams to check that
elements match.
Second approach is to use Apache Commons Collections. It provides a static method isEqualList(), that
takes two lists as an argument and return a boolean value of their equality. It is wrapper method around
equals() implementation.
(Listing 3-11)
@Test
void compareListsTest(){
List<Integer> list1 = Arrays.asList(1, 52, 12, 39, 45, 98, 100, 565, 6, 13);
List<Integer> list2 = Arrays.asList(1, 52, 12, 39, 45, 98, 100, 565, 6, 13);
Copy lists
There are three ways to copy elements from one array into another:
• Using streams
• Using a static helper method from the Collections utility class
• Using the List.copyOf() static method (this option is available since Java 10)
This first approach is based on the usage of streams. It is the most flexible from mentioned approaches,
as it allows to run additional assertions, like null checks or duplicate checks. In the simplest form, all
elements of streams are collected using the toList() collector.
(Listing 3-12)
The second option is to use the helper class Collections. It provides the static method copy(), which has
two arguments. This method copies all of the elements from the source list into the destination list.
After the operation, the index of each copied element in the destination list will be identical to its index
in the source list. The destination list's size must be greater than or equal to the source list's size. If it is
greater, the remaining elements in the destination list are unaffected.
(Listing 3-13)
Finally, elements between two lists can be copied using the static method List.copyOf(), that is
available since Java 10. The method returns an immutable list, which contains the elements of the given
collection (e.g. can be used with other collections as sources), in its iteration order. The given collection
must not be null, and it must not contain any null elements. If the given collection is subsequently
modified, the returned list will not reflect such modifications.
(Listing 3-14)
Java includes set implementations as a part of Java Collections Framework. The root interface
java.util.Set defines a data structure, which contains to duplicates and at most 1 null element. Only one
null element is allowed because, from a technical point of view, one presented null value is unique by
its nature. You may ask, how Java understands if an element is unique? For a unique element e1 is true,
that there is no element e2 in set already that e1.equals(e2) == true.
These implementations differ on a following basis: does the set allow null elements and are its elements
sorted? It is important to note, that a behavior of a set is not specified, when the value of an entity
(element) was changed in a manner that affects the equals() method comparison logic, while the object
is in the set.
@Test
void addToSetTest(){
Set<Integer> numbers = new HashSet<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
assertThat(numbers.add(2)).isFalse();
assertThat(numbers.size()).isEqualTo(3);
}
You can note in the listing 4-1, that even we add 4 elements, the size stands of numbers is 3. This is due
to the fact, that element with value 2 is already presented. Therefore, an insertion of repeated element
returns false boolean value, as it presented in the listing 4-2:
(Listing 4-2)
Union operations
In mathematical definition of sets, the union operation means all elements of two sets. From
programming point view, this states a set, that contains values from two lists. There are two ways to
implement union in Java:
• Using addAll()
• Using the static method union() from Apache Commons Collections
There are some aspects. The first approach appends elements from the one set to another and returns a
boolean value, which states a success/failure of operation.
(Listing 4-3)
@Test
void unionJavaTest() {
HashSet<Integer> numbers1 = SetUtils.hashSet(20, 12, 35, 40, 19);
HashSet<Integer> numbers2 = SetUtils.hashSet(67, 9, 11, 5, 17);
boolean result = numbers1.addAll(numbers2);
Assertions.assertThat(numbers1).containsAll(numbers2);
Assertions.assertThat(result).isTrue();
}
The drawback of the core Java approach, is that it modifies one of sets. To avoid this, use Apache
Commons Collections, which supplies SetUtils.union() static method. It accepts two sets and return a
SetView instance (that can be converted to a set using toSet() operation), and this protects original
collections from modifications.
(Listing 4-4)
@Test
void unionCommonsTest() {
HashSet<Integer> numbers1 = SetUtils.hashSet(20, 12, 35, 40, 19);
HashSet<Integer> numbers2 = SetUtils.hashSet(67, 9, 11, 5, 17);
Set<Integer> results = SetUtils.union(numbers1, numbers2).toSet();
Assertions.assertThat(results).containsAll(numbers1).containsAll(numbers2);
}
Delete an element
To remove an element from a set, we could use a method remove(Object E). As same with the
mentioned case of an insertion, delete operation uses the equals() implementation to determine an
existence of an element in the set. This method returns boolean value, which is true if the element E
was removed. In other words, set will not contain the element once the call returns.
(Listing 4-5)
@Test
void removeFromSetTest(){
Set<Integer> numbers = new HashSet<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
assertThat(numbers.remove(3)).isTrue();
}
Please pay attention on the method’s signature: it accepts a general Object, rather than a concrete type.
Therefore, ClassCastException would be thrown in case when the argument does not match its type to
the type of the set.
Intersection operation
In a set theory, intersection is defined as following: set, which contains all elements of set A, that are
also elements of set B. In other words, by intersection we understand elements, that present in both
sets. In Java we can find an intersection with two main approaches (similarly to union):
• With retainAll()
• Using Apache Commons Collections
The native approach is specified as the retainAll() method. Alike the addAll() method, it also modifies
an original collection, by removing all elements, that are not presented in another set.
(Listing 4-6)
@Test
void intersectionJavaTest(){
HashSet<Integer> numbers1 = SetUtils.hashSet(20, 12, 35, 40, 19);
HashSet<Integer> numbers2 = SetUtils.hashSet(35, 17, 12, 95, 3, 19);
numbers1.retainAll(numbers2);
Assertions.assertThat(numbers1).containsExactlyInAnyOrder(35, 19, 12);
}
As it was stated already, the mutability of Java collections affects elements of the original numbers1
set. This can be avoided either by creating an intermediate set (using copy constructor to take all
elements from the numbers1 set) or by using Apache Commons Collections utils.
The Apache Commons Collections contain a static method SetUtils.intersection(), which takes two sets
as arguments. This method returns the SetView instance, that can be converted to a set by calling toSet()
method.
(Listing 4-7)
@Test
void intersectionCommonsTest(){
HashSet<Integer> numbers1 = SetUtils.hashSet(20, 12, 35, 40, 19);
HashSet<Integer> numbers2 = SetUtils.hashSet(35, 17, 12, 95, 3, 19);
Set<Integer> results = SetUtils.intersection(numbers1, numbers2);
Assertions.assertThat(results).containsExactlyInAnyOrder(35, 19, 12);
}
Replace an element
Unlike lists, which permit to replace an arbitrary element, sets do not allow that functionality. By
default, if set already holds an element, new addition will not be added and will not replace the existing
one. We can implement this functionality manually. For that, we need to assert, that element does exist
in set, than delete it, like it is shown in listing 4-8:
(Listing 4-8)
if (set.add(e) == false){
set.remove(e);
set.add(e);
}
In this snippet, we start with checking, that a set does have an element E. We can do it using the add()
method, that, as we observed already, returns false is an element already there. In this case we first
remove the existing element and then insert a new element.
• Using iterators
• Using streams
(Listing 4-9)
In the listing 4-9, we create a new TreeSet instance, that contains integer numbers. Note, that tree sets
store sorted elements. Therefore, by calling the iterator’s next() method, we obtain the first element:
(Listing 4-10)
This code can be refactored using Apache Commons Utils. It ships a class IteratorUtils, that contains
several handy static helpers, which amplify possibilities working with iterators in Java. For instance, to
get the first element we can use the first() method.
(Listing 4-11)
It also allow developers to get an arbitrary element from an iterator by specifying its index. It can be
done by calling a static method get(), which takes an iterator and an index. The return result is an
element or the method can throw an exception, if an index is out of bounds. Take a look on the code
listing below:
(Listing 4-12)
The alternative to iterators is stream. We assume, that the initial set has ordered elements. Therefore,
we can use the filter() method to determinate an element, which satisfies a predicate. Calling the
findFirst() terminal operation will return that value (as instance of Optional):
(Listing 4-13)
(Listing 4-14)
@Test
void getSizeTest(){
Set<Integer> numbers = Set.of(1,2,3,4,5);
numbers.add(1);
int size = numbers.size();
assertThat(size).isEqualTo(5);
}
• Using streams
• Using iterators
(Listing 4-15)
@Test
void createSubsetTest(){
Set<Integer> numbers = Set.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// approach 1: streams
Set<Integer> streamSubset = numbers.stream().limit(5).collect(Collectors.toSet());
assertThat(streamSubset).hasSize(5).contains(1,2,3,4,5);
// approach 2: iterators
Iterator<Integer> iterator = numbers.iterator();
Set<Integer> iteratorSubset = new HashSet<>();
int limit = 5;
for (int i = 0; i<limit && iterator.hasNext(); i++){
iteratorSubset.add(iterator.next());
}
assertThat(iteratorSubset).hasSize(5).hasSameElementsAs(streamSubset);
}
Difference
In a set theory, a difference between sets is a set of a set of all elements of set A, which are not elements
of a set B. Java does not provide built-in solution for this operation, but it can be achieved using the
mentioned Commons Collections library. The SetUtils class offers a static difference() method. It
returns a SetView (can be converted to a Java set), which contains all elements of set A that are not a
member of set B.
(Listing 4-16)
@Test
void differenceTest() {
HashSet<Integer> numbers1 = SetUtils.hashSet(20, 12, 35, 40, 19);
HashSet<Integer> numbers2 = SetUtils.hashSet(35, 17, 12, 95, 3, 19);
Set<Integer> results = SetUtils.difference(numbers1, numbers2).toSet();
Assertions.assertThat(results).containsExactlyInAnyOrder(20, 40);
}
Copy sets
There are two general approaches to copy elements from one set to another:
• Using streams
• Using the static method Set.copyOf() (is available since JDK 10)
The usage of streams to copy elements between sets are similar to the one with lists. Like we have
observed in the previous chapter, the simplest form is using toSet() collector on elements of the stream.
(Listing4-17)
In the Java 10 release was introduced an another approach to copy elements. The static Set.copyOf()
method returns an immutable set, which contains the elements of the given collection (e.g. this method
can be used with any collection type). The given collection must not be null, and it must not contain
any null elements. If the given collection contains duplicate elements, an arbitrary element of the
duplicates is preserved. If the given collection is subsequently modified, the returned Set will not
reflect such modifications.
(Listing 4-18)
Like the List.copyOf() method, this Set.copyOf() does not permit to have an immutable collection as a
source.
Chapter 5. Queues
In computer programming, the queue stands for a linear collection of elements that are inserted and
removed based on FIFO principle. FIFO is an abbreviation, which means First-in-First-out. In other
words, an element, which was inserted first, will be removed first, unlike stacks, where the first
element is removed last. To distinct these two principles let have an example with a ferry. The first
situation is a car embarkation. This is an example of a LIFO order: the first car, that comes into a ferry,
will exit last. An another example is a queue in a ticket office to buy tickets for this ferry. Here governs
FIFO principle: the person, which came first to the ticket gate, will buy and exit a queue first.
In a basic structure, queues support only two operations: enqueue and dequeue:
The first front element is also called head and elements that are positioned after are called tail. In Java,
queues are defined in the java.util.Queue interface. There are two main implementations: ArrayDeque
and PriorityQueue:
The important difference between these implementations is an order of elements. The ArrayDeque
holds elements on FIFO basis, so head element is the first inserted. The PriorityQueue in its regard
holds elements based on their natural ordering (if they implement Comparable interface) or based on a
specified comparator function. Therefore, head element in this case will be the least element.
Queue declaration
In order to use a queue you should declare it. Queues can be defined using public constructors that help
specify capacity. No args constuctors create queues with an initial capacity sufficient to hold 16
elements:
(Listing 5-1)
// initial capacity = 16
Queue<Person> people = new ArrayDeque<>();
// for the ferry that can transport 50 cars
Queue<Car> cars = new ArrayDeque<>(50);
(Listing 5-2)
@Test
void enqueueTest(){
Queue<Person> people = new ArrayDeque<>();
Special note must be made in a case of the ArrayDeque. This class also inherits the Deque interface.
This interface defines a data structure that permits to add and remove elements on both ends in
comparing with the classical queue. This adds some methods that are specific only for this class:
• offerFirst() and addFirst() methods insert an element to the head. A difference between them is
the same as between offer() and add() methods
• offerLast() and addLast() methods insert an element to the tail
• push() is an equivalent for the addFirst() method. In a case of an insertion of a null element, this
method will throw a NullPointerException
(Listing 5-3)
@Test
void dequeueTest(){
Queue<Car> cars = new ArrayDeque<>();
Car skoda = new Car(4567, "Skoda Rapid");
cars.offer(skoda);
assertThat(cars.peek()).isEqualTo(skoda);
cars.offer(new Car(1234, "Mazda 3"));
cars.offer(new Car(2345, "Kia Cerato"));
cars.poll();
assertThat(cars.peek().getLicensePlateNumber()).isEqualTo(1234);
}
The difference between both methods is same with enqueue methods. Similar to the previous situation,
Java recommends to use the native for queues method poll(). We also need to mention here methods,
which are inherited by the ArrayDeque from the Deque interface:
(Listing 5-4)
@Test
void getHeadTest(){
Queue<Person> people = new ArrayDeque<>();
people.offer(new Person("Alejandra", "Morales"));
people.offer(new Person("Beatriz", "Sanchez"));
people.offer(new Person("Carmen", "Hidalgo"));
As it was mentioned already, an array deque permits to get both the first and the last elements.
Therefore it provides two custom methods to get the head element: getFirst() and peekFirst(). These
methods differ in the aforesaid manner.
(Listing 5-5)
@Test
void getTailTest(){
Queue<Person> people = new ArrayDeque<>();
people.offer(new Person("Alejandra", "Morales"));
people.offer(new Person("Beatriz", "Sanchez"));
people.offer(new Person("Carmen", "Hidalgo"));
assertThat(tail).hasSize(2);
}
From a technical point of view, map is a data structure, which stores its elements in a key-value format.
In other programming languages such components are called hashes, associative arrays or dictionaries.
Maps bring a number of advantages. Such, they are able to maintain an arbitrary number of different
objects in a same time and the upper limit of the size is not needed to be known ahead, compare to
array based structures. The core idea behind is that all of them use some unique identifier to access
stored objects. In Java maps such identifier is called key. Maps do not allow duplicate keys, however
they allow multiple same values for unique keys. Some Java implementations permit a single null key
(except for tree maps). Take a look on the graph, that represents hierarchy of Java maps:
The HashMap class, comparing to the LinkedHashMap class and the TreeMap class, does not guarantee
a sorted order of elements. To have this ordering, keys should be comparable (implement a
Comparable interface) or a developer should provide a comparator function during map's
initialization. An important note about Java maps is that they are not synchronized.
The difference between these methods is following. The first method (put()) is an optional operation
and in the case of its usage, if the map previously contained a mapping for the key, the old value is
replaced by the specified value. The second method (putIfAbsent()) inserts a new value, only if the key
is not presented.
(Listing 6-1)
@Test
void addToMapTest(){
HashMap<Integer, Person> people = new HashMap<>();
people.put(1, new Person("Alejandra", "Gutierrez"));
people.put(2, new Person("Beatriz", "Gomez"));
people.put(3, new Person("Carmen", "Hidalgo"));
people.put(4, new Person("Dolores", "Sanchez"));
assertThat(people.get(3)).isEqualTo(new Person("Carmen", "Hidalgo"));
}
For safe insertion can used Apache Commons Collections MapUtils class. It contains a special static
helper method safeAddToMap(), which protects from adding null values into a map. It checks, that the
element is not null, otherwise replaces it with an empty string value.
(Listing 6-2)
@Test
void getFromMapTest(){
TreeMap<String, String> words = new TreeMap<>();
words.put("apple", "manzana");
words.put("orange", "naranja");
words.put("pineapple", "pina");
words.put("lemon", "limon");
assertThat(words.get("cucumber")).isNull();
assertThat(words.getOrDefault("cucumber", "not a
fruit")).isEqualToIgnoringCase("not a fruit");
}
• V remove (K key)
• boolean remove (K key, V value)
The first operation is optional. It returns a value or null, if object with such key does not exist.
However, if the concrete map implementation does permit null values, then null return does not
necessarily indicate that the map contained no mapping for the key; it's also possible that the map
explicitly mapped the key to null.
The second method removes the value V for the key K only if it is currently mapped to the specified
value. The return boolean value indicates was the result successful.
(Listing 6-3)
@Test
void removeFromMapTest(){
HashMap<Integer, Person> people = new HashMap<>();
people.put(1, new Person("Alejandra", "Gutierrez"));
people.put(2, new Person("Beatriz", "Gomez"));
people.put(3, new Person("Carmen", "Hidalgo"));
assertThat(people).hasSize(3);
people.remove(3);
assertThat(people).doesNotContainKey(3);
}
The difference between both methods is that the first one replaces and returns an old value or null in a
case of absence. The second one replaces a value only it is equal to the specified an old value and
returns a boolean result of a replacement.
(Listing 6-4)
@Test
void replaceElementTest(){
TreeMap<String, String> words = new TreeMap<>();
words.put("apple", "manzana");
words.put("orange", "naranja");
words.put("pineapple", "pina");
words.put("lemon", "limon");
words.replace("lemon", "limón");
String limon = words.get("lemon");
assertThat(limon).isEqualToIgnoringCase("limón");
To reverse keys/values in map use Apache Collections Commons. This library has a class MapUtils that
provides a static method invertMap(), which does the inversion operation. This method assumes that
the inverse mapping is well defined. If the input map had multiple entries with the same value mapped
to different keys, the returned map will map one of those keys to the value, but the exact key which will
be mapped is undefined.
Let have an example with a phone book, where we will swap phone number (key) and a name (value).
(Listing 6-5)
@Test
void invertCommonsTest() {
Map<String, String> phones = new HashMap<>();
System.out.println("Original map: ");
phones.put("111222333", "Jana Novakova");
phones.put("444555666", "Zuzka Dvorakova");
phones.put("777888999", "Alex Vodicka");
• contains(E e, int index) checks that the element presents on the specific position
• contains(E... elements) accepts varargs, e.g. one or more elements and checks for their presence
in any order
(Listing 7-1)
@Test
void containsElementTest(){
List<Integer> numbers = Lists.newArrayList(1, 52, 12, 39, 45, 98, 100, 565, 6, 13);
assertThat(numbers).contains(12);
assertThat(numbers).doesNotContain(50);
}
Besides the contains() method, AssertJ provides also the doesNotContain() method, which asserts for
an absence of entity (s).
(Listing 7-2)
@Test
void containsAllElementsNoMatterOrderTest(){
List<Integer> numbers = Lists.newArrayList(1, 52, 12, 39, 45, 98, 100, 565, 6, 13);
List<Integer> values = Lists.newArrayList(52, 39, 12, 1, 100);
assertThat(numbers).containsAll(values);
}
(Listing 7-3)
@Test
void containsAllElementsInOrderTest(){
List<Integer> numbers = Lists.newArrayList(1, 52, 12, 39, 45, 98, 100, 565, 6, 13);
List<Integer> values = Lists.newArrayList(numbers);
assertThat(numbers).containsExactlyElementsOf(values);
}
(Listing 7-4)
@Test
void noDuplicatesTest(){
List<Integer> numbers = Lists.newArrayList(1, 52, 12, 39, 45, 98, 100, 565, 6, 13);
assertThat(numbers).doesNotHaveDuplicates();
}
(Listing 7-5)
@Test
void containsOnlyOnceTest(){
List<Integer> numbers = Lists.newArrayList(1, 1, 52, 12, 12, 45, 45);
assertThat(numbers).containsOnlyOnce(52);
}
Chapter 8. Iterators
The iterator pattern is one of approaches to access elements of a collection, alongside with streams.
From a technical point of view, the iterator traverses elements in a sequential and predictable order. In
Java, the behavior of iterators is defined in the java.util.Iterator contract, which is a member of Java
Collections Framework.
Iterators are similar to enumerators, but there are differences between these concepts too. The
enumerator provides indirect and iterative access to each element of a data structure exactly once. From
the other side, iterators does the same task, but the traversal order is predictable. With this abstraction a
caller can work with collection elements, without a direct access to them. Also, iterators allow to delete
values from a collection during the iteration.
In order to prevent this unchecked exception, you should call the hasNext() method prior to accessing
an element.
(Listing 8-1)
The important thing to note here, is that once elements are consumed, the iterator can not be used. That
means, that calling the iterator after traversing will lead to an exception:
(Listing 8-2)
The execution of the above code snippet will lead to the following result:
We have already mentioned Apache Commons Collections. This library contains the helper class
IteratorUtils, which has a number of static utility methods to work with iterators. While some of them
violate the core pattern, they can be useful. So, alongside with a sequential access, it possible to access
a particular element by its index, as well there is a wrapper method to get the first element.
(Listing 8-3)
Consume an element
Since Java 8 iterators permit also to specify a consumer function, which can be performed on each
element. This is a shortcut of what we can do using a while block. Let consider the first example
implemented with the forEachRemaining() method. Take a look on the code snippet below:
(Listing 8-4)
It is possible to achieve a similar result with IteratorUtils. This utility class has two static methods:
(Listing 8-5)
Remove elements
Finally, we need to observe how to use an iterator to delete elements. The java.util.Iterator interface
contains the remove() method. It deletes from the underlying collection the last element returned by the
iterator. This method can be called only once per call of the next() function. Note, that this operation is
optional.
(Listing 8-6)
This code snippet executes and prints elements of the array list before and after removing elements,
like it is snown on the screenshot below:
Iurii Mednikov is a certified Java programmer and a certified Linux expert. He studied his bachelor
degree in Computing in the Prague College (Czech Republic) and he is now running his own software
consulting business in Prague. Iurii Mednikov writes regularly for his own blog, for peer-reviewed
academic journals, as well for platforms as Medium, Dzone and Dev.to. He is a member of the
Association for Computing Machinery (ACM). In his free time, Iurii loves to run, to practice karate
(shotokan) and to learn [human] languages.