Intermediate

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.

Java version: Java SE 21 (basically backwards compatible since Java SE 8)


Table of Contents

  1. Course Overview
  2. What are Collections and why use them?
  1. Collections with iteration order: Lists
  1. Key/value pairs: The Maps
  1. Introduction to Java Streams
  1. Operations and Factories on Collections
  1. Collections with uniqueness: The Sets
  1. Complete source code

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:

  1. Module 1: Overview
  2. Module 2: Introduction to collections (this module)
  3. Module 3: Lists — collections with iteration order, the most used
  4. Module 4: Maps — collections of key/value pairs
  5. Module 5: Java Streams — API introduced in Java 8 to process collections
  6. Module 6: Operations and factories on collections
  7. 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:

  1. Extract the ZIP file to a directory
  2. Ensure that the JAVA_HOME variable is configured to point to a Java 17+ JDK
  3. Run the gradlew script (which downloads Gradle the first time)
  4. Import the project into your IDE (IntelliJ IDEA recommended) by opening the build.gradle file 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:

  1. No native add method — to add an element, you must manually copy the entire array
  2. No duplicate protection — nothing prevents adding the same element twice
  3. 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

  • List and its subinterfaces — ordered by index (most used)
  • Set, SortedSet, NavigableSet — uniqueness of elements
  • Queue, Deque — FIFO/LIFO behaviors
  • Map, SortedMap — key/value pairs (does not derive from Collection but 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:

InterfaceKey propertyExample implementation
ListIteration order by indexArrayList, LinkedList
SetUniqueness of elementsHashSet, TreeSet
MapKey/Value PairsHashMap, TreeMap
Queue/DequeFIFO/LIFOArrayDeque, 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:

MethodDescription
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 use var products = new ArrayList<>(), the inferred type will be ArrayList<Object>, not Collection<Product>. To use var correctly, 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:

  1. The characteristics of lists — what distinguishes them from sets, queues or maps
  2. A concrete example of live coding around Shipments (product shipments)
  3. The different implementations (ArrayList vs LinkedList) 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):

MethodDescription
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: subList returns 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 Shipment
  • shouldReplaceItems — replace an existing product
  • shouldNotReplaceMissingItems — attempt to replace a missing product
  • shouldIdentifyVanRequirements — prepare the Shipment and distribute the products
  • shouldStripHeavyProducts — 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 to products.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/set only replaces the first occurrence and returns a success boolean. The version with replaceAll replaces 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 0 if absent means that there are no light products (lightVanProducts will be empty). Some use cases would require returning products.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:

  1. A new array of larger size is allocated (usually 1.5x the current size)
  2. 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: precedes ArrayList, thread-safe by coarse synchronization → very slow
  • Stack: inherits from Vector, obsolete → use Deque instead

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.

OperationArrayListLinkedList
get(index)O(1) — direct access to the arrayO(N) — node traversal
add(element) at the endΩ(1) dampedO(1)
add(index, element) in the middleO(N) — element offsetO(N) — traverse to index
remove(index) in the middleO(N) — element offsetO(N) — traverse to index
contains(element)Y(N)Y(N)
MemoryGood (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 Dictionary class already exists in the JDK (it precedes the Collections API) and is now deprecated. Java therefore chose the mathematical term “Map”. Don’t use the Dictionary class — use the Map interface 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 HashMap correctly

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:

  • addProduct is Y(N) — loop over all products to look for duplicates
  • lookupById is 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)
  • addProduct is O(1)containsKey is O(1) with HashMap
  • lookupById is O(1)get is 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

MethodDescription
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 put vs replace: put adds the pair even if the key does not exist; replace does nothing if the key is absent.

Reading values

MethodDescription
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 null as a value, prefer containsKey followed by get to avoid NullPointerException.

Nulls in Maps

  • HashMap: accepts a null key and null values
  • TreeMap: accepts null values but no null keys (because the keys must be compared)

4.4 Views on Maps

Three methods return views on the Map:

MethodReturn
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 (add not supported)
  • values(): no addition via view
  • entrySet(): additions are possible with Map.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

MethodDescription
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

MethodDescription
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: if defaultValue is null, getOrDefault can still return null. 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:

  1. When we call put(key, value), Java calculates hashCode() of the key
  2. This hashcode is used (modulo the capacity) to determine a bucket (box in an internal array)
  3. 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)
  4. To retrieve a value with get(key), Java recalculates the hashcode, finds the bucket, then uses equals to 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, containsKey are O(log N) (slower than HashMap)
  • Keys must be comparable (Comparable or Comparator provided)

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) is true, then a.hashCode() must be equal to b.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:

ImplementationWhen to use it
HashMap95% of cases — general practitioner, very fast
TreeMapWhen Key Order Is Necessary
LinkedHashMapFor an LRU cache or when insertion order is important
IdentityHashMapFor graph serialization/traversal
EnumMapFor 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:

  1. Using Streams with live examples (filter, map, sorted)
  2. Catalog of intermediate operations
  3. Terminal operations catalog
  4. Collectors — to construct complex values
  5. 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).

OperationSignatureDescription
filterfilter(Predicate<T> p)Remove elements that do not match the predicate
mapmap(Function<T, R> f)Transforms each element into a new element
flatMapflatMap(Function<T, Stream<R>> f)Transforms each element into a Stream and flattens them
sortedsorted() or sorted(Comparator<T> c)Sort items
distinctdistinct()Remove duplicates
limitlimit(long n)Keep the first N elements
skipskip(long n)Ignore the first N elements
peekpeek(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

OperationDescription
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)

OperationDescription
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

OperationDescription
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

OperationDescription
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):

CollectorDescription
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 of int
  • LongStreamlong stream
  • DoubleStreamdouble stream
// 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?

StreamsLoops
High-level construction — less boilerplateLow-level construction — more control
Powered by JDK developersMay be faster in certain critical cases
Often more readable — resembles a business descriptionBorderline cases with checked exceptions → often simpler
Bad interaction with checked exceptionsAny 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:

  1. Factories — JDK methods that construct new collection instances (not new ArrayList()), often immutable or non-modifiable
  2. Operations — utility algorithms (max, min, sort, rotate, mix…)
  3. 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:

OptionsEditable?Copy or view?Support null?
Collections.unmodifiableMap(m)No (UnsupportedOperationException)View (related to m)Yes
Map.copyOf(m)NoIndependent copyNo (NPE if null)
Map.of(...)NoNew MapNo

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:

  • ImmutableUnsupportedOperationException on any modification
  • Do not support nullsNullPointerException if a null is provided
  • For Set.of and Map.of, the iteration order is not guaranteed
  • Map.of supports up to 10 pairs; beyond that, use Map.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 HashSet is not a SequencedCollection (no order defined)
  • A TreeSet (SortedSet) is a SequencedCollection (sort order defined)
  • A LinkedHashSet is a SequencedCollection (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:

MethodDescription
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 Collections utility class provides many algorithms (sort, shuffle, rotate, binarySearch, max, min, disjoint, frequency)
  • Java 21 introduces SequencedCollection and SequencedMap for 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:

  1. Live demonstration of the characteristics of the Sets
  2. The hashCode/equals contract and what happens if it is violated
  3. 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) is true, then a.hashCode() must be equal to b.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:

  1. Equality by reference: two objects are equal only if they are the same object (default behavior of Object.equals)
  2. 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 generate equals and hashCode based 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, remove are 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, remove are O(log N)
  • Keeps elements in their natural sort order or according to a Comparator
  • Implements SortedSet and NavigableSet → 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:

OperationHashSetTreeSet
addO(1) amortizedO(log N)
containsO(1) amortizedO(log N)
removeO(1) amortizedO(log N)
Iteration orderNot guaranteedSort order
FeaturesBasic setSortedSet + NavigableSet

7.5 SortedSet and NavigableSet (with demo)

SortedSet

SortedSet extends Set and sets an order on the elements. It adds the following methods:

MethodDescription
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 extends SortedSet and adds more precise navigation methods:

MethodDescription
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
  • HashSet is the general-purpose implementation (fastest in most cases)
  • TreeSet is useful when you need SortedSet or NavigableSet interfaces
  • The hashCode/equals contract is absolutely critical — if violated, HashMap and HashSet do 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

Interested in this course?

Contact us to book it or get a custom training plan for your team.