This chapter presents the Java collections framework and its three main interfaces, List, Set, and Map, including a discussion and demonstration of generics. The equals() and hashCode() methods are also discussed in the context of Java collections. Utility classes for managing arrays, objects, and time/date values have corresponding dedicated sections too. After studying this chapter, you will be able to use all the main data structures in your programs.
The following topics will be covered in this chapter:
Let’s begin!
To be able to execute the code examples provided in this chapter, you will need the following:
Instructions on how to set up a Java SE and IntelliJ IDEA editor were provided in Chapter 1 of this book, Getting Started with Java 17. The files with code examples for this chapter are available on GitHub in the https://github.com/PacktPublishing/Learn-Java-17-Programming.git repository, in the examples/src/main/java/com/packt/learnjava/ch06_collections folder.
The Java collections framework consists of classes and interfaces that implement a collection data structure. Collections are similar to arrays in that they can hold references to objects and can be managed as a group. The difference is that arrays require their capacity to be defined before they can be used, while collections can increase and decrease their size automatically as needed. You just add or remove an object reference to a collection, and the collection changes its size accordingly. Another difference is that collections cannot have their elements be primitive types, such as short, int, or double. If you need to store such type values, the elements must be of a corresponding wrapper type, such as Short, Integer, or Double.
Java collections support various algorithms for storing and accessing elements of a collection: an ordered list, a unique set, a dictionary (called a map in Java), a stack, a queue, and some others. All classes and interfaces of the Java collections framework belong to the java.util package of the Java Class Library (JCL). The java.util package contains the following:
Reviewing all the classes and interfaces of the java.util package would require a dedicated book. So, in this section, we will just have a brief overview of the three main interfaces—List, Set, and Map—and one implementation class for each of them—ArrayList, HashSet, and HashMap. We start with methods that are shared by the List and Set interfaces. The principal difference between List and Set is that Set does not allow the duplication of elements. Another difference is that List preserves the order of elements and also allows them to be sorted.
To identify an element inside a collection, the equals() method is used. To improve performance, classes that implement the Set interface often use the hashCode() method too. This facilitates rapid calculation of an integer (called a hash value or hash code) that is most of the time (but not always) unique to each element. Elements with the same hash value are placed in the same bucket. While establishing whether there is already a certain value in the set, it is enough to check the internal hash table and see whether such a value has already been used. If not, the new element is unique. If yes, then the new element can be compared (using the equals() method) with each of the elements with the same hash value. Such a procedure is faster than comparing a new element with each element of the set one by one.
That is why we often see that the name of a class has a hash prefix, indicating that the class uses a hash value, so the element must implement the hashCode() method. While doing this, you must make sure that it is implemented so that every time the equals() method returns true for two objects, the hash values of these two objects returned by the hashCode() method are equal too. Otherwise, the just-described algorithm of using the hash value will not work.
And finally, before talking about java.util interfaces, a few words about generics.
You can see these most often in declarations such as these:
List<String> list = new ArrayList<String>();
Set<Integer> set = new HashSet<Integer>();
In the preceding examples, generics are element nature declarations surrounded by angle brackets. As you can see, they are redundant, as they are repeated in the left- and right-hand sides of the assignment statement. That is why Java allows replacement of the generics on the right side with empty brackets (<>) called a diamond, as illustrated in the following code snippet:
List<String> list = new ArrayList<>();
Set<Integer> set = new HashSet<>();
Generics inform the compiler about the expected type of collection elements. This way, the compiler can check whether an element a programmer tries to add to a declared collection is of a compatible type. Observe the following, for example:
List<String> list = new ArrayList<>();
list.add("abc");
list.add(42); //compilation error
This helps to avoid runtime errors. It also tips off the programmer (because an IDE compiles the code when a programmer writes it) about possible manipulations of collection elements.
We will also see these other types of generics:
With that, let’s start with how an object of a class that implements the List or Set interface can be created—or, in other words, the List or Set type of variable can be initialized. To demonstrate the methods of these two interfaces, we will use two classes: ArrayList (implements List) and HashSet (implements Set).
Since Java 9, the List or Set interfaces have static of() factory methods that can be used to initialize a collection, as outlined here:
Here are a few examples:
//Collection<String> coll
// = List.of("s1", null); //does not allow null
Collection<String> coll = List.of("s1", "s1", "s2");
//coll.add("s3"); //does not allow add element
//coll.remove("s1"); //does not allow remove element
//((List<String>) coll).set(1, "s3");
//does not allow modify element
System.out.println(coll); //prints: [s1, s1, s2]
//coll = Set.of("s3", "s3", "s4");
//does not allow duplicate
//coll = Set.of("s2", "s3", null);
//does not allow null
coll = Set.of("s3", "s4");
System.out.println(coll);
//prints: [s3, s4] or [s4, s3]
//coll.add("s5"); //does not allow add element
//coll.remove("s2"); //does not allow remove element
As you might expect, the factory method for Set does not allow duplicates, so we have commented the line out (otherwise, the preceding example would stop running at that line). What is less expected is that you cannot have a null element, and you cannot add/remove/modify elements of a collection after it was initialized using one of the of() methods. That’s why we have commented out some lines of the preceding example. If you need to add elements after a collection is initialized, you have to initialize it using a constructor or some other utilities that create a modifiable collection (we will see an example of Arrays.asList() shortly).
The Collection interface provides two methods for adding elements to an object that implements Collection (the parent interface of List and Set) that look like this:
Here’s an example of using the add() method:
List<String> list1 = new ArrayList<>();
list1.add("s1");
list1.add("s1");
System.out.println(list1); //prints: [s1, s1]
Set<String> set1 = new HashSet<>();
set1.add("s1");
set1.add("s1");
System.out.println(set1); //prints: [s1]
And here is an example of using the addAll() method:
List<String> list1 = new ArrayList<>();
list1.add("s1");
list1.add("s1");
System.out.println(list1); //prints: [s1, s1]
List<String> list2 = new ArrayList<>();
list2.addAll(list1);
System.out.println(list2); //prints: [s1, s1]
Set<String> set = new HashSet<>();
set.addAll(list1);
System.out.println(set); //prints: [s1]
Here is an example of the add() and addAll() methods’ functionality:
List<String> list1 = new ArrayList<>();
list1.add("s1");
list1.add("s1");
System.out.println(list1); //prints: [s1, s1]
List<String> list2 = new ArrayList<>();
list2.addAll(list1);
System.out.println(list2); //prints: [s1, s1]
Set<String> set = new HashSet<>();
set.addAll(list1);
System.out.println(set); //prints: [s1]
Set<String> set1 = new HashSet<>();
set1.add("s1");
Set<String> set2 = new HashSet<>();
set2.add("s1");
set2.add("s2");
System.out.println(set1.addAll(set2)); //prints: true
System.out.println(set1); //prints: [s1, s2]
Notice how, in the last example in the preceding code snippet, the set1.addAll(set2) method returns true, although not all elements were added. To see the case of the add() and addAll() methods returning false, look at the following example:
Set<String> set = new HashSet<>();
System.out.println(set.add("s1")); //prints: true
System.out.println(set.add("s1")); //prints: false
System.out.println(set); //prints: [s1]
Set<String> set1 = new HashSet<>();
set1.add("s1");
set1.add("s2");
Set<String> set2 = new HashSet<>();
set2.add("s1");
set2.add("s2");
System.out.println(set1.addAll(set2)); //prints: false
System.out.println(set1); //prints: [s1, s2]
The ArrayList and HashSet classes also have constructors that accept a collection, as illustrated in the following code snippet:
Collection<String> list1 = List.of("s1", "s1", "s2");
System.out.println(list1); //prints: [s1, s1, s2]
List<String> list2 = new ArrayList<>(list1);
System.out.println(list2); //prints: [s1, s1, s2]
Set<String> set = new HashSet<>(list1);
System.out.println(set); //prints: [s1, s2]
List<String> list3 = new ArrayList<>(set);
System.out.println(list3); //prints: [s1, s2]
Now, after we have learned how a collection can be initialized, we can turn to other methods in the List and Set interfaces.
The Collection interface extends the java.lang.Iterable interface, which means that classes that implement the Collection interface—directly or not—also implement the java.lang.Iterable interface. There are only three methods in the Iterable interface, as outlined here:
Iterable<String> list = List.of("s1", "s2", "s3");
System.out.println(list); //prints: [s1, s2, s3]
for(String e: list){
System.out.print(e + " "); //prints: s1 s2 s3
}
Iterable<String> list = List.of("s1", "s2", "s3");
System.out.println(list); //prints: [s1, s2, s3]
list.forEach(e -> System.out.print(e + " "));
//prints: s1 s2 s3
As we have mentioned already, the List and Set interfaces extend the Collection interface, which means that all methods of the Collection interface are inherited by List and Set. These methods are listed here:
Collection<String> list1 = List.of("s1", "s2", "s3");
System.out.println(list1); //prints: [s1, s2, s3]
Collection<String> list2 = List.of("s1", "s2", "s3");
System.out.println(list2); //prints: [s1, s2, s3]
System.out.println(list1.equals(list2));
//prints: true
Collection<String> list3 = List.of("s2", "s1", "s3");
System.out.println(list3); //prints: [s2, s1, s3]
System.out.println(list1.equals(list3));
//prints: false
Collection<String> set1 = Set.of("s1", "s2", "s3");
System.out.println(set1);
//prints: [s2, s3, s1] or different order
Collection<String> set2 = Set.of("s2", "s1", "s3");
System.out.println(set2);
//prints: [s2, s1, s3] or different order
System.out.println(set1.equals(set2));
//prints: true
Collection<String> set3 = Set.of("s4", "s1", "s3");
System.out.println(set3);
//prints: [s4, s1, s3] or different order
System.out.println(set1.equals(set3));
//prints: false
The List interface has several other methods that do not belong to any of its parent interfaces, as outlined here:
Collection<String> list = List.of("s1", "s2", "s3");
System.out.println(list); //prints: [s1, s2, s3]
List<String> list1 = List.copyOf(list);
//list1.add("s4"); //run-time error
//list1.set(1, "s5"); //run-time error
//list1.remove("s1"); //run-time error
Set<String> set = new HashSet<>();
System.out.println(set.add("s1"));
System.out.println(set); //prints: [s1]
Set<String> set1 = Set.copyOf(set);
//set1.add("s2"); //run-time error
//set1.remove("s1"); //run-time error
Set<String> set2 = Set.copyOf(list);
System.out.println(set2); //prints: [s1, s2, s3]
List<String> list = List.of("s1", "s2", "s3");
ListIterator<String> li = list.listIterator();
while(li.hasNext()){
System.out.print(li.next() + " ");
//prints: s1 s2 s3
}
while(li.hasPrevious()){
System.out.print(li.previous() + " ");
//prints: s3 s2 s1
}
ListIterator<String> li1 = list.listIterator(1);
while(li1.hasNext()){
System.out.print(li1.next() + " ");
//prints: s2 s3
}
ListIterator<String> li2 = list.listIterator(1);
while(li2.hasPrevious()){
System.out.print(li2.previous() + " ");
//prints: s1
}
List<String> list = new ArrayList<>();
list.add("S2");
list.add("s3");
list.add("s1");
System.out.println(list); //prints: [S2, s3, s1]
list.sort(String.CASE_INSENSITIVE_ORDER);
System.out.println(list); //prints: [s1, S2, s3]
//list.add(null); //causes NullPointerException
list.sort(Comparator.naturalOrder());
System.out.println(list); //prints: [S2, s1, s3]
list.sort(Comparator.reverseOrder());
System.out.println(list); //prints: [s3, s1, S2]
list.add(null);
list.sort(Comparator.nullsFirst(Comparator
.naturalOrder()));
System.out.println(list);
//prints: [null, S2, s1, s3]
list.sort(Comparator.nullsLast(Comparator
.naturalOrder()));
System.out.println(list);
//prints: [S2, s1, s3, null]
Comparator<String> comparator =
(s1, s2) -> s1 == null ? -1 : s1.compareTo(s2);
list.sort(comparator);
System.out.println(list);
//prints: [null, S2, s1, s3]
Comparator<String> comparator = (s1, s2) ->
s1 == null ? -1 : s1.compareTo(s2);
list.sort(comparator);
System.out.println(list);
//prints: [null, S2, s1, s3]
There are principally two ways to sort a list, as follows:
The Comparable interface only has a compareTo() method. In the preceding example, we have implemented the Comparator interface basing it on the Comparable interface implementation in the String class. As you can see, this implementation provided the same sort order as Comparator.nullsFirst(Comparator.naturalOrder()). This style of implementation is called functional programming, which we will discuss in more detail in Chapter 13, Functional Programming.
The Set interface has the following methods that do not belong to any of its parent interfaces:
The Map interface has many methods similar to the List and Set methods, as listed here:
The Map interface, however, does not extend Iterable, Collection, or any other interface, for that matter. It is designed to be able to store values by their keys. Each key is unique, while several equal values can be stored with different keys on the same map. The combination of key and value constitutes Entry, which is an internal interface of Map. Both value and key objects must implement the equals() method. A key object must also implement the hashCode() method.
Many methods of the Map interface have exactly the same signature and functionality as in the List and Set interfaces, so we are not going to repeat them here. We will only walk through the Map-specific methods, as follows:
The following Map methods are much too complicated for the scope of this book, so we are just mentioning them for the sake of completeness. They allow multiple values to be combined or calculated and aggregated in a single existing value in the Map interface, or a new one to be created:
This last group of computing and merging methods is rarely used. The most popular, by far, are the V put(K key, V value) and V get(Object key) methods, which allow the use of the main Map function of storing key-value pairs and retrieving the value using the key. The Set<K> keySet() method is often used for iterating over the map’s key-value pairs, although the entrySet() method seems a more natural way of doing that. Here is an example:
Map<Integer, String> map = Map.of(1, "s1", 2, "s2", 3, "s3");
for(Integer key: map.keySet()){
System.out.print(key + ", " + map.get(key) + ", ");
//prints: 3, s3, 2, s2, 1, s1,
}
for(Map.Entry e: map.entrySet()){
System.out.print(e.getKey() + ", " + e.getValue() + ", ");
//prints: 2, s2, 3, s3, 1, s1,
}
The first of the for loops in the preceding code example uses a more widespread way to access the key-pair values of a map by iterating over the keys. The second for loop iterates over the set of entries, which (in our opinion) is a more natural way to do it. Notice that the printed-out values are not in the same order we have put them in the map. That is because, since Java 9, unmodifiable collections (that is, what of() factory methods produce) have added randomization to the order of Set elements, which changes the order of elements between different code executions. Such a design was done to make sure a programmer does not rely on a certain order of Set elements, which is not guaranteed for a set.
Please note that collections produced by of() factory methods used to be called immutable in Java 9, and unmodifiable since Java 10. That is because immutable implies that you cannot change anything in collections, while, in fact, collection elements can be changed if they are modifiable objects. For example, let’s build a collection of objects of the Person1 class, as follows:
class Person1 {
private int age;
private String name;
public Person1(int age, String name) {
this.age = age;
this.name = name == null ? "" : name;
}
public void setName(String name){ this.name = name; }
@Override
public String toString() {
return "Person{age=" + age +
", name=" + name + "}";
}
}
In the following code snippet, for simplicity, we will create a list with one element only and will then try to modify the element:
Person1 p1 = new Person1(45, "Bill");
List<Person1> list = List.of(p1);
//list.add(new Person1(22, "Bob"));
//UnsupportedOperationException
System.out.println(list);
//prints: [Person{age=45, name=Bill}]
p1.setName("Kelly");
System.out.println(list);
//prints: [Person{age=45, name=Kelly}]
As you can see, although it is not possible to add an element to the list created by the of() factory method, its element can still be modified if a reference to the element exists outside the list.
There are two classes with static methods handling collections that are very popular and helpful, as follows:
The fact that the methods are static means they do not depend on the object state, so they are also called stateless methods or utility methods.
Many methods in the Collections class manage collections and analyze, sort, and compare them. There are more than 70 of them, so we won’t have a chance to talk about all of them. Instead, we are going to look at the ones most often used by mainstream application developers, as follows:
List<String> list1 = Arrays.asList("s1","s2");
List<String> list2 = Arrays.asList("s3", "s4", "s5");
Collections.copy(list2, list1);
System.out.println(list2); //prints: [s1, s2, s5]
//List<String> list =
//List.of("a", "X", "10", "20", "1", "2");
List<String> list =
Arrays.asList("a", "X", "10", "20", "1", "2");
Collections.sort(list);
System.out.println(list);
//prints: [1, 10, 2, 20, X, a]
Note that we could not use the List.of() method to create a list because the list would be unmodifiable and its order could not be changed. Also, look at the resulting order: numbers come first, then capital letters, followed by lowercase letters. That is because the compareTo() method in the String class uses code points of the characters to establish the order. Here is the code that demonstrates this:
List<String> list =
Arrays.asList("a", "X", "10", "20", "1", "2");
Collections.sort(list);
System.out.println(list); //prints: [1, 10, 2, 20, X, a]
list.forEach(s -> {
for(int i = 0; i < s.length(); i++){
System.out.print(" " +
Character.codePointAt(s, i));
}
if(!s.equals("a")) {
System.out.print(",");
//prints: 49, 49 48, 50, 50 48, 88, 97
}
});
As you can see, the order is defined by the value of the code points of the characters that compose the string.
class Person {
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name == null ? "" : name;
}
public int getAge() { return this.age; }
public String getName() { return this.name; }
@Override
public String toString() {
return "Person{name=" + name +
", age=" + age + "}";
}
}
class ComparePersons implements Comparator<Person> {
public int compare(Person p1, Person p2){
int result = p1.getName().compareTo(p2.getName());
if (result != 0) { return result; }
return p1.age - p2.getAge();
}
}
Now, we can use the Person and ComparePersons classes, as follows:
List<Person> persons =
Arrays.asList(new Person(23, "Jack"),
new Person(30, "Bob"),
new Person(15, "Bob"));
Collections.sort(persons, new ComparePersons());
System.out.println(persons);
//prints: [Person{name=Bob, age=15},
// Person{name=Bob, age=30},
// Person{name=Jack, age=23}]
As we have mentioned already, there are many more utilities in the Collections class, so we recommend you look through the related documentation at least once and understand all its capabilities.
The org.apache.commons.collections4.CollectionUtils class in the Apache Commons project contains static stateless methods that complement the methods of the java.util.Collections class. They help to search, process, and compare Java collections.
To use this class, you would need to add the following dependency to the Maven pom.xml configuration file:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
There are many methods in this class, and more methods will probably be added over time. These utilities are created in addition to Collections methods, so they are more complex and nuanced and do not fit the scope of this book. To give you an idea of the methods available in the CollectionUtils class, here is a brief description of the methods, grouped according to their functionality:
This last method should probably belong to the utility class that handles arrays, and that is what we are going to discuss now.
There are two classes with static methods handling collections that are very popular and helpful, as follows:
We will briefly review each of them.
We have already used the java.util.Arrays class several times. It is the primary utility class for array management. This utility class used to be very popular because of the asList(T...a) method. It was the most compact way of creating and initializing a collection and is shown in the following snippet:
List<String> list = Arrays.asList("s0", "s1");
Set<String> set = new HashSet<>(Arrays.asList("s0", "s1");
It is still a popular way of creating a modifiable list—we used it, too. However, after a List.of() factory method was introduced, the Arrays class declined substantially.
Nevertheless, if you need to manage arrays, then the Arrays class may be a big help. It contains more than 160 methods, and most of them are overloaded with different parameters and array types. If we group them by the method name, there will be 21 groups, and if we further group them by functionality, only the following 10 groups will cover all the Arrays class functionality:
All of these methods are helpful, but we would like to draw your attention to the equals(a1, a2) and deepEquals(a1, a2) methods. They are particularly helpful for array comparison because an array object cannot implement an equals() custom method and uses the implementation of the Object class instead (which compares only references). The equals(a1, a2) and deepEquals(a1, a2) methods allow a comparison of not just a1 and a2 references, but use the equals() method to compare elements as well. Here is a code example to demonstrate how these methods work:
String[] arr1 = {"s1", "s2"};
String[] arr2 = {"s1", "s2"};
System.out.println(arr1.equals(arr2)); //prints: false
System.out.println(Arrays.equals(arr1, arr2));
//prints: true
System.out.println(Arrays.deepEquals(arr1, arr2));
//prints: true
String[][] arr3 = {{"s1", "s2"}};
String[][] arr4 = {{"s1", "s2"}};
System.out.println(arr3.equals(arr4)); //prints: false
System.out.println(Arrays.equals(arr3, arr4));
//prints: false
System.out.println(Arrays.deepEquals(arr3, arr4));
//prints: true
As you can see, Arrays.deepEquals() returns true every time two equal arrays are compared when every element of one array equals the element of another array in the same position, while the Arrays.equals() method does the same, but for one-dimensional (1D) arrays only.
The org.apache.commons.lang3.ArrayUtils class complements the java.util.Arrays class by adding new methods to the array managing the toolkit and the ability to handle null in cases when, otherwise, NullPointerException could be thrown. To use this class, you would need to add the following dependency to the Maven pom.xml configuration file:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
The ArrayUtils class has around 300 overloaded methods that can be collected in the following 12 groups:
The following two utilities are described in this section:
They are especially useful during class creation, so we will concentrate largely on methods related to this task.
The Objects class has only 17 methods that are all static. Let’s look at some of them while applying them to the Person class. Let’s assume this class will be an element of a collection, which means it has to implement the equals() and hashCode() methods. The code is illustrated in the following snippet:
class Person {
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge(){ return this.age; }
public String getName(){ return this.name; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
if(!(o instanceof Person)) return false;
Person = (Person)o;
return age == person.getAge() &&
Objects.equals(name, person.getName());
}
@Override
public int hashCode(){
return Objects.hash(age, name);
}
}
Notice that we do not check the name property for null because Object.equals() does not break when any of the parameters is null. It just does the job of comparing objects. If only one of them is null, it returns false. If both are null, it returns true.
Using Object.equals() is a safe way to implement the equals() method; however, if you need to compare objects that may be arrays, it is better to use the Objects.deepEquals() method because it not only handles null, as the Object.equals() method does, but also compares values of all array elements, even if the array is multidimensional, as illustrated here:
String[][] x1 = {{"a","b"},{"x","y"}};
String[][] x2 = {{"a","b"},{"x","y"}};
String[][] y = {{"a","b"},{"y","y"}};
System.out.println(Objects.equals(x1, x2));
//prints: false
System.out.println(Objects.equals(x1, y));
//prints: false
System.out.println(Objects.deepEquals(x1, x2));
//prints: true
System.out.println(Objects.deepEquals(x1, y));
//prints: false
The Objects.hash() method handles null values too. One important thing to remember is that the list of properties compared in the equals() method has to match the list of properties passed into Objects.hash() as parameters. Otherwise, two equal Person objects will have different hash values, which makes hash-based collections work incorrectly.
Another thing worth noticing is that there is another hash-related Objects.hashCode() method that accepts only one parameter, but the value it generates is not equal to the value generated by Objects.hash() with only one parameter. Observe the following, for example:
System.out.println(Objects.hash(42) ==
Objects.hashCode(42)); //prints: false
System.out.println(Objects.hash("abc") ==
Objects.hashCode("abc")); //prints: false
To avoid this caveat, always use Objects.hash().
Another potential source of confusion is demonstrated in the following code snippet:
System.out.println(Objects.hash(null)); //prints: 0
System.out.println(Objects.hashCode(null)); //prints: 0
System.out.println(Objects.hash(0)); //prints: 31
System.out.println(Objects.hashCode(0)); //prints: 0
As you can see, the Objects.hashCode() method generates the same hash value for null and 0, which can be problematic for some algorithms based on the hash value.
static <T> int compare (T a, T b, Comparator<T> c) is another popular method that returns 0 (if the arguments are equal); otherwise, it returns the result of c.compare(a, b). It is very useful for implementing the Comparable interface (establishing a natural order for custom object sorting). Observe the following, for example:
class Person implements Comparable<Person> {
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge(){ return this.age; }
public String getName(){ return this.name; }
@Override
public int compareTo(Person p){
int result = Objects.compare(name, p.getName(),
Comparator.naturalOrder());
if (result != 0) {
return result;
}
return Objects.compare(age, p.getAge(),
Comparator.naturalOrder());
}
}
This way, you can easily change the sorting algorithm by setting the Comparator.reverseOrder() value or by adding Comparator.nullFirst() or Comparator.nullLast().
Also, the Comparator implementation we used in the previous section can be made more flexible by using the Objects.compare() method, as follows:
class ComparePersons implements Comparator<Person> {
public int compare(Person p1, Person p2){
int result = Objects.compare(p1.getName(),
p2.getName(), Comparator.naturalOrder());
if (result != 0) {
return result;
}
return Objects.compare(p1.getAge(), p2.getAge(),
Comparator.naturalOrder());
}
}
Finally, the last two methods of the Objects class that we are going to discuss are methods that generate a string representation of an object. They come in handy when you need to call a toString() method on an object but are not sure whether the object reference is null. Observe the following, for example:
List<String> list = Arrays.asList("s1", null);
for(String e: list){
//String s = e.toString(); //NullPointerException
}
In the preceding example, we know the exact value of each element; however, imagine a scenario where the list is passed into the method as a parameter. Then, we are forced to write something like this:
void someMethod(List<String> list){
for(String e: list){
String s = e == null ? "null" : e.toString();
}
This doesn’t seem to be a big deal. But after writing such code a dozen times, a programmer naturally thinks about some kind of utility method that does all of that, and that is when the following two methods of the Objects class help:
The following code snippet demonstrates these two methods:
List<String> list = Arrays.asList("s1", null);
for(String e: list){
String s = Objects.toString(e);
System.out.print(s + " "); //prints: s1 null
}
for(String e: list){
String s = Objects.toString(e, "element was null");
System.out.print(s + " ");
//prints: s1 element was null
}
As of the time of writing, the Objects class has 17 methods. We recommend you become familiar with them so as to avoid writing your own utilities in the event that the same utility already exists.
The last statement of the previous section applies to the org.apache.commons.lang3.ObjectUtils class of the Apache Commons library that complements the methods of the java.util.Objects class described in the preceding section. The scope of this book and its allotted size does not allow for a detailed review of all the methods under the ObjectUtils class, so we will describe them briefly in groups according to their related functionality. To use this class, you would need to add the following dependency to the Maven pom.xml configuration file:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
All the methods of the ObjectUtils class can be organized into seven groups, as follows:
There are many classes in the java.time package and its sub-packages. They were introduced as a replacement for other (older packages) that handled date and time. The new classes are thread-safe (hence, better suited for multithreaded processing), and what is also important is that they are more consistently designed and easier to understand. Also, the new implementation follows International Organization for Standardization (ISO) standards as regards date and time formats, but allows any other custom format to be used as well.
We will describe the following five main classes and demonstrate how to use them:
All these and other classes of the java.time package, as well as its sub-packages, are rich in various functionality that covers all practical cases. But we are not going to discuss all of them; we will just introduce the basics and the most popular use cases.
The LocalDate class does not carry time. It represents a date in ISO 8601 format (yyyy-MM-dd) and is shown in the following code snippet:
System.out.println(LocalDate.now());
//prints: current date in format yyyy-MM-dd
That is the current date in this location at the time of writing. The value was picked up from the computer clock. Similarly, you can get the current date in any other time zone using that static now(ZoneId zone) method. A ZoneId object can be constructed using the static ZoneId.of(String zoneId) method, where String zoneId is any of the string values returned by the ZoneId.getAvailableZoneIds() method, as illustrated in the following code snippet:
Set<String> zoneIds = ZoneId.getAvailableZoneIds();
for(String zoneId: zoneIds){
System.out.println(zoneId);
}
The preceding code prints almost 600 time zone identifiers (IDs). Here are a few of them:
Asia/Aden
Etc/GMT+9
Africa/Nairobi
America/Marigot
Pacific/Honolulu
Australia/Hobart
Europe/London
America/Indiana/Petersburg
Asia/Yerevan
Europe/Brussels
GMT
Chile/Continental
Pacific/Yap
CET
Etc/GMT-1
Canada/Yukon
Atlantic/St_Helena
Libya
US/Pacific-New
Cuba
Israel
GB-Eire
GB
Mexico/General
Universal
Zulu
Iran
Navajo
Egypt
Etc/UTC
SystemV/AST4ADT
Asia/Tokyo
Let’s try to use "Asia/Tokyo", for example, as follows:
ZoneId = ZoneId.of("Asia/Tokyo");
System.out.println(LocalDate.now(zoneId));
//prints: current date in Tokyo in format yyyy-MM-dd
A LocalDate object can represent any date in the past, or in the future too, using the following methods:
The following code snippet demonstrates the methods listed in the preceding bullets:
LocalDate lc1 = LocalDate.parse("2023-02-23");
System.out.println(lc1); //prints: 2023-02-23
LocalDate lc2 = LocalDate.parse("20230223",
DateTimeFormatter.BASIC_ISO_DATE);
System.out.println(lc2); //prints: 2023-02-23
DateTimeFormatter frm =
DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate lc3 = LocalDate.parse("23/02/2023", frm);
System.out.println(lc3); //prints: 2023-02-23
LocalDate lc4 = LocalDate.of(2023, 2, 23);
System.out.println(lc4); //prints: 2023-02-23
LocalDate lc5 = LocalDate.of(2023, Month.FEBRUARY, 23);
System.out.println(lc5); //prints: 2023-02-23
LocalDate lc6 = LocalDate.ofYearDay(2023, 54);
System.out.println(lc6); //prints: 2023-02-23
A LocalDate object can provide various values, as illustrated in the following code snippet:
LocalDate lc = LocalDate.parse("2023-02-23");
System.out.println(lc); //prints: 2023-02-23
System.out.println(lc.getYear()); //prints: 2023
System.out.println(lc.getMonth()); //prints: FEBRUARY
System.out.println(lc.getMonthValue()); //prints: 2
System.out.println(lc.getDayOfMonth()); //prints: 23
System.out.println(lc.getDayOfWeek()); //prints: THURSDAY
System.out.println(lc.isLeapYear()); //prints: false
System.out.println(lc.lengthOfMonth()); //prints: 28
System.out.println(lc.lengthOfYear()); //prints: 365
A LocalDate object can be modified, like this:
LocalDate lc = LocalDate.parse("2023-02-23");
System.out.println(lc.withYear(2024)); //prints: 2024-02-23
System.out.println(lc.withMonth(5)); //prints: 2023-05-23
System.out.println(lc.withDayOfMonth(5)); //prints: 2023-02-05
System.out.println(lc.withDayOfYear(53)); //prints: 2023-02-22
System.out.println(lc.plusDays(10)); //prints: 2023-03-05
System.out.println(lc.plusMonths(2)); //prints: 2023-04-23
System.out.println(lc.plusYears(2)); //prints: 2025-02-23
System.out.println(lc.minusDays(10)); //prints: 2023-02-13
System.out.println(lc.minusMonths(2)); //prints: 2022-12-23
System.out.println(lc.minusYears(2)); //prints: 2021-02-23
A LocalDate object can be compared, like this:
LocalDate lc1 = LocalDate.parse("2023-02-23");
LocalDate lc2 = LocalDate.parse("2023-02-22");
System.out.println(lc1.isAfter(lc2)); //prints: true
System.out.println(lc1.isBefore(lc2)); //prints: false
There are many other helpful methods in the LocalDate class. If you have to work with dates, we recommend that you read the application programming interface (API) of this class and other classes of the java.time package and its sub-packages.
The LocalTime class contains time without a date. It has similar methods to the methods of the LocalDate class. Here is how an object of the LocalTime class can be created:
System.out.println(LocalTime.now()); //prints: 21:15:46.360904
ZoneId = ZoneId.of("Asia/Tokyo");
System.out.println(LocalTime.now(zoneId));
//prints: 12:15:46.364378
LocalTime lt1 = LocalTime.parse("20:23:12");
System.out.println(lt1); //prints: 20:23:12
LocalTime lt2 = LocalTime.of(20, 23, 12);
System.out.println(lt2); //prints: 20:23:12
Each component of time value can be extracted from a LocalTime object, as follows:
LocalTime lt2 = LocalTime.of(20, 23, 12);
System.out.println(lt2); //prints: 20:23:12
System.out.println(lt2.getHour()); //prints: 20
System.out.println(lt2.getMinute()); //prints: 23
System.out.println(lt2.getSecond()); //prints: 12
System.out.println(lt2.getNano()); //prints: 0
An object of the LocalTime class can be modified, as follows:
LocalTime lt2 = LocalTime.of(20, 23, 12);
System.out.println(lt2.withHour(3)); //prints: 03:23:12
System.out.println(lt2.withMinute(10)); //prints: 20:10:12
System.out.println(lt2.withSecond(15)); //prints: 20:23:15
System.out.println(lt2.withNano(300));
//prints: 20:23:12.000000300
System.out.println(lt2.plusHours(10)); //prints: 06:23:12
System.out.println(lt2.plusMinutes(2)); //prints: 20:25:12
System.out.println(lt2.plusSeconds(2)); //prints: 20:23:14
System.out.println(lt2.plusNanos(200));
//prints: 20:23:12.000000200
System.out.println(lt2.minusHours(10)); //prints: 10:23:12
System.out.println(lt2.minusMinutes(2)); //prints: 20:21:12
System.out.println(lt2.minusSeconds(2)); //prints: 20:23:10
System.out.println(lt2.minusNanos(200));
//prints: 20:23:11.999999800
And two objects of the LocalTime class can also be compared, as follows:
LocalTime lt2 = LocalTime.of(20, 23, 12);
LocalTime lt4 = LocalTime.parse("20:25:12");
System.out.println(lt2.isAfter(lt4)); //prints: false
System.out.println(lt2.isBefore(lt4)); //prints: true
There are many other helpful methods in the LocalTime class. If you have to work with dates, we recommend that you read the API of this class and other classes of the java.time package and its sub-packages.
The LocalDateTime class contains both the date and time and has all the methods the LocalDate and LocalTime classes have, so we are not going to repeat them here. We will only show how an object of the LocalDateTime class can be created, as follows:
System.out.println(LocalDateTime.now());
//prints: 2019-03-04T21:59:00.142804
ZoneId = ZoneId.of("Asia/Tokyo");
System.out.println(LocalDateTime.now(zoneId));
//prints: 2019-03-05T12:59:00.146038
LocalDateTime ldt1 =
LocalDateTime.parse("2020-02-23T20:23:12");
System.out.println(ldt1); //prints: 2020-02-23T20:23:12
DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss");
LocalDateTime ldt2 =
LocalDateTime.parse("23/02/2020 20:23:12", formatter);
System.out.println(ldt2); //prints: 2020-02-23T20:23:12
LocalDateTime ldt3 =
LocalDateTime.of(2020, 2, 23, 20, 23, 12);
System.out.println(ldt3); //prints: 2020-02-23T20:23:12
LocalDateTime ldt4 =
LocalDateTime.of(2020, Month.FEBRUARY, 23, 20, 23, 12);
System.out.println(ldt4); //prints: 2020-02-23T20:23:12
LocalDate ld = LocalDate.of(2020, 2, 23);
LocalTime lt = LocalTime.of(20, 23, 12);
LocalDateTime ldt5 = LocalDateTime.of(ld, lt);
System.out.println(ldt5); //prints: 2020-02-23T20:23:12
There are many other helpful methods in the LocalDateTime class. If you have to work with dates, we recommend that you read the API of this class and other classes of the java.time package and its sub-packages.
The java.time.Period and java.time.Duration classes are designed to contain an amount of time, as outlined here:
The following code snippet demonstrates their creation and usage using the LocalDateTime class, but the same methods exist in the LocalDate (for Period) and LocalTime (for Duration) classes:
LocalDateTime ldt1 = LocalDateTime.parse("2023-02-23T20:23:12");
LocalDateTime ldt2 = ldt1.plus(Period.ofYears(2));
System.out.println(ldt2); //prints: 2025-02-23T20:23:12
The following methods work the same way as the methods of the LocalTime class:
LocalDateTime ldt = LocalDateTime.parse("2023-02-23T20:23:12");
ldt.minus(Period.ofYears(2));
ldt.plus(Period.ofMonths(2));
ldt.minus(Period.ofMonths(2));
ldt.plus(Period.ofWeeks(2));
ldt.minus(Period.ofWeeks(2));
ldt.plus(Period.ofDays(2));
ldt.minus(Period.ofDays(2));
ldt.plus(Duration.ofHours(2));
ldt.minus(Duration.ofHours(2));
ldt.plus(Duration.ofMinutes(2));
ldt.minus(Duration.ofMinutes(2));
ldt.plus(Duration.ofMillis(2));
ldt.minus(Duration.ofMillis(2));
Some other methods of creating and using Period objects are demonstrated in the following code snippet:
LocalDate ld1 = LocalDate.parse("2023-02-23");
LocalDate ld2 = LocalDate.parse("2023-03-25");
Period = Period.between(ld1, ld2);
System.out.println(period.getDays()); //prints: 2
System.out.println(period.getMonths()); //prints: 1
System.out.println(period.getYears()); //prints: 0
System.out.println(period.toTotalMonths()); //prints: 1
period = Period.between(ld2, ld1);
System.out.println(period.getDays()); //prints: -2
Duration objects can be similarly created and used, as illustrated in the following code snippet:
LocalTime lt1 = LocalTime.parse("10:23:12");
LocalTime lt2 = LocalTime.parse("20:23:14");
Duration = Duration.between(lt1, lt2);
System.out.println(duration.toDays()); //prints: 0
System.out.println(duration.toHours()); //prints: 10
System.out.println(duration.toMinutes()); //prints: 600
System.out.println(duration.toSeconds()); //prints: 36002
System.out.println(duration.getSeconds()); //prints: 36002
System.out.println(duration.toNanos());
//prints: 36002000000000
System.out.println(duration.getNano()); //prints: 0.
There are many other helpful methods in Period and Duration classes. If you have to work with dates, we recommend that you read the API of this class and other classes of the java.time package and its sub-packages.
Java 16 includes a new time format that shows a period of the day as AM, in the morning, and similar. The following two methods demonstrate usage of the DateTimeFormatter.ofPattern() method with the LocalDateTime and LocalTime classes:
void periodOfDayFromDateTime(String time, String pattern){
LocalDateTime date = LocalDateTime.parse(time);
DateTimeFormatter frm =
DateTimeFormatter.ofPattern(pattern);
System.out.print(date.format(frm));
}
void periodOfDayFromTime(String time, String pattern){
LocalTime date = LocalTime.parse(time);
DateTimeFormatter frm =
DateTimeFormatter.ofPattern(pattern);
System.out.print(date.format(frm));
}
The following code demonstrates the effect of "h a" and "h B" patterns:
periodOfDayFromDateTime("2023-03-23T05:05:18.123456",
"MM-dd-yyyy h a"); //prints: 03-23-2023 5 AM
periodOfDayFromDateTime("2023-03-23T05:05:18.123456",
"MM-dd-yyyy h B"); //prints: 03-23-2023 5 at night
periodOfDayFromDateTime("2023-03-23T06:05:18.123456",
"h B"); //prints: 6 in the morning
periodOfDayFromTime("11:05:18.123456", "h B");
//prints: 11 in the morning
periodOfDayFromTime("12:05:18.123456", "h B");
//prints: 12 in the afternoon
periodOfDayFromTime("17:05:18.123456", "h B");
//prints: 5 in the afternoon
periodOfDayFromTime("18:05:18.123456", "h B");
//prints: 6 in the evening
periodOfDayFromTime("20:05:18.123456", "h B");
//prints: 8 in the evening
periodOfDayFromTime("21:05:18.123456", "h B");
//prints: 9 at night
You can use "h a" and "h B" patterns to make the time presentation more user-friendly.
This chapter introduced you to the Java collections framework and its three main interfaces: List, Set, and Map. Each of the interfaces was discussed and its methods were demonstrated with one of the implementing classes. The generics were explained and demonstrated as well. The equals() and hashCode() methods have to be implemented in order for an object to be capable of being handled by Java collections correctly.
The Collections and CollectionUtils utility classes have many useful methods for collection handling and were presented in examples, along with the Arrays, ArrayUtils, Objects, and ObjectUtils classes.
The class methods of the java.time package allow time/date values to be managed and were demonstrated in specific practical code snippets.
You can now use all the main data structures we talked about in this chapter in your programs.
In the next chapter, we will overview JCL and some external libraries, including those that support testing. Specifically, we will explore the org.junit, org.mockito, org.apache.log4j, org.slf4j, and org.apache.commons packages and their sub-packages.
List<String> list1 = Arrays.asList("s1","s2", "s3");
List<String> list2 = Arrays.asList("s3", "s4");
Collections.copy(list1, list2);
System.out.println(list1);
Integer[][] ar1 = {{42}};
Integer[][] ar2 = {{42}};
System.out.print(Arrays.equals(ar1, ar2) + " ");
System.out.println(Arrays.deepEquals(arr3, arr4));
String[] arr1 = { "s1", "s2" };
String[] arr2 = { null };
String[] arr3 = null;
System.out.print(ArrayUtils.getLength(arr1) + " ");
System.out.print(ArrayUtils.getLength(arr2) + " ");
System.out.print(ArrayUtils.getLength(arr3) + " ");
System.out.print(ArrayUtils.isEmpty(arr2) + " ");
System.out.print(ArrayUtils.isEmpty(arr3));
String str1 = "";
String str2 = null;
System.out.print((Objects.hash(str1) ==
Objects.hashCode(str2)) + " ");
System.out.print(Objects.hash(str1) + " ");
System.out.println(Objects.hashCode(str2) + " ");
String[] arr = {"c", "x", "a"};
System.out.print(ObjectUtils.min(arr) + " ");
System.out.print(ObjectUtils.median(arr) + " ");
System.out.println(ObjectUtils.max(arr));
LocalDate lc = LocalDate.parse("1900-02-23");
System.out.println(lc.withYear(21));
LocalTime lt2 = LocalTime.of(20, 23, 12);
System.out.println(lt2.withNano(300));
LocalDate ld = LocalDate.of(2020, 2, 23);
LocalTime lt = LocalTime.of(20, 23, 12);
LocalDateTime ldt = LocalDateTime.of(ld, lt);
System.out.println(ldt);
LocalDateTime ldt =
LocalDateTime.parse("2020-02-23T20:23:12");
System.out.print(ldt.minus(Period.ofYears(2)) + " ");
System.out.print(ldt.plus(Duration.ofMinutes(12)) + " ");
System.out.println(ldt);