If you look at Figures 9.4 and 9.5, you might think it is overkill to have lots of interfaces and abstract classes to implement a modest number of concrete collection classes. However, these figures don’t tell the whole story. By using views, you can obtain other objects that implement the Collection or Map interfaces. You saw one example of this with the keySet method of the map classes. At first glance, it appears as if the method creates a new set, fills it with all the keys of the map, and returns it. However, that is not the case. Instead, the keySet method returns an object of a class that implements the Set interface and whose methods manipulate the original map. Such a collection is called a view.
The technique of views has a number of useful applications in the collections framework. We will discuss these applications in the following sections.
1. Small Collections
Java 9 introduces static methods yielding a set or list with given elements, and a map with given key/value pairs.
List<String> names = List.of(“Peter”, “Paul”, “Mary”);
Set<Integer> numbers = Set.of(2, 3, 5);
yield a list and a set with three elements. For a map, you specify the keys and values, like this:
Map<String, Integer> scores = Map.of(“Peter”, 2, “Paul”, 3, “Mary”, 5);
The elements, keys, or values may not be null.
The List and Set interfaces have eleven of methods with zero to ten arguments, and an of method with a variable number of arguments. The specializations are provided for efficiency.
For the Map interface, it is not possible to provide a version with variable arguments since the argument types alternate between the key and value types. There is a static method of Entries that accepts an arbitrary number of Map.Entry<K, V> objects, which you can create with the static entry method. For example,
import static java.utit.Map.*;
Map<String, Integer> scores = ofEntries(
The of and ofEntries methods produce objects of classes that have an instance variable for each element, or that are backed by an array.
These collection objects are unmodifiable. Any attempt to change their contents results in an UnsupportedOperationException.
If you want a mutable collection, you can pass the unmodifiable collection to the constructor:
var names = new ArrayList<>(List.of(“Peter”, “Paul”, “Mary”));
The method call
returns an immutable object that implements the List interface and gives the illusion of having n elements, each of which appears as anObject.
For example, the following call creates a List containing 100 strings, all set to “DEFAULT”:
List<String> settings = Collections.nCopies(100, “DEFAULT”);
There is very little storage cost—the object is stored only once.
You can form subrange views for a number of collections. For example, suppose you have a list staff and want to extract elements 10 to 19. Use the subList method to obtain a view into the subrange of the list:
List<Employee> group2 = staff.subList(10, 20);
The first index is inclusive, the second exclusive—just like the parameters for the substring operation of the String class.
You can apply any operations to the subrange, and they automatically reflect the entire list. For example, you can erase the entire subrange:
group2.clear(); // staff reduction
The elements get automatically cleared from the staff list, and group2 becomes empty.
For sorted sets and maps, you use the sort order, not the element position, to form subranges. The SortedSet interface declares three methods:
SortedSet<E> subSet(E from, E to)
SortedSet<E> headSet(E to)
SortedSet<E> tailSet(E from)
These return the subsets of all elements that are larger than or equal to from and strictly smaller than to. For sorted maps, the similar methods
SortedMap<K, V> subMap(K from, K to)
SortedMap<K, V> headMap(K to)
SortedMap<K, V> tailMap(K from)
return views into the maps consisting of all entries in which the keys fall into the specified ranges.
The NavigableSet interface introduced in Java 6 gives more control over these subrange operations. You can specify whether the bounds are included:
NavigableSet<E> subSet(E from, boolean fromInclusive, E to, boolean toInclusive)
NavigableSet<E> headSet(E to, boolean toInclusive)
NavigableSet<E> tailSet(E from, boolean fromInclusive)
3. Unmodifiable Views
The Collections class has methods that produce unmodifiable views of collections. These views add a runtime check to an existing collection. If an attempt to modify the collection is detected, an exception is thrown and the collection remains untouched.
You obtain unmodifiable views by eight methods:
Each method is defined to work on an interface. For example, Collections .unmodifiableList works with an ArrayList, a LinkedList, or any other class that implements the List interface.
For example, suppose you want to let some part of your code look at, but not touch, the contents of a collection. Here is what you could do:
var staff = new LinkedList<String>();
The Collections.unmodifiableList method returns an object of a class implementing the List interface. Its accessor methods retrieve values from the staff collection. Of course, the lookAt method can call all methods of the List interface, not just the accessors. But all mutator methods (such as add) have been redefined to throw an UnsupportedOperationException instead of forwarding the call to the underlying collection.
The unmodifiable view does not make the collection itself immutable. You can still modify the collection through its original reference (staff, in our case). And you can still call mutator methods on the elements of the collection.
The views wrap the interface and not the actual collection object, so you only have access to those methods that are defined in the interface. For example, the LinkedList class has convenience methods, addFirst and addLast, that are not part of the List interface. These methods are not accessible through the unmodifiable view.
4. Synchronized Views
If you access a collection from multiple threads, you need to ensure that the collection is not accidentally damaged. For example, it would be disastrous if one thread tried to add to a hash table while another thread was rehashing the elements.
Instead of implementing thread-safe collection classes, the library designers used the view mechanism to make regular collections thread safe. For example, the static synchronizedMap method in the Collections class can turn any map into a Map with synchronized access methods:
var map = Collections.synchronizedMap(new HashMap<String, Employee>());
You can now access the map object from multiple threads. The methods such as get and put are synchronized—each method call must be finished completely before another thread can call another method. We discuss the issue of synchronized access to data structures in greater detail in Chapter 12.
5. Checked Views
Checked views are intended as debugging support for a problem that can occur with generic types. As explained in Chapter 8, it is actually possible to smuggle elements of the wrong type into a generic collection. For example:
var strings = new ArrayList<String>();
ArrayList rawList = strings; // warning only, not an error,
// for compatibility with legacy code
rawList.add(new Date()); // now strings contains a Date object!
The erroneous add command is not detected at runtime. Instead, a class cast exception will happen later when another part of the code calls get and casts the result to a String.
A checked view can detect this problem. Define a safe list as follows:
List<String> safeStrings = Cotlections.checkedList(strings, String.class);
The view’s add method checks that the inserted object belongs to the given class and immediately throws a CtassCastException if it does not. The advantage is that the error is reported at the correct location:
ArrayList rawList = safeStrings;
rawList.add(new Date()); // checked list throws a CtassCastException
6. A Note on Optional Operations
A view usually has some restriction—it may be read-only, it may not be able to change the size, or it may support removal but not insertion (as is the case for the key view of a map). A restricted view throws an UnsupportedOperationException if you attempt an inappropriate operation.
In the API documentation for the collection and iterator interfaces, many methods are described as “optional operations.” This seems to be in conflict with the notion of an interface. After all, isn’t the purpose of an interface to lay out the methods that a class must implement? Indeed, this arrangement is unsatisfactory from a theoretical perspective. A better solution might have been to design separate interfaces for read-only views and views that can’t change the size of a collection. However, that would have tripled the number of interfaces, which the designers of the library found unacceptable.
Should you extend the technique of “optional” methods to your own designs? We think not. Even though collections are used frequently, the coding style for implementing them is not typical for other problem domains. The designers of a collection class library have to resolve a particularly brutal set of conflicting requirements. Users want the library to be easy to learn, convenient to use, completely generic, idiot-proof, and at the same time as efficient as hand-coded algorithms. It is plainly impossible to achieve all these goals simultaneously, or even to come close. But in your own programming problems, you will rarely encounter such an extreme set of constraints. You should be able to find solutions that do not rely on the extreme measure of “optional” interface operations.
Source: Horstmann Cay S. (2019), Core Java. Volume I – Fundamentals, Pearson; 11th edition.