Chap 15 Collections Part 2
Chap 15 Collections Part 2
15
Chapter Topics
• An overview of the Java Collections Framework in the java.util
package: core interfaces and their implementations
• Understanding the functionality provided by the Collection<E>
interface, and its role in the Java Collections Framework
• How to create and use lists, and how their functionality is defined
by the List<E> interface and implemented by the classes Array-
List<E>, Vector<E>, and LinkedList<E>
• How to create and use sets, and how their functionality is defined
by the Set<E> interface and implemented by the classes HashSet<E>
and LinkedHashSet<E>
• How to create and use sorted and navigable sets, and how their
functionality is defined by the SortedSet<E> and NavigableSet<E>
interfaces, respectively, and implemented by the class TreeSet<E>
• How to create and use queues and deques, and how their func-
tionality is defined by the Queue<E> and Deque<E> interfaces, and
implemented by the classes PriorityQueue<E> and ArrayDeque<E>,
respectively
• How to create and use maps, and how their functionality is
defined by the Map<K, V> interface and implemented by the classes
HashMap<K, V>, LinkedHashMap<K, V>, and Hashtable<K, V>
• How to create and use sorted and navigable maps, and how their
functionality is defined by the SortedMap<K, V> and NavigableMap<K,
V> interfaces, respectively, and implemented by the class TreeMap<K, V>
• Using the utility methods found in the Collections and Arrays
classes, with emphasis on sorting and searching in lists and arrays
781
782 CHAPTER 15: COLLECTIONS: PART II
[5.3] Sort collections and arrays using Comparator and §15.5, p. 810
Comparable interfaces §15.10, p. 845
❍ Sorting collections and arrays is covered in this chapter. §15.11, p. 858
❍ For Comparable and Comparator interfaces, see §14.4, p. 761,
§15.12, p. 865
and §14.5, p. 769, respectively.
15.1: THE JAVA COLLECTIONS FRAMEWORK 783
The topics covered in this chapter can be divided into two main parts:
• In-depth coverage of collections, deques, and maps (§15.1 to §15.10)
• Sorting and searching in collections and arrays using the utility methods from
the Collections and the Arrays classes (§15.11 and §15.12)
Some topics related to collections and maps are covered elsewhere in the book:
• The ArrayList<E> class is covered in Chapter 12, p. 643.
• Using streams with collections is covered in Chapter 16, p. 879.
• Thread-safe or concurrent collections and maps are covered in Chapter 22,
p. 1365.
• Static methods from the utility class Arrays are used in many examples
throughout the book.
Core Interfaces
The generic Collection<E> interface is a generalized interface for maintaining col-
lections, and is the root of the interface inheritance hierarchy for collections shown
in Figure 15.1a. These generic subinterfaces are summarized in Table 15.1. The
Collection<E> interface extends the Iterable<E> interface that specifies an iterator to
sequentially access the elements of an Iterable object (p. 791).
784 CHAPTER 15: COLLECTIONS: PART II
«interface»
java.lang.Iterable<E>
«interface»
Collection<E>
«interface» «interface»
NavigableSet<E> NavigableMap<K,V>
(a) (b)
Table 15.1 Core Interfaces and Concrete Classes in the Collections Framework
Table 15.1 Core Interfaces and Concrete Classes in the Collections Framework (Continued)
The elements in a Set<E> must be unique—that is, no two elements in the set can be
equal. The order of elements in a List<E> is positional, and individual elements
can be accessed randomly according to their position in the list.
Queues and deques, represented respectively by the Queue<E> and the Deque<E> inter-
faces, define collections whose elements can be processed according to various
policies.
As can be seen in Figure 15.1b, the Map<K, V> interface does not extend the
Collection<E> interface because conceptually, a map is not a collection. A map does
not contain elements. It contains mappings (also called entries) from a set of key
objects to a set of value objects. A key can, at most, be associated with one value—
that is, it must be unique. As the name implies, the SortedMap<K, V> interface
extends the Map<K, V> interface to maintain its mappings sorted in key order. It is
superseded by the NavigableMap<K, V> interface which should be the preferred
choice in new code.
Implementations
The java.util package provides implementations of a selection of well-known
abstract data types, based on the core interfaces. Figures 15.2 and 15.3 show the
inheritance relationship between the core interfaces and the corresponding imple-
mentations. None of the concrete implementations inherit directly from the
Collection<E> interface. The abstract classes that provide the basis on which con-
crete classes are implemented are not shown in Figures 15.2 and 15.3.
786
Figure 15.2
«interface»
java.lang.Iterable<E>
«interface» «interface»
Iterator<E> Collection<E>
Interfaces
«interface» «interface» «interface»
Deque<E> Queue<E> SortedSet<E>
«interface»
NavigableSet<E>
The Core Collection Interfaces and Their Implementations
Concrete
classes
LinkedList<E> LinkedHashSet<E>
CHAPTER 15: COLLECTIONS: PART II
15.1: THE JAVA COLLECTIONS FRAMEWORK 787
«interface» «interface»
Map.Entry<K,V> Map<K,V>
«interface»
SortedMap<K,V>
Interfaces
«interface»
NavigableMap<K,V>
Concrete
HashMap<K,V> TreeMap<K,V> Hashtable<K,V>
classes
LinkedHashMap<K,V>
Methods
the imple- Data
mentations structures on
Concrete expect the which
collections Dup- Ordered/ elements implementation
and maps Interface licates Sorted to provide is based
ArrayList<E> List<E> Allowed Insertion equals() Resizable array
order
LinkedList<E> List<E>, Allowed Insertion/ equals() Linked list
Deque<E> priority/
deque
order
Vector<E> List<E> Allowed Insertion equals() Resizable array
order
HashSet<E> Set<E> Unique No order equals(), Hash table
elements hashCode()
Methods
the imple- Data
mentations structures on
Concrete expect the which
collections Dup- Ordered/ elements implementation
and maps Interface licates Sorted to provide is based
Hash- Map<K,V> Unique No order equals(), Hash table
table<K,V> keys (no hashCode()
null key
and no
null
values)
TreeMap<K,V> Navigable- Unique Sorted in equals(), Balanced tree
Map<K,V> keys (no key order hashCode(),
null key) compareTo()
From Table 15.2, we also see that elements in a LinkedHashSet<E> are ordered, in a
TreeSet<E> they are sorted, and in a HashSet<E> they have no order (i.e., they are
unordered). Sorting implies ordering the elements in a collection according to some
ranking criteria, usually based on the values of the elements. However, elements in
an ArrayList<E> are maintained in the order they are inserted in the list, known as
the insertion order. The elements in such a list are thus ordered, but they are not
sorted, as it is not the values of the elements that determine their ranking in the list
but their position in the list. Thus ordering does not necessarily imply sorting. In a
HashSet<E>, the elements are unordered. No ranking of elements is implied in such
a set. Whether a collection is sorted, ordered, or unordered also has implications
when iterating over the collection (p. 791).
In order for sorting and searching to work properly, the collections and maps in
Table 15.2 also require that their elements implement the appropriate methods for
object comparison. Comparing objects is covered in Chapter 14, p. 741. Sorting and
searching in collections is covered in §15.11, p. 856.
Methods defined for collections that specify functional interfaces as a parameter
and require lambda expressions as arguments are covered in Chapter 13, p. 673.
Using streams with collections is covered in Chapter 16, p. 879.
The collection and map implementations discussed in this chapter, except for Vec-
tor<E> and Hashtable<K, V>, are not thread-safe—that is, their integrity can be jeopar-
dized by concurrent access. Concurrent collections and maps are covered in
Chapter 22, p. 1365.
The Java Collections Framework provides a plethora of collections and maps for
use in single-threaded and concurrent applications, which should eliminate the
need to implement one from scratch.
790 CHAPTER 15: COLLECTIONS: PART II
15.2 Collections
The Collection<E> interface specifies the contract that all collections should imple-
ment. Some of the operations in the interface are optional, meaning that a collection
may choose to provide a stub implementation of such an operation that throws an
UnsupportedOperationException when invoked—for example, when a collection is
unmodifiable. The implementations of collections from the java.util package sup-
port all the optional operations in the Collection<E> interface (see Figure 15.2 and
Table 15.2).
Many of the methods return a boolean value to indicate whether the collection was
modified as a result of the operation.
Basic Operations
The basic operations are used to query a collection about its contents and allow ele-
ments to be added to and removed from a collection. Many examples in this chap-
ter make use of these operations.
int size()
boolean isEmpty()
boolean contains(Object element)
boolean add(E element) Optional
boolean remove(Object element) Optional
The size() method returns the number of elements in the collection, and the
isEmpty() method determines whether there are any elements in the collection.
The contains() method checks for membership of the argument object in the
collection, using object value equality.
The add() and remove() methods return true if the collection was modified as a
result of the operation.
By returning the value false, the add() method indicates that the collection
excludes duplicates, and that the collection already contains an object equal to
the argument object.
Note that we can only add an object of a specific type (E). However, a collection
allows us to determine whether it has an element equal to an arbitrary object,
or remove an element that is equal to an arbitrary object.
Bulk Operations
These operations are performed on a collection as a single entity. See §15.4, p. 804,
for an example.
15.2: COLLECTIONS 791
boolean containsAll(Collection<?> c)
boolean addAll(Collection<? extends E> c) Optional
boolean removeAll(Collection<?> c) Optional
boolean retainAll(Collection<?> c) Optional
void clear() Optional
These bulk operations can be used to perform the equivalent of set logic on
arbitrary collections (i.e., also lists and not just sets). The containsAll() method
returns true if all elements of the specified collection are also contained in the
current collection.
The addAll(), removeAll(), and retainAll() methods are destructive in the sense
that the collection on which they are invoked can be modified. The operations
performed by these methods are visualized by Venn diagrams in Figure 15.4.
The addAll() method requires that the element type of the other collection is
the same as, or a subtype of, the element type of the current collection. The
removeAll() and retainAll() operations can be performed with collections of
any type.
The clear() method removes all elements in the collection so that the collection
is empty.
a b a a a
The Iterable<E> interface defines the methods shown below. The enhanced for(:)
loop can be used to iterate over objects that implement this interface. The Collection<E>
interface extends the Iterable<E> interface, making all collections iterable.
Example 15.1 illustrates various ways to iterate over a collection that is created at
(1). The example uses an ArrayList<E> for a collection of names that are Strings.
First an empty ArrayList is created and names are added to the collection. The
operation performed on the collection prints each name in uppercase.
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
default void
forEachRemaining(Consumer<? super E> action) From Iterator<E> interface.
The specified action is performed on each of the remaining elements in the col-
lection associated with an iterator, unless an exception is thrown during its
execution. The functional interface Consumer<T> is covered in §13.7, p. 709.
After obtaining the iterator for a collection, the methods provided by the Itera-
tor<E> interface can be used systematically to iterate over the elements of the
underlying collection one at a time.
In Example 15.1, an explicit iterator is obtained at (2) and used in the loop at (3) to
iterate over all elements in the underlying collection. A call to the hasNext() method
in the condition of the loop ensures that there is still an element to retrieve from the
collection. At (4) the current element is retrieved by the iterator from the collection
by calling the next() method. No casts are necessary at (4), as the compiler guaran-
tees that the iterator will return a String object from the underlying collection.
Iterator<String> iterator = collectionOfNames.iterator(); // (2)
while (iterator.hasNext()) { // (3)
String name = iterator.next(); // (4)
System.out.print(name.toUpperCase() + " ");
}
Note that the methods are invoked on the iterator, not the underlying collection. In
Example 15.1, the hasNext() method is called before the next() method to ensure that
there is still an element remaining to be processed; otherwise, a java.util.NoSuch-
ElementException can be raised at runtime. Using an explicit iterator puts this
responsibility on the user code.
In Example 15.1, we used an iterator in a while loop at (3) for iterating over the col-
lection. It is quite common to use an iterator in a for(;;) loop for this purpose,
where the iterator is obtained in the initialization part, and the increment part is
left empty:
for (Iterator<String> iterator = collectionOfNames.iterator();
iterator.hasNext(); /* Empty increment. */) {
String name = iterator.next();
System.out.print(name.toUpperCase() + " ");
}
The majority of the iterators provided in the java.util package are said to be fail-
fast. When an iterator has already been obtained, structurally modifying the under-
lying collection by other means will invalidate the iterator. Subsequent use of
15.2: COLLECTIONS 795
In Example 15.1, the toString() method of the collection class is used implicitly in
print statements to generate a text representation for the collection. The collection
classes override the Object.toString() method to provide a text representation of
their contents. The standard text representation generated by the toString()
method for a collection is
[element1, element2, ..., elementn]
where each elementi, where 1 <= i <= n, is the text representation generated by the
toString() method of the individual elements in the collection.
interface extends the Iterable<E> interface, and therefore, all collections that imple-
ment the Collection<E> interface can be iterated over using the for(:) loop. A col-
lection that implements the Collection<E> interface and thereby the Iterable<E>
interface has the element type E. This element type E must be assignable to the type
of the variable in the for(:) loop. The variable is assigned the reference value of a
new element in the collection each time the body of the loop is executed.
The semantics of the for(:) loop also apply when iterating over a collection. In par-
ticular, any structural change to the collection (adding or removing elements) in
the for(:) loop will result in a ConcurrentModificationException—in other words,
the underlying collection is structurally immutable.
Example 15.1 illustrates using a for(:) loop to iterate over a collection. The collection
of names is iterated over in the for(:) loop at (7), printing each name in uppercase.
for (String name : collectionOfNames) { // (7)
System.out.print(name.toUpperCase() + " ");
}
Behind the scenes, however, an appropriate iterator is used to iterate over the col-
lection, but the for(:) loop simplifies iterating over a collection in the source code.
Note that if the collection is ordered or sorted, the iterator will iterate over the col-
lection in the ordering used to maintain the elements in the collection. For example,
in the case of an ArrayList, the iterator will yield the elements in the same order as
the insertion order. In the case of a TreeSet<E>, the iterator will yield the elements
in the sort order used to maintain the elements in the set. If the collection is unor-
dered, the order in which the iterator will yield the elements is not predictable.
Thus we cannot be sure in which order the elements of a HashSet will be iterated by
the for(:) loop.
Filtering
We illustrate here two ways of filtering a collection—in this case, removing ele-
ments from the collection that satisfy certain criteria.
15.2: COLLECTIONS 797
Best practices dictate that the three methods of the iterator should be used in lock-
step inside a loop, as shown in Example 15.1. In particular, the next() method must
be called before the remove() method for each element in the collection; otherwise, a
java.lang.IllegalStateException or an UnsupportedOperationException is raised at
runtime, depending on whether the collection provides this optional operation or
not. As noted earlier, using an explicit iterator places the responsibility of book-
keeping on the user code.
In Example 15.1, the call to the removeIf() method at (13) takes a Predicate to test
whether a name starts with the letter A, resulting in those names satisfying the
predicate to be removed from the collection.
// [Alice, Dick, Harriet]
collectionOfNames.removeIf(name -> name.startsWith("A")); // (13)
// [Dick, Harriet]
Ample examples of filtering can also be found in §13.3, p. 691, and §16.5, p. 910.
798 CHAPTER 15: COLLECTIONS: PART II
Streams
The two methods of the Collections interface shown below allow a collection to be
used as the source of a stream. A stream makes the elements of a collection available
as a sequence of elements on which sequential and parallel aggregate operations can be
performed.
default Stream<E> stream()
default Stream<E> parallelStream()
Return a sequential stream or a possibly parallel stream with this collection as
its source, respectively.
Example 15.1 illustrates at (14) how a stream can be used to iterate over the collec-
tion of names in order to print them in uppercase.
collectionOfNames.stream() // (14)
.forEach(name -> System.out.print(name.toUpperCase() + " "));
Details are revealed in §16.4, p. 897, which is devoted entirely to streams, provid-
ing many examples of using streams on collections.
Array Operations
The following operations convert collections to arrays. See also array operations on
array lists (§12.6, p. 658).
Object[] toArray()
<T> T[] toArray(T[] a)
The first toArray() method returns an array of type Object filled with all the ele-
ments of the collection. The second method is a generic method that stores the
elements of a collection in an array of type T. The default method uses the gen-
erator function to allocate the returned array.
If the given array is big enough, the elements are stored in this array. If there is
room to spare in the array; that is, the length of the array is greater than the
number of elements in the collection—the spare room is filled with null values
before the array is returned. If the array is too small, a new array of type T and
appropriate size is created. If T is not a supertype of the runtime type of every
element in the collection, an ArrayStoreException is thrown.
default <T> T[] toArray(IntFunction<T[]> generator)
Allows creation of an array of a particular runtime type given by the parame-
terization of the type parameter T[], using the specified generator function
(§13.8, p. 717) to allocate the array of the desired type and the specified length.
The default implementation calls the generator function with 0 and then
passes the resulting array of length 0 to the toArray(T[]) generic method.
Example 15.2 illustrates converting a set to an array. At (1), the call to the non-
generic toArray() method returns an array of type Object. Since an array of type
15.2: COLLECTIONS 799
Object is not a subtype of an array of type String, the compiler reports an error
at (2).
At (3), the call to the generic toArray() method returns an array of size 3 and type
Object[], but element type String, when the method was passed a zero-length
array of type Object. In other words, the method created a suitable-size array of
type Object, since the array passed in the argument was too small. This array was
filled with the elements of the set, which are strings. Although the array returned
is of type Object, the objects stored in it are of type String. The output from the pro-
gram confirms these observations.
At (4), the call to the generic toArray() method returns an array of size 3 and type
String[], having element type String, when the method was passed a zero-length
array of type String. Now the method creates a new suitable-size array of type
String and fills it with the elements of the set, which are strings. The output from
the program shows that the array passed in the argument is not the same as the
array returned by the method call.
At (5), the call to the generic toArray() method returns the same array it was passed
in the argument, since it is of appropriate size and type. In other words, the array
passed in the argument is filled with the elements of the list, and returned. This is
corroborated by the output from the program.
At (6), the actual type parameter is Integer. The generic toArray() method throws
an ArrayStoreException because String objects in the set denoted by strSet cannot
be stored in an array of type Integer.
Lastly, Example 15.2 illustrates converting a set to an array using the default
toArray(IntFunction<T[]>) generic method, which is a convenience method that
actually leverages the toArray(T[]) generic method.
IntFunction<String[]> createStrArray = nn -> new String[nn]; // (7)
String[] strArray5 = strSet.toArray(createStrArray); // (8a)
String[] strArray6 = strSet.toArray(String[]::new); // (8b)
String[] strArray7 = strSet.toArray(createStrArray.apply(0)); // (8c)
The lambda expression at (7) creates an array of length nn and of type String[]. This
lambda expression is passed to the toArray(IntFunction<String[]>) method at (8a).
Equivalently, we can pass a method reference to the method, as shown at (8b). Of
course, the lambda expression at (7) only creates an array when the apply() method
of the IntFunction<T[]> interface is called with a value for the length of the array.
The default behavior of the toArray(IntFunction<T[]>) generic method is illustrated
at (8c): The apply() method of the IntFunction<String[]> interface is explicitly called
with the value 0 to create a zero-length String array which is then passed as a
parameter to the toArray(String[]) method.
import java.util.Arrays;
import java.util.Collection;
800 CHAPTER 15: COLLECTIONS: PART II
import java.util.HashSet;
import java.util.function.IntFunction;
15.3 Lists
Lists are collections that maintain their elements in order and can contain dupli-
cates. The elements in a list are ordered. Each element, therefore, has a position in
the list. A zero-based index can be used to access the element at the position des-
ignated by the index value. The position of an element can change as elements are
inserted or deleted from the list—that is, as the list is changed structurally.
The ListIterator<E> interface is a bidirectional iterator for lists. It extends the Iter-
ator<E> interface and allows the list to be iterated over in either direction. When
iterating over lists, it can be helpful to imagine a cursor moving forward or back-
ward between the elements when calls are made to the next() and the previous()
methods, respectively. The element that the cursor passes over is returned. When
the remove() method is called, the element last passed over is removed from the list.
802 CHAPTER 15: COLLECTIONS: PART II
Finding the number of digits that are correctly placed is achieved by using two list
iterators at (4), which allow digits in the same position in the guess and the
secretSolution lists to be compared.
15.3: LISTS 803
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
/* Initialize the solution list. This program has a fixed solution: 53272 */
List<Integer> secretSolution = new ArrayList<>(); // (1)
Collections.addAll(secretSolution, 5, 3, 2, 7, 2);
15.4 Sets
The Java Collections Framework provides the Set<E> interface to model the math-
ematical set abstraction.
a.containsAll(b) b ⊆ a (subset)
a.addAll(b) a = a ∪ b (union)
a.removeAll(b) a = a – b (difference)
a.retainAll(b) a = a ∩ b (intersection)
a.clear() a = ∅ (empty set)
The code below shows that a set created by the Set.of() method cannot be modi-
fied. The set returned is also not an instance of the class HashSet.
Set<String> set = Set.of("Tom", "Dick", "Harriet");
// set.add("Harry"); // UnsupportedOperationException
// set.remove(2); // UnsupportedOperationException
System.out.println(set); // [Harriet, Tom, Dick]
System.out.println(set instanceof HashSet); // false
The code below shows how we can make a copy of a collection, in this case, a set.
The copyOf() method creates a copy of the set passed as an argument at (1). The set
created is unmodifiable analogous to the sets created with the Set.of() methods.
The code also shows that modifying the original set does not reflect in the copy of
the set.
Set<String> fab4 = new HashSet<>();
fab4.add("John"); fab4.add("Paul"); fab4.add("George"); fab4.add("Ringo");
System.out.println(fab4); // [George, John, Ringo, Paul]
Set<String> fabAlways = Set.copyOf(fab4); // (1)
fab4.remove("John"); fab4.remove("George"); // Modify original set
System.out.println(fab4); // [Ringo, Paul]
System.out.println(fabAlways); // [John, Paul, Ringo, George]
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
public class IterationHashSetAndLinkedHashSet {
public static void main(String[] args) {
Set<String> set1 = new HashSet<>(); // (1)
set1.add("breakfast"); set1.add("lunch"); set1.add("dinner");
System.out.println("Serving meals from a HashSet (order can vary):");
for (String meal : set1) {
System.out.println(meal);
}
Set<String> set2 = new LinkedHashSet<>(); // (2)
set2.add("breakfast"); set2.add("lunch"); set2.add("dinner");
System.out.println("Serving meals from a LinkedHashSet" +
" (always insertion order):");
808 CHAPTER 15: COLLECTIONS: PART II
Note that the retainAll() operation is destructive. The code at (4) does not affect
the encountered and the characters sets. If the size of the common subset is zero, the
sets are disjunct; otherwise, the relationship must be narrowed down. The subset
and superset relations are determined at (5), using the containsAll() method.
// Determine superset and subset relations. (5)
boolean isSubset = encountered.containsAll(characters);
boolean isSuperset = characters.containsAll(encountered);
The sets are equivalent if both of the previous relations are true. If the relations are
both false—that is, no subset or superset relationship exists, the sets only have the
15.4: SETS 809
subset computed at (4) in common. The encountered set is updated with the con-
tents of the characters set to accumulate all characters encountered so far. The
addAll() method is used for this purpose at (6):
encountered.addAll(characters); // (6)
As we can see from the output, the program prints the contents of the sets in the
standard text representation for collections.
import java.util.HashSet;
import java.util.Set;
if (areDisjunct) {
System.out.println(characters + " and " + encountered + " are disjunct.");
} else {
// Determine superset and subset relations. (5)
boolean isSubset = encountered.containsAll(characters);
boolean isSuperset = characters.containsAll(encountered);
if (isSubset && isSuperset)
System.out.println(characters + " is equivalent to " + encountered);
else if (isSubset)
System.out.println(characters + " is a subset of " + encountered);
else if (isSuperset)
System.out.println(characters + " is a superset of " + encountered);
else
System.out.println(characters + " and " + encountered + " have " +
commonSubset + " in common.");
}
}
}
}
// Range-view operations
SortedSet<E> headSet(<E> toElement)
SortedSet<E> tailSet(<E> fromElement)
SortedSet<E> subSet(<E> fromElement, <E> toElement)
The headSet() method returns a view of a portion of this sorted set, whose
elements are strictly less than the specified element. Similarly, the tailSet()
method returns a view of the portion of this sorted set, whose elements are
greater than or equal to the specified element. The subSet() method returns a
view of the portion of this sorted set, whose elements range from fromElement,
inclusive, to toElement, exclusive (also called half-open interval). It throws an
IllegalArgumentException if the fromElement is greater than the toElement.
15.5: SORTED SETS AND NAVIGABLE SETS 811
Note that the views present the elements sorted in the same order as the under-
lying sorted set. Also, changes made through views are reflected in the under-
lying sorted set, and vice versa.
// Comparator access
Comparator<? super E> comparator()
Returns the comparator associated with this sorted set, or null if it uses the nat-
ural ordering of its elements. This comparator, if defined, is used by default
when a sorted set is constructed, and used when copying elements into new
sorted sets.
The NavigableSet<E> interface replaces the SortedSet<E> interface and is the pre-
ferred choice when a sorted set is required. In addition to the methods of the Sorted-
Set<E> interface, the NavigableSet<E> interface adds the following new methods:
// First-last elements
E pollFirst()
E pollLast()
The pollFirst() method removes and returns the first element and the poll-
Last() method removes and returns the last element currently in this navigable
set. The element is determined according to some policy employed by the
set—for example, queue policy. Both return null if the sorted set is empty.
// Range-view operations
NavigableSet<E> headSet(E toElement, boolean inclusive)
NavigableSet<E> tailSet(E fromElement, boolean inclusive)
NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
E toElement, boolean toInclusive)
These operations are analogous to the ones in the SortedSet<E> interface
(p. 810), returning different views of the underlying navigable set, depending
on the bound elements. However, the bound elements can be excluded or
included by the operation, depending on the value of the boolean argument
inclusive.
812 CHAPTER 15: COLLECTIONS: PART II
// Closest-matches
E ceiling(E e)
E floor(E e)
E higher(E e)
E lower(E e)
The method ceiling() returns the least element in the navigable set greater
than or equal to argument e. The method floor() returns the greatest element
in the navigable set less than or equal to argument e. The method higher()
returns the least element in the navigable set strictly greater than argument e.
The method lower() returns the greatest element in the navigable set strictly
less than argument e. All methods return null if the required element is not
found.
// Reverse order
Iterator<E> descendingIterator()
NavigableSet<E> descendingSet()
The first method returns a reverse-order iterator for the navigable set. The sec-
ond method returns a reverse-order view of the elements in the navigable set.
TreeSet(SortedSet<E> set)
A constructor that creates a new set containing the same elements as the spec-
ified sorted set, with the same ordering.
import java.util.Collections;
import java.util.NavigableSet;
import java.util.TreeSet;
import static java.lang.System.out;
out.println("\nSubset-views:"); // (3)
out.println(strSetA.headSet("ballroom", true)); // [Java, Strictly, ballroom]
out.println(strSetA.headSet("ballroom", false)); // [Java, Strictly]
out.println(strSetA.tailSet("Strictly", true)); // [Strictly, ballroom,
// dancing]
out.println(strSetA.tailSet("Strictly", false)); // [ballroom, dancing]
out.println(strSetA.subSet("A", false, "Z", false )); // [Java, Strictly]
out.println(strSetA.subSet("a", false, "z", false )); // [ballroom, dancing]
out.println("\nClosest-matches:"); // (4)
out.println(strSetA.ceiling("ball")); // ballroom
out.println(strSetA.floor("ball")); // Strictly
out.println(strSetA.higher("ballroom")); // dancing
out.println(strSetA.lower("ballroom")); // Strictly
814 CHAPTER 15: COLLECTIONS: PART II
out.println("\nFirst-last elements:");
out.println(strSetA.pollFirst()); // Java
out.println(strSetA.pollLast()); // dancing
Subset-views:
[Java, Strictly, ballroom]
[Java, Strictly]
[Strictly, ballroom, dancing]
[ballroom, dancing]
[Java, Strictly]
[ballroom, dancing]
Closest-matches:
ballroom
Strictly
dancing
Strictly
Reverse order:
[dancing, ballroom, Strictly, Java]
First-last elements:
Java
dancing
15.6 Queues
In this section we look at the different types of queues provided by the Java Collec-
tions Framework.
elements in FIFO (first in, first out) ordering, but other orderings are also quite
common: LIFO (last in, first out) ordering (also called stacks) and priority ordering
(also called priority queues). The order in which elements of a queue can be
retrieved for processing is dictated either by the natural ordering of the elements
or by a comparator. A queue can be unbounded or capacity-restricted, depending on
its implementation.
The Queue<E> interface extends the Collection<E> interface with the following meth-
ods. A summary of these methods is presented in Table 15.4.
// Insert
boolean add(E element)
boolean offer(E element)
Both methods insert the specified element in the queue. The return value indi-
cates the success or failure of the operation. The add() method inherited from
the Collection<E> interface throws an IllegalStateException if the queue is full,
but the offer() method does not.
// Remove
E remove()
E poll()
Both methods retrieve the head element and remove it from the queue. If the
queue is empty, the remove() method throws a NoSuchElementException, but the
poll() method returns the null value.
// Examine
E element()
E peek()
Both methods retrieve the head element, but do not remove it from the queue.
If the queue is empty, the element() method throws a NoSuchElementException,
but the peek() method returns the null value.
Insert at the tail add(e) can throw offer(e) returns true or false
IllegalArgumentException
Remove from the head remove() can throw poll() returns head element
NoSuchElementException or null
Examine element at the head element() can throw peek() returns head element
NoSuchElementException or null
should be considered, and not the LinkedList<E> class. (The LinkedList<E> class is
also eclipsed by the introduction of the ArrayDeque<E> class when it comes to imple-
menting deques, as we will see shortly.)
As the name suggests, the PriorityQueue<E> class is the obvious implementation for
a queue with priority ordering. The implementation is based on a priority heap, a
tree-like structure that yields an element at the head of the queue according to the
priority ordering, which is defined either by the natural ordering of its elements or
by a comparator. In the case of several elements having the same priority, one of
them is chosen arbitrarily.
Elements of a PriorityQueue<E> are not sorted. The queue only guarantees that ele-
ments can be removed in priority order, and any iteration using an iterator does not
guarantee to abide by the priority order.
The PriorityQueue<E> class provides the following constructors:
PriorityQueue()
PriorityQueue(int initialCapacity)
The default constructor creates a new, empty PriorityQueue with default initial
capacity and natural ordering. The second constructor creates a new, empty
PriorityQueue with the specified initial capacity and natural ordering.
Example 15.7 illustrates using priority queues. The example shows how priority
queues maintain objects of the Task class. The natural ordering of the objects in this
class is based on the task number (Integer). This natural ordering will result in the
priority queue yielding its elements in ascending order of the task numbers—that
is, tasks with smaller task numbers will have higher priority.
In Example 15.7, the main() method in the class TaskExecutor creates an array with
some tasks at (1). The method essentially creates empty priority queues with
15.6: QUEUES 817
different priority orderings, at (2) through (7), and calls the testPQ() method at (8)
to execute tasks passed in the array using the supplied priority queue.
private static void testPQ(Task[] taskArray, PriorityQueue<Task> pq) { // (8)
...
}
The testPQ() method at (8) loads the queue at (9) from the array of tasks. It calls the
offer() method to insert a task in the priority queue. The method then calls the peek()
method at (10) to examine the task at the head of the queue. The tasks are executed
by removing them one by one at (11) by calling the poll() method. The output shows
the order in which the tasks are executed, depending on the priority ordering.
The priority queue pq1 at (2) has its priority ordering defined by the natural order-
ing of the tasks.
PriorityQueue<Task> pq1 = new PriorityQueue<>(); // (2)
does not reflect the tasks in priority order. It just shows what tasks are in the queue.
The text representation of the queue is generated by the print method running an
iterator over the queue. The iterator is under no obligation to take the priority
order into consideration. The output also shows that the task with the highest pri-
ority (i.e., the smallest task number) is at the head of the queue:
Task at the head: 100@breakfast
The call to the poll() method in the while loop at (11) removes tasks in priority
order, as verified by the output:
Doing tasks: 100@breakfast 200@tea 200@lunch 300@dinner
Since two of the tasks have the same priority, the queue selects which one should
be chosen first. The queue is empty when the peek() method returns null.
The priority queue pq2 at (3) has its priority ordering defined by the reverse natural
ordering returned by the static method reverseOrder() of the Comparator<E> interface:
PriorityQueue<Task> pq2 = new PriorityQueue<>(Comparator.reverseOrder());
Both priority queues pq3 and pq4 use reversed ordering based on the task name. This
ordering is defined for pq3 and pq4 by a lambda expression at (4) and by methods
of the Comparator<E> interface at (7), respectively. The latter implementation is obvi-
ously to be preferred:
PriorityQueue<Task> pq4 = new PriorityQueue<>( // (5)
Comparator.comparing(Task::getTaskName).reversed()
);
The static method comparing() extracts a comparator that uses the getName() method
of the Task class, and the default method reversed() reverses this comparator.
818 CHAPTER 15: COLLECTIONS: PART II
The priority queues pq5 and pq6 use total ordering based on multiple fields of the
Task class: on the task number, followed by the task name. This ordering is defined
for pq5 and pq6 by a lambda expression at (6) and by methods of the Comparator<E>
interface at (7), respectively. The latter implementation first extracts a comparator
based on the task number, and chains it with one based on the task name:
PriorityQueue<Task> pq6 = new PriorityQueue<>( // (7)
Comparator.comparing(Task::getTaskNumber)
.thenComparing(Task::getTaskName)
);
We leave it to the reader to verify that the output conforms to the priority ordering
of priority queues at (3) through (7).
@Override
public boolean equals(Object obj) {// Equality based on the task number.
return (this == obj)
|| (obj instanceof Task other
&& this.taskNumber.equals(other.taskNumber));
}
@Override
public int compareTo(Task task2) { // Natural ordering based on the task number.
return this.taskNumber.compareTo(task2.taskNumber);
}
@Override
public int hashCode() { // Hash code based on the task number.
return this.taskNumber.hashCode();
}
@Override
public String toString() { return taskNumber + "@" + taskName; }
import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;
import static java.lang.System.out;
15.6: QUEUES 819
// Runs tasks.
private static void testPQ(Task[] taskArray, PriorityQueue<Task> pq) { // (8)
// Load the tasks: (9)
for (Task task : taskArray)
pq.offer(task);
out.println("Queue before executing tasks: " + pq);
15.7 Deques
In this section we look at deques—that is, linear collections that allow processing of
elements from both ends.
// Insert
boolean offerFirst(E element)
boolean offerLast(E element) Queue equivalent: offer()
void addFirst(E element)
void addLast(E element) Queue equivalent: add()
void push(E element) Synonym: addFirst()
Insert the specified element in the deque. They all throw a NullPointer-
Exception if the specified element is null. The addFirst() and addLast() methods
throw an IllegalStateException if the element cannot be added, but the offer-
First() and offerLast() methods do not.
822 CHAPTER 15: COLLECTIONS: PART II
// Remove
E removeFirst() Queue equivalent: remove()
E removeLast()
E pollFirst() Queue equivalent: poll()
E pollLast()
E pop() Synonym: removeFirst()
boolean removeFirstOccurence(Object obj)
boolean removeLastOccurence(Object obj)
Remove an element from the deque. The removeFirst() and removeLast() meth-
ods throw a NoSuchElementException if the deque is empty. The pollFirst() and
pollLast() methods return null if the deque is empty.
// Examine
E getFirst() Queue equivalent: element()
E getLast()
E peekFirst() Queue equivalent: peek()
E peekLast()
Retrieve an element from the deque, but do not remove it from the deque. The
getFirst() and getLast() methods throw a NoSuchElementException if the deque
is empty. The peekFirst() and peekLast() methods return null if the deque is
empty.
// Misc.
Iterator<E> descendingIterator()
Returns an iterator to iterate over the deque in reverse order—that is, from the
tail to the head.
Table 15.5 summarizes the methods provided by the Deque<E> interface, showing
which operations can be performed at the head and which ones at the tail of the
deque. Each row indicates the runtime behavior of the methods in that row,
whether they throw an exception or not. Any method whose name starts with
either "offer", "poll", or "peek" does not throw an exception. Counterpart methods
inherited from the Queue<E> interface are marked by an asterisk (*) in the table.
Remove from the head Remove from the tail Runtime behavior on failure
pollFirst(), poll()* pollLast() Returns null if empty
Example 15.8 illustrates the methods for inserting, examining, and removing ele-
ments from a deque. The program output shows how each method affects the
deque when inserting, examining, and removing elements from either the head or
the tail of a deque.
import java.util.ArrayDeque;
import java.util.Deque;
// Insert elements:
deque.offerFirst("A (H)"); // Insert at the head
System.out.println("After inserting at the head: " + deque);
deque.offerLast("B (T)"); // Insert at the tail
System.out.println("After inserting at the tail: " + deque);
deque.push("C (H)"); // Insert at the head
System.out.println("After inserting at the head: " + deque);
deque.addFirst("D (H)"); // Insert at the head
System.out.println("After inserting at the head: " + deque);
deque.addLast("E (T)"); // Insert at the tail
System.out.println("After inserting at the tail: " + deque);
// Examine element:
System.out.println("Examine at the head: " + deque.getFirst());
System.out.println("Examine at the tail: " + deque.getLast());
System.out.println("Examine at the head: " + deque.peekFirst());
System.out.println("Examine at the tail: " + deque.peekLast());
// Remove elements:
deque.removeFirst(); // Remove from the head
System.out.println("After removing from the head: " + deque);
deque.removeLast(); // Remove from the tail
System.out.println("After removing from the tail: " + deque);
deque.pollFirst(); // Remove from the head
System.out.println("After removing from the head: " + deque);
deque.pollLast(); // Remove from the tail
System.out.println("After removing from the tail: " + deque);
deque.pop(); // Remove from the head
System.out.println("After removing from the head: " + deque);
}
}
Example 15.9 illustrates using an ArrayDeque both as a LIFO stack and as a FIFO
queue. Elements from an array are pushed onto the stack at (3), and then popped
from the stack at (5). The call to the isEmpty() method in the while loop at (4) deter-
mines whether the stack is empty. The output shows that the elements were
popped in the reverse order to the order in which they were inserted—that is, LIFO
ordering.
Similarly, elements from an array are inserted at the tail of a FIFO queue at (8), and
then removed from the head of the FIFO queue at (10). The call to the isEmpty()
method in the while loop at (4) determines whether the FIFO queue is empty. The
output shows that the elements were removed in the same order they were
inserted—that is, FIFO ordering.
Note that in Example 15.9 the stack grows at the head of the deque, but the FIFO
queue grows at the tail of the deque.
import java.util.ArrayDeque;
import java.util.Arrays;
Review Questions
Which code, when inserted at (1), will result in the following output from the pro-
gram:
Before: [Apple, Orange, Apple]
After: [Orange]
addRange(set1, 1);
ArrayList<Integer> list1 = new ArrayList<>();
addRange(list1, 2);
TreeSet<Integer> set2 = new TreeSet<>();
addRange(set2, 3);
ArrayDeque<Integer> deque = new ArrayDeque<>();
addRange(deque, 5);
set1.removeAll(list1);
list1.addAll(set2);
deque.addAll(list1);
set1.removeAll(deque);
System.out.println(set1);
}
static void addRange(Collection<Integer> col, int step) {
for (int i = step * 2; i <= 25; i += step) {
col.add(i);
}
}
}
15.7 Which of the following statements, when inserted independently at (1), will guar-
antee that the following program will print [1, 9]?
import java.util.*;
public class RightOne {
public static void main(String[] args) {
// (1) INSERT DECLARATION HERE
collection.add(1); collection.add(9); collection.add(1);
System.out.println(collection);
}
}
15.8 Which of the following statements, when inserted independently at (1), will result
in program output that does not include the word "shell"?
import static java.lang.System.out;
import java.util.*;
public class RQ400A400 {
public static void main(String[] args) {
NavigableSet<String> strSetA = new TreeSet<>();
Collections.addAll(strSetA, "sea", "shell", "soap", "swan");
// (1) INSERT STATEMENT HERE
}
}
15.8 Maps
A map defines mappings from keys to values. The <key, value> pair is called a map-
ping, also referred to as an entry. A map does not allow duplicate keys; in other
words, the keys are unique. Each key maps to one value at most, implementing
what is called a single-valued map. Thus there is a many-to-one relationship between
keys and values. For example, in a student-grade map, many students (keys) can be
awarded the same grade (value), but each student has only one grade. Replacing
15.8: MAPS 831
the value that is associated with a key results in the old entry being removed and
a new entry being inserted.
Both the keys and the values must be objects, with primitive values being wrapped
in their respective primitive wrapper objects when they are put in a map.
static <K,?V> Map<K,?V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4,
K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8,
K k9, V v9, K k10, V v10)
This method is overloaded, accepting any number of entries (k, v) from 0 to 10.
It returns an unmodifiable map containing the number of mappings specified.
It throws a NullPointerException if any key or value is null. It throws an
IllegalArgumentException if there are any duplicate keys.
The code below shows that a map created by the Map.of() method cannot be mod-
ified. We cannot change or remove any entry in the map. Note that the Map.of()
method allows up to 10 entries, and there is no variable arity Map.of() method. The
map returned is also not an instance of the HashMap class.
Map<Integer, String> jCourses = Map.of(
200, "Basic Java", 300, "Intermediate Java",
400, "Advanced Java", 500, "Kickass Java");
// jCourses.put(200, "Java Jive"); // UnsupportedOperationException
// jCourses.remove(500); // UnsupportedOperationException
System.out.println(jCourses instanceof HashMap); // false
System.out.println(jCourses);
// {200=Basic Java, 400=Advanced Java, 300=Intermediate Java, 500=Kickass Java}
834 CHAPTER 15: COLLECTIONS: PART II
The Map.of() method does not allow duplicate keys, and keys or values cannot be
null:
Map<Integer, String> coursesMap1
= Map.of(101, "Java 1.1", 101, "Java 17"); // IllegalArgumentException
Map<Integer, String> coursesMap2
= Map.of(101, "Java 1.1", 101, null); // NullPointerException
The following code creates unmodifiable map entries, where both the key and the
value are immutable:
// Map.Entry<Integer, String> e0 = Map.entry(100, null); // NullPointerException
Map.Entry<Integer, String> e1 = Map.entry(200, "Basic Java");
Map.Entry<Integer, String> e2 = Map.entry(300, "Intermediate Java");
Map.Entry<Integer, String> e3 = Map.entry(400, "Advanced Java");
Map.Entry<Integer, String> e4 = Map.entry(500, "Kickass Java");
The following code creates mutable course names that we will use in the next map.
StringBuilder mc1 = new StringBuilder("Basic Java");
StringBuilder mc2 = new StringBuilder("Intermediate Java");
StringBuilder mc3 = new StringBuilder("Advanced Java");
StringBuilder mc4 = new StringBuilder("Kickass Java");
The following code creates unmodifiable map entries, where the keys are immuta-
ble but the values are mutable:
Map.Entry<Integer, StringBuilder> me1 = Map.entry(200, mc1);
Map.Entry<Integer, StringBuilder> me2 = Map.entry(300, mc2);
Map.Entry<Integer, StringBuilder> me3 = Map.entry(400, mc3);
Map.Entry<Integer, StringBuilder> me4 = Map.entry(500, mc4);
System.out.println(unmodMapWithMutableCourses);
// {400=Advanced Java, 500=Smartass Java, 200=Basic Java, 300=Intermediate Java}
The code below shows how we can make a copy of a map. The Map.copyOf()
method creates a copy of the map passed as an argument at (1). The map created
is unmodifiable analogous to the maps created with the Map.of() or Map.ofEntries()
methods. The code also shows that modifying the original map does not reflect in
the copy of the map.
// Original map:
Map<Integer, StringBuilder> courseMap = new HashMap<>();
courseMap.put(200, mc1); courseMap.put(300, mc2);
courseMap.put(400, mc3); courseMap.put(500, mc4);
The code above prints the contents of the maps, showing that the copy of the map
was not modified:
Original: {500=Smartass Java, 300=Intermediate Java}
Copy: {300=Intermediate Java, 500=Smartass Java, 200=Basic Java,
400=Advanced Java}
Table 15.6 summarizes typical scenarios when using these advanced key-based
methods. Given the value associated with a key and the resultValue returned by the
remapping function if it is executed, the action performed and the value returned
by each method are shown for each scenario.
In the code below, six method calls are made corresponding to the column for each
method in Table 15.6. Each method call shows the value returned by the method as
a comment in the call statement.
The merge() method executes as follows:
• If the key is associated with the null value in the map, as shown at (3) and (4),
or has no entry, as shown at (5) and (6), the key is associated with the non-null
given value.
838 CHAPTER 15: COLLECTIONS: PART II
• If the result is non-null, then the key is associated with the result, as at (20).
// Before: {112=Norway, 999=UK, 190=null, 911=null}
etnMap.computeIfPresent(112, (key, oVal) -> null); // (19) null, removed
etnMap.computeIfPresent(999, (key, oVal) -> "Uganda"); // (20) Uganda, updated
etnMap.computeIfPresent(190, (key, oVal) -> null); // (21) null, no change
etnMap.computeIfPresent(911, (key, oVal) -> "USA"); // (22) null, no change
etnMap.computeIfPresent(100, (key, oVal) -> null); // (23) null, no action
etnMap.computeIfPresent(110, (key, oVal) -> "China"); // (24) null, no action
// After: {999=Uganda, 190=null, 911=null}
Bulk Operations
Bulk operations can be performed on an entire map.
int size()
boolean isEmpty()
Return the number of entries (i.e., number of unique keys in the map) and
whether the map is empty or not, respectively.
void clear() Optional
void putAll(Map<? extends K, ? extends V> map) Optional
The first method deletes all entries from the current map.
The second method copies all entries from the specified map to the current map.
If a key from the specified map is already in the current map, its associated
value in the current map is replaced with the associated value from the speci-
fied map.
default void forEach(BiConsumer<? super K,? super V> action)
The specified action is performed for each entry in this map until all entries
have been processed or the action throws an exception. Iteration is usually per-
formed in entry set iteration order. The functional interface BiConsumer<T,U> is
covered in §13.7, p. 711, where this method is illustrated. It is also used in
Example 15.10, p. 844.
default void replaceAll(BiFunction<? super K,? super V,? extends V> func)
The value of each entry is replaced with the result of invoking the given two-
arity function on the entry until all entries have been processed or the function
throws an exception. The functional interface BiFunction<T,U,R> is covered in
§13.9, p. 717, where this method is illustrated.
Collection Views
Views allow information in a map to be represented as collections. Elements can be
removed from a map via a view, but cannot be added. An iterator over a view will
throw an exception if the underlying map is modified concurrently. Example 15.10,
p. 844, illustrates collection views.
840 CHAPTER 15: COLLECTIONS: PART II
Set<K> keySet()
Collection<V> values()
Set<Map.Entry<K, V>> entrySet()
Create different views of a map. Changes in the map are reflected in the view,
and vice versa. These methods return a set view of keys, a collection view of
values, and a set view of <key, value> entries, respectively. Note that the Collec-
tion returned by the values() method is not a Set, as several keys can map to
the same value—that is, duplicate values can be included in the returned col-
lection. An entry in the entry set view can be manipulated by the methods
defined in the Map.Entry<K, V> interface.
Example 15.10, p. 844, provides an example of using collection views.
The classes HashMap<K, V> and Hashtable<K, V> implement unordered maps. The
class LinkedHashMap<K, V> implements ordered maps, and the class TreeMap<K, V>
implements sorted maps (p. 845).
While the HashMap<K, V> class is not thread-safe and permits a null key and null val-
ues (analogous to the LinkedHashMap<K, V> class), the Hashtable<K, V> class is thread-
safe and permits only non-null keys and values (see Table 15.7). The thread-safety
provided by the Hashtable<K, V> class comes with a performance penalty. Thread-
safe use of maps is also provided by the methods in the Collections class (p. 856).
Like the Vector<E> class, the Hashtable<K, V> class is also a legacy class that has been
retrofitted to implement the Map<K, V> interface.
These map implementations are based on a hashing algorithm. Operations on a
map thus rely on the hashCode() and equals() methods of the key objects (§14.2,
p. 744, and §14.3, p. 753).
15.9: MAP IMPLEMENTATIONS 841
The LinkedHashMap<K, V> implementation is a subclass of the HashMap<K, V> class. The
relationship between the map classes LinkedHashMap<K, V> and HashMap<K, V> is anal-
ogous to the relationship between their counterpart set classes LinkedHashSet<E>
and HashSet<E>. The entries of a HashMap<K, V> (analogous to a HashSet<E>) are unor-
dered. The entries of a LinkedHashMap<K, V> (analogous to a LinkedHashSet<E>) are
ordered. By default, the entries of a LinkedHashMap<K, V> are in key insertion order—
that is, the order in which the keys are inserted in the map. This order does not
change if a key is reinserted because no new entry is created if the key’s entry
already exists. The elements in a LinkedHashSet<E> are also at (element) insertion
order. However, a LinkedHashMap<K, V> can also maintain its entries in access order—
that is, the order in which its entries are accessed, from least-recently accessed to
most-recently accessed entries. This ordering mode can be specified in one of the
constructors of the LinkedHashMap<K, V> class.
Both the HashMap<K, V> and the LinkedHashMap<K, V> classes provide comparable per-
formance, but the HashMap<K, V> class is the natural choice if ordering is not an issue.
Operations such as adding, removing, or finding an entry based on a key are in
constant time, as these are based on hashing the key. Operations such as finding
the entry with a specific value are in linear time, as these involve searching through
the entries.
Adding, removing, and finding entries in a LinkedHashMap<K, V> can be slightly
slower than in a HashMap<K, V>, as an ordered doubly linked list has to be main-
tained. Iteration over a map is through one of its collection views. For an underly-
ing LinkedHashMap<K, V>, the iteration time is proportional to the size of the map—
regardless of its capacity. However, for an underlying HashMap<K, V>, it is propor-
tional to the capacity of the map.
The concrete map implementations override the toString() method. The standard
text representation generated by the toString() method for a map is
{key1=value1, key2=value2, ..., keyn=valuen}
where each keyi and each valuei, where 1 <= i <= n, is the text representation gener-
ated by the toString() method of the individual key and value objects in the map,
respectively.
As was the case with collections, implementation classes provide a standard con-
structor that creates a new empty map, and a constructor that creates a new map
based on an existing map. Additional constructors create empty maps with given
initial capacities and load factors. The HashMap<K, V> class provides the following
constructors:
HashMap()
HashMap(int initialCapacity)
HashMap(int initialCapacity, float loadFactor)
Constructs an empty HashMap, using either a default or specified initial capacity
and a load factor.
842 CHAPTER 15: COLLECTIONS: PART II
The LinkedHashMap<K, V> and Hashtable<K, V> classes have constructors analogous
to the four constructors for the HashMap<K, V> class. In addition, the LinkedHashMap<K,
V> class provides a constructor where the ordering mode can also be specified:
Example 15.10 prints a textual histogram for the frequency of weight measurements
in a weight group, where a weight group is defined as an interval of five units. The
weight measurements are supplied as program arguments. A HashMap<Integer,
Integer> is used, where the key is the weight group and the value is the frequency.
The example illustrates many key-based operations on maps, the creation of key
views, and iteration over a map.
We have intentionally used a HashMap<K,V>. It is instructive to compare this with the
solution in Example 15.11 that uses a TreeMap<K,V> that simplifies the solution fur-
ther, when the entries are maintained in key-sort order.
The program proceeds as follows:
• An empty HashMap<Integer, Integer> is created at (1), where the key is the
weight group and the associated value is the frequency.
• A for(:) loop is used at (2) to read the weights specified as program argu-
ments, converting each weight to its corresponding weight group and updat-
ing the frequency of the weight group.
Each program argument is parsed to a double value at (3), which is then used to
determine the correct weight group at (4). The call to the merge() method at (5)
updates the frequency map:
// With method reference:
groupFreqMap.merge(weightGroup, 1, Integer::sum); // (5)
// With lambda expression:
groupFreqMap.merge(weightGroup, 1,
(oldVal, givenVal) -> Integer.sum(oldVal, givenVal));
If the weight group is not in the map, its frequency is set to 1; otherwise, the
method reference Integer::sum increments the frequency by the given value 1. In
both cases an appropriate entry for the weight group is put in the frequency
map. Note the arguments passed to the method reference and the lambda
expression: oldVal is the current value associated with the key and givenVal is
the specified value, 1, passed to the merge() method.
Generic types guarantee that the keys and the values in the map are of the cor-
rect type, and autoboxing/unboxing of primitive values guarantees the correct
type of an operand in an expression.
15.9: MAP IMPLEMENTATIONS 843
Some other strategies to update the frequency map are outlined here, but none
is more elegant than the one with the merge() method.
The straightforward solution below requires an explicit null check to determine
whether the value returned by the get() method is null, or risks a NullPointer-
Exception at runtime when incrementing the null value in the frequency reference.
Integer frequency = groupFreqMap.get(weightGroup);
if (frequency == null) frequency = 0;
groupFreqMap.put(weightGroup, ++frequency);
Below, the getOrDefault() method never returns a null value, since it returns the
associated frequency value or the specified default value 0 depending on
whether the weight group is found or not found in the map, respectively. No
explicit null check is necessary.
Integer frequency = groupFreqMap.getOrDefault(weightGroup, 0);
groupFreqMap.put(weightGroup, ++frequency);
The putIfAbsent() method puts an entry with frequency 0 for the weight group
if it is not found so that the get() method will always return a non-null value
which can be safely incremented.
groupFreqMap.putIfAbsent(weightGroup, 0);
Integer frequency = groupFreqMap.get(weightGroup);
groupFreqMap.put(weightGroup, ++frequency);
The compute() method always updates the value associated with the key if the
two-arity function returns a non-null value, which is always the case below. The
null check is now done in the lambda expression.
groupFreqMap.compute(weightGroup, (k, v) -> v == null ? 1 : v + 1);
• The program creates a sorted set of keys (which are weight groups) from the
groupFreqMap at (6). The Map.keySet() method returns a set view of keys, which is
passed as an argument to a TreeSet<E> to create a sorted set—in this case, the
natural ordering of Integers is used.
TreeSet<Integer> sortedKeySet = new TreeSet<>(groupFreqMap.keySet()); // (6)
• The histogram is printed by implicitly iterating over the sorted key set at (7).
Since a sorted set is an Iterable<E>, we can use the forEach() method that takes
a consumer to print the histogram.
sortedKeySet.forEach(key -> // (7)
System.out.printf("%5s: %s%n", key,
String.join("", Collections.nCopies(groupFreqMap.get(key), "*")))
For each key, the corresponding value (i.e., the frequency) is retrieved and
converted to a string with the corresponding number of "*". The method
Collections.nCopies() creates a list with an equal number of elements as its first
argument and where each element is the same as the second argument. The ele-
ments of this list are joined to create a string by the String.join() method with
the first argument specifying the delimiter to use between the elements, which
in this case is the empty string.
Alternately, a for(:) loop can be used to explicitly iterate over the sorted set
view of the frequency map.
844 CHAPTER 15: COLLECTIONS: PART II
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeSet;
System.out.println("Histogram:");
// Implicit iteration over the key set.
sortedKeySet.forEach(key -> // (7)
System.out.printf("%5s: %s%n", key,
String.join("", Collections.nCopies(groupFreqMap.get(key), "*")))
);
}
}
// Range-view operations
SortedMap<K,V> headMap(K toKey) Sorted set: headSet()
SortedMap<K,V> tailMap(K fromKey) Sorted set: tailSet()
SortedMap<K,V> subMap(K fromKey, K toKey) Sorted set: subSet()
Return different views analogous to those for a SortedSet<E>. The views
returned are a portion of the map whose keys are strictly less than toKey,
greater than or equal to fromKey, and between fromKey (inclusive) and toKey
(exclusive), respectively. That is, these partial map views include fromKey if it is
present in the map, but toKey is excluded.
// Comparator access
Comparator<? super K> comparator()
Returns the key comparator that defines the key-sort order, or null if the
sorted map uses natural ordering for the keys.
// First-last entries
// Remove
Map.Entry<K, V> pollFirstEntry() Navigable set: pollFirst()
Map.Entry<K, V> pollLastEntry() Navigable set: pollLast()
// Examine
Map.Entry<K, V> firstEntry()
Map.Entry<K, V> lastEntry()
The pollFirstEntry() method removes and returns the first entry, and the poll-
LastEntry() method removes and returns the last entry currently in this navi-
gable map. The entry is determined according to the ordering policy employed
by the map—for example, natural ordering. Both return null if the navigable
set is empty. The last two methods only retrieve, and do not remove, the value
that is returned.
// Range-view operations
NavigableMap<K, V> headMap(K toKey, Navigable set: headSet()
boolean inclusive)
NavigableMap<K, V> tailMap(K fromKey, Navigable set: tailSet()
boolean inclusive)
NavigableMap<K, V> subMap(K fromKey, Navigable set: subSet()
boolean fromInclusive,
K toKey,
boolean toInclusive)
These operations are analogous to the ones in the SortedMap<K, V> interface
(p. 845), returning different views of the underlying navigable map, depend-
ing on the bound elements. However, the bound elements can be excluded or
included by the operation, depending on the value of the boolean argument
inclusive.
// Closest-matches
Map.Entry<K, V> ceilingEntry(K key) Navigable set: ceiling()
K ceilingKey(K key)
Map.Entry<K, V> floorEntry(K key) Navigable set: floor()
K floorKey(K key)
Map.Entry<K, V> higherEntry(K key) Navigable set: higher()
K higherKey(K key)
Map.Entry<K, V> lowerEntry(K key) Navigable set: lower()
K lowerKey(K key)
The ceiling methods return the least entry (or key) in the navigable map >= to
the argument key. The floor methods return the greatest entry (or key) in the
navigable map <= to the argument key. The higher methods return the least
entry (or key) in the navigable map > the argument key. The lower methods
return the greatest entry (or key) in the navigable map < the argument key. All
methods return null if there is no such key.
15.10: SORTED MAPS AND NAVIGABLE MAPS 847
// Navigation-views
NavigableMap<K, V> descendingMap() Navigable set: descendingSet()
NavigableSet<K> descendingKeySet()
NavigableSet<K> navigableKeySet()
The first method returns a reverse-order map view of the entries in the navigable
map. The second method returns a reverse-order key set view for the entries in
the navigable map. The last method returns an ascending-order key set view for
the entries in the navigable map.
TreeMap()
A standard constructor used to create a new empty sorted map, according to
the natural ordering of the keys.
TreeMap(Comparator<? super K> c)
A constructor that takes an explicit comparator for the keys, that is used to
order the entries in the map.
TreeMap(Map<? extends K, ? extends V> map)
A constructor that can create a sorted map based on a map, according to the
natural ordering of the keys of the specified map.
TreeMap(SortedMap<K, ? extends V> map)
A constructor that creates a new map containing the same entries as the spec-
ified sorted map, with the same ordering for the keys as the specified map.
Example 15.11 illustrates using navigable maps. It also prints a textual histogram
like the one in Example 15.10, but using a TreeMap<K, V>, and in addition, it prints
848 CHAPTER 15: COLLECTIONS: PART II
some statistics about the navigable map. See also the output from running the
example. Some remarks about Example 15.11:
• An empty NavigableMap<Integer, Integer> is created at (1), where the key is the
weight group and the value is the frequency. The loop at (2) reads the weights
specified as program arguments, and creates a frequency map, as described in
Example 15.10.
• The method printHistogram() at (15) prints a histogram of the frequencies in a
navigable map:
public static <K> void printHistogram(NavigableMap<K, Integer> freqMap) {...}
It is a generic method with one type parameter, K, that specifies the type of the
keys, and the type of the values (i.e., frequencies) is Integer. It prints the map
and the number of entries in the map at (16) and (17), respectively. The forEach()
method is invoked on the map at (18) to iterate over the entries in order to print
the histogram, as explained in Example 15.10.
Printing the histogram at (3) shows the number of entries ordered in ascending
key order and the size of the map:
Group frequency map: {10=1, 60=1, 75=3, 80=1, 90=1, 95=2, 185=1}
No. of weight groups: 7
...
• Calls to the methods firstEntry() and lastEntry() at (4) and (5):
out.println("First entry: " + groupFreqMap.firstEntry()); // (4)
out.println("Last entry: " + groupFreqMap.lastEntry()); // (5)
return the following entries, respectively:
First entry: 10=1
Last entry: 185=1
• Calls to the methods floorEntry() and higherKey() with the key value 77 at (6)
and with the key value 90 at (7), respectively:
out.println("Greatest entry <= 77: "
+ groupFreqMap.floorEntry(77)); // (6)
out.println("Smallest key > 90: "
+ groupFreqMap.higherKey(90)); // (7)
give the following output, respectively:
Greatest entry <= 77: 75=3
Smallest key > 90: 95
• Calls to the methods tailMap(75, true) and headMap(75, false) at (8) and (9) with
the argument 75:
...
printHistogram(groupFreqMap.tailMap(75, true)); // (8)
...
printHistogram(groupFreqMap.headMap(75, false)); // (9)
return the following map views, respectively:
Tail map (Groups >= 75): {75=3, 80=1, 90=1, 95=2, 185=1}
...
Head map (Groups < 75): {10=1, 60=1}
...
15.10: SORTED MAPS AND NAVIGABLE MAPS 849
import java.util.Collections;
import java.util.Map;
import java.util.NavigableMap;
import java.util.TreeMap;
import static java.lang.System.out;
Review Questions
15.10 Which of the following methods are defined by the java.util.Map.Entry<K, V>
interface?
Select the three correct answers.
(a) K getKey()
(b) K setKey(K value)
(c) V getValue()
852 CHAPTER 15: COLLECTIONS: PART II
System.out.println(set2);
}
}
15.15 What will be the output of the following program when compiled and run?
import java.util.*;
public class MapModify {
public static void main(String[] args) {
NavigableMap<String, Integer> grades = new TreeMap<>();
grades.put("A", 5); grades.put("B", 10); grades.put("C", 15);
grades.put("D", 20); grades.put("E", 25);
15.16 Which code, when inserted independently at (1), will result in the following output
from the program: {Soap=10, Salts=30}?
import java.util.*;
public class Mapping {
public static void main(String[] args) {
NavigableMap<String, Integer> myMap
= new TreeMap<>(Collections.reverseOrder());
myMap.put("Soap", 10); myMap.put("Shampoo", 20); myMap.put("Salts", 30);
// (1) INSERT CODE HERE
System.out.println(myMap);
}
}
System.out.println(sumVal);
}
}
// Backing map:
Map<Integer, StringBuilder> backingMap = new HashMap<>();
backingMap.put(200, mc1); backingMap.put(300, mc2);
backingMap.put(400, mc3); backingMap.put(500, mc4);
858 CHAPTER 15: COLLECTIONS: PART II
However, changes to the backing map are reflected in the unmodifiable view:
backingMap.remove(200); backingMap.remove(400);
System.out.println("Backing map: " + backingMap);
System.out.println("Unmodifiable view: " + unmodViewMap);
// Backing map: {500=Java Complete, 300=Java II}
// Unmodifiable view: {500=Java Complete, 300=Java II}
Since the values in the unmodifiable view of a map are mutable, the code below
shows that changing a value of an entry in the unmodifiable view is reflected in the
backing map.
StringBuilder mutableCourse = unmodViewMap.get(500); // Get the course.
mutableCourse.replace(5, 8, "Complete"); // Change the course name.
System.out.println("Backing map: " + backingMap);
System.out.println("Unmodifiable view: " + unmodViewMap);
// Backing map: {400=Java III, 500=Java Complete, 200=Java I, 300=Java II}
// Unmodifiable view: {400=Java III, 500=Java Complete, 200=Java I, 300=Java II}
The Collections class provides two generic static methods for sorting lists.
<E extends Comparable<? super E>> void sort(List<E> list)
<E> void sort(List<E> list, Comparator<? super E> comparator)
The first method sorts the elements in the list according to their natural order-
ing. The second method does the sorting according to the total ordering
defined by the comparator.
In addition, all elements in the list must be mutually comparable: The method
call e1.compareTo(e2) (or e1.compare(e2) in the case of the comparator) must not
throw a ClassCastException for any elements e1 and e2 in the list. In other
words, it should be possible to compare any two elements in the list. Note that
the second method does not require that the type parameter E is Comparable<E>.
If the specified comparator is null then the natural ordering for the elements is
used, requiring elements in this list to implement the Comparable<E> interface.
15.11: THE COLLECTIONS CLASS 859
The List<E> interface also defines an abstract method for sorting lists, with seman-
tics equivalent to its namesake in the Collections class.
// Defined in the List <E> interface.
void sort(Comparator<? super E> comparator)
This list is sorted according to the total ordering defined by the specified com-
parator. If the specified comparator is null then the natural ordering for the
elements is used, requiring elements in this list to implement the Comparable<E>
interface.
This code shows how a list of strings is sorted according to different criteria. We
have used the sort() method from the List<E> interface and from the Collections
class.
List<String> strList = new ArrayList<>();
Collections.addAll(strList, "biggest", "big", "bigger", "Bigfoot");
The output below shows the list before sorting, followed by the results from the
calls to the sort() methods above, respectively:
Before sorting: [biggest, big, bigger, Bigfoot]
After sorting in natural order: [Bigfoot, big, bigger, biggest]
After sorting in length order: [big, bigger, Bigfoot, biggest]
After sorting in reverse natural order: [biggest, bigger, big, Bigfoot]
After sorting in insensitive order: [big, Bigfoot, bigger, biggest]
After sorting in reverse insensitive order: [biggest, bigger, Bigfoot, big]
It is important to note that either the element type of the list must implement the
Comparable<E> interface or a Comparator<E> must be provided. The following code
sorts a list of StringBuilder according to their natural ordering, as the class String-
Builder implements the Comparable<StringBuilder> interface.
List<StringBuilder> sbList = new ArrayList<>();
Collections.addAll(sbList, new StringBuilder("smallest"),
new StringBuilder("small"), new StringBuilder("smaller"));
Collections.sort(sbList); // [small, smaller, smallest]
860 CHAPTER 15: COLLECTIONS: PART II
Below is an example of a list whose elements are not mutually comparable. Raw
types are used intentionally to create such a list. Predictably, the sort() method
throws an exception because the primitive wrapper classes do not permit interclass
comparison.
List freakList = new ArrayList(); // Raw types.
Collections.addAll(freakList, 23, 3.14, 10L);
freakList.sort(null); // ClassCastException
The comparator returned by the reverseOrder() method can be used with sorted col-
lections. The elements in the following sorted set would be maintained in descend-
ing order:
Set<Integer> intSet = new TreeSet<>(Collections.reverseOrder());
Collections.addAll(intSet, 9, 11, -4, 1);
System.out.println(intSet); // [11, 9, 1, -4]
The following utility methods in the Collections class apply to any list, regardless
of whether the elements are Comparable<E> or not:
void reverse(List<?> list)
Reverses the order of the elements in the list.
The effect of these utility methods can be limited to a sublist—that is, a segment of
the list. The following code illustrates rotation of elements in a list. Note how the
rotation in the sublist view is reflected in the original list.
// intList refers to the following list: [9, 11, -4, 1, 7]
Collections.rotate(intList, 2); // Two to the right. [1, 7, 9, 11, -4]
Collections.rotate(intList, -2); // Two to the left. [9, 11, -4, 1, 7]
List intSublist = intList.subList(1,4);// Sublist: [11, -4, 1]
Collections.rotate(intSublist, -1); // One to the left. [-4, 1, 11]
// intList is now: [9, -4, 1, 11, 7]
15.11: THE COLLECTIONS CLASS 861
Searching in Collections
The Collections class provides two static methods for finding elements in sorted
lists.
<E> int binarySearch(List<? extends Comparable<? super E>> list, E key)
<E> int binarySearch(List<? extends E> list, E key,
Comparator<? super E> cmp))
These methods use binary search to find the index of the key element in the
specified sorted list. The first method requires that the list is sorted according
to natural ordering, whereas the second method requires that it is sorted
according to the total ordering dictated by the comparator. The elements in the
list and the key must also be mutually comparable.
Successful searches return the index of the key in the list. A non-negative value
indicates a successful search. Unsuccessful searches return a negative value given
by the formula –(insertion point + 1), where insertion point is the index where the
key would have been, had it been in the list. In the code below, the return value -3
indicates that the key would have been at index 2, had it been in the list.
Collections.sort(strList);
// Sorted in natural order: [Bigfoot, big, bigger, biggest]
// Search in natural order:
out.println(Collections.binarySearch(strList, "bigger")); // Successful: 2
out.println(Collections.binarySearch(strList, "bigfeet")); // Unsuccessful: -3
out.println(Collections.binarySearch(strList, "bigmouth")); // Unsuccessful: -5
Proper use of the search methods requires that the list is sorted, and the search is
performed according to the same sort order. Otherwise, the search results are
unpredictable. The example below shows the results of the search when the list str-
List above was sorted in reverse natural ordering, but was searched assuming nat-
ural ordering. Most importantly, the return values reported for unsuccessful
searches for the respective keys are incorrect in the list that was sorted in reverse
natural ordering.
Collections.sort(strList, Collections.reverseOrder());
// Sorted in reverse natural order: [biggest, bigger, big, Bigfoot]
// Searching in natural order:
out.println(Collections.binarySearch(strList, "bigger")); // 1
out.println(Collections.binarySearch(strList, "bigfeet")); // -1 (INCORRECT)
out.println(Collections.binarySearch(strList, "bigmouth")); // -5 (INCORRECT)
Searching the list in reverse natural ordering requires that an appropriate compar-
ator is supplied during the search (as during the sorting), resulting in correct
results:
Collections.sort(strList, Collections.reverseOrder());
// Sorted in reverse natural order: [biggest, bigger, big, Bigfoot]
// Searching in reverse natural order:
out.println(Collections.binarySearch(strList, "bigger",
Collections.reverseOrder())); // 1
862 CHAPTER 15: COLLECTIONS: PART II
out.println(Collections.binarySearch(strList, "bigfeet",
Collections.reverseOrder())); // -3
out.println(Collections.binarySearch(strList, "bigmouth",
Collections.reverseOrder())); // -1
The following methods find the maximum and minimum elements in a collection:
<E extends Object & Comparable<? super E>>
E max(Collection<? extends E> c)
<E> E max(Collection<? extends E> c, Comparator<? super E> cmp)
<E extends Object & Comparable<? super E>>
E min(Collection<? extends E> c)
<E> E min(Collection<? extends E> cl, Comparator<? super E> cmp)
The one-argument methods require that the elements have a natural order-
ing—that is, are Comparable<E>. The other methods require that the elements
have a total ordering enforced by the comparator. Calling any of the methods
with an empty collection as a parameter results in a NoSuchElementException.
The time for the search is proportional to the size of the collection.
These methods are analogous to the methods first() and last() in the SortedSet<E>
class (p. 810), and the methods firstKey() and lastKey() in the SortedMap<K, V>
class (p. 845).
The following code snippet illustrates how the replaceAll() method of the List<E>
interface can be used to apply a UnaryOperator to each element of the list. The
lambda expression at (1a) is executed for each string in the list strList, replacing
15.11: THE COLLECTIONS CLASS 863
each element with an uppercase version of the string. Equivalent method reference
is used for the same purpose at (1b).
// Before: [biggest, big, bigger, Bigfoot]
strList.replaceAll(str -> str.toUpperCase()); // (1a)
strList.replaceAll(String::toUpperCase); // (1b)
// After: [BIGGEST, BIG, BIGGER, BIGFOOT]
In contrast, the replaceAll() method of the Collections class can be used to replace
all occurrences of a specific value in the collection with a new value. In the list pal-
indromes of strings, the occurrence of the string "road" is replaced by the string
"anna" in the method call below.
// Before: [eye, level, radar, road]
Collections.replaceAll(palindromes, "road", "anna");
// After: [eye, level, radar, anna]
The majority of the methods found in the Collections class that replace the ele-
ments of a collection operate on a List, while one method operates on arbitrary
Collections. They all change the contents of the collection in some way.
The addAll() method is a convenient method for loading an existing collection with
a few individually specified elements or an array of small size. Several examples of
its usage can be found in this chapter. The array passed should be an array of
objects. Note also the autoboxing of the int values specified at (1) and (2). The
addAll() method does not allow primitive arrays as a variable arity argument, as
attempted at (3).
List<Integer> intList = new ArrayList<>(); // []
Collections.addAll(intList, 9, 1, 1); // (1) Varargs
// After: [9, 1, 1]
Collections.addAll(intList, new Integer[] {1, 1, 9}); // (2) An array of Integers
864 CHAPTER 15: COLLECTIONS: PART II
// After: [9, 1, 1, 1, 1, 9]
Collections.addAll(intList, new int[] {1, 9, 1}); // (3) Compile-time error!
All elements of a list can be replaced with the same element, as shown at (1):
List<String> strList = new ArrayList<>();
Collections.addAll(strList, "liberty", "equality", "fraternity");
// Before: [liberty, equality, fraternity]
Collections.fill(strList, "CENSORED"); // (1)
// After: [CENSORED, CENSORED, CENSORED]
Sorting Arrays
Sorting implies ordering the elements according to some ranking criteria, usually
based on the values of the elements. The values of numeric data types are compared
and ranked by using the relational operators that define the numerical order of the
values. Objects are typically compared according to their natural ordering defined
by their class. The class implements the compareTo() method of the Comparable<E>
interface. The ordering defined by this method is called the natural ordering for the
objects of the class, where obj1.compareTo(obj2) returns the following result:
• A positive value if obj1 is greater than obj2
• The value 0 if obj1 is equal to obj2
• A negative value if obj1 is less than obj2
The wrapper classes for primitive values and the String class implement the
compareTo() method (§8.3, p. 432), thereby giving their objects a natural ordering.
Arrays with objects of these classes can readily be sorted as the sort algorithm can
take advantage of their natural ordering.
The Arrays class provides enough overloaded versions of the sort() method to sort
practically any type of array. The discussion on sorting lists (p. 858) is also applica-
ble to sorting arrays.
void sort(type[] array)
void sort(type[] array, int fromIndex, int toIndex)
The permitted type for elements includes byte, char, double, float, int, long,
short, and Object.
These methods sort the elements in the array according to their natural order-
ing.
In the case of an array of objects being passed as an argument, the objects must
be mutually comparable according to the natural ordering defined by the
Comparable<E> interface; that is, it should be possible to compare any two objects
in the array according to their natural ordering without throwing a ClassCast-
Exception.
The experiment with a list of strings (p. 858) is repeated in the following with an
array of strings, giving identical results. An array of strings is sorted according to
different criteria.
String[] strArray = {"biggest", "big", "bigger", "Bigfoot"};
Arrays.sort(strArray); // Natural order
Arrays.sort(strArray, Comparator.comparing(String::length)); // Length order
Arrays.sort(strArray, Collections.reverseOrder()); // Reverse natural order
Arrays.sort(strArray, String.CASE_INSENSITIVE_ORDER);// Case insensitive order
Arrays.sort(strArray, // Reverse case insensitive order
Collections.reverseOrder(String.CASE_INSENSITIVE_ORDER));
The output below shows the array before sorting, followed by the results from the
calls to the Arrays.sort() methods above, respectively:
Before sorting: [biggest, big, bigger, Bigfoot]
After sorting in natural order: [Bigfoot, big, bigger, biggest]
After sorting in length order: [big, bigger, Bigfoot, biggest]
After sorting in reverse natural order: [biggest, bigger, big, Bigfoot]
After sorting in case insensitive order: [big, Bigfoot, bigger, biggest]
After sorting in reverse case insensitive order: [biggest, bigger, Bigfoot, big]
The examples below illustrate sorting an array of primitive values (int) at (1), an
array of type Object containing mutually comparable elements (String) at (2), and
a half-open interval in reverse natural ordering at (3). A ClassCastException is
thrown when the elements are not mutually comparable, at (4) and (5).
int[] intArray = {5, 3, 7, 1}; // int
Arrays.sort(intArray); // (1) Natural order: [1, 3, 5, 7]
Searching in Arrays
A common operation on an array is to search the array for a given element, called the
key. The Arrays class provides enough overloaded versions of the binarySearch()
method to search in practically any type of array that is sorted. The discussion on
searching in lists (p. 861) is also applicable to searching in arrays.
15.12: THE ARRAYS CLASS 867
Results are unpredictable if the array is not sorted, or the ordering used in the
search is not the same as the sort order. Searching in the strArray using reverse nat-
ural ordering when the array is sorted in natural ordering gives the wrong result:
out.println(Arrays.binarySearch(strArray, "bigger",
Collections.reverseOrder())); // -1 (INCORRECT)
A ClassCastException is thrown if the key and the elements are not mutually com-
parable:
out.println(Arrays.binarySearch(strArray, 4)); // Key: 4 => ClassCastException
868 CHAPTER 15: COLLECTIONS: PART II
However, this incompatibility is caught at compile time in the case of arrays with
primitive values:
// Sorted int array (natural order): [1, 3, 5, 7]
out.println(Arrays.binarySearch(intArray, 4.5));// Key: 4.5 => Compile-time error!
The method binarySearch() derives its name from the divide-and-conquer algo-
rithm that it uses to perform the search. It repeatedly divides the remaining ele-
ments to be searched into two halves and selects the half containing the key in which
to continue the search, until either the key is found or there are no more elements
left to search.
Comparing Arrays
The java.util.Arrays class provides a rich set of static methods for comparing
arrays of primitive data types and objects. In this section we only consider the basic
methods for array equality, array comparison, and array mismatch. We encourage
the curious reader to explore the Arrays class API for more flexible comparison of
arrays.
Array Equality
The Arrays.equals() static method can be used to determine if two arrays are equal.
boolean equals(type[] a, type[] b)
The permitted type for elements includes all primitive types (boolean, byte, char,
double, float, int, long, short) and Object.
Two arrays are considered equal if they contain the same elements in the same
order—that is, they have the same length and corresponding pairs of elements
are equal. Two array references are also considered equal if both are null.
Two primitive values v1 and v2 are considered equal if new Wrapper-
Class(v1).equals(new WrapperClass(v2)),where WrapperClass is the wrapper
class corresponding to their primitive data type.
Two objects obj1 and obj2 are considered equal if Objects.equals(obj1, obj2).
Given two arrays fruitBasketA and fruitBasketB of strings, shown below graphi-
cally, we can conclude that they are equal, since they have the same length and cor-
responding pairs of fruits are equal.
Index 0 1 2 3
fruitBasketA ==> [oranges, apples, plums, kiwi]
fruitBasketB ==> [oranges, apples, plums, kiwi]
Equals: true
However, the arrays fruitBasketA and fruitBasketC below would not be considered
equal. Although they have the same length and the same fruits, their correspond-
ing pairs of fruits are not equal. The first index where this occurs is index 2.
15.12: THE ARRAYS CLASS 869
Index 0 1 2 3
fruitBasketA ==> [oranges, apples, plums, kiwi]
fruitBasketC ==> [oranges, apples, kiwi, plums]
Equals: false
Obviously, the two arrays fruitBasketA and fruitBasketE that have different lengths
are not equal, as we can see below:
Index 0 1 2 3
fruitBasketA ==> [oranges, apples, plums, kiwi]
fruitBasketE ==> [oranges, apples]
Equals: false
The following code demonstrates the examples provided above, where System.out
is statically imported:
String[] fruitBasketA = { "oranges", "apples", "plums", "kiwi" };
String[] fruitBasketB = { "oranges", "apples", "plums", "kiwi" };
String[] fruitBasketC = { "oranges", "apples", "kiwi", "plums" };
String[] fruitBasketE = { "oranges", "apples" };
...
out.println("Equals: " + Arrays.equals(fruitBasketA, fruitBasketB)); // true
out.println("Equals: " + Arrays.equals(fruitBasketA, fruitBasketC)); // false
out.println("Equals: " + Arrays.equals(fruitBasketA, fruitBasketE)); // false
According to the equals() method, two null arrays are equal. However, the first
statement below will not compile. The compiler issues an error, as the method call
matches many overloaded methods named equals in the Arrays API. A cast is nec-
essary to disambiguate the method call, as shown in the second statement.
out.println(Arrays.equals(null, null)); // Ambiguous method call.
// Compile-time error!
out.println(Arrays.equals((String[]) null, null)); // true
Array Comparison
The compare() method of the Comparable<E> interface allows two objects to be com-
pared. The comparison relationship is generalized by the Arrays.compare() static
method to lexicographically compare arrays.
int compare(type[] a, type[] b)
<T extends Comparable<? super T>> int compare(T[] a, T[] b)
The permitted type for elements includes all primitive types (boolean, byte, char,
double, float, int, long, short).
These methods return the value 0 if the first and second arrays are equal; a
value less than 0 if the first array is lexicographically less than the second array;
and a value greater than 0 if the first array is lexicographically greater than the
second array.
The second method only permits arrays whose objects implement the compare-
To() method of the Comparable<E> interface.
Two null array references are considered equal, and a null array reference is
considered lexicographically less than a non-null array reference.
870 CHAPTER 15: COLLECTIONS: PART II
Consider the case where the two arrays diceA and diceD that represent the result of
rolling a dice a fixed number of times. The arrays have a prefix ([5, 2]) which is
common to both arrays. At index 2, diceA[2] (value 6) is greater than diceD[2] (value
3). In this case, the compare() method returns a positive value to indicate that array
diceA is lexicographically greater than array diceD. Note that the index 2 is equal to
the length of the prefix [5, 2] and it is a valid index in both arrays. The prefix [5,
2] is called the common prefix of diceA and diceD.
Index 0 1 2 3
diceA ==> [5, 2, 6, 3]
diceD ==> [5, 2, 3]
Common prefix: [5,2]
Compare value: 1
In the example below, the compare() method returns a negative value to indicate that
diceA is lexicographically less than diceE because diceA[1] (value 2) is less than
diceE[1] (value 6), where the common prefix [5] has length 1.
Index 0 1 2 3
diceA ==> [5, 2, 6, 3]
diceE ==> [5, 6]
Common prefix: [5]
Compare value: -1
If the arrays share a common prefix, as in the examples above, the lexicographical
comparison is determined by the corresponding pair of values at the index given
by the length of the common prefix. The length of a common prefix is always
greater than or equal to 0 and less than the minimum length of the two arrays. By
this definition, the length of a common prefix can never be equal to the length of
the two arrays. A common prefix of length 0 means that the elements at index 0
from each array determine the comparison result.
Where the prefix is equal to one of the arrays, we need to consider the lengths of the
arrays:
• If the lengths are equal, as shown in the example below, the arrays must be lex-
icographically equal and the compare() method returns the value 0.
Index 0 1 2 3
diceA ==> [5, 2, 6, 3]
diceB ==> [5, 2, 6, 3]
Prefix: [5, 2, 6, 3]
Compare value: 0
• If the lengths are not equal, as shown in the example below, lexicographical
comparison is based on the lengths of the arrays. The longer array (diceC,
length 4) is lexicographically greater than the shorter array (diceD, length 3). The
compare() method returns a positive value. Note that the length of the prefix, 3 in
this example, is a valid index in the longer array, diceC, but not in the shorter
array, diceD.
Index 0 1 2 3
diceC ==> [5, 2, 3, 6]
15.12: THE ARRAYS CLASS 871
Array Mismatch
The Arrays.mismatch() static method returns the first index where a mismatch
occurs between two arrays. If there is no mismatch, the arrays are equal, and the
value -1 is returned. The index returned is determined by the prefix of the arrays.
The discussion on common and proper prefixes in the subsection on comparing
arrays is highly relevant for determining mismatch (p. 869).
int mismatch(type[] a, type[] b)
The permitted type for elements includes all primitive types (boolean, byte, char,
double, float, int, long, short) and Object.
This method returns the index of the first mismatch between two arrays, where
0 <= index <= Math.min(a.length, b.length). Otherwise, it returns -1 if no mis-
match is found.
The method throws a NullPointerException if either of the arrays is null.
In the following example, the common prefix is empty. Its length is 0. The elements
at index 0, fruitBasketA[0] and fruitBasketG[0], mismatch. The index 0 is returned.
Index 0 1 2 3
fruitBasketA ==> [oranges, apples, plums, kiwi]
fruitBasketG ==> [apples]
Common prefix: []
Mismatch index: 0
In the next example, the array lengths are equal and the prefix is the same as both
arrays. There is no mismatch between the two arrays. The value -1 is returned.
Index 0 1 2 3
fruitBasketA ==> [oranges, apples, plums, kiwi]
fruitBasketB ==> [oranges, apples, plums, kiwi]
Prefix: [oranges, apples, plums, kiwi]
Mismatch value: -1
In the example below, the arrays have a proper prefix, [oranges, apples], as the pre-
fix is the same as one of the arrays and the arrays have different lengths. Mismatch
therefore occurs at the index given by the length of the proper prefix (2) in the long-
er array, fruitBasketA. Index 2 is only valid in this array. The mismatch() method
returns the index 2.
Index 0 1 2 3
fruitBasketA ==> [oranges, apples, plums, kiwi]
fruitBasketE ==> [oranges, apples]
Proper prefix: [oranges, apples]
Mismatch index: 2
The following code can be used to demonstrate the index value returned by the
mismatch() method in the examples above, where System.out is statically imported:
The code below illustrates how the Arrays.fill() method can be used. Each ele-
ment in the local array bar is assigned the character '*' using the fill() method,
and the array is printed.
for (int i = 0; i < 5; ++i) {
char[] bar = new char[i];
Arrays.fill(bar, '*');
System.out.printf("%d %s%n", i, Arrays.toString(bar));
}
The Arrays.toString() method has been used in many examples to convert the con-
tents of an array to a text representation. The Arrays.deepToString() method can be
used to convert the contents of a multidimensional array to a text representation,
where each element that is an array is also converted to a text representation.
char[][] twoDimArr = new char[2][2];
Arrays.fill(twoDimArr[0], '*');
Arrays.fill(twoDimArr[1], '*');
System.out.println(Arrays.deepToString(twoDimArr)); // [[*, *], [*, *]]
The code below illustrates how each element of an existing array can be initialized
to the result of applying a function.
String[] strArr = new String[5];
Arrays.setAll(strArr, i -> "Str@" + i); // [Str@0, Str@1, Str@2, Str@3, Str@4]
Review Questions
15.21 Which of the following statements are true about the following program?
import java.util.*;
public class InitOnly {
public static void main(String[] args) {
List<String> list1 = new ArrayList<>();
Collections.addAll(list1, "Hi", "Hello");
Collections.addAll(list1, "Howdy");
System.out.println(list1); // (1)
15.22 Which of the following statements are true about the following program?
import java.util.Arrays;
public class GetThatIndex {
public static void main(String[] args) {
if (args.length != 1) return;
printIndex(args[0]);
}
String r1 = trip.pollFirst();
trip.offerFirst(s1);
trip.offerFirst(s2);
String r2 = trip.pollFirst();
String r3 = trip.peekFirst();
trip.offerLast(s3);
trip.offerLast(s1);
String r4 = trip.pollLast();
String r5 = trip.peekLast();
System.out.println(r1 + " " + r2 + " " + r3 + " " + r4 + " " + r5);
}
}