This course provides you with a deep dive into the Java Collection API and many of the available collection implementations. We’ll review each of the key collection interfaces and their associated implementations.
Learning Objectives
In this course you'll learn:
- What Collections are and when and why you would implement them
- The Collection API and hierarchy of interfaces
- The key differences between a Set, List, and Queue
- How to perform sorting within a collection
- How to implement comparators
- How to pick and implement the right collection for a particular requirement
Prerequisites
- A basic understanding of the Java programming language
- A basic understanding of software development
- A basic understanding of the software development life cycle
Intended Audience
- Software Engineers interested in advancing their Java skills
- Software Architects interested in using advanced features of Java to design and build both applications and frameworks
- Anyone interested in advanced Java application development and associated tooling
- Anyone interested in understanding the advanced areas and features of the Java SDK
- [Instructor] Okay, welcome back. In this lecture, we'll dive deeper into developing and working with collections within Java. In particular, we'll review the following topics. Collecting and sorting, comparators, using the right collection for the right job, and lambda expressions within collections. At the conclusion of this lecture, you should be able to perform each of the items listed above. Take a moment to rate yourself on each of these items on a scale of one through to five. At the conclusion of this lecture, these objectives will be reviewed. You should rate yourself again to see how much benefit you received from this lecture. The Collections class. There is a class called collections. Note, this is different and separate from the interface called collection. The collections class provides a large number of static utility methods. These methods provide all sorts of useful operations on different types of collections. For example, any collection of type list can be sorted and shuffled. In addition, any collection type can be wrapped inside an unmodifiable wrapper or wrapped inside a collection that adds thread synchronization. You may want to look at the full API of this class to get a better idea of the services available. Optional methods in collection interfaces. In the standard interfaces, there are a number of methods that are defined as optional. This is indicated by the fact that in the interface, the methods are declared as possibly throwing the operation unsupported exception. While it may seem to be the lazy way of creating a framework for collection implementations, there is actually value added to the framework because of this feature. For example, you may wish to create your own collection, yet do not want to provide the ability to remove elements once they are added. The remove method is optional and you can write your implementation such that the remove method throws this exception when it is called. The thing to remember is that this is a runtime exception and developers using an API with optional methods may wish to place such calls in a try/catch block for this exception. In practice, a developer would read the documentation for the implementation and realize which methods are optional, to determine whether that implementation is suitable for their application. Optional methods. It is the JavaDoc documentation that specifically declares several methods as optional operation. These methods will throw the unsupported operation exception if the collection does not support the operation. The set interface is a direct extension of collection and does not add any methods. Any implementation class that implements the set interface is guaranteeing that the elements in the collection are unique according to the equals method of the elements themselves. The SortedSet interface. A set that maintains its elements in order would implement this interface to indicate that the elements are sorted and to provide additional methods to access the elements in sorted order. While sort order is primarily defined by the elements themselves, a sorted set can bypass that mechanism using its own sorter. The comparable interface. Elements that will be maintained in sorted order will ideally implement this interface. It contains a single method which allows it to compare itself to another object, usually of the same type. Note that many standard classes, such as string and the primitive wrapper classes, implement this interface. When elements of this type are placed in a sorted set, the sorted set can ask each element if it is greater than or less than the adjacent element. The generic comparable interface allows for the definition of a comparable implementation that is specific for an item type. When the generic type is omitted, the type of the other object needs to be checked and downcasted to the appropriate type before the actual code of comparing instance variables can be done. The comparator interface. A sorted set may not wish to rely on the elements to determine their own sort order, or the elements may not implement the comparable interface. In both cases, the sorted set will have to rely on an implementation of comparator to perform these comparisons. Notice that the compare method takes two objects to compare. Similar to the comparable interface, the types of the two parameters need to be checked and downcasted when the generic type of the comparator is not specified. Until Java 8, creating a comparator required the definition of a new class that implements this interface and providing an implementation of the compare method. A lot of the implementations did a simple comparison of one or maybe two fields of the objects to be compared. Java 8 introduced several static methods on the comparator interface to simplify the creation of the comparator instances. The comparing method, for example, takes a function. The function X is a sorting key extractor and the resulting comparator uses the natural ordering of the provided key to determine the ordering. Overloaded methods even allow for an external comparator to be used to define the sorting order of the extracted keys. In the example shown here, a string comparator, defined as a constant on the string class, is used. The nulls first and nulls last methods can be used to wrap existing comparators into null-safe wrappers, defining the location of these null values in the result. The comparator interface also received several new methods. All of these methods have been defined as default methods in order to make sure that existing implementations of this interface do not have to be updated. For the most part, these default implementations contain implementations that can be used out of the box. The reversed method can be used to create a comparator based on another comparator, but this time resulting in a reversed sorting order. The then comparing methods can be used to define a multi-level comparator. When the result of the first comparator is zero, elements are equal, the second comparator is applied. We have seen that the static comparing method can be used to define a function to define the sorting order. A then comparing method may also accept a function to define the second level of the comparison. In the example shown here, the flight elements are first sorted by destination. For a single destination, the flights are then sorted by departure time. The navigable set interface represents a navigable set in the Java collection framework. A navigable set interface inherits from the SortedSet interface. It behaves like a sorted set with the exception that we have navigation methods available in addition to the sorting mechanisms of the sorted set. For example, it provides the methods lower, floor, ceiling, and higher for finding the closest match to a search target. The queue interface. Queues provide insertion, extraction, and inspection operations for processing elements. The add method, defined by the collection interface, inserts an element and throws an unchecked exception when the element cannot be inserted. The offer method inserts an element if possible and returns false otherwise. Both the remove and poll methods remove and return the head of the queue. When the queue is empty, the remove method will throw an exception, while the poll method returns null. When using the element and peek methods, an element is returned but not removed from the head of the queue. Even though most queue implementations work on a first-in, first-out, or FIFO, principle, the actual ordering of the elements in the queue depends on the implementation class that is being used. Where a linked list implementation maintains the insertion order, when a priority queue is used the elements are ordered using either their natural ordering or using an external comparator implementation. Map interfaces. While presented as one of the three high-level interfaces in the collections framework, this interface actually does not extend collection directly. As an interface that represents storage of key/value pairs, it really is not a collection. In fact, a map is really a pair of collections, a collection of keys and a collection of values. This concept is supported by the fact that map provides methods to access both the keys and the values as collections. The Hashtable class is an implementation of this interface. SortedMap extends map and guarantees that the keys are maintained in sorted order. The Hashtable class cannot implement this interface because the hashing mechanism is implicitly reordering the storage for faster access. Creating a generic map means that you can define the type of the key and the type of the value of objects stored in the map. The declaration and the instantiation of a generic map is only different to other types of collections, such as list and set, and that we have to define two types, one type for the key and the other type for the value. Now, map stores only object references, and it is for this reason that it's impossible to use primitive data types like double or int. Instead, wrapper classes, like integer or double, must be used instead. Functional programming on collections. With the introduction of lambda expressions and method references in Java 8, the collection API was expanded to provide a large number of methods that allow and use the definitions of functions. The iterable interface. In Java 5, the iterable interface was added to the JDK. Classes that implement this interface can become a source for use in the for-each loop, also introduced in Java 5. As a result, the iterable interface became the superinterface of the collection interface. Keep in mind that the iterable interface is not part of the collection API. Other interfaces, like BeanContext, DirectoryStream, and Path, all of which are not part of the collection API, are also subinterfaces. With the introduction of lambda expressions, method references, and the new stream API, the collection API could now be updated to support these new features. Throughout this lecture, we will introduce some of the new methods that have been added to the collection API. With the introduction of the new forEach method, a consumer can be defined that will be applied to each element in the collection. The example shown here prints each element in the collection. The removeIf method allows for the definition of a predicate. All collection entries that match the given predicate will be removed from the collection. The list interface has also received new methods. The sort method has been moved from the collections class into the list interface. Since Java 8, the Collections.sort method actually invokes the List.sort method in order to sort the elements. With the introduction of lambda expressions, it now has also become easier to define a function that should be applied to elements within the list. As a result, a replaceAll method has been defined on the list interface. The UnaryOperator allows for replacement objects to be created. Immutable lists and sets. The collections class contains a method to create unmodifiable instances. As can be seen here, we're creating a list of employees using the Collections.unmodifiableList method. Java 9 added several convenience factory methods. Both the list and set interfaces now have a static of method, simplifying the creation of immutable instances. Immutable maps. In addition to the list and set interface updates, the map interface also received several helper static factory methods. Naturally, these methods accept both the key and the value of each entry. The map interface also provides a static ofEntries method, which accepts Map.Entry instances. Using the right collection. The collection API defines several collection types. Set, for unique elements. List, for non-unique elements. Map, for key/value pairs with unique keys. And queue, for ordered elements by insertion and removal, or FIFO. When picking the right collection for your implementation, you should consider what the primary operations will be on the collection. For example, if its value searching, use a hash. If it's indexed access, go with an array. If it's about insertions and deletions, then use a linked list. If it's ordering, consider queues. In sorting, maybe trees. Also, consider the standard implementations for each type of collection. If you're wanting to use a hash, consider using the HashSet or HashMap implementations. If you need to use an array, consider the ArrayList implementation. And or, if you're gonna go with a linked list, then you've got the LinkedList implementation available. Using the wrong kind of collection can cause mammoth performance degradation. First of all, consider that Vector and Hashtable are synchronized. Do you need that? Second, are you searching or traversing? Third, is ordering important? Picking the right implementation can be critical. This also illustrates that value of returning a collection interface type, rather than a specific implementation to your user. This gives you the freedom to change the implementation without affecting the user later on. Again, let's consider the key differences for each type of collection. A set. A set guarantees that the elements are unique. However, a set cannot be indexed, nor can it be sorted or shuffled using the static utility methods. A set may be sorted if it is actually the subtype SortedSet. List. The list type is probably the most flexible. While, by definition, it does not guarantee uniqueness, an implementation can stipulate that its entries are unique. It provides indexed access, which works well for array-based implementations. Not so well for LinkedList-based implementations. List also has the greatest amount of support from the static utility methods and can be sorted, shuffled, and reversed. Map. By definition, a map is not a collection. However, both its keys and its values can be accessed as a collection. Map allows data to be stored as key/value pairs. The HashMap implementation class hashes the keys and so cannot guarantee ordering. Collections and multithreading. If you were to write your own collection class, you could make the methods thread-safe. However, doing so would mean that in all cases, the users of your class would have to pay for this overhead. The convention is to create collection classes that are not thread-safe. In most cases, a collection class will not be accessed by multiple threads, simply by virtue of the application design. By keeping its methods unsynchronized, you are getting faster performance for the more common cases. In the cases where thread-safety is a concern, you can use the standard synchronization wrappers provided with the JDK. These wrappers work equally well with any implementation of a collection. Simply use the static methods of the collections class to create a synchronized wrapper around a given collection class, Whether it be your own collection class or one of the standard collection classes provided in the JDK. Optimizing collection constructors. Storage allocation is expensive. If you can, try to size the initial collection large enough to hold all of the elements. This will save reallocation later and can be a very large performance improvement for long-running programs. Okay, before we complete this lecture, pause this video and consider the following questions to test yourself on the content that we have just reviewed. Write down each of your answers and then resume the video to compare answers. Okay, the answers to the above questions are: One, a class implements the comparable interface to define its natural ordering. A comparator is an interface implemented to define an external ordering mechanism. Two, when elements in a collection need to be sorted, but the elements in the collection do not implement comparable. Three, not really. Collections only maintain references to objects. However, when relying on autoboxing, you can add a primitive which will then automatically be wrapped into its corresponding wrapper class. Four, consumer.
Jeremy is a Content Lead Architect and DevOps SME here at Cloud Academy where he specializes in developing DevOps technical training documentation.
He has a strong background in software engineering, and has been coding with various languages, frameworks, and systems for the past 25+ years. In recent times, Jeremy has been focused on DevOps, Cloud (AWS, Azure, GCP), Security, Kubernetes, and Machine Learning.
Jeremy holds professional certifications for AWS, Azure, GCP, Terraform, Kubernetes (CKA, CKAD, CKS).