Java version: Java SE 21 (basically backwards compatible since Java SE 8)
Table of Contents
- 2.1 Introduction and course outline
- 2.2 The project and the array problem
- 2.3 The collection of collections
- 2.4 Collection Behaviors
- 2.5 Collection behavior demonstration
- 3.1 Introduction
- 3.2 Key characteristics of Lists
- 3.3 Shipments example (part 1)
- 3.4 Example Shipments (part 2)
- 3.5 Example Shipments (part 3)
- 3.6 List implementations
- 3.7 Implementation performance
- 3.8 Conclusions
- 4.1 Introduction
- 4.2 Why use a Map?
- 4.3 Map API
- 4.4 Views on Maps
- 4.5 Advanced operations
- 4.6 Advanced Operations Demonstration
- 4.7 Map implementations
- 4.8 Specialized implementations
- 4.9 Correct use of HashMap and summary
- 5.1 Introduction
- 5.2 Live stream encoding
- 5.3 Intermediate operations on Streams
- 5.4 Terminal operations on Streams
- 5.5 Les Collectors
- 5.6 Performance and implementation
- 5.7 Conclusion
- 6.1 Introduction
- 6.2 Non-editable collections (live coding)
- 6.3 Factory Methods options (live coding)
- 6.4 Factory Methods — full reference
- 6.5 Collection operations (live coding)
- 6.6 Collection operations and conclusion
- 6.7 SequencedCollections (live encoding)
- 6.8 SequencedCollections — details
- 7.1 Introduction
- 7.2 Set characteristics (demonstration)
- 7.3 hashCode and equals
- 7.4 Set implementations
- 7.5 SortedSet and NavigableSet (with demo)
- 7.6 General conclusion
- 8.1 Project setup (Gradle)
- 8.2 Common classes
- 8.3 Module 2 — What are Collections
- 8.4 Module 3 — Lists
- 8.5 Module 4 — Maps
- 8.6 Module 5 — Streams
- 8.7 Module 6 — Operations and Factories
- 8.8 Module 7 — Sets
- 8.9 JUnit Tests
1. Course Overview
Welcome to this course on Collections in Java SE 21. I am Richard Warburton, Software Developer, Java Champion, author of Pluralsight books and courses in the field of software development and Java.
Every Java developer knows that, when coding in Java, you use the collections framework constantly, throughout your application. Therefore, mastering this fundamental skill can bring considerable improvements to your Java coding abilities.
This course will cover:
- Why use collections
- Which collection to choose and in what situation
- APIs and behaviors of lists, maps and sets
- Important collection algorithms and how to process data with the Streams API
- How different collection implementations work and why choose one over another
At the end of this course, you will know how to use Java collections to process data efficiently.
Prerequisites: Be familiar with basic Java software development skills. It is then advisable to dive into the courses on Java Fundamental: Generics and Java Interfaces and Abstractions.
2. What are Collections and why use them?
Total module duration: 30m 27s
2.1 Introduction and course outline
This module is titled “What are Collections and Why Use Them?” ”. Let’s start with a version check: This course was created with Java SE 21 and is 100% applicable to any version from Java SE 21 onwards. Java maintains strong backward compatibility. The majority of content is also applicable from Java SE 8, but two important features were added after Java SE 8:
- Sequenced Collections were introduced in Java SE 21
- Collection factories were introduced in Java SE 9
The Roman coin purse analogy: a collection is an object that contains zero or more other objects called elements. You might think that arrays already do this — and indeed, arrays contain zero or more elements. The question then becomes: what are the problems with arrays that cause us to prefer collections?
Course module plan:
- Module 1: Overview
- Module 2: Introduction to collections (this module)
- Module 3: Lists — collections with iteration order, the most used
- Module 4: Maps — collections of key/value pairs
- Module 5: Java Streams — API introduced in Java 8 to process collections
- Module 6: Operations and factories on collections
- Module 7: Sets — collections with uniqueness
Why implementing your own data structures is a bad idea:
Implementing data structures is very difficult. This is why big tech companies love asking interview questions based on data structures — there are so many edge cases, it’s very difficult to get everything right and efficient. It’s fantastic that the collections framework ships with the JDK. Trying to reinvent the wheel when you have a built-in collections framework can be a fun exercise, but it’s not a good use of your time and you risk writing buggy code compared to the well-tested code that ships with the JDK.
Data structure properties:
- Some data structures define order in the collection
- Some provide key/value pairs (Maps)
- Some guarantee the uniqueness of the elements
2.2 The project and the array problem
Here’s how to set up the demo project and understand why arrays have fundamental limitations that force us to use Java collections.
Project configuration:
- Extract the ZIP file to a directory
- Ensure that the
JAVA_HOMEvariable is configured to point to a Java 17+ JDK - Run the
gradlewscript (which downloads Gradle the first time) - Import the project into your IDE (IntelliJ IDEA recommended) by opening the
build.gradlefile and choosing “Open as a Project”
Project structure: The project contains before (with TODOs) and after (with full implementation) packages for each module.
The problem with arrays:
Arrays have several fundamental problems:
- No native
addmethod — to add an element, you must manually copy the entire array - No duplicate protection — nothing prevents adding the same element twice
- Fixed size — once created, an array cannot change size directly
// Problème avec les arrays
Product[] products = { door, floorPanel };
// Pour ajouter un élément, il faut écrire une méthode manuelle
private static Product[] add(Product product, Product[] array) {
int length = array.length;
var newArray = Arrays.copyOf(array, length + 1);
newArray[length] = product;
return newArray;
}
// Et les doublons ne sont pas gérés automatiquement !
products = add(window, products); // OK
products = add(window, products); // Doublon accepté silencieusement
Java collections solve all of these problems and more.
2.3 The collection of collections
The Java collections framework exposes multiple types of collections. Here is the hierarchy:
Main interface: Collection
Listand its subinterfaces — ordered by index (most used)Set,SortedSet,NavigableSet— uniqueness of elementsQueue,Deque— FIFO/LIFO behaviorsMap,SortedMap— key/value pairs (does not derive fromCollectionbut is part of the framework)
Interfaces vs Implementations:
This is a fundamental point: the interfaces define the functional characteristics (order, uniqueness, etc.) without specifying how the collection is implemented. The implementation classes (like ArrayList, HashSet, HashMap) provide non-functional characteristics (CPU consumption, memory, performance).
Best practice: Always declare a variable with the type of the interface, not that of the implementation.
// Bonne pratique : type de l'interface
List<Product> products = new ArrayList<>();
// À éviter : type de l'implémentation
ArrayList<Product> products = new ArrayList<>();
Summary of main interfaces:
| Interface | Key property | Example implementation |
|---|---|---|
List | Iteration order by index | ArrayList, LinkedList |
Set | Uniqueness of elements | HashSet, TreeSet |
Map | Key/Value Pairs | HashMap, TreeMap |
Queue/Deque | FIFO/LIFO | ArrayDeque, LinkedList |
2.4 Collection Behaviors
The Collection interface extends the Iterable interface, which allows the creation of an Iterator — the standard way of iterating through items in a collection. Since Java 8, Streams offer an often more powerful alternative.
Methods common to all collections:
| Method | Description |
|---|---|
size() | Number of items in collection |
isEmpty() | Shortcut for size() == 0 (may be faster on some implementations) |
add(E e) | Ensures the item is in the collection |
addAll(Collection<? extends E> c) | Adds all items from another collection |
remove(Object o) | Remove an item from the collection |
removeAll(Collection<?> c) | Remove all elements from the collection passed as argument |
retainAll(Collection<?> c) | Removes all elements that are NOT in the collection passed as argument |
contains(Object o) | Returns true if the element is in the collection |
containsAll(Collection<?> c) | Returns true if all elements in the collection are present |
toArray() | Converts collection to array |
clear() | Delete all items |
The Iterable and Iterator interface:
// Parcours avec un Iterator (utile pour la suppression en cours d'itération)
Iterator<Product> iterator = products.iterator();
while (iterator.hasNext()) {
final Product product = iterator.next();
if (product.weight() > 20) {
iterator.remove(); // Suppression sécurisée pendant l'itération
}
}
// ATTENTION : ceci lève une ConcurrentModificationException
for (var product : products) {
if (product.getWeight() > 20) {
products.remove(product); // INTERDIT pendant une boucle for-each !
}
}
Why not delete in a for-each: The for-each internally uses an Iterator. If you modify the collection directly (not via iterator.remove()), this invalidates the iterator and throws a ConcurrentModificationException.
2.5 Demonstration of Collections behaviors
Here are concrete examples using the methods of the Collection interface:
Declaring a collection with generic type:
// Déclaration préférable : type de l'interface avec paramètre générique
Collection<Product> products = new ArrayList<>();
Note on
var: If you usevar products = new ArrayList<>(), the inferred type will beArrayList<Object>, notCollection<Product>. To usevarcorrectly, specify the generic type:var products = new ArrayList<Product>().
Added items:
products.add(door);
products.add(floorPanel);
products.add(window);
Using Collection interface methods:
System.out.println(products.size()); // 3
System.out.println(products.isEmpty()); // false
System.out.println(products.contains(floorPanel)); // true
products.remove(floorPanel);
System.out.println(products.contains(floorPanel)); // false
Removing a set of elements with removeAll:
List<Product> toRemove = new ArrayList<>();
toRemove.add(door);
toRemove.add(floorPanel);
products.removeAll(toRemove);
System.out.println(products); // Contient uniquement window
Safe removal with Iterator.remove():
Iterator<Product> iterator = products.iterator();
while (iterator.hasNext()) {
final Product product = iterator.next();
if (product.weight() > 20) {
iterator.remove(); // Retire les produits de plus de 20 kg
}
}
3. Collections with iteration order: Lists
Total module duration: 36m 21s
3.1 Introduction
Lists are the most popular form of collection. They have a defined iteration order, and each element has an index — a number that defines its position in the list.
This module covers three key areas:
- The characteristics of lists — what distinguishes them from sets, queues or maps
- A concrete example of live coding around
Shipments(product shipments) - The different implementations (
ArrayListvsLinkedList) with their performance characteristics
3.2 Key characteristics of Lists
Definition: A List is a collection that has an iteration order. If you iterate over the elements of a List (without modifying it) and then iterate again, you will see the elements in the same order.
API specific to Lists (in addition to the Collection interface):
| Method | Description |
|---|---|
add(int index, E element) | Inserts at a given index; shifts the following elements |
get(int index) | Reads the element at a given index (equivalent to array[index]) |
set(int index, E element) | Updates the item at a given index |
remove(int index) | Remove element at given index |
indexOf(Object o) | Returns the first index of the element (or -1 if absent) |
lastIndexOf(Object o) | Returns the last index of the element |
subList(int from, int to) | Returns a view of the list between from (inclusive) and to (excluding) |
addAll(int index, Collection c) | Inserts all items in a collection from an index |
replaceAll(UnaryOperator<E> op) | Replaces each element with the result of the operator |
sort(Comparator<? super E> c) | Sort the list according to the comparator |
Important on
subList:subListreturns a view (not a copy). Any changes to the view are reflected in the original list, and vice versa.
Methods inherited from Collection:
The add(E e) method without index adds to the end of the list.
3.3 Shipments Example (part 1)
The example of Shipments consists of taking a set of products and distributing them over two types of vans according to their weight:
- Light Van (
lightVanProducts): products ≤ 20 kg (cheaper to use) - Heavy van (
heavyVanProducts): products > 20 kg
We also want to be able to replace products in the shipment.
Interface tested (ShipmentTest):
shouldAddItems— add products to ShipmentshouldReplaceItems— replace an existing productshouldNotReplaceMissingItems— attempt to replace a missing productshouldIdentifyVanRequirements— prepare the Shipment and distribute the productsshouldStripHeavyProducts— remove all heavy products
Implementation of add:
public void add(Product product) {
products.add(product);
}
Implementation of replace with indexOf and set:
public boolean replace(Product oldProduct, Product newProduct) {
int position = products.indexOf(oldProduct);
if (position == MISSING_PRODUCT) { // MISSING_PRODUCT = -1
return false;
} else {
products.set(position, newProduct);
return true;
}
}
indexOf searches the list and returns the index of the first element equal to oldProduct, or -1 if absent. set replaces the element at this index.
Implementation of prepare — sorting and splitting:
public void prepare() {
// Tri par poids (utilise le Comparator BY_WEIGHT)
products.sort(Product.BY_WEIGHT);
// Trouver le point de séparation
int splitPoint = findSplitPoint();
// Créer deux vues de la liste
lightVanProducts = products.subList(0, splitPoint);
heavyVanProducts = products.subList(splitPoint, products.size());
}
private int findSplitPoint() {
int size = products.size();
for (int i = 0; i < size; i++) {
var product = products.get(i);
if (product.weight() > LIGHT_VAN_MAX_WEIGHT) {
return i;
}
}
return 0;
}
Note:
Collections.sort(products, Product.BY_WEIGHT)is an alternative toproducts.sort(Product.BY_WEIGHT). The instance method is preferred.
3.4 Shipments Example (part 2)
Alternative with replaceAll:
The replaceAll method takes an UnaryOperator (a function that takes a value and returns a value). It is called for each element in the list and replaces the element with the returned result.
// Alternative avec replaceAll (sémantique légèrement différente)
public void replace(Product oldProduct, Product newProduct) {
products.replaceAll(product -> {
if (product == oldProduct) {
return newProduct;
} else {
return product;
}
});
// Note : replaceAll ne retourne pas d'indicateur de succès
}
Difference: The version with
indexOf/setonly replaces the first occurrence and returns a success boolean. The version withreplaceAllreplaces all matching occurrences but does not return a flag.
Implementation of stripHeavyProducts with removeIf:
public boolean stripHeavyProducts() {
return products.removeIf(product -> product.weight() > LIGHT_VAN_MAX_WEIGHT);
}
removeIf takes a Predicate and removes all elements for which the predicate returns true. It returns true if at least one element has been deleted.
3.5 Shipments Example (part 3)
This section completes the implementation of the splitting of products between the two vans.
Detail of findSplitPoint:
private int findSplitPoint() {
int size = products.size();
for (int i = 0; i < size; i++) {
final Product product = products.get(i);
if (product.weight() > LIGHT_VAN_MAX_WEIGHT) {
return i; // Premier produit nécessitant la camionnette lourde
}
}
return 0; // Tous les produits entrent dans la camionnette légère
}
Warning: Returning
0if absent means that there are no light products (lightVanProductswill be empty). Some use cases would require returningproducts.size()instead. It depends on the desired semantics.
Both views with subList:
lightVanProducts = products.subList(0, splitPoint); // [0, splitPoint[
heavyVanProducts = products.subList(splitPoint, products.size()); // [splitPoint, end[
The parameters of subList are start inclusive and end exclude (as with most Java APIs).
Shipment Iterator:
public Iterator<Product> iterator() {
return products.iterator();
}
By implementing Iterable<Product>, the Shipment class can be traversed with a for-each.
3.6 List implementations
Two main implementations exist for the List interface:
ArrayList
An ArrayList is a List whose internal structure is an array. It’s fantastic in terms of performance most of the time:
- Reading (
get): very fast — direct access to the array by index - Added at the end of the list: very fast most of the time — writing in the next free box
Growth mechanism:
When all squares of the internal array are occupied, the ArrayList grows:
- A new array of larger size is allocated (usually 1.5x the current size)
- All elements are copied to the new table
The ArrayList starts with an array of capacity 10 by default. We can specify an initial capacity to avoid resizing:
// Capacité initiale de 100 éléments
List<Product> products = new ArrayList<>(100);
trimToSize() method: reduces the size of the internal array to the current number of elements to save memory.
LinkedList
A LinkedList is a doubly linked list. Each element is a node containing:
- The value of the element
- A pointer to the previous node
- A pointer to the next node
Main advantage: adding and deleting in the middle is fast because you only need to change the pointers.
Outdated implementations NOT to be used
Vector: precedesArrayList, thread-safe by coarse synchronization → very slowStack: inherits fromVector, obsolete → useDequeinstead
3.7 Performance of implementations
Reminder: These comparisons are general guidelines. Profiling your production system is always the best approach.
Big O notation: indicates how the performance of an operation evolves with the size N of the list.
| Operation | ArrayList | LinkedList |
|---|---|---|
get(index) | O(1) — direct access to the array | O(N) — node traversal |
add(element) at the end | Ω(1) damped | O(1) |
add(index, element) in the middle | O(N) — element offset | O(N) — traverse to index |
remove(index) in the middle | O(N) — element offset | O(N) — traverse to index |
contains(element) | Y(N) | Y(N) |
| Memory | Good (compact array) | Bad (overhead of pointers and node objects) |
Why the addition at the end of ArrayList is Ω(1) amortized:
Even if growth (copying all elements) is O(N), it rarely happens. On average, over many adds, the cost comes to O(1) per operation.
In practice: use ArrayList in the majority of cases. LinkedList generally performs worse than ArrayList even for inserts/deletes in the middle, because poor cache locality due to sparse nodes in memory negates the theoretical advantage.
3.8 Conclusions
- Lists are collections that have an order; each element has an index
- Many operations are available:
subList,indexOf,get,set,sort - Interfaces define functional characteristics; implementations define non-functional features
- Lists are extremely used on a daily basis by Java developers
4. Key/value pairs: Maps
Total module duration: 47m 57s
4.1 Introduction
Maps are collections of pairs. Unlike Lists and Sets, they do not contain individual values, but key → value pairs.
Real analogy: a dictionary (in which words are associated with definitions or translations).
Why “Map” and not “Dictionary”? The
Dictionaryclass already exists in the JDK (it precedes the Collections API) and is now deprecated. Java therefore chose the mathematical term “Map”. Don’t use theDictionaryclass — use theMapinterface and its subclasses.
Fundamental rule: keys are unique in a Map. Uniqueness is defined by the key’s equals method. Values do not have to be unique.
This module covers:
- Use cases for Maps (why use them)
- Views on Maps (
keySet,values,entrySet) - Advanced operations (Java 8 to 17)
- Different implementations (
HashMap,TreeMap) - How to use
HashMapcorrectly
4.2 Why use a Map?
Scenario: implement a product by ID lookup table (ProductLookupTable).
public interface ProductLookupTable {
Product lookupById(int id);
void addProduct(Product productToAdd);
void clear();
}
Naive implementation with an ArrayList:
public class NaiveProductLookupTable implements ProductLookupTable {
private final List<Product> products = new ArrayList<>();
@Override
public void addProduct(final Product productToAdd) {
var id = productToAdd.getId();
for (var product : products) {
if (product.getId() == id) {
throw new IllegalArgumentException(
"Unable to add product, duplicate id for: " + productToAdd);
}
}
products.add(productToAdd);
}
@Override
public Product lookupById(final int id) {
for (var product : products) {
if (product.getId() == id) {
return product;
}
}
return null;
}
}
Naive version issues:
addProductis Y(N) — loop over all products to look for duplicateslookupByIdis Y(N) — loop through all products to find the matching one
Implementation with a HashMap:
public class MapProductLookupTable implements ProductLookupTable {
private final Map<Integer, Product> idToProduct = new HashMap<>();
@Override
public void addProduct(final Product productToAdd) {
var id = productToAdd.getId();
if (idToProduct.containsKey(id)) {
throw new IllegalArgumentException(
"Unable to add product, duplicate id for: " + productToAdd);
}
idToProduct.put(id, productToAdd);
}
@Override
public Product lookupById(final int id) {
return idToProduct.get(id);
}
public void clear() {
idToProduct.clear();
}
}
Advantages of the Map version:
- Simpler code (fewer lines)
addProductis O(1) —containsKeyis O(1) with HashMaplookupByIdis O(1) —getis O(1) with HashMap
Out of 20,000 products, the Map version is orders of magnitude faster.
4.3 The Map API
Adding and replacing values
| Method | Description |
|---|---|
put(K key, V value) | Associates the value with the key. Return the old value (or null) |
putAll(Map<? extends K, ? extends V> m) | Copies all entries from the map passed as an argument |
replace(K key, V value) | Replaces value only if key is already present |
replace(K key, V oldValue, V newValue) | Replaces only if current value matches |
Difference
putvsreplace:putadds the pair even if the key does not exist;replacedoes nothing if the key is absent.
Reading values
| Method | Description |
|---|---|
get(Object key) | Returns the associated value, or null if absent |
getOrDefault(Object key, V defaultValue) | Returns the value, or defaultValue if absent |
containsKey(Object key) | true if key is present |
containsValue(Object value) | true if the value is present |
Best practice: If you never store
nullas a value, prefercontainsKeyfollowed bygetto avoidNullPointerException.
Nulls in Maps
HashMap: accepts anullkey andnullvaluesTreeMap: acceptsnullvalues but nonullkeys (because the keys must be compared)
4.4 Views on Maps
Three methods return views on the Map:
| Method | Return |
|---|---|
keySet() | Set<K> — all keys |
values() | Collection<V> — all values |
entrySet() | Set<Map.Entry<K,V>> — all key/value pairs |
Important property of views: they are bidirectional. Modifying the view modifies the underlying Map, and modifying the Map modifies the view.
var idToProduct = new HashMap<Integer, Product>();
idToProduct.put(1, door);
idToProduct.put(2, floorPanel);
idToProduct.put(3, window);
// keySet
var ids = idToProduct.keySet(); // {1, 2, 3}
// Suppression via la vue → modifie la Map !
ids.remove(1);
// idToProduct contient maintenant {2=floorPanel, 3=window}
// values
var products = idToProduct.values();
products.remove(window);
// idToProduct ne contient plus window
// entrySet — itération sur les paires clé/valeur
for (var entry : idToProduct.entrySet()) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
}
Restrictions on views:
keySet(): no addition via view (addnot supported)values(): no addition via viewentrySet(): additions are possible withMap.entry(key, value), but the behavior depends on the implementation
4.5 Advanced operations
These operations were introduced in Java versions 8 through 17 and often use lambda expressions.
Add and update operations
| Method | Description |
|---|---|
putIfAbsent(K key, V value) | Add only if key is not already present |
computeIfAbsent(K key, Function<K,V> f) | Calculates and adds value if key is missing |
computeIfPresent(K key, BiFunction<K,V,V> f) | Updates value if key is present |
compute(K key, BiFunction<K,V,V> f) | Updates the value whether present or not |
merge(K key, V value, BiFunction<V,V,V> f) | Merge a new value with the old one |
replaceAll(BiFunction<K,V,V> f) | Replaces all values according to function |
Read operations
| Method | Description |
|---|---|
getOrDefault(K key, V defaultValue) | Returns the value or defaultValue if absent (without modifying the Map) |
forEach(BiConsumer<K,V> action) | Cycles through all key/value pairs |
Important note on
getOrDefault: ifdefaultValueisnull,getOrDefaultcan still returnnull. This method does not modify the Map.
Example with computeIfAbsent: useful for building list maps (multimap)
// Grouper des produits par catégorie de poids
Map<String, List<Product>> byCategory = new HashMap<>();
for (var product : products) {
String category = product.getWeight() > 20 ? "heavy" : "light";
byCategory.computeIfAbsent(category, k -> new ArrayList<>()).add(product);
}
Example with merge: useful for counting occurrences
// Compter les occurrences de chaque mot
Map<String, Integer> wordCount = new HashMap<>();
for (String word : words) {
wordCount.merge(word, 1, Integer::sum);
// Si mot absent → ajoute 1 ; si présent → ajoute 1 à l'ancien compteur
}
4.6 Advanced Operations Demonstration
Here are the advanced operations illustrated on the product catalog example:
getOrDefault vs get:
var defaultProduct = new Product(-1, "Whatever the customer wants", 100);
var idToProduct = new HashMap<Integer, Product>();
idToProduct.put(1, door);
idToProduct.put(2, floorPanel);
idToProduct.put(3, window);
// get retourne null si la clé est absente
System.out.println(idToProduct.get(10)); // null
// getOrDefault retourne la valeur par défaut
var result = idToProduct.getOrDefault(10, defaultProduct);
System.out.println(result); // Product{id=-1, name='Whatever...', weight=100}
// La Map n'est PAS modifiée
System.out.println(idToProduct.get(10)); // toujours null
computeIfAbsent — calculates and stores a new value:
result = idToProduct.computeIfAbsent(10, (id) -> new Product(id, "Custom Product", 10));
System.out.println(result); // Product{id=10, name='Custom Product', weight=10}
System.out.println(idToProduct.get(10)); // Product{id=10, name='Custom Product', weight=10}
// La Map EST modifiée
replaceAll — updates all values:
idToProduct.replaceAll((key, oldProduct) ->
new Product(oldProduct.getId(),
oldProduct.getName(),
oldProduct.getWeight() + 10)); // Augmente le poids de 10 pour tous
System.out.println(idToProduct);
putIfAbsent — simplification of addition with duplicate check:
// Avant
if (idToProduct.containsKey(id)) {
throw new IllegalArgumentException("Duplicate id: " + productToAdd);
}
idToProduct.put(id, productToAdd);
// Après, avec putIfAbsent
var oldProduct = idToProduct.putIfAbsent(id, productToAdd);
if (oldProduct != null) {
throw new IllegalArgumentException("Duplicate id: " + productToAdd);
}
4.7 Map implementations
HashMap
HashMap is the recommended general-purpose implementation. If you are unsure, use HashMap.
Internal operation:
- When we call
put(key, value), Java calculateshashCode()of the key - This hashcode is used (modulo the capacity) to determine a bucket (box in an internal array)
- If several keys fall into the same bucket (collision), they are stored in a linked list (or a red-black tree if the list becomes too long — since Java 8)
- To retrieve a value with
get(key), Java recalculates the hashcode, finds the bucket, then usesequalsto find the correct key
Performance characteristics:
- O(1) amortized for
put,get,containsKey - Optimal performance if the hashcodes are well distributed
Load factor:
- Default: 0.75
- When the number of entries exceeds
capacity × load factor, the Map resizes (rehash)
// HashMap avec capacité initiale et facteur de charge personnalisés
Map<Integer, Product> map = new HashMap<>(32, 0.5f);
TreeMap
TreeMap maintains keys in their natural order (or according to a Comparator). It is implemented with a red-black tree.
Advantages of TreeMap:
- Guaranteed key order
- Access to subsets of the Map based on this order (
headMap,tailMap,subMap)
Disadvantages:
put,get,containsKeyare O(log N) (slower than HashMap)- Keys must be comparable (
ComparableorComparatorprovided)
4.8 Specialized implementations
LinkedHashMap
LinkedHashMap maintains an internal order of the elements, either the insertion order or the access order (LRU — Least Recently Used). It’s a HashMap with a doubly linked list between the entries.
Primary use case: implement an LRU cache.
// Cache LRU avec taille maximale
int MAX_CACHE_SIZE = 100;
Map<Integer, Product> lruCache = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Product> eldest) {
return size() > MAX_CACHE_SIZE; // Supprime le plus ancien si dépassement
}
};
The true parameter in the constructor enables access ordering (LRU). When removeEldestEntry returns true, the oldest entry (eldest) is removed.
IdentityHashMap
IdentityHashMap uses System.identityHashCode() and referential equality (==) for keys instead of hashCode()/equals().
Use case: graph serialization, graph traversal where the exact identity of the object (and not its content) is what matters.
// Chaque nœud est une clé basée sur son identité, pas son contenu
Map<Node, NodeInfo> visited = new IdentityHashMap<>();
EnumMap
EnumMap is a Map optimized for enum type keys. It uses an internal array indexed by the ordinal of the enum, which makes it extremely fast.
enum Day { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY }
Map<Day, List<Meeting>> schedule = new EnumMap<>(Day.class);
4.9 Correct use of HashMap and summary
The hashCode/equals contract:
This is the most important rule for using HashMap and HashSet correctly. If you violate it, your program will behave unpredictably.
Fundamental rule:
If
a.equals(b)istrue, thena.hashCode()must be equal tob.hashCode().
The reverse is not necessary: two different objects can have the same hashcode (collision), but equal objects must have the same hashcode.
Problem demonstration with mutable keys:
var brokenMap = new HashMap<MutableString, String>();
var key = new MutableString("abc");
brokenMap.put(key, "abc");
System.out.println(brokenMap.get(key)); // "abc" — OK
key.set("def"); // On mute la clé !
System.out.println(brokenMap.get(key)); // null — PROBLÈME !
System.out.println(brokenMap); // {def=abc} — l'entrée est toujours là
What happens: when we mutate the key, its hashcode changes. get(key) calculates the new hashcode, searches in the wrong bucket, and finds nothing. The entry is “lost” in the Map.
Security rule: HashMap keys must not be mutable. Use immutable types like String, Integer, or Java record.
Maps module summary:
| Implementation | When to use it |
|---|---|
HashMap | 95% of cases — general practitioner, very fast |
TreeMap | When Key Order Is Necessary |
LinkedHashMap | For an LRU cache or when insertion order is important |
IdentityHashMap | For graph serialization/traversal |
EnumMap | For keys of type enum |
5. Introduction to Java Streams
Total module duration: 32m 53s
5.1 Introduction
Streams are a powerful abstraction introduced in Java 8. They provide a way to perform aggregate operations on entire collections at once. This is an alternative to the traditional approach with for-each loops and Iterators.
Why Streams?
For-each loops and iterators are low-level constructs. Each time you use them, you have to write a lot of boilerplate code — often simple code, but potentially error-prone and a waste of time.
With Java 8, a simpler solution was introduced. Streams enable functional programming in Java. We write our business logic in the form of compact functions (lambda expressions or method references) and we apply them to streams of values.
This module covers:
- Using Streams with live examples (filter, map, sorted)
- Catalog of intermediate operations
- Terminal operations catalog
- Collectors — to construct complex values
- Performance: primitive streams and parallelism
5.2 Live Coding of Streams
Objective: find the names of light products (weight < 30), sorted by weight.
Imperative version (loops):
private static List<String> namesOfLightProductsWeightSortedLoop(List<Product> products) {
List<Product> lightProducts = new ArrayList<>();
for (Product product : products) {
if (product.getWeight() < 30) {
lightProducts.add(product);
}
}
lightProducts.sort(comparingInt(Product::getWeight));
List<String> productNames = new ArrayList<>();
for (Product product : lightProducts) {
productNames.add(product.getName());
}
return Collections.unmodifiableList(productNames);
}
Version with Streams:
private static List<String> namesOfLightProductsWeightSortedStreams(List<Product> products) {
return products
.stream() // Création du stream
.filter(product -> product.getWeight() < 30) // Opération intermédiaire : filtre
.sorted(comparingInt(Product::getWeight)) // Opération intermédiaire : tri
.map(Product::getName) // Opération intermédiaire : transformation
.toList(); // Opération terminale : collecte en List
}
Benefits of the Streams version:
- More concise (4 lines vs 15 lines)
- More readable — looks like a description of the business problem
- The List returned by
toList()is unmodifiable automatically
Anatomy of a Streams pipeline:
source.stream() → Création du Stream
.opIntermédiaire1() → Retourne un Stream
.opIntermédiaire2() → Retourne un Stream
...
.opTerminale() → Retourne une valeur finale (pas un Stream)
5.3 Intermediate operations on Streams
Intermediate operations are all but the last operation in the pipeline. They all return a Stream (allowing chaining, like the Builder pattern).
| Operation | Signature | Description |
|---|---|---|
filter | filter(Predicate<T> p) | Remove elements that do not match the predicate |
map | map(Function<T, R> f) | Transforms each element into a new element |
flatMap | flatMap(Function<T, Stream<R>> f) | Transforms each element into a Stream and flattens them |
sorted | sorted() or sorted(Comparator<T> c) | Sort items |
distinct | distinct() | Remove duplicates |
limit | limit(long n) | Keep the first N elements |
skip | skip(long n) | Ignore the first N elements |
peek | peek(Consumer<T> c) | Performs an action for each element without changing the stream (useful for debugging) |
Examples:
// filter : produits de poids > 20
stream.filter(p -> p.getWeight() > 20)
// map : extraire les noms des produits
stream.map(Product::getName) // Stream<String>
// flatMap : aplatir des listes dans une liste
List<List<Product>> nestedLists = ...;
nestedLists.stream()
.flatMap(Collection::stream) // Transforme List<List<Product>> en Stream<Product>
// sorted : tri par poids
stream.sorted(comparingInt(Product::getWeight))
// distinct : supprime les doublons
stream.distinct()
// limit / skip : pagination
stream.skip(20).limit(10) // Éléments 21 à 30
5.4 Terminal operations on Streams
Terminal operations are always the last operation in the pipeline. They return a value that is not a Stream.
Collection in containers
| Operation | Description |
|---|---|
toList() | Collection in List unmodifiable (Java 16+) |
toArray() | Collection in Object[] |
toArray(IntFunction<T[]> f) | Typed array collection |
collect(Collector<T,A,R> c) | Collection according to a personalized Collector |
// toList() — retourne une List unmodifiable
List<String> names = stream.toList();
// toArray typé
Product[] array = productStream.toArray(Product[]::new);
Match methods (match)
| Operation | Description |
|---|---|
anyMatch(Predicate<T> p) | true if at least one element matches |
allMatch(Predicate<T> p) | true if all elements match |
noneMatch(Predicate<T> p) | true if no element matches |
boolean hasHeavyProduct = products.stream().anyMatch(p -> p.getWeight() > 50);
boolean allLight = products.stream().allMatch(p -> p.getWeight() < 30);
Searching for items
| Operation | Description |
|---|---|
findFirst() | Returns an Optional<T> with the first element |
findAny() | Returns an Optional<T> with any element (better for parallelism) |
min(Comparator<T> c) | Returns the minimum element according to the comparator |
max(Comparator<T> c) | Returns maximum element |
count() | Number of elements in the Stream |
Optional<Product> lightestProduct = products.stream()
.min(comparingInt(Product::getWeight));
lightestProduct.ifPresent(p -> System.out.println("Lightest: " + p));
Discount
| Operation | Description |
|---|---|
reduce(BinaryOperator<T> op) | Reduces elements to a single value |
reduce(T identity, BinaryOperator<T> op) | Reduced with an initial value |
forEach(Consumer<T> c) | Performs an action for each item |
// Somme des poids
int totalWeight = products.stream()
.mapToInt(Product::getWeight)
.sum();
// Réduction manuelle
Optional<Integer> totalWeight = products.stream()
.map(Product::getWeight)
.reduce(Integer::sum);
5.5 The Collectors
Collectors are the “big brother” of the Streams API. They allow you to construct more complex values than simple lists or tables. They are used with the terminal operation collect().
var result = products.stream()
.filter(product -> product.getWeight() < 30)
.sorted(comparingInt(Product::getWeight))
.collect(Collectors.toList()); // ou .toList() directement
Main Collectors (java.util.stream.Collectors):
| Collector | Description |
|---|---|
toList() | Collection in List (editable) |
toUnmodifiableList() | Collection in non-editable List |
toSet() | Collection in Set |
toMap(keyMapper, valueMapper) | Collection in Map |
groupingBy(classifier) | Groups elements by criterion in a Map<K, List<V>> |
groupingBy(classifier, downstream) | Bundles with a secondary collector |
counting() | Count the elements |
summingInt(ToIntFunction f) | Sum of integer values |
averagingInt(ToIntFunction f) | Average of integer values |
joining() | Concatenate Strings |
joining(delimiter) | Concatenate with a delimiter |
partitioningBy(Predicate p) | Partition into two groups (true/false) |
Example with groupingBy and counting:
// Compter le nombre de produits légers par nom
Map<String, Long> lightProducts = products
.stream()
.filter(product -> product.getWeight() < 30)
.sorted(comparingInt(Product::getWeight))
.collect(groupingBy(Product::getName, counting()));
System.out.println(lightProducts);
// {Floor Panel=3, Glass Window=2}
Example with toMap:
// Créer une Map id → produit
Map<Integer, Product> idToProduct = products.stream()
.collect(Collectors.toMap(Product::getId, p -> p));
5.6 Performance and implementation
Primitive types and boxed types
In Java, there is a fundamental difference between primitive types (int, long, double) and boxed types (Integer, Long, Double):
- Primitive
int: 4 bytes, stored directly (excellent for CPU cache) - Boxed
Integer: object on the heap (~24 bytes on some 64-bit JVMs), with a pointer
Result: boxed types are much slower and consume much more memory.
Primitive Streams
The JDK provides specialized streams for primitives to avoid the cost of boxing/unboxing:
IntStream— stream ofintLongStream—longstreamDoubleStream—doublestream
// Conversion d'un Stream<Product> en IntStream
products.stream()
.mapToInt(Product::getWeight) // Retourne IntStream
.sum(); // Opération terminale spécialisée
// Statistiques avec IntStream
IntSummaryStatistics stats = products.stream()
.mapToInt(Product::getWeight)
.summaryStatistics();
System.out.println("Max: " + stats.getMax());
System.out.println("Average: " + stats.getAverage());
Parallelism
Streams support parallel processing via parallelStream() or .parallel(). They use the common ForkJoinPool of the JVM.
// Stream parallèle
long count = products.parallelStream()
.filter(p -> p.getWeight() > 20)
.count();
When to use parallel streams:
- Large collections (> 10,000 items typically)
- CPU-intensive operations
- No dependencies between elements (stateless)
Risks of parallelism:
- Additional cost if the collection is small
- Problems if lambdas have side effects or share mutable state
- Non-deterministic results with
findAny
5.7 Conclusion
Are Streams still better than Loops?
| Streams | Loops |
|---|---|
| High-level construction — less boilerplate | Low-level construction — more control |
| Powered by JDK developers | May be faster in certain critical cases |
| Often more readable — resembles a business description | Borderline cases with checked exceptions → often simpler |
| Bad interaction with checked exceptions | Any type of operation possible |
When NOT to use Streams:
- When lambdas should throw checked exceptions (the Streams API does not use checked exceptions)
- For very high frequency systems (like high frequency trading) where every nanosecond counts
- When the readability of a particular case is better with a loop
6. Operations and Factories on Collections
Total module duration: 36m 2s
6.1 Introduction
This module covers the factory utilities and methods provided by the JDK for working with collections, independent of specific implementations or interfaces.
Three main areas:
- Factories — JDK methods that construct new collection instances (not
new ArrayList()), often immutable or non-modifiable - Operations — utility algorithms (max, min, sort, rotate, mix…)
- Sequenced Collections — new in Java 21, uniform API for collections with defined order
6.2 Non-editable collections (live encoding)
Problem: When a class returns a reference to its internal list, consumer code can modify this list, corrupting the internal state of the class.
// PROBLÈME : getLightVanProducts retourne une référence directe
List<Product> lightVanProducts = shipment.getLightVanProducts();
lightVanProducts.remove(window); // Modifie la liste INTERNE du Shipment !
Solution: return an unmodifiable view with Collections.unmodifiableList:
public void prepare() {
products.sort(Product.BY_WEIGHT);
int splitPoint = findSplitPoint();
// Collections.unmodifiableList protège l'encapsulation
lightVanProducts = Collections.unmodifiableList(
products.subList(0, splitPoint));
heavyVanProducts = Collections.unmodifiableList(
products.subList(splitPoint, products.size()));
}
Now any attempt to modify from outside will throw an UnsupportedOperationException:
lightVanProducts.remove(window); // UnsupportedOperationException !
Important: a non-editable view is linked to the underlying collection. If the original list (products) changes, the view reflects these changes. It’s not a copy.
6.3 Factory Methods options (live coding)
There are several options for creating non-modifiable or immutable Maps (and Lists, Sets):
var mutableCountryToPopulation = new HashMap<String, Integer>();
mutableCountryToPopulation.put("UK", 67);
mutableCountryToPopulation.put("USA", 328);
// Option 1 : vue non modifiable (liée à la Map originale)
var unmodifiable = Collections.unmodifiableMap(mutableCountryToPopulation);
// Option 2 : copie immuable (indépendante)
var copied = Map.copyOf(mutableCountryToPopulation);
Key difference between unmodifiableMap and Map.copyOf:
mutableCountryToPopulation.put("Germany", 83);
System.out.println(unmodifiable);
// {UK=67, USA=328, Germany=83} → reflète la modification !
System.out.println(copied);
// {UK=67, USA=328} → NE reflète PAS la modification (c'est une copie)
Summary of differences:
| Options | Editable? | Copy or view? | Support null? |
|---|---|---|---|
Collections.unmodifiableMap(m) | No (UnsupportedOperationException) | View (related to m) | Yes |
Map.copyOf(m) | No | Independent copy | No (NPE if null) |
Map.of(...) | No | New Map | No |
6.4 Factory Methods — full reference
Empty collections
// Listes, maps et sets vides — immuables, consomment très peu de mémoire
List<Product> emptyList = Collections.emptyList();
Set<Product> emptySet = Collections.emptySet();
Map<Integer, Product> emptyMap = Collections.emptyMap();
Advantage: these methods return shared static instances — no memory allocation.
Use case: return an empty collection from a method rather than creating a new ArrayList<>().
Singleton collections
// Collections avec exactement un élément — immuables
List<Product> singleList = Collections.singletonList(door);
Set<Product> singleSet = Collections.singleton(door);
Map<Integer, Product> singleMap = Collections.singletonMap(1, door);
More memory efficient than new ArrayList<>() followed by add().
Factory methods List.of, Set.of, Map.of (Java 9+)
// Création concise de collections immuables
List<Product> products = List.of(door, floorPanel, window);
Set<Product> productSet = Set.of(door, floorPanel, window);
Map<Integer, Product> productMap = Map.of(
1, door,
2, floorPanel,
3, window
);
Properties:
- Immutable —
UnsupportedOperationExceptionon any modification - Do not support nulls —
NullPointerExceptionif a null is provided - For
Set.ofandMap.of, the iteration order is not guaranteed Map.ofsupports up to 10 pairs; beyond that, useMap.ofEntries
// Map.ofEntries pour plus de 10 paires
Map<Integer, Product> largeMap = Map.ofEntries(
Map.entry(1, door),
Map.entry(2, floorPanel),
Map.entry(3, window)
// ... autant qu'on veut
);
copyOf (Java 10+)
List<Product> immutableCopy = List.copyOf(mutableList);
Set<Product> immutableSetCopy = Set.copyOf(mutableList);
Map<Integer, Product> immutableMapCopy = Map.copyOf(mutableMap);
6.5 Collection operations (live coding)
The java.util.Collections class provides a multitude of algorithmic and utility operations.
var products = new ArrayList<>(List.of(window, floorPanel, door));
System.out.println(products); // [Glass Window, Floor Panel, Wooden Door]
Collections.rotate(list, distance): shifts all elements of distance positions to the right; elements that overflow return to the beginning.
Collections.rotate(products, 1);
// [Wooden Door, Glass Window, Floor Panel]
// Le dernier (Wooden Door) est passé en premier
Collections.rotate(products, 2);
// Décale de 2 positions
Collections.shuffle(list, random): randomly shuffles the elements.
Collections.shuffle(products, ThreadLocalRandom.current());
Collections.binarySearch(list, key): Binary search on a sorted list. Returns the index or -(insert point) - 1 if absent.
var alphabet = IntStream.range('A', 'Z').mapToObj(x -> (char)x).toList();
int index = Collections.binarySearch(alphabet, 'M');
System.out.println("index = " + index); // 12
6.6 Operations on Collections and conclusion
Collections.disjoint(c1, c2): returns true if the two collections have no element in common.
var _1to3 = List.of(1, 2, 3);
var _4to6 = List.of(4, 5, 6);
var _2to4 = List.of(2, 3, 4);
System.out.println(Collections.disjoint(_1to3, _4to6)); // true
System.out.println(Collections.disjoint(_1to3, _2to4)); // false
Collections.frequency(collection, element): number of occurrences of an element.
var letters = "ABCDEFAADSEA".chars().mapToObj(x -> (char)x).toList();
int count = Collections.frequency(letters, 'A'); // 4
Collections.addAll(collection, elements...): adds several elements in a single line (varargs).
var products = new ArrayList<Product>();
Collections.addAll(products, door, floorPanel, window);
Collections.max(collection, comparator) and Collections.min(...):
var max = Collections.max(products, Product.BY_WEIGHT);
System.out.println(max == door); // true (door pèse 35 kg)
Collections.swap(list, i, j): swaps two elements.
Collections.swap(products, 1, 2);
Collections.reverse(list): reverses the order of the elements.
Collections.reverse(products);
Collections.fill(list, element): replaces all elements with the given value.
Collections.fill(products, door); // [door, door, door]
Collections.sort(list, comparator): sorts the list. Equivalent to list.sort(comparator).
6.7 SequencedCollections (live coding)
Problem before Java 21: no uniform API to access the first/last element of different collections.
List<Integer> list = List.of(1, 2, 3);
SortedSet<Integer> set = new TreeSet<>(list);
Deque<Integer> queue = new ArrayDeque<>(list);
LinkedHashSet<Integer> hashSet = new LinkedHashSet<>(list);
// Avant Java 21 : chaque collection avait sa propre façon d'accéder au premier élément
System.out.println(list.get(0)); // List → get(0)
System.out.println(set.first()); // SortedSet → first()
System.out.println(queue.getFirst()); // Deque → getFirst()
System.out.println(hashSet.iterator().next()); // LinkedHashSet → iterator !
// Avant Java 21 : accès au dernier élément
System.out.println(list.get(list.size() - 1)); // List → get(size - 1)
System.out.println(set.last()); // SortedSet → last()
System.out.println(queue.getLast()); // Deque → getLast()
// LinkedHashSet → pas vraiment possible facilement !
Java 21 solution: SequencedCollection interface:
// Avec Java 21 : API uniforme via SequencedCollection
System.out.println(list.getFirst()); // 1
System.out.println(set.getFirst()); // 1
System.out.println(queue.getFirst()); // 1
System.out.println(hashSet.getFirst()); // 1
System.out.println(list.getLast()); // 3
System.out.println(set.getLast()); // 3
System.out.println(queue.getLast()); // 3
System.out.println(hashSet.getLast()); // 3
// Itération en ordre inverse avec reversed()
for (var x : list.reversed()) {
System.out.println(x); // 3, 2, 1
}
6.8 SequencedCollections — details
Hierarchy of SequencedCollections in the JDK:
Collection
└── SequencedCollection (Java 21)
├── List
├── Deque
└── SequencedSet (Java 21)
├── Set (base)
└── SortedSet
└── NavigableSet
Key points:
- An ordinary
HashSetis not aSequencedCollection(no order defined) - A
TreeSet(SortedSet) is aSequencedCollection(sort order defined) - A
LinkedHashSetis aSequencedCollection(insertion order defined)
SequencedMap:
Similar to SequencedCollection, but for Maps:
Map
└── SequencedMap (Java 21)
└── SortedMap
└── NavigableMap
SequencedMap methods: putFirst, putLast, firstEntry, lastEntry, reversed.
Methods of the SequencedCollection interface:
| Method | Description |
|---|---|
getFirst() | Returns the first element |
getLast() | Returns the last element |
addFirst(E e) | Add to head (optional, not supported on immutable collections) |
addLast(E e) | Add at the end |
removeFirst() | Remove and return the first element |
removeLast() | Remove and return the last element |
reversed() | Returns a reverse view of the collection |
Module 6 Summary:
- Unmodifiable Collections (
Collections.unmodifiableList) create write-protected views, linked to the underlying collection - Immutable collections (
List.of,Map.copyOf) are copies that do not reflect changes to the original - The
Collectionsutility class provides many algorithms (sort,shuffle,rotate,binarySearch,max,min,disjoint,frequency) - Java 21 introduces
SequencedCollectionandSequencedMapfor uniform API
7. Collections with uniqueness: The Sets
Total module duration: 24m 27s
7.1 Introduction
Sets are collections where uniqueness is the fundamental property. Java has a concept of an equals method: if a.equals(b) returns true, we say that the two objects are “identical”. A Set is a collection where no two objects exist such that a.equals(b) is true.
Implementations of Set may require implementing other methods (hashCode) or defining a sort order to effectively implement this equality contract.
This module covers:
- Live demonstration of the characteristics of the Sets
- The
hashCode/equalscontract and what happens if it is violated - Performance and characteristics of the different implementations (
HashSet,TreeSet)
7.2 Characteristics of Sets (demonstration)
Scenario: a product catalog (ProductCatalogue) that receives products from multiple suppliers, but should not store duplicates.
Naive implementation with a List:
public class ProductCatalogue implements Iterable<Product> {
private List<Product> products = new ArrayList<>();
public void addSupplier(final Supplier supplier) {
products.addAll(supplier.getProducts());
}
@Override
public Iterator<Product> iterator() {
return products.iterator();
}
}
Problem: If two suppliers sell the same product (Glass Window), it appears twice in the list — the containsInAnyOrder check in the test fails because there are duplicates.
Naive solution: manually filter duplicates in the List:
public void addSupplier(final Supplier supplier) {
supplier.getProducts().stream()
.filter(product -> !products.contains(product))
.forEach(products::add);
}
Problem: List.contains is O(N), so addSupplier becomes O(N²) — very slow for large collections.
Elegant solution: use a HashSet:
public class ProductCatalogue implements Iterable<Product> {
private final Set<Product> products = new HashSet<>();
public void addSupplier(final Supplier supplier) {
products.addAll(supplier.getProducts()); // Les doublons sont gérés automatiquement !
}
@Override
public Iterator<Product> iterator() {
return products.iterator();
}
}
The HashSet automatically guarantees uniqueness. addAll with a Set is O(N) — the Set simply checks if each element is already present (O(1) with HashSet).
WeightAwareProductCatalogue with NavigableSet:
public class WeightAwareProductCatalogue implements Iterable<Product> {
private final NavigableSet<Product> products =
new TreeSet<>(Product.BY_WEIGHT); // Trié par poids
public void addSupplier(final Supplier supplier) {
products.addAll(supplier.getProducts());
}
public Set<Product> findLighterProducts(final Product product) {
return products.headSet(product); // Tous les produits plus légers
}
}
7.3 hashCode and equals
The hashCode/equals contract:
This contract defines the obligations that your code must meet for HashMap and HashSet to work correctly.
The rule:
If
a.equals(b)istrue, thena.hashCode()must be equal tob.hashCode().
Warning: this is a one-way implication! Two different objects can have the same hashcode (normal collision), but two equal objects MUST have the same hashcode.
Why does this rule exist?
HashMap and HashSet use the hashcode to define a bucket (slot in an internal array), then use equals to distinguish the entries in that bucket. If two equal objects have different hashcodes, they can land in different buckets and the Set will not detect the duplicate.
Two types of equality:
- Equality by reference: two objects are equal only if they are the same object (default behavior of
Object.equals) - Equality by value: two objects are equal if they have the same values in their fields
Implication: If you use equality by reference, you can have several “identical” objects (same content) in a HashSet, because they are not the same references. If you use equality by value, duplicates are automatically excluded.
Complete example of Product with hashCode and equals:
public class Product {
private final int id;
private final String name;
private final int weight;
// ... constructeur, getters ...
@Override
public boolean equals(final Object o) {
if (!(o instanceof Product)) return false;
final Product product = (Product) o;
return Objects.equals(id, product.id)
&& Objects.equals(weight, product.weight)
&& Objects.equals(name, product.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name, weight);
}
}
Note about Java records: records (like
public record Product(String name, int weight)) automatically generateequalsandhashCodebased on all fields. This is the simplest solution for creating immutable types with the right contract.
// Version record — equals et hashCode générés automatiquement
public record Product(String name, int weight) {
public static final Comparator<Product> BY_WEIGHT =
Comparator.comparingInt(Product::weight);
}
7.4 Set Implementations
HashSet
HashSet is based on HashMap and even uses its implementation internally. A HashSet is essentially a HashMap where values are ignored (only keys are used).
Operation:
- Use
hashCode()to determine bucket - Use
equals()to check uniqueness in bucket - Collisions create linked lists in buckets (or trees since Java 8)
Features:
- Very fast:
add,contains,removeare O(1) amortized - No guaranteed order on iteration
- Suitable for 90% of use cases
TreeSet
TreeSet is based on TreeMap and uses a binary red-black tree with a defined sort order.
Features:
add,contains,removeare O(log N)- Keeps elements in their natural sort order or according to a
Comparator - Implements
SortedSetandNavigableSet→ additional features
When to use TreeSet: when you need to access the largest/smallest element, or work with order-based subsets.
// TreeSet trié par poids
NavigableSet<Product> products = new TreeSet<>(Product.BY_WEIGHT);
products.add(door); // 35 kg
products.add(window); // 10 kg
products.add(floorPanel); // 25 kg
System.out.println(products.first()); // Glass Window (10 kg)
System.out.println(products.last()); // Wooden Door (35 kg)
Performance comparison:
| Operation | HashSet | TreeSet |
|---|---|---|
add | O(1) amortized | O(log N) |
contains | O(1) amortized | O(log N) |
remove | O(1) amortized | O(log N) |
| Iteration order | Not guaranteed | Sort order |
| Features | Basic set | SortedSet + NavigableSet |
7.5 SortedSet and NavigableSet (with demo)
SortedSet
SortedSet extends Set and sets an order on the elements. It adds the following methods:
| Method | Description |
|---|---|
first() | First element (smallest) |
last() | Last element (largest) |
headSet(E toElement) | Subset of elements strictly lower than toElement (exclusive) |
tailSet(E fromElement) | Subset of elements greater than or equal to fromElement (inclusive) |
subSet(E from, E to) | Subset of from (inclusive) to to (exclusive) |
// SortedSet avec nombres
SortedSet<Integer> numbers = new TreeSet<>(Set.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
System.out.println(numbers.headSet(4)); // [1, 2, 3] — strictement inférieurs à 4
System.out.println(numbers.tailSet(8)); // [8, 9, 10] — supérieurs ou égaux à 8
System.out.println(numbers.subSet(4, 6)); // [4, 5] — de 4 (inclus) à 6 (exclus)
Important: just like
subList, these views are linked to the underlying collection. Changes are bidirectional.
NavigableSet
NavigableSet extends SortedSet and adds more precise navigation methods:
| Method | Description |
|---|---|
lower(E e) | Largest element strictly less than e |
higher(E e) | Smallest element strictly greater than e |
floor(E e) | Largest element less than or equal to e |
ceiling(E e) | Smallest element greater than or equal to e |
pollFirst() | Remove and return the first element |
pollLast() | Remove and return the last element |
descendingSet() | View in reverse order |
// NavigableSet avec produits triés par poids
NavigableSet<Product> products = new TreeSet<>(Product.BY_WEIGHT);
// window=10, floorPanel=25, door=35
// findLighterProducts : tous les produits plus légers qu'un produit donné
Set<Product> lighterThanDoor = products.headSet(door);
// [window (10), floorPanel (25)] — strictement inférieurs en poids à door (35)
System.out.println(products.lower(floorPanel)); // window — poids < 25
System.out.println(products.higher(floorPanel)); // door — poids > 25
System.out.println(products.floor(floorPanel)); // floorPanel — poids <= 25
System.out.println(products.ceiling(floorPanel)); // floorPanel — poids >= 25
7.6 General conclusion
Conclusion on the Sets:
- Sets are widely used, less than Lists and Maps, but much more than Queues
HashSetis the general-purpose implementation (fastest in most cases)TreeSetis useful when you needSortedSetorNavigableSetinterfaces- The
hashCode/equalscontract is absolutely critical — if violated,HashMapandHashSetdo not work as expected
General conclusion of the course:
Java collections are a fundamental component of everyday Java development. Choosing the right collection can:
- Simplify the code — fewer lines, more readable
- Reduce bugs — JDK implementations are very well tested
- Improve performance — the right collection can make an order of magnitude difference
Quick decision guide:
Besoin d'une collection ordonnée par index ?
→ List (ArrayList pour la plupart des cas, LinkedList très rarement)
Besoin d'unicité ?
→ Set (HashSet en général, TreeSet pour les opérations de tri/navigation)
Besoin de paires clé/valeur ?
→ Map (HashMap en général, TreeMap pour les clés ordonnées,
LinkedHashMap pour LRU cache, EnumMap pour les clés enum)
Besoin de traiter des collections en pipeline ?
→ Streams API (avec filter, map, collect, etc.)
Besoin d'une collection immuable ?
→ List.of(), Set.of(), Map.of() ou .copyOf()
8. Full source code
8.1 Project setup (Gradle)
build.gradle
apply plugin: 'java'
defaultTasks 'clean', 'build'
sourceCompatibility = 21
version = '1.0'
repositories {
mavenCentral()
}
dependencies {
testImplementation group: 'junit', name: 'junit', version: '4.13.2'
testImplementation group: 'org.hamcrest', name: 'hamcrest', version: '2.2'
}
settings.gradle
rootProject.name = 'Collections'
gradle/wrapper/gradle-wrapper.properties
#Mon May 18 22:22:28 BST 2015
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip
8.2 Common classes
src/main/java/com/monotonic/collections/common/Product.java
package com.monotonic.collections.common;
import java.util.Comparator;
public record Product(String name, int weight) {
@Override
public String toString() {
return "Product{" +
"name='" + name + '\'' +
", weight=" + weight +
'}';
}
public static final Comparator<Product> BY_WEIGHT =
Comparator.comparingInt(Product::weight);
}
src/main/java/com/monotonic/collections/common/Supplier.java
package com.monotonic.collections.common;
import java.util.ArrayList;
import java.util.List;
public class Supplier {
private final String name;
private final List<Product> products = new ArrayList<>();
public Supplier(String name) {
this.name = name;
}
public String getName() { return name; }
public List<Product> getProducts() { return products; }
@Override
public String toString() {
return "Supplier{name='" + name + "', products=" + products + '}';
}
}
src/main/java/com/monotonic/collections/common/TheArrayProblem.java
package com.monotonic.collections.common;
import java.util.Arrays;
public class TheArrayProblem {
public static void main(String[] args) {
Product door = new Product("Wooden Door", 35);
Product floorPanel = new Product("Floor Panel", 25);
Product[] products = { door, floorPanel };
System.out.println(Arrays.toString(products));
final Product window = new Product("Window", 15);
products = add(window, products);
System.out.println(Arrays.toString(products));
// Duplicate — ajout sans vérification de doublon
products = add(window, products);
System.out.println(Arrays.toString(products));
}
private static Product[] add(Product product, Product[] array) {
int length = array.length;
Product[] newArray = Arrays.copyOf(array, length + 1);
newArray[length] = product;
return newArray;
}
}
8.3 Module 2 — What are Collections
CollectionConcepts.java (after)
package com.monotonic.collections._2_what_are_collections.after;
import com.monotonic.collections.common.Product;
import java.util.*;
public class CollectionConcepts {
public static void main(String[] args) {
var door = new Product("Wooden Door", 35);
var floorPanel = new Product("Floor Panel", 25);
var window = new Product("Glass Window", 10);
Collection<Product> products = new ArrayList<>();
products.add(door);
products.add(floorPanel);
products.add(window);
// Itération avec for-each
for (var product : products) {
System.out.println(product);
}
// Suppression sécurisée avec Iterator
Iterator<Product> iterator = products.iterator();
while (iterator.hasNext()) {
final Product product = iterator.next();
if (product.weight() > 20) {
iterator.remove();
}
}
System.out.println(products);
System.out.println(products.size());
System.out.println(products.isEmpty());
System.out.println(products.contains(floorPanel));
products.remove(floorPanel);
System.out.println(products.contains(floorPanel));
// removeAll
List<Product> toRemove = new ArrayList<>();
toRemove.add(door);
toRemove.add(floorPanel);
products.removeAll(toRemove);
System.out.println(products);
}
}
TheArrayProblem.java (after)
package com.monotonic.collections._2_what_are_collections.after;
import com.monotonic.collections.common.Product;
import java.util.Arrays;
public class TheArrayProblem {
public static void main(String[] args) {
var door = new Product("Wooden Door", 35);
var floorPanel = new Product("Floor Panel", 25);
Product[] products = { door, floorPanel };
System.out.println(Arrays.toString(products));
var window = new Product("Glass Window", 10);
products = add(window, products);
System.out.println(Arrays.toString(products));
// Démonstration des doublons non gérés
products = add(window, products);
System.out.println(Arrays.toString(products));
}
private static Product[] add(Product product, Product[] array) {
int length = array.length;
var newArray = Arrays.copyOf(array, length + 1);
newArray[length] = product;
return newArray;
}
}
8.4 Module 3 — Lists
Shipment.java (final version in _3_lists)
package com.monotonic.collections._3_lists;
import com.monotonic.collections.common.Product;
import java.util.*;
public class Shipment implements Iterable<Product> {
private static final int LIGHT_VAN_MAX_WEIGHT = 20;
public static final int PRODUCT_NOT_PRESENT = -1;
private final List<Product> products = new ArrayList<>();
private List<Product> lightVanProducts;
private List<Product> heavyVanProducts;
public void add(Product product) {
products.add(product);
}
public void replace(Product oldProduct, Product newProduct) {
final int index = products.indexOf(oldProduct);
if (index != PRODUCT_NOT_PRESENT) {
products.set(index, newProduct);
}
}
public void prepare() {
products.sort(Product.BY_WEIGHT);
int splitPoint = findSplitPoint();
lightVanProducts = products.subList(0, splitPoint);
heavyVanProducts = products.subList(splitPoint, products.size());
}
private int findSplitPoint() {
for (int i = 0; i < products.size(); i++) {
final Product product = products.get(i);
if (product.weight() > LIGHT_VAN_MAX_WEIGHT) {
return i;
}
}
return 0;
}
public List<Product> getHeavyVanProducts() { return heavyVanProducts; }
public List<Product> getLightVanProducts() { return lightVanProducts; }
public Iterator<Product> iterator() { return products.iterator(); }
}
Shipment.java (after — full version with stripHeavyProducts)
package com.monotonic.collections._3_lists.after;
import com.monotonic.collections.common.Product;
import java.util.*;
public class Shipment implements Iterable<Product> {
private static final int LIGHT_VAN_MAX_WEIGHT = 20;
private static final int MISSING_PRODUCT = -1;
private final List<Product> products = new ArrayList<>();
private List<Product> lightVanProducts;
private List<Product> heavyVanProducts;
public void add(Product product) {
products.add(product);
}
public boolean replace(Product oldProduct, Product newProduct) {
int position = products.indexOf(oldProduct);
if (position == MISSING_PRODUCT) {
return false;
} else {
products.set(position, newProduct);
return true;
}
}
public void prepare() {
products.sort(Product.BY_WEIGHT);
int splitPoint = findSplitPoint();
lightVanProducts = products.subList(0, splitPoint);
heavyVanProducts = products.subList(splitPoint, products.size());
}
private int findSplitPoint() {
int size = products.size();
for (int i = 0; i < size; i++) {
var product = products.get(i);
if (product.weight() > LIGHT_VAN_MAX_WEIGHT) {
return i;
}
}
return 0;
}
public List<Product> getHeavyVanProducts() { return heavyVanProducts; }
public List<Product> getLightVanProducts() { return lightVanProducts; }
public Iterator<Product> iterator() { return products.iterator(); }
public boolean stripHeavyProducts() {
return products.removeIf(product -> product.weight() > LIGHT_VAN_MAX_WEIGHT);
}
}
8.5 Module 4 — Maps
ProductLookupTable.java (interface)
package com.monotonic.collections._4_maps.after;
public interface ProductLookupTable {
Product lookupById(int id);
void addProduct(Product productToAdd);
void clear();
}
NaiveProductLookupTable.java (implementation with ArrayList)
package com.monotonic.collections._4_maps.after;
import java.util.ArrayList;
import java.util.List;
public class NaiveProductLookupTable implements ProductLookupTable {
private final List<Product> products = new ArrayList<>();
@Override
public void addProduct(final Product productToAdd) {
var id = productToAdd.getId();
for (var product : products) {
if (product.getId() == id) {
throw new IllegalArgumentException(
"Unable to add product, duplicate id for: " + productToAdd);
}
}
products.add(productToAdd);
}
@Override
public Product lookupById(final int id) {
for (var product : products) {
if (product.getId() == id) {
return product;
}
}
return null;
}
public void clear() { products.clear(); }
}
MapProductLookupTable.java (implementation with HashMap)
package com.monotonic.collections._4_maps.after;
import java.util.HashMap;
import java.util.Map;
public class MapProductLookupTable implements ProductLookupTable {
private final Map<Integer, Product> idToProduct = new HashMap<>();
@Override
public void addProduct(final Product productToAdd) {
var id = productToAdd.getId();
if (idToProduct.containsKey(id)) {
throw new IllegalArgumentException(
"Unable to add product, duplicate id for: " + productToAdd);
}
idToProduct.put(id, productToAdd);
}
@Override
public Product lookupById(final int id) {
return idToProduct.get(id);
}
public void clear() { idToProduct.clear(); }
}
ViewsOverMaps.java
package com.monotonic.collections._4_maps.after;
import java.util.*;
public class ViewsOverMaps {
public static void main(String[] args) {
var idToProduct = new HashMap<Integer, Product>();
idToProduct.put(1, ProductFixtures.door);
idToProduct.put(2, ProductFixtures.floorPanel);
idToProduct.put(3, ProductFixtures.window);
// keySet — supprimer de la vue supprime dans la Map
var ids = idToProduct.keySet();
ids.remove(1);
// values
var products = idToProduct.values();
products.remove(ProductFixtures.window);
// entrySet
var entries = idToProduct.entrySet();
for (var entry : entries) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
}
}
}
AdvancedOperations.java
package com.monotonic.collections._4_maps.after;
import java.util.HashMap;
public class AdvancedOperations {
public static void main(String[] args) {
var defaultProduct = new Product(-1, "Whatever the customer wants", 100);
var idToProduct = new HashMap<Integer, Product>();
idToProduct.put(1, ProductFixtures.door);
idToProduct.put(2, ProductFixtures.floorPanel);
idToProduct.put(3, ProductFixtures.window);
// getOrDefault — ne modifie pas la Map
var result = idToProduct.getOrDefault(10, defaultProduct);
System.out.println(result);
System.out.println(idToProduct.get(10)); // null
// computeIfAbsent — modifie la Map
result = idToProduct.computeIfAbsent(10, (id) -> new Product(id, "Custom Product", 10));
System.out.println(result);
System.out.println(idToProduct.get(10)); // Product{id=10, ...}
// replaceAll — met à jour toutes les valeurs
idToProduct.replaceAll((key, oldProduct) ->
new Product(oldProduct.getId(),
oldProduct.getName(),
oldProduct.getWeight() + 10));
System.out.println(idToProduct);
}
}
MutableHashMapKeys.java (demonstration of the mutable keys problem)
package com.monotonic.collections._4_maps.after;
import java.util.*;
public class MutableHashMapKeys {
public static void main(String[] args) {
var brokenMap = new HashMap<MutableString, String>();
var value = "abc";
var key = new MutableString(value);
brokenMap.put(key, value);
System.out.println(brokenMap.get(key)); // "abc"
System.out.println(brokenMap); // {abc=abc}
key.set("def"); // Mutation de la clé !
System.out.println(brokenMap.get(key)); // null — PROBLÈME !
System.out.println(brokenMap); // {def=abc} — entrée perdue
}
private static class MutableString {
private String value;
public MutableString(final String value) { set(value); }
public String get() { return value; }
public void set(final String value) {
Objects.requireNonNull(value);
this.value = value;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
return value.equals(((MutableString) o).value);
}
@Override
public int hashCode() { return value.hashCode(); }
@Override
public String toString() { return value; }
}
}
LookupTableComparison.java (benchmark ArrayList vs HashMap)
package com.monotonic.collections._4_maps.after;
import java.util.*;
public class LookupTableComparison {
private static final int ITERATIONS = 5;
private static final int NUMBER_OF_PRODUCTS = 20_000;
private static final List<Product> products = generateProducts();
public static void main(String[] args) {
runLookups(new MapProductLookupTable());
runLookups(new NaiveProductLookupTable());
}
private static List<Product> generateProducts() {
final List<Product> products = new ArrayList<>();
final Random weightGenerator = new Random();
for (int i = 0; i < NUMBER_OF_PRODUCTS; i++) {
products.add(new Product(i, "Product" + i, 10 + weightGenerator.nextInt(10)));
}
Collections.shuffle(products);
Collections.shuffle(products);
Collections.shuffle(products);
return products;
}
private static void runLookups(final ProductLookupTable lookupTable) {
System.out.println("Running lookups with " + lookupTable.getClass().getSimpleName());
for (int i = 0; i < ITERATIONS; i++) {
final long startTime = System.currentTimeMillis();
for (Product product : products) { lookupTable.addProduct(product); }
for (Product product : products) {
final Product result = lookupTable.lookupById(product.getId());
if (result != product) {
throw new IllegalStateException("Wrong result for " + product);
}
}
lookupTable.clear();
System.out.println(System.currentTimeMillis() - startTime + "ms");
}
}
}
8.6 Module 5 — Streams
StreamProducts.java (after — comparison loops vs streams)
package com.monotonic.collections._5_streams.after;
import com.monotonic.collections._5_streams.Product;
import java.util.*;
import static java.util.Comparator.comparingInt;
public class StreamProducts {
public static void main(String[] args) {
var door = new Product(1, "Wooden Door", 35);
var floorPanel = new Product(2, "Floor Panel", 25);
var window = new Product(3, "Glass Window", 10);
var products = List.of(door, floorPanel, window, floorPanel, window);
System.out.println(namesOfLightProductsWeightSortedStreams(products));
}
// Version Streams — concise et lisible
private static List<String> namesOfLightProductsWeightSortedStreams(
final List<Product> products) {
return products
.stream()
.filter(product -> product.getWeight() < 30)
.sorted(comparingInt(Product::getWeight))
.map(Product::getName)
.toList();
}
// Version boucles — pour comparaison
private static List<String> namesOfLightProductsWeightSortedLoop(
List<Product> products) {
List<Product> lightProducts = new ArrayList<>();
for (Product product : products) {
if (product.getWeight() < 30) {
lightProducts.add(product);
}
}
lightProducts.sort(comparingInt(Product::getWeight));
List<String> productNames = new ArrayList<>();
for (Product product : lightProducts) {
productNames.add(product.getName());
}
return Collections.unmodifiableList(productNames);
}
}
EnterTheCollector.java (after — groupingBy and counting)
package com.monotonic.collections._5_streams.after;
import com.monotonic.collections._5_streams.Product;
import java.util.*;
import java.util.stream.Collectors;
import static java.util.Comparator.comparingInt;
import static java.util.stream.Collectors.*;
public class EnterTheCollector {
public static void main(String[] args) {
var door = new Product(1, "Wooden Door", 35);
var floorPanel = new Product(2, "Floor Panel", 25);
var window = new Product(3, "Glass Window", 10);
var products = List.of(door, floorPanel, window, floorPanel, window, floorPanel);
// Compter le nombre de produits légers par nom
Map<String, Long> lightProducts = products
.stream()
.filter(product -> product.getWeight() < 30)
.sorted(comparingInt(Product::getWeight))
.collect(groupingBy(Product::getName, counting()));
System.out.println(lightProducts);
// {Floor Panel=3, Glass Window=2}
}
}
8.7 Module 6 — Operations and Factories
UnmodifiableVsImmutable.java
package com.monotonic.collections._6_operations.after;
import java.util.*;
public class UnmodifiableVsImmutable {
public static void main(String[] args) {
var mutableCountryToPopulation = new HashMap<String, Integer>();
mutableCountryToPopulation.put("UK", 67);
mutableCountryToPopulation.put("USA", 328);
// Vue non modifiable — liée à la Map originale
var unmodifiable = Collections.unmodifiableMap(mutableCountryToPopulation);
// Copie immuable — indépendante
var copied = Map.copyOf(mutableCountryToPopulation);
System.out.println("copied = " + copied);
System.out.println("unmodifiable = " + unmodifiable);
mutableCountryToPopulation.put("Germany", 83);
System.out.println("copied = " + copied); // Pas de Germany !
System.out.println("unmodifiable = " + unmodifiable); // Contient Germany !
// Création directe avec Map.of
var countryToPopulation = Map.of("UK", 67, "USA", 328);
}
}
Shipment.java (module 6 version with Collections.unmodifiableList)
package com.monotonic.collections._6_operations.after;
import com.monotonic.collections.common.Product;
import java.util.*;
public class Shipment implements Iterable<Product> {
private static final int LIGHT_VAN_MAX_WEIGHT = 20;
private static final int MISSING_PRODUCT = -1;
private final List<Product> products = new ArrayList<>();
private List<Product> lightVanProducts;
private List<Product> heavyVanProducts;
public void add(Product product) { products.add(product); }
public boolean replace(Product oldProduct, Product newProduct) {
int position = products.indexOf(oldProduct);
if (position == MISSING_PRODUCT) return false;
products.set(position, newProduct);
return true;
}
public void prepare() {
products.sort(Product.BY_WEIGHT);
int splitPoint = findSplitPoint();
// Collections.unmodifiableList protège l'encapsulation
lightVanProducts = Collections.unmodifiableList(
products.subList(0, splitPoint));
heavyVanProducts = Collections.unmodifiableList(
products.subList(splitPoint, products.size()));
}
private int findSplitPoint() {
for (int i = 0; i < products.size(); i++) {
if (products.get(i).weight() > LIGHT_VAN_MAX_WEIGHT) return i;
}
return 0;
}
public List<Product> getHeavyVanProducts() { return heavyVanProducts; }
public List<Product> getLightVanProducts() { return lightVanProducts; }
public Iterator<Product> iterator() { return products.iterator(); }
}
CollectionOperations.java (after)
package com.monotonic.collections._6_operations.after;
import com.monotonic.collections.common.Product;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.IntStream;
public class CollectionOperations {
public static Product door = new Product("Wooden Door", 35);
public static Product floorPanel = new Product("Floor Panel", 25);
public static Product window = new Product("Glass Window", 10);
public static void main(String[] args) {
var products = new ArrayList<>(List.of(window, floorPanel, door));
System.out.println(products);
Collections.rotate(products, 2);
System.out.println(products);
Collections.shuffle(products, ThreadLocalRandom.current());
System.out.println(products);
var alphabet = makeAlphabet();
System.out.println(alphabet);
int index = Collections.binarySearch(alphabet, 'M');
System.out.println("index = " + index);
}
private static List<Character> makeAlphabet() {
return IntStream.range('A', 'Z').mapToObj(x -> (char)x).toList();
}
}
SequencedCollections.java (after — Java 21 API)
package com.monotonic.collections._6_operations.after;
import java.util.*;
public class SequencedCollections {
public static void main(String[] args) {
List<Integer> list = List.of(1, 2, 3);
SortedSet<Integer> set = new TreeSet<>(list);
Deque<Integer> queue = new ArrayDeque<>(list);
LinkedHashSet<Integer> hashSet = new LinkedHashSet<>(list);
// Itération uniforme (avant Java 21)
for (var x : list) { System.out.println(x); }
set.stream().forEach(System.out::println);
// Avant Java 21 : API non uniforme pour le premier élément
System.out.println(list.get(0));
System.out.println(set.first());
System.out.println(queue.getFirst());
System.out.println(hashSet.iterator().next());
// Avant Java 21 : API non uniforme pour le dernier élément
System.out.println(list.get(list.size() - 1));
System.out.println(set.last());
System.out.println(queue.getLast());
// Java 21 : SequencedCollection — API uniforme
System.out.println(list.getFirst());
System.out.println(set.getFirst());
System.out.println(queue.getFirst());
System.out.println(hashSet.getFirst());
System.out.println(list.getLast());
System.out.println(set.getLast());
System.out.println(queue.getLast());
System.out.println(hashSet.getLast());
// Itération en ordre inverse avec reversed()
for (var x : list.reversed()) {
System.out.println(x); // 3, 2, 1
}
}
}
SlideExamples.java (all operations of the Collections class)
package com.monotonic.collections._6_operations.after;
import com.monotonic.collections.common.Product;
import java.util.*;
public class SlideExamples {
public static void main(String[] args) {
// disjoint
var _1to3 = List.of(1, 2, 3);
var _2to4 = List.of(2, 3, 4);
var _4to6 = List.of(4, 5, 6);
System.out.println(Collections.disjoint(_1to3, _4to6)); // true
System.out.println(Collections.disjoint(_1to3, _2to4)); // false
// frequency
var letters = "ABCDEFAADSEA".chars().mapToObj(x -> (char)x).toList();
int count = Collections.frequency(letters, 'A');
System.out.println(count); // 4
// addAll
var door = new Product("Wooden Door", 35);
var floorPanel = new Product("Floor Panel", 25);
var window = new Product("Glass Window", 10);
var products = new ArrayList<Product>();
Collections.addAll(products, door, floorPanel, window);
System.out.println(products);
// max
var max = Collections.max(products, Product.BY_WEIGHT);
System.out.println(max == door); // true
// swap
Collections.swap(products, 1, 2);
System.out.println(products);
// reverse
Collections.reverse(products);
System.out.println(products);
// fill
Collections.fill(products, door);
System.out.println(products);
}
}
8.8 Module 7 — Sets
ProductCatalogue.java (after — HashSet)
package com.monotonic.collections._7_sets.after;
import com.monotonic.collections.common.Product;
import com.monotonic.collections.common.Supplier;
import java.util.*;
public class ProductCatalogue implements Iterable<Product> {
private final Set<Product> products = new HashSet<>();
public void addSupplier(final Supplier supplier) {
products.addAll(supplier.getProducts());
// Le HashSet garantit automatiquement l'unicité
}
@Override
public Iterator<Product> iterator() {
return products.iterator();
}
}
WeightAwareProductCatalogue.java (after — TreeSet with NavigableSet)
package com.monotonic.collections._7_sets.after;
import com.monotonic.collections.common.Product;
import com.monotonic.collections.common.Supplier;
import java.util.*;
public class WeightAwareProductCatalogue implements Iterable<Product> {
private final NavigableSet<Product> products =
new TreeSet<>(Product.BY_WEIGHT); // Trié par poids
public void addSupplier(final Supplier supplier) {
products.addAll(supplier.getProducts());
}
public Set<Product> findLighterProducts(final Product product) {
return products.headSet(product); // Vue sur les produits plus légers
}
@Override
public Iterator<Product> iterator() {
return products.iterator();
}
}
8.9 JUnit Tests
ShipmentTest.java (after)
package com.monotonic.collections._3_lists.after;
import com.monotonic.collections.common.Product;
import org.junit.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.*;
public class ShipmentTest {
public static Product door = new Product("Wooden Door", 35);
public static Product floorPanel = new Product("Floor Panel", 25);
public static Product window = new Product("Glass Window", 10);
private Shipment shipment = new Shipment();
@Test
public void shouldAddItems() {
shipment.add(door);
shipment.add(window);
assertThat(shipment, contains(door, window));
}
@Test
public void shouldReplaceItems() {
shipment.add(door);
shipment.add(window);
assertTrue(shipment.replace(door, floorPanel));
assertThat(shipment, contains(floorPanel, window));
}
@Test
public void shouldNotReplaceMissingItems() {
shipment.add(window);
assertFalse(shipment.replace(door, floorPanel));
assertThat(shipment, contains(window));
}
@Test
public void shouldIdentifyVanRequirements() {
shipment.add(door);
shipment.add(window);
shipment.add(floorPanel);
shipment.prepare();
assertThat(shipment.getLightVanProducts(), contains(window));
assertThat(shipment.getHeavyVanProducts(), contains(floorPanel, door));
}
@Test
public void shouldStripHeavyProducts() {
shipment.add(door);
shipment.add(window);
shipment.add(floorPanel);
assertTrue(shipment.stripHeavyProducts());
assertThat(shipment, contains(window));
}
}
ProductCatalogueTest.java (after)
package com.monotonic.collections._7_sets.after;
import com.monotonic.collections.common.Product;
import com.monotonic.collections.common.Supplier;
import org.junit.Test;
import java.util.Collections;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
public class ProductCatalogueTest {
public static Product door = new Product("Wooden Door", 35);
public static Product floorPanel = new Product("Floor Panel", 25);
public static Product window = new Product("Glass Window", 10);
@Test
public void shouldNotRecordDuplicateProducts() {
Supplier johnsGlazing = new Supplier("John's Glazing");
johnsGlazing.getProducts().add(window);
Supplier allPurpose = new Supplier("All Purpose Supplies Ltd.");
Collections.addAll(allPurpose.getProducts(), door, floorPanel, window);
ProductCatalogue catalogue = new ProductCatalogue();
catalogue.addSupplier(johnsGlazing);
catalogue.addSupplier(allPurpose);
// window est fourni par deux fournisseurs mais ne doit apparaître qu'une fois
assertThat(catalogue, containsInAnyOrder(door, floorPanel, window));
}
}
WeightAwareProductCatalogueTest.java (after)
package com.monotonic.collections._7_sets.after;
import com.monotonic.collections.common.Product;
import com.monotonic.collections.common.Supplier;
import org.junit.Test;
import java.util.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
public class WeightAwareProductCatalogueTest {
public static Product door = new Product("Wooden Door", 35);
public static Product floorPanel = new Product("Floor Panel", 25);
public static Product window = new Product("Glass Window", 10);
@Test
public void shouldFindLighterProducts() {
Supplier johnsGlazing = new Supplier("John's Glazing");
johnsGlazing.getProducts().add(window);
Supplier allPurpose = new Supplier("All Purpose Supplies Ltd.");
Collections.addAll(allPurpose.getProducts(), door, floorPanel, window);
WeightAwareProductCatalogue catalogue = new WeightAwareProductCatalogue();
catalogue.addSupplier(johnsGlazing);
catalogue.addSupplier(allPurpose);
// Trouver tous les produits plus légers que door (35 kg)
Set<Product> lighterProducts = catalogue.findLighterProducts(door);
// Attendu : [window (10kg), floorPanel (25kg)] — triés par poids
assertThat(lighterProducts, contains(window, floorPanel));
}
}
Search Terms
collections · java · se · backend · architecture · full-stack · web · operations · implementations · streams · live · coding · collection · maps · methods · demonstration · factory · lists · map · sets · shipments · behaviors · characteristics · factories